Compare commits

..

1 Commits

Author SHA1 Message Date
Vendicated
0bfe6f069e Add Vencord.Swc 2022-10-02 19:00:00 +02:00
151 changed files with 1593 additions and 9766 deletions

View File

@ -1,93 +0,0 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"ignorePatterns": ["dist", "browser"],
"plugins": ["header", "simple-import-sort", "unused-imports"],
"rules": {
// Since it's only been a month and Vencord has already been stolen
// by random skids who rebranded it to "AlphaCord" and erased all license
// information
"header/header": [
2,
"block",
[
{
"pattern": "!?",
"template": " "
},
" * Vencord, a modification for Discord's desktop app",
{
"pattern": " \\* Copyright \\(c\\) \\d{4}",
"template": " * 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/>.",
""
],
2
],
"quotes": ["error", "double", { "avoidEscape": true }],
"jsx-quotes": ["error", "prefer-double"],
"no-mixed-spaces-and-tabs": "error",
"indent": ["error", 4, { "SwitchCase": 1 }],
"arrow-parens": ["error", "as-needed"],
"eol-last": ["error", "always"],
"func-call-spacing": ["error", "never"],
"no-multi-spaces": "error",
"no-trailing-spaces": "error",
"no-whitespace-before-property": "error",
"semi": ["error", "always"],
"semi-style": ["error", "last"],
"space-in-parens": ["error", "never"],
"block-spacing": ["error", "always"],
"object-curly-spacing": ["error", "always"],
"eqeqeq": ["error", "always", { "null": "ignore" }],
"spaced-comment": ["error", "always", { "markers": ["!"] }],
"yoda": "error",
"prefer-destructuring": ["error", { "object": true, "array": false }],
"operator-assignment": ["error", "always"],
"no-useless-computed-key": "error",
"no-unneeded-ternary": ["error", { "defaultAssignment": false }],
"no-invalid-regexp": "error",
"no-constant-condition": ["error", { "checkLoops": false }],
"no-duplicate-imports": "error",
"no-extra-semi": "error",
"consistent-return": ["warn", { "treatUndefinedAsUnspecified": true }],
"dot-notation": "error",
"no-useless-escape": "error",
"no-fallthrough": "error",
"for-direction": "error",
"no-async-promise-executor": "error",
"no-cond-assign": "error",
"no-dupe-else-if": "error",
"no-duplicate-case": "error",
"no-irregular-whitespace": "error",
"no-loss-of-precision": "error",
"no-misleading-character-class": "error",
"no-prototype-builtins": "error",
"no-regex-spaces": "error",
"no-shadow-restricted-names": "error",
"no-unexpected-multiline": "error",
"no-unsafe-optional-chaining": "error",
"no-useless-backreference": "error",
"use-isnan": "error",
"prefer-const": "error",
"prefer-spread": "error",
"simple-import-sort/imports": "error",
"simple-import-sort/exports": "error",
"unused-imports/no-unused-imports": "error"
}
}

View File

@ -1,56 +0,0 @@
name: Build latest
on:
push:
branches:
- main
env:
FORCE_COLOR: true
jobs:
Build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2 # Install pnpm using packageManager key in package.json
- name: Use Node.js 18
uses: actions/setup-node@v2
with:
node-version: 18
cache: "pnpm"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build web
run: pnpm buildWeb
- name: Build
run: pnpm build --standalone
- name: Get some values needed for the release
id: vars
shell: bash
run: |
echo "::set-output name=sha_short::$(git rev-parse --short HEAD)"
- uses: dev-drprasad/delete-tag-and-release@085c6969f18bad0de1b9f3fe6692a3cd01f64fe5 # v0.2.0
with:
delete_release: true
tag_name: devbuild
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create the release
uses: softprops/action-gh-release@1e07f4398721186383de40550babbdf2b84acfc5 # v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: devbuild
name: Dev Build ${{ steps.vars.outputs.sha_short }}
draft: false
prerelease: false
files: |
dist/*

View File

@ -1,27 +0,0 @@
name: test
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2 # Install pnpm using packageManager key in package.json
- name: Use Node.js 18
uses: actions/setup-node@v2
with:
node-version: 18
cache: "pnpm"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Lint & Test if it compiles
run: pnpm test

18
.gitignore vendored
View File

@ -1,20 +1,2 @@
dist
node_modules
*.exe
vencord_installer
.idea
yarn.lock
package-lock.json
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
*.tsbuildinfo
src/userplugins

16
.vscode/settings.json vendored
View File

@ -1,16 +0,0 @@
{
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"[typescript]": {
"editor.defaultFormatter": "vscode.typescript-language-features"
},
"[typescriptreact]": {
"editor.defaultFormatter": "vscode.typescript-language-features"
},
"javascript.format.semicolons": "insert",
"typescript.format.semicolons": "insert",
"typescript.preferences.quoteStyle": "double",
"javascript.preferences.quoteStyle": "double"
}

View File

@ -4,8 +4,6 @@ First of all, thank you for contributing! :3
To ensure your contribution is robust, please follow the below guide!
For a friendly introduction to plugins, see [Megu's Plugin Guide!](docs/2_PLUGINS.md)
## Style Guide
- This project has a very minimal .editorconfig. Make sure your editor supports this!
@ -15,6 +13,7 @@ For a friendly introduction to plugins, see [Megu's Plugin Guide!](docs/2_PLUGIN
- Follow the file naming convention. File names should usually be camelCase, unless they export a Class
or React Component, in which case they should be PascalCase
## Contributing a Plugin
Because plugins modify code directly, incompatibilities are a problem.
@ -24,6 +23,7 @@ This way we can ensure compatibility and high quality patches.
Follow the below guide to make your first plugin!
### Finding the right module to patch
If the thing you want to patch is an action performed when interacting with a part of the UI, use React DevTools.
@ -50,14 +50,12 @@ This is the regex that will operate on the module found with "find". Just like i
this only matches exactly the part you want to patch and no other parts in the file.
The easiest way to write and test your regex is the following:
- Get the ID of the module you want to patch. To do this, go to it in the sources tab and scroll up until you
see something like `447887: (e,t,n)=>{` (Obviously the number will differ).
- Now paste the following into the console: `Vencord.Webpack.wreq.m[447887].toString()` (Changing the number to your ID)
- Now either test regexes on this string in the console or use a tool like https://regex101.com
Also pay attention to the following:
- Never hardcode variable or parameter names or any other minified names. They will change in the future. The only Exception to this rule
are the react props parameter which seems to always be `e`, but even then only rely on this if it is necessary.
Instead, use one of the following approaches where applicable:
@ -77,6 +75,6 @@ and use those in your replacement
Make sure your replacement does not introduce any whitespace. While this might seem weird, random whitespace may mess up other patches.
This includes spaces, tabs and especially newlines
---
___
And that's it! Now open a Pull Request with your Plugin

View File

@ -1,49 +1,34 @@
# Vencord
A Discord client mod that does things differently
My own Discord Desktop mod :)
## Features
- Works on Discord's latest update that breaks all other mods
- Browser Support (experimental): Run Vencord in your Browser instead of the desktop app
- Custom Css and Themes: Manually edit `%appdata%/Vencord/settings/quickCss.css` / `~/.config/Vencord/settings/quickCss.css` with your favourite editor and the client will automatically apply your changes. To import BetterDiscord themes, just add `@import url(theUrl)` on the top of this file. (Make sure the url is a github raw URL or similar and only contains plain text, and NOT a nice looking website)
- Many Useful™ plugins - [List](https://github.com/Vendicated/Vencord/tree/main/src/plugins)
- Experiments
- Proper context isolation -> Works in newer Electron versions (Confirmed working on versions 13-22)
- Works on Discord's latest swc update that breaks all other mods
- Proper context isolation -> Works in newer Electron versions (Confirmed working on versions 13-21)
- Inline patches: Patch Discord's code with regex replacements! See [the experiments plugin](src/plugins/experiments.ts) for an example. While being more complex, this is more powerful than monkey patching since you can patch only small parts of functions instead of fully replacing them, access non exported/local variables and even replace constants (like in the aforementioned experiments patch!)
- Experiments
- Custom Css: Manually edit `%appdata%/Vencord/settings/quickCss.css` / `~/.config/Vencord/settings/quickCss.css` with your favourite editor and the client will automatically apply your changes
- Many Useful™ plugins - [List](https://github.com/Vendicated/Vencord/tree/main/src/plugins)
## Installing / Uninstalling
Read [Megu's Installation Guide!](docs/1_INSTALLING.md)
## Installing on Browser
Run the same commands as in the regular install method. Now run
## Installing
Make sure you have NodeJs and git installed. I will be using pnpm, you can use npm instead
```sh
pnpm buildWeb
git clone https://github.com/Vendicated/Vencord
cd Vencord
pnpm i
pnpm build
```
You will find the built extension at dist/extension.zip. Now just install this extension in your Browser
## Installing Plugins
Vencord comes with a bunch of plugins out of the box!
However, if you want to install your own ones, create a `userplugins` folder in the `src` directory and create or clone your plugins in there.
Don't forget to rebuild!
Want to learn how to create your own plugin, and maybe PR it into Vencord? See the [Contributing](#contributing) section below!
The builds are now in the dist/ folder (Vencord/dist).
Now install with either the powershell/bash script or use [X1nto's installer](https://github.com/X1nto/VencordInstaller/releases/latest)
## Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md) and [Megu's Plugin Guide!](docs/2_PLUGINS.md)
[contribute]: CONTRIBUTING.md
[contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute]
## Join
[join]: https://discord.gg/D9uwnFnqmd
[join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join]

View File

@ -1,21 +0,0 @@
/*!
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import "./VencordNativeStub";
export * from "../src/Vencord";

View File

@ -1,66 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import IpcEvents from "../src/utils/IpcEvents";
import * as DataStore from "../src/api/DataStore";
// Discord deletes this so need to store in variable
const { localStorage } = window;
// listeners for ipc.on
const listeners = {} as Record<string, Set<Function>>;
const handlers = {
[IpcEvents.GET_REPO]: () => "https://github.com/Vendicated/Vencord", // shrug
[IpcEvents.GET_SETTINGS_DIR]: () => "LocalStorage",
[IpcEvents.GET_QUICK_CSS]: () => DataStore.get("VencordQuickCss").then(s => s ?? ""),
[IpcEvents.SET_QUICK_CSS]: (css: string) => {
DataStore.set("VencordQuickCss", css);
listeners[IpcEvents.QUICK_CSS_UPDATE]?.forEach(l => l(null, css));
},
[IpcEvents.GET_SETTINGS]: () => localStorage.getItem("VencordSettings") || "{}",
[IpcEvents.SET_SETTINGS]: (s: string) => localStorage.setItem("VencordSettings", s),
[IpcEvents.GET_UPDATES]: () => ({ ok: true, value: [] }),
[IpcEvents.OPEN_EXTERNAL]: (url: string) => open(url, "_blank"),
};
function onEvent(event: string, ...args: any[]) {
const handler = handlers[event];
if (!handler) throw new Error(`Event ${event} not implemented.`);
return handler(...args);
}
// probably should make this less cursed at some point
window.VencordNative = {
getVersions: () => ({}),
ipc: {
send: (event: string, ...args: any[]) => void onEvent(event, ...args),
sendSync: onEvent,
on(event: string, listener: () => {}) {
(listeners[event] ??= new Set()).add(listener);
},
off(event: string, listener: () => {}) {
return listeners[event]?.delete(listener);
},
invoke: (event: string, ...args: any[]) => Promise.resolve(onEvent(event, ...args))
},
};

View File

@ -1,24 +0,0 @@
if (typeof browser === "undefined") {
var browser = chrome;
}
browser.webRequest.onHeadersReceived.addListener(({ responseHeaders, url }) => {
const cspIdx = responseHeaders.findIndex(h => h.name === "content-security-policy");
if (cspIdx !== -1)
responseHeaders.splice(cspIdx, 1);
if (url.endsWith(".css")) {
const contentType = responseHeaders.find(h => h.name === "content-type");
if (contentType)
contentType.value = "text/css";
else
responseHeaders.push({
name: "content-type",
value: "text/json"
});
}
return {
responseHeaders
};
}, { urls: ["*://*.discord.com/*"] }, ["blocking", "responseHeaders"]);

View File

@ -1,8 +0,0 @@
if (typeof browser === "undefined") {
var browser = chrome;
}
var script = document.createElement("script");
script.src = browser.runtime.getURL("dist/Vencord.js");
// documentElement because we load before body/head are ready
document.documentElement.appendChild(script);

View File

@ -1,32 +0,0 @@
{
"manifest_version": 2,
"name": "Vencord Web",
"description": "Yeee",
"version": "1.0.0",
"author": "Vendicated",
"homepage_url": "https://github.com/Vendicated/Vencord",
"background": {
"scripts": [
"background.js"
]
},
"content_scripts": [
{
"run_at": "document_start",
"matches": [
"*://*.discord.com/*"
],
"js": [
"content.js"
]
}
],
"permissions": [
"*://*.discord.com/*",
"webRequest",
"webRequestBlocking"
],
"web_accessible_resources": [
"dist/Vencord.js"
]
}

View File

@ -1,24 +0,0 @@
// ==UserScript==
// @name Vencord
// @description A Discord client mod - Web version
// @version %version%
// @author Vendicated (https://github.com/Vendicated)
// @namespace https://github.com/Vendicated/Vencord
// @supportURL https://github.com/Vendicated/Vencord
// @license GPL-3.0
// @match *://*.discord.com/*
// @grant none
// @run-at document-start
// @compatible chrome Chrome + Tampermonkey or Violentmonkey
// @compatible firefox Firefox Tampermonkey
// @compatible opera Opera + Tampermonkey or Violentmonkey
// @compatible edge Edge + Tampermonkey or Violentmonkey
// @compatible safari Safari + Tampermonkey or Violentmonkey
// ==/UserScript==
// this UserScript DOES NOT work on Firefox with Violentmonkey or Greasemonkey due to a bug that makes it impossible
// to overwrite stuff on the window on sites that use CSP. Use Tampermonkey or use a chromium based browser
// https://github.com/violentmonkey/violentmonkey/issues/997
// this is a compiled and minified version of Vencord. For the source code, visit the GitHub repo

123
build.mjs Normal file → Executable file
View File

@ -1,3 +1,122 @@
// FIXME: Delete this soon, for now it is needed so people can update
#!/usr/bin/node
import { execSync } from "child_process";
import esbuild from "esbuild";
import { readdirSync } from "fs";
import { performance } from "perf_hooks";
import("./scripts/build/build.mjs");
/**
* @type {esbuild.WatchMode|false}
*/
const watch = process.argv.includes("--watch");
// https://github.com/evanw/esbuild/issues/619#issuecomment-751995294
/**
* @type {esbuild.Plugin}
*/
const makeAllPackagesExternalPlugin = {
name: 'make-all-packages-external',
setup(build) {
let filter = /^[^.\/]|^\.[^.\/]|^\.\.[^\/]/; // Must not start with "/" or "./" or "../"
build.onResolve({ filter }, args => ({ path: args.path, external: true }));
},
};
/**
* @type {esbuild.Plugin}
*/
const globPlugins = {
name: "glob-plugins",
setup: build => {
build.onResolve({ filter: /^plugins$/ }, args => {
return {
namespace: "import-plugins",
path: args.path
};
});
build.onLoad({ filter: /^plugins$/, namespace: "import-plugins" }, () => {
const files = readdirSync("./src/plugins");
let code = "";
let obj = "";
for (let i = 0; i < files.length; i++) {
if (files[i] === "index.ts") {
continue;
}
const mod = `__pluginMod${i}`;
code += `import ${mod} from "./${files[i].replace(/.tsx?$/, "")}";\n`;
obj += `[${mod}.name]: ${mod},`;
}
code += `export default {${obj}}`;
return {
contents: code,
resolveDir: "./src/plugins"
};
});
}
};
const gitHash = execSync("git rev-parse --short HEAD", { encoding: "utf-8" }).trim();
/**
* @type {esbuild.Plugin}
*/
const gitHashPlugin = {
name: "git-hash-plugin",
setup: build => {
const filter = /^git-hash$/;
build.onResolve({ filter }, args => ({
namespace: "git-hash", path: args.path
}));
build.onLoad({ filter, namespace: "git-hash" }, () => ({
contents: `export default "${gitHash}"`
}));
}
};
await Promise.all([
esbuild.build({
logLevel: "info",
entryPoints: ["src/preload.ts"],
outfile: "dist/preload.js",
format: "cjs",
bundle: true,
platform: "node",
target: ["esnext"],
sourcemap: "linked",
plugins: [makeAllPackagesExternalPlugin],
watch
}),
esbuild.build({
logLevel: "info",
entryPoints: ["src/patcher.ts"],
outfile: "dist/patcher.js",
bundle: true,
format: "cjs",
target: ["esnext"],
external: ["electron"],
platform: "node",
sourcemap: "linked",
plugins: [makeAllPackagesExternalPlugin],
watch
}),
esbuild.build({
logLevel: "info",
entryPoints: ["src/Vencord.ts"],
outfile: "dist/renderer.js",
format: "iife",
bundle: true,
target: ["esnext"],
footer: { js: "//# sourceURL=VencordRenderer" },
globalName: "Vencord",
external: ["plugins", "git-hash"],
plugins: [
globPlugins,
gitHashPlugin
],
sourcemap: false,
watch,
minify: true,
})
]).catch(err => {
console.error("Build failed");
console.error(err.message);
});

View File

@ -1,200 +0,0 @@
# Installation Guide
Welcome to Megu's Installation Guide! In this file, you will learn about how to download, install, and uninstall Vencord!
## Sections
- [Installation Guide](#installation-guide)
- [Sections](#sections)
- [Dependencies](#dependencies)
- [Installing Vencord](#installing-vencord)
- [Updating Vencord](#updating-vencord)
- [Uninstalling Vencord](#uninstalling-vencord)
- [Manually Installing Vencord](#manually-installing-vencord)
- [On Windows](#on-windows)
- [On Linux](#on-linux)
- [On MacOS](#on-macos)
- [Manual Patching](#manual-patching)
- [Manually Uninstalling Vencord](#manually-uninstalling-vencord)
## Dependencies
- Install Git from https://git-scm.com/download
- Install Node.JS LTS from here: https://nodejs.dev/en/
## Installing Vencord
> :exclamation: If this doesn't work, see [Manually Installing Vencord](#manually-installing-vencord)
Install `pnpm`:
> :exclamation: this may need to be run as admin depending on your system, and you may need to close and reopen your terminal.
```shell
npm i -g pnpm
```
Clone Vencord:
```shell
git clone https://github.com/Vendicated/Vencord
cd Vencord
```
Install dependencies:
```shell
pnpm install --frozen-lockfile
```
Build Vencord:
```shell
pnpm build
```
Inject vencord into your client:
```shell
pnpm inject
```
Then fully close Discord from your taskbar or task manager, and restart it. Vencord should be injected - you can check this by looking for the Vencord section in Discord settings.
## Updating Vencord
If you're using Discord already, go into the `Updater` tab in settings.
Sometimes it may be neccessary to manually update if the GUI updater fails.
To pull latest changes:
```shell
git pull
```
If this fails, you likely need to reset your local changes to vencord to resolve merge errors:
> :exclamation: This command will remove any local changes you've made to vencord. Make sure you back up if you made any code changes you don't want to lose!
```shell
git reset --hard
git pull
```
and then to build the changes:
```shell
pnpm build
```
Then just refresh your client
## Uninstalling Vencord
Simply run:
```shell
pnpm uninject
```
The above command may ask you to also run:
```shell
pnpm install --frozen-lockfile
pnpm uninject
```
## Manually Installing Vencord
- [Windows](#on-windows)
- [Linux](#on-linux)
- [MacOS](#on-macos)
### On Windows
Press Win+R and enter: `%LocalAppData%` and hit enter. In this page, find the page (Discord, DiscordPTB, DiscordCanary, etc) that you want to patch.
Now follow the instructions at [Manual Patching](#manual-patching)
### On Linux
The Discord folder is usually in one of the following paths:
- /usr/share
- /usr/lib64
- /opt
- /home/$USER/.local/share
If you use flatpak, it will usually be in one of the following paths:
- /var/lib/flatpak/app/com.discordapp.Discord/current/active/files
- /home/$USER/.local/share/flatpak/app/com.discordapp.Discord/current/active/files
You will need to give flatpak access to vencord with one of the following commands:
> :exclamation: If not on stable, replace `com.discordapp.Discord` with your branch name, e.g., `com.discordapp.DiscordCanary`
> :exclamation: Replace `/path/to/vencord/` with the path to your vencord folder (NOT the dist folder)
If Discord flatpak install is in /home/:
```shell
flatpak override --user com.discordapp.Discord --filesystem="/path/to/vencord/"
```
If Discord flatpak install not in /home/:
```shell
sudo flatpak override com.discordapp.Discord --filesystem="/path/to/vencord"
```
Now follow the instructions at [Manual Patching](#manual-patching)
### On MacOS
Open finder and go to your Applications folder. Right-Click on the Discord application you want to patch, and view contents.
Go to the `Contents/Resources` folder.
Now follow the instructions at [Manual Patching](#manual-patching)
### Manual Patching
> :exclamation: If using Flatpak on linux, go to the folder that contains the `app.asar` file, and skip to where we create the `app` folder below.
> :exclamation: On Linux/MacOS, there's a chance there won't be an `app-<number>` folder, but there probably is a `resources` folder, so keep reading :)
Inside there, look for the `app-<number>` folders. If you have multiple, use the highest number. If that doesn't work, do it for the rest of the `app-<number>` folders.
Inside there, go to the `resources` folder. There should be a file called `app.asar`. If there isn't, look at a different `app-<number>` folder instead.
Make a new folder in `resources` called `app`. In here, we will make two files:
`package.json` and `index.js`
In `index.js`:
> :exclamation: Replace the path in the first line with the path to `patcher.js` in your vencord dist folder.
> On Windows, you can get this by shift-rightclicking the patcher.js file and selecting "copy as path"
```js
require("C:/Users/<your user>/path/to/vencord/dist/patcher.js");
require("../app.asar");
```
And in `package.json`:
```json
{ "name": "discord", "main": "index.js" }
```
Finally, fully close & reopen your Discord client and check to see that `Vencord` appears in settings!
### Manually Uninstalling Vencord
> :exclamation: Do not delete `app.asar` - Only delete the `app` folder we created.
Use the instructions above to find the `app` folder, and delete it. Then Close & Reopen Discord.
If you need more help, ask in the support channel in our [Discord Server](https://discord.gg/D9uwnFnqmd).

View File

@ -1,111 +0,0 @@
# Plugins Guide
Welcome to Megu's Plugin Guide! In this file, you will learn about how to write your own plugin!
You don't need to run `pnpm build` every time you make a change. Instead, use `pnpm watch` - this will auto-compile Vencord whenever you make a change. If using code patches (recommended), you will need to CTRL+R to load the changes.
## Plugin Entrypoint
> If it doesn't already exist, create a folder called `userplugins` in the `src` directory of this repo.
1. Create a folder in `src/userplugins/` with the name of your plugin. For example, `src/userplugins/epicPlugin/` - All of your plugin files will go here.
2. Create a file in that folder called `index.ts`
3. In `index.ts`, copy-paste the following template code:
```ts
import definePlugin from "../../utils/types";
export default definePlugin({
name: "Epic Plugin",
description: "This plugin is absolutely epic",
authors: [
{
id: 12345n,
name: "Your Name",
},
],
patches: [],
// Delete these two below if you are only using code patches
start() {},
stop() {},
});
```
Change the name, description, and authors to your own information.
Replace `12345n` with your user ID ending in `n` (e.g., `545581357812678656n`). If you don't want to share your Discord account, use `0n` instead!
## How Plugins Work In Vencord
Vencord uses a different way of making mods than you're used to.
Instead of monkeypatching webpack, we directly modify the code before Discord loads it.
This is _significantly_ more efficient than monkeypatching webpack, and is surprisingly easy, but it may be confusing at first.
## Making your patch
For an in-depth guide into patching code, see [CONTRIBUTING.md](../CONTRIBUTING.md)
in the `index.ts` file we made earlier, you'll see a `patches` array.
> You'll see examples of how patches are used in all the existing plugins, and it'll be easier to understand by looking at those examples, so do that first, and then return here!
> For a good example of a plugin using code patches AND runtime patching, check `src/plugins/unindent.ts`, which uses code patches to run custom runtime code.
One of the patches in the `isStaff` plugin, looks like this:
```ts
{
match: /(\w+)\.isStaff=function\(\){return\s*!1};/,
replace: "$1.isStaff=function(){return true};",
},
```
The above regex matches the string in discord that will look something like:
```js
abc.isStaff = function () {
return !1;
};
```
Remember that Discord code is minified, so there won't be any newlines, and there will only be spaces where necessary. So the source code looks something like:
```
abc.isStaff=function(){return!1;}
```
You can find these snippets by opening the devtools (`ctrl+shift+i`) and pressing `ctrl+shift+f`, searching for what you're looking to modify in there, and beautifying the file to make it more readable.
In the `match` regex in the example shown above, you'll notice at the start there is a `(\w+)`.
Anything in the brackets will be accessible in the `replace` string using `$<number>`. e.g., the first pair of brackets will be `$1`, the second will be `$2`, etc.
The replacement string we used is:
```
"$1.isStaff=function(){return true;};"
```
Which, using the above example, would replace the code with:
> **Note**
> In this example, `$1` becomes `abc`
```js
abc.isStaff = function () {
return true;
};
```
The match value _can_ be a string, rather than regex, however usually regex will be better suited, as it can work with unknown values, whereas strings must be exact matches.
Once you've made your plugin, make sure you run `pnpm test` and make sure your code is nice and clean!
If you want to publish your plugin into the Vencord repo, move your plugin from `src/userplugins` into the `src/plugins` folder and open a PR!
> **Warning**
> Make sure you've read [CONTRIBUTING.md](../CONTRIBUTING.md) before opening a PR
If you need more help, ask in the support channel in our [Discord Server](https://discord.gg/D9uwnFnqmd).

90
install.ps1 Normal file
View File

@ -0,0 +1,90 @@
# Vencord Windows Installer
$patcher = "$PWD\dist\patcher.js"
$patcher_safe = $patcher -replace '\\', '\\'
$APP_PATCH = @"
require("$patcher_safe");
require("../app.asar");
"@
$PACKAGE_JSON = @"
{
"main": "index.js",
"name": "discord"
}
"@
$branch_paths = Get-ChildItem -Directory -Path $env:LOCALAPPDATA |
Select-String -Pattern "Discord\w*" -AllMatches |
Select-String -Pattern "DiscordGames" -NotMatch # Ignore DiscordGames folder
$branches = @()
foreach ($branch in $branch_paths) {
$branch = $branch.Line.Split("\")[-1]
if ($branch -eq "Discord") {
$branch = "Discord Stable"
} else {
$branch = $branch.Replace("Discord", "Discord ")
}
$branches = $branches + $branch
}
$branch_count = $branches.Count
Write-Output "Found $branch_count Branches"
Write-Output "====================================="
Write-Output "===== Select a Branch to patch ======"
$i = 0
foreach ($branch in $branches) {
Write-Output "=== $i. $branch"
$i++
}
Write-Output "====================================="
$pos = Read-Host "Enter a number"
if ($null -eq $branches[$pos]) {
Write-Output "Invalid branch selection"
exit
}
$branch = $branches.Get($pos)
$discord_root = $branch_paths.Get($pos)
Write-Output "`nPatching $branch"
$app_folders = Get-ChildItem -Directory -Path $discord_root |
Select-String -Pattern "app-"
foreach ($folder in $app_folders)
{
$version = [regex]::match($folder, 'app-([\d\.]+)').Groups[1].Value
Write-Output "Patching Version $version"
$resources = "$folder\resources"
if (-not(Test-Path -Path "$resources")) {
Write-Error "Resources folder does not exist. Outdated version?`n"
continue
}
if (-not(Test-Path -Path "$resources\app.asar")) {
Write-Error "Failed to find app.asar in $folder`n"
continue
}
$app = "$resources\app"
if (Test-Path -Path $app) {
Write-Error "Are you already patched? App folder already exists at $resources`n"
continue
}
$null = New-Item -Path $app -ItemType Directory
$null = Tee-Object -InputObject $APP_PATCH -FilePath "$app\index.js"
$null = Tee-Object -InputObject $PACKAGE_JSON -FilePath "$app\package.json"
Write-Output "Patched $branch (version $version) successfully"
}

75
install.sh Executable file
View File

@ -0,0 +1,75 @@
#!/bin/sh
#
# Super simple installer. You should probably run this as root.
# If you are getting permission issues, this is probably why.
#
# If this doesn't work for you, or you're not on Linux, just
# - locate your Discord folder
# - inside the resources folder, create a new folder "app"
# - inside app create the files index.js and package.json.
# See the two tee commands at the end of the file for their contents
patcher="$PWD/dist/patcher.js"
discord_bin="$(which discord)"
discord_actual="$(readlink "$discord_bin")"
if [ -z "$discord_actual" ]; then
case "$(head -n1 "$discord_bin")" in
# has shebang?
\#!/*)
# Wrapper script, assume 2nd line has exec electron call and try to match asar path
path="$(tail -1 "$discord_bin" | grep -Eo "\S+/app.asar" | sed 's/${name}/discord/')"
if [ -z "$path" ]; then
echo "Unsupported Install. $discord_bin is wrapper script but last line isn't exec call?"
exit
elif [ -e "$path" ]; then
discord="$(dirname "$path")"
else
echo "Unsupported Install. $path not found"
exit 1
fi
;;
*)
echo "Unsupported Install. $discord_bin is neither symlink nor a wrapper script.";
exit 1
;;
esac
else
discord="$(dirname "$discord_actual")"
fi
resources="$discord/resources"
app="$resources/app"
app_asar="app.asar"
if [ ! -e "$resources" ]; then
if [ -e "$discord/app.asar.unpacked" ]; then
# System Electron Install
mv "$discord/app.asar" "$discord/_app.asar"
mv "$discord/app.asar.unpacked" "$discord/_app.asar.unpacked"
app="$discord/app.asar"
app_asar="_app.asar"
else
echo "Unsupported Install. $discord has no resources folder but also isn't system electron install"
exit
fi
fi
if [ -e "$app" ]; then
echo "app folder exists. Looks like your Discord is already modified."
exit
fi
mkdir "$app"
tee > "$app/index.js" << EOF
require("$patcher");
require("../$app_asar");
EOF
tee > "$app/package.json" << EOF
{
"main": "index.js",
"name": "discord"
}
EOF

View File

@ -1,51 +1,20 @@
{
"name": "vencord",
"private": "true",
"version": "1.0.0",
"description": "A Discord client mod that does things differently",
"keywords": [],
"homepage": "https://github.com/Vendicated/Vencord#readme",
"bugs": {
"url": "https://github.com/Vendicated/Vencord/issues"
},
"repository": {
"type": "git",
"url": "git+https://github.com/Vendicated/Vencord.git"
},
"license": "GPL-3.0",
"author": "Vendicated",
"directories": {
"doc": "docs"
},
"scripts": {
"build": "node scripts/build/build.mjs",
"buildWeb": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/buildWeb.mjs",
"inject": "node scripts/patcher/install.js",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
"lint:fix": "pnpm lint --fix",
"test": "pnpm lint && pnpm build && pnpm testTsc",
"testTsc": "tsc --noEmit",
"uninject": "node scripts/patcher/uninstall.js",
"watch": "node scripts/build/build.mjs --watch"
},
"dependencies": {
"console-menu": "^0.1.0",
"fflate": "^0.7.4"
},
"devDependencies": {
"@types/flux": "^3.1.11",
"@types/node": "^18.7.13",
"@types/react": "^18.0.17",
"@types/yazl": "^2.4.2",
"@typescript-eslint/parser": "^5.39.0",
"discord-types": "^1.3.26",
"esbuild": "^0.15.5",
"eslint": "^8.24.0",
"eslint-plugin-header": "^3.1.1",
"eslint-plugin-simple-import-sort": "^8.0.0",
"eslint-plugin-unused-imports": "^2.0.0",
"standalone-electron-types": "^1.0.0",
"type-fest": "^3.1.0",
"typescript": "^4.8.4"
"electron": "^20.1.0",
"esbuild": "^0.15.5"
},
"packageManager": "pnpm@7.13.4"
"dependencies": {
"@swc/wasm-web": "^1.3.4",
"discord-types": "^1.3.26",
"electron-devtools-installer": "^3.2.0",
"jsposed": "^1.0.2",
"prettier": "^2.7.1"
},
"scripts": {
"build": "node build.mjs",
"watch": "node build.mjs --watch"
}
}

1246
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1,94 +0,0 @@
#!/usr/bin/node
/*
* 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 esbuild from "esbuild";
import { commonOpts, gitHash, globPlugins, isStandalone } from "./common.mjs";
const defines = {
IS_STANDALONE: isStandalone
};
if (defines.IS_STANDALONE === "false")
// If this is a local build (not standalone), optimise
// for the specific platform we're on
defines["process.platform"] = JSON.stringify(process.platform);
const header = `
// Vencord ${gitHash}
// Standalone: ${defines.IS_STANDALONE}
// Platform: ${defines["process.platform"] || "Universal"}
`.trim();
/**
* @type {esbuild.BuildOptions}
*/
const nodeCommonOpts = {
...commonOpts,
format: "cjs",
platform: "node",
target: ["esnext"],
minify: true,
bundle: true,
external: ["electron", ...commonOpts.external],
define: defines,
banner: {
js: header
}
};
await Promise.all([
esbuild.build({
...nodeCommonOpts,
entryPoints: ["src/preload.ts"],
outfile: "dist/preload.js",
footer: { js: "//# sourceURL=VencordPreload\n//# sourceMappingURL=vencord://preload.js.map" },
sourcemap: "external",
}),
esbuild.build({
...nodeCommonOpts,
entryPoints: ["src/patcher.ts"],
outfile: "dist/patcher.js",
footer: { js: "//# sourceURL=VencordPatcher\n//# sourceMappingURL=vencord://patcher.js.map" },
sourcemap: "external",
}),
esbuild.build({
...commonOpts,
entryPoints: ["src/Vencord.ts"],
outfile: "dist/renderer.js",
format: "iife",
target: ["esnext"],
footer: { js: "//# sourceURL=VencordRenderer\n//# sourceMappingURL=vencord://renderer.js.map" },
globalName: "Vencord",
sourcemap: "external",
plugins: [
globPlugins,
...commonOpts.plugins
],
define: {
IS_WEB: "false",
IS_STANDALONE: isStandalone
}
}),
]).catch(err => {
console.error("Build failed");
console.error(err.message);
// make ci fail
if (!commonOpts.watch)
process.exitCode = 1;
});

View File

@ -1,90 +0,0 @@
#!/usr/bin/node
/*
* 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 esbuild from "esbuild";
import { zip } from "fflate";
import { readFileSync, writeFileSync } from "fs";
import { readFile } from "fs/promises";
import { join } from "path";
// wtf is this assert syntax
import PackageJSON from "../../package.json" assert { type: "json" };
import { commonOpts, fileIncludePlugin, gitHashPlugin, gitRemotePlugin, globPlugins } from "./common.mjs";
/**
* @type {esbuild.BuildOptions}
*/
const commonOptions = {
...commonOpts,
entryPoints: ["browser/Vencord.ts"],
globalName: "Vencord",
format: "iife",
external: ["plugins", "git-hash"],
plugins: [
globPlugins,
gitHashPlugin,
gitRemotePlugin,
fileIncludePlugin
],
target: ["esnext"],
define: {
IS_WEB: "true",
IS_STANDALONE: "true"
}
};
await Promise.all(
[
esbuild.build({
...commonOptions,
outfile: "dist/browser.js",
footer: { js: "//# sourceURL=VencordWeb" },
}),
esbuild.build({
...commonOptions,
outfile: "dist/Vencord.user.js",
banner: {
js: readFileSync("browser/userscript.meta.js", "utf-8").replace("%version%", PackageJSON.version)
},
footer: {
// UserScripts get wrapped in an iife, so define Vencord prop on window that returns our local
js: "Object.defineProperty(window,'Vencord',{get:()=>Vencord});"
},
})
]
);
zip({
dist: {
"Vencord.js": readFileSync("dist/browser.js")
},
...Object.fromEntries(await Promise.all(["background.js", "content.js", "manifest.json"].map(async f => [
f,
await readFile(join("browser", f))
]))),
}, {}, (err, data) => {
if (err) {
console.error(err);
process.exitCode = 1;
} else {
writeFileSync("dist/extension.zip", data);
console.info("Extension written to dist/extension.zip");
}
});

View File

@ -1,156 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { exec, execSync } from "child_process";
import esbuild from "esbuild";
import { existsSync } from "fs";
import { readdir, readFile } from "fs/promises";
import { join } from "path";
import { promisify } from "util";
export const watch = process.argv.includes("--watch");
export const isStandalone = JSON.stringify(process.argv.includes("--standalone"));
// https://github.com/evanw/esbuild/issues/619#issuecomment-751995294
/**
* @type {esbuild.Plugin}
*/
export const makeAllPackagesExternalPlugin = {
name: "make-all-packages-external",
setup(build) {
const filter = /^[^./]|^\.[^./]|^\.\.[^/]/; // Must not start with "/" or "./" or "../"
build.onResolve({ filter }, args => ({ path: args.path, external: true }));
},
};
/**
* @type {esbuild.Plugin}
*/
export const globPlugins = {
name: "glob-plugins",
setup: build => {
const filter = /^~plugins$/;
build.onResolve({ filter }, args => {
return {
namespace: "import-plugins",
path: args.path
};
});
build.onLoad({ filter, namespace: "import-plugins" }, async () => {
const pluginDirs = ["plugins", "userplugins"];
let code = "";
let plugins = "\n";
let i = 0;
for (const dir of pluginDirs) {
if (!existsSync(`./src/${dir}`)) continue;
const files = await readdir(`./src/${dir}`);
for (const file of files) {
if (file === "index.ts") {
continue;
}
const mod = `p${i}`;
code += `import ${mod} from "./${dir}/${file.replace(/.tsx?$/, "")}";\n`;
plugins += `[${mod}.name]:${mod},\n`;
i++;
}
}
code += `export default {${plugins}};`;
return {
contents: code,
resolveDir: "./src"
};
});
}
};
export const gitHash = execSync("git rev-parse --short HEAD", { encoding: "utf-8" }).trim();
/**
* @type {esbuild.Plugin}
*/
export const gitHashPlugin = {
name: "git-hash-plugin",
setup: build => {
const filter = /^~git-hash$/;
build.onResolve({ filter }, args => ({
namespace: "git-hash", path: args.path
}));
build.onLoad({ filter, namespace: "git-hash" }, () => ({
contents: `export default "${gitHash}"`
}));
}
};
/**
* @type {esbuild.Plugin}
*/
export const gitRemotePlugin = {
name: "git-remote-plugin",
setup: build => {
const filter = /^~git-remote$/;
build.onResolve({ filter }, args => ({
namespace: "git-remote", path: args.path
}));
build.onLoad({ filter, namespace: "git-remote" }, async () => {
const res = await promisify(exec)("git remote get-url origin", { encoding: "utf-8" });
const remote = res.stdout.trim()
.replace("https://github.com/", "")
.replace("git@github.com:", "")
.replace(/.git$/, "");
return { contents: `export default "${remote}"` };
});
}
};
/**
* @type {esbuild.Plugin}
*/
export const fileIncludePlugin = {
name: "file-include-plugin",
setup: build => {
const filter = /^~fileContent\/.+$/;
build.onResolve({ filter }, args => ({
namespace: "include-file",
path: args.path,
pluginData: {
path: join(args.resolveDir, args.path.slice("include-file/".length))
}
}));
build.onLoad({ filter, namespace: "include-file" }, async ({ pluginData: { path } }) => {
const [name, format] = path.split(";");
return {
contents: `export default ${JSON.stringify(await readFile(name, format ?? "utf-8"))}`
};
});
}
};
/**
* @type {esbuild.BuildOptions}
*/
export const commonOpts = {
logLevel: "info",
bundle: true,
watch,
minify: !watch,
sourcemap: watch ? "inline" : "",
legalComments: "linked",
plugins: [fileIncludePlugin, gitHashPlugin, gitRemotePlugin],
external: ["~plugins", "~git-hash", "~git-remote"]
};

View File

@ -1,341 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
const path = require("path");
const readline = require("readline");
const fs = require("fs");
const menu = require("console-menu");
const BRANCH_NAMES = [
"Discord",
"DiscordPTB",
"DiscordCanary",
"DiscordDevelopment",
"discord",
"discordptb",
"discordcanary",
"discorddevelopment",
"discord-ptb",
"discord-canary",
"discord-development",
// Flatpak
"com.discordapp.Discord",
"com.discordapp.DiscordPTB",
"com.discordapp.DiscordCanary",
"com.discordapp.DiscordDevelopment",
];
const MACOS_DISCORD_DIRS = [
"Discord.app",
"Discord PTB.app",
"Discord Canary.app",
"Discord Development.app",
];
if (process.platform === "linux" && process.env.SUDO_USER) {
process.env.HOME = fs
.readFileSync("/etc/passwd", "utf-8")
.match(new RegExp(`^${process.env.SUDO_USER}.+$`, "m"))[0]
.split(":")[5];
}
const LINUX_DISCORD_DIRS = [
"/usr/share",
"/usr/lib64",
"/opt",
`${process.env.HOME}/.local/share`,
"/var/lib/flatpak/app",
`${process.env.HOME}/.local/share/flatpak/app`,
];
const FLATPAK_NAME_MAPPING = {
DiscordCanary: "discord-canary",
DiscordPTB: "discord-ptb",
DiscordDevelopment: "discord-development",
Discord: "discord",
};
const ENTRYPOINT = path
.join(process.cwd(), "dist", "patcher.js")
.replace(/\\/g, "/");
function question(question) {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
terminal: false,
});
return new Promise(resolve => {
rl.question(question, answer => {
rl.close();
resolve(answer);
});
});
}
async function getMenuItem(installations) {
const menuItems = installations.map(info => ({
title: info.patched ? "[MODIFIED] " + info.location : info.location,
info,
}));
const result = await menu(
[
...menuItems,
{ title: "Specify custom path", info: "custom" },
{ title: "Exit without patching", exit: true }
],
{
header: "Select a Discord installation to patch:",
border: true,
helpMessage:
"Use the up/down arrow keys to select an option. " +
"Press ENTER to confirm.",
}
);
if (!result || !result.info || result.exit) {
console.log("No installation selected.");
process.exit(0);
}
if (result.info === "custom") {
const customPath = await question("Please enter the path: ");
if (!customPath || !fs.existsSync(customPath)) {
console.log("No such Path or not specifed.");
process.exit();
}
const resourceDir = path.join(customPath, "resources");
if (!fs.existsSync(path.join(resourceDir, "app.asar"))) {
console.log("Unsupported Install. resources/app.asar not found");
process.exit();
}
const appDir = path.join(resourceDir, "app");
result.info = {
branch: "unknown",
patched: fs.existsSync(appDir),
location: customPath,
versions: [{
path: appDir,
name: null
}],
arch: process.platform === "linux" ? "linux" : "win32",
isFlatpak: false,
};
}
if (result.info.patched) {
const answer = await question(
"This installation has already been modified. Overwrite? [Y/n]: "
);
if (!["y", "yes", "yeah", ""].includes(answer.toLowerCase())) {
console.log("Not patching.");
process.exit(0);
}
}
return result.info;
}
function getWindowsDirs() {
const dirs = [];
for (const dir of fs.readdirSync(process.env.LOCALAPPDATA)) {
if (!BRANCH_NAMES.includes(dir)) continue;
const location = path.join(process.env.LOCALAPPDATA, dir);
if (!fs.statSync(location).isDirectory()) continue;
const appDirs = fs
.readdirSync(location, { withFileTypes: true })
.filter(file => file.isDirectory())
.filter(file => file.name.startsWith("app-"))
.map(file => path.join(location, file.name));
const versions = [];
let patched = false;
for (const fqAppDir of appDirs) {
const resourceDir = path.join(fqAppDir, "resources");
if (!fs.existsSync(path.join(resourceDir, "app.asar"))) {
continue;
}
const appDir = path.join(resourceDir, "app");
if (fs.existsSync(appDir)) {
patched = true;
}
versions.push({
path: appDir,
name: /app-([0-9.]+)/.exec(fqAppDir)[1],
});
}
if (appDirs.length) {
dirs.push({
branch: dir,
patched,
location,
versions,
arch: "win32",
flatpak: false,
});
}
}
return dirs;
}
function getDarwinDirs() {
const dirs = [];
for (const dir of fs.readdirSync("/Applications")) {
if (!MACOS_DISCORD_DIRS.includes(dir)) continue;
const location = path.join("/Applications", dir, "Contents");
if (!fs.existsSync(location)) continue;
if (!fs.statSync(location).isDirectory()) continue;
const appDirs = fs
.readdirSync(location, { withFileTypes: true })
.filter(file => file.isDirectory())
.filter(file => file.name.startsWith("Resources"))
.map(file => path.join(location, file.name));
const versions = [];
let patched = false;
for (const resourceDir of appDirs) {
if (!fs.existsSync(path.join(resourceDir, "app.asar"))) {
continue;
}
const appDir = path.join(resourceDir, "app");
if (fs.existsSync(appDir)) {
patched = true;
}
versions.push({
path: appDir,
name: null, // MacOS installs have no version number
});
}
if (appDirs.length) {
dirs.push({
branch: dir,
patched,
location,
versions,
arch: "win32",
});
}
}
return dirs;
}
function getLinuxDirs() {
const dirs = [];
for (const dir of LINUX_DISCORD_DIRS) {
if (!fs.existsSync(dir)) continue;
for (const branch of fs.readdirSync(dir)) {
if (!BRANCH_NAMES.includes(branch)) continue;
const location = path.join(dir, branch);
if (!fs.statSync(location).isDirectory()) continue;
const isFlatpak = location.includes("/flatpak/");
let appDirs = [];
if (isFlatpak) {
const fqDir = path.join(location, "current", "active", "files");
if (!/com\.discordapp\.(\w+)\//.test(fqDir)) continue;
const branchName = /com\.discordapp\.(\w+)\//.exec(fqDir)[1];
if (!Object.keys(FLATPAK_NAME_MAPPING).includes(branchName)) {
continue;
}
const appDir = path.join(
fqDir,
FLATPAK_NAME_MAPPING[branchName]
);
if (!fs.existsSync(appDir)) continue;
if (!fs.statSync(appDir).isDirectory()) continue;
const resourceDir = path.join(appDir, "resources");
appDirs.push(resourceDir);
} else {
appDirs = fs
.readdirSync(location, { withFileTypes: true })
.filter(file => file.isDirectory())
.filter(
file =>
file.name.startsWith("app-") ||
file.name === "resources"
)
.map(file => path.join(location, file.name));
}
const versions = [];
let patched = false;
for (const resourceDir of appDirs) {
if (!fs.existsSync(path.join(resourceDir, "app.asar"))) {
continue;
}
const appDir = path.join(resourceDir, "app");
if (fs.existsSync(appDir)) {
patched = true;
}
const version = /app-([0-9.]+)/.exec(resourceDir);
versions.push({
path: appDir,
name: version && version.length > 1 ? version[1] : null,
});
}
if (appDirs.length) {
dirs.push({
branch,
patched,
location,
versions,
arch: "linux",
isFlatpak,
});
}
}
}
return dirs;
}
module.exports = {
BRANCH_NAMES,
MACOS_DISCORD_DIRS,
LINUX_DISCORD_DIRS,
FLATPAK_NAME_MAPPING,
ENTRYPOINT,
question,
getMenuItem,
getWindowsDirs,
getDarwinDirs,
getLinuxDirs,
};

View File

@ -1,130 +0,0 @@
#!/usr/bin/node
/*
* 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/>.
*/
const path = require("path");
const fs = require("fs");
const { execSync } = require("child_process");
console.log("\nVencord Installer\n");
if (!fs.existsSync(path.join(process.cwd(), "node_modules"))) {
console.log("You need to install dependencies first. Run:", "pnpm install --frozen-lockfile");
process.exit(1);
}
if (!fs.existsSync(path.join(process.cwd(), "dist", "patcher.js"))) {
console.log("You need to build the project first. Run:", "pnpm build");
process.exit(1);
}
const {
getMenuItem,
getWindowsDirs,
getDarwinDirs,
getLinuxDirs,
ENTRYPOINT,
} = require("./common");
switch (process.platform) {
case "win32":
install(getWindowsDirs());
break;
case "darwin":
install(getDarwinDirs());
break;
case "linux":
install(getLinuxDirs());
break;
default:
console.log("Unknown OS");
break;
}
async function install(installations) {
const selected = await getMenuItem(installations);
// Attempt to give flatpak perms
if (selected.isFlatpak) {
try {
const { branch } = selected;
const cwd = process.cwd();
const globalCmd = `flatpak override ${branch} --filesystem=${cwd}`;
const userCmd = `flatpak override --user ${branch} --filesystem=${cwd}`;
const cmd = selected.location.startsWith("/home")
? userCmd
: globalCmd;
execSync(cmd);
console.log("Successfully gave write perms to Discord Flatpak.");
} catch (e) {
console.log("Failed to give write perms to Discord Flatpak.");
console.log(
"Try running this script as an administrator:",
"sudo pnpm inject"
);
process.exit(1);
}
}
for (const version of selected.versions) {
const dir = version.path;
// Check if we have write perms to the install directory...
try {
fs.accessSync(selected.location, fs.constants.W_OK);
} catch (e) {
console.error("No write access to", selected.location);
console.error(
"Try running this script as an administrator:",
"sudo pnpm inject"
);
process.exit(1);
}
if (fs.existsSync(dir) && fs.lstatSync(dir).isDirectory()) {
fs.rmSync(dir, { recursive: true });
}
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(
path.join(dir, "index.js"),
`require("${ENTRYPOINT}");`
);
fs.writeFileSync(
path.join(dir, "package.json"),
JSON.stringify({
name: "discord",
main: "index.js",
})
);
const requiredFiles = ["index.js", "package.json"];
if (requiredFiles.every(f => fs.existsSync(path.join(dir, f)))) {
console.log(
"Successfully patched",
version.name
? `${selected.branch} ${version.name}`
: selected.branch
);
} else {
console.log("Failed to patch", dir);
console.log("Files in directory:", fs.readdirSync(dir));
}
}
}

View File

@ -1,78 +0,0 @@
#!/usr/bin/node
/*
* 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/>.
*/
const path = require("path");
const fs = require("fs");
console.log("\nVencord Uninstaller\n");
if (!fs.existsSync(path.join(process.cwd(), "node_modules"))) {
console.log("You need to install dependencies first. Run:", "pnpm install --frozen-lockfile");
process.exit(1);
}
const {
getMenuItem,
getWindowsDirs,
getDarwinDirs,
getLinuxDirs,
} = require("./common");
switch (process.platform) {
case "win32":
uninstall(getWindowsDirs());
break;
case "darwin":
uninstall(getDarwinDirs());
break;
case "linux":
uninstall(getLinuxDirs());
break;
default:
console.log("Unknown OS");
break;
}
async function uninstall(installations) {
const selected = await getMenuItem(installations);
for (const version of selected.versions) {
const dir = version.path;
// Check if we have write perms to the install directory...
try {
fs.accessSync(selected.location, fs.constants.W_OK);
} catch (e) {
console.error("No write access to", selected.location);
console.error(
"Try running this script as an administrator:",
"sudo pnpm uninject"
);
process.exit(1);
}
if (fs.existsSync(dir)) {
fs.rmSync(dir, { recursive: true });
}
console.log(
"Successfully unpatched",
version.name
? `${selected.branch} ${version.name}`
: selected.branch
);
}
}

View File

@ -1,24 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
process.emit = (originalEmit => function (name, data) {
if (name === "warning" && data?.name === "ExperimentalWarning")
return false;
return originalEmit.apply(process, arguments);
})(process.emit);

View File

@ -1,49 +1,32 @@
/*!
* 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/>.
*/
export * as Api from "./api";
export * as Plugins from "./plugins";
export * as Util from "./utils";
export * as QuickCss from "./utils/quickCss";
export * as Updater from "./utils/updater";
export * as Webpack from "./webpack";
export * as Api from "./api";
export * as Updater from "./utils/updater";
export * as QuickCss from "./utils/quickCss";
export * as Swc from "@swc/wasm-web";
import initSwc from "@swc/wasm-web";
import { popNotice, showNotice } from "./api/Notices";
import { PlainSettings,Settings } from "./api/settings";
import { Settings } from "./api/settings";
import { startAllPlugins } from "./plugins";
export { PlainSettings,Settings };
export { Settings };
import "./webpack/patchWebpack";
import "./utils/quickCss";
import { checkForUpdates, UpdateLogger } from "./utils/updater";
import { checkForUpdates, UpdateLogger } from './utils/updater';
import { onceReady } from "./webpack";
import { Router } from "./webpack/common";
import IpcEvents from "./utils/IpcEvents";
export let Components: any;
export let Components;
async function init() {
await initSwc(await VencordNative.ipc.invoke(IpcEvents.GET_SWC_WASM));
await onceReady;
startAllPlugins();
Components = await import("./components");
if (!IS_WEB) {
try {
const isOutdated = await checkForUpdates();
if (isOutdated && Settings.notifyAboutUpdates)
@ -60,7 +43,6 @@ async function init() {
} catch (err) {
UpdateLogger.error("Failed to check for updates", err);
}
}
}
init();

View File

@ -1,24 +1,5 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022
*
* 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 { IpcRenderer, ipcRenderer } from "electron";
import IPC_EVENTS from "./utils/IpcEvents";
import IPC_EVENTS from './utils/IpcEvents';
import { IpcRenderer, ipcRenderer } from 'electron';
function assertEventAllowed(event: string) {
if (!(event in IPC_EVENTS)) throw new Error(`Event ${event} not allowed.`);
@ -46,5 +27,14 @@ export default {
assertEventAllowed(event);
return ipcRenderer.invoke(event, ...args);
}
},
require(mod: string) {
const settings = ipcRenderer.sendSync(IPC_EVENTS.GET_SETTINGS);
try {
if (!JSON.parse(settings).unsafeRequire) throw "no";
} catch {
throw new Error("Unsafe require is not allowed. Enable it in settings and try again.");
}
return require(mod);
}
};

View File

@ -1,61 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Message } from "discord-types/general";
import type { PartialDeep } from "type-fest";
import { lazyWebpack, mergeDefaults } from "../../utils/misc";
import { filters, waitFor } from "../../webpack";
import { Argument } from "./types";
const createBotMessage = lazyWebpack(filters.byCode('username:"Clyde"'));
const MessageSender = lazyWebpack(filters.byProps(["receiveMessage"]));
let SnowflakeUtils: any;
waitFor("fromTimestamp", m => SnowflakeUtils = m);
export function generateId() {
return `-${SnowflakeUtils.fromTimestamp(Date.now())}`;
}
/**
* Send a message as Clyde
* @param {string} channelId ID of channel to send message to
* @param {Message} message Message to send
* @returns {Message}
*/
export function sendBotMessage(channelId: string, message: PartialDeep<Message>): Message {
const botMessage = createBotMessage({ channelId, content: "", embeds: [] });
MessageSender.receiveMessage(channelId, mergeDefaults(message, botMessage));
return message as Message;
}
/**
* Get the value of an option by name
* @param args Arguments array (first argument passed to execute)
* @param name Name of the argument
* @param fallbackValue Fallback value in case this option wasn't passed
* @returns Value
*/
export function findOption<T>(args: Argument[], name: string): T & {} | undefined;
export function findOption<T>(args: Argument[], name: string, fallbackValue: T): T & {};
export function findOption(args: Argument[], name: string, fallbackValue?: any) {
return (args.find(a => a.name === name)?.value || fallbackValue) as any;
}

View File

@ -1,129 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { makeCodeblock } from "../../utils/misc";
import { generateId, sendBotMessage } from "./commandHelpers";
import { ApplicationCommandInputType, ApplicationCommandType, Argument, Command, CommandContext, Option } from "./types";
export * from "./commandHelpers";
export * from "./types";
export let BUILT_IN: Command[];
export const commands = {} as Record<string, Command>;
// hack for plugins being evaluated before we can grab these from webpack
const OptPlaceholder = Symbol("OptionalMessageOption") as any as Option;
const ReqPlaceholder = Symbol("RequiredMessageOption") as any as Option;
/**
* Optional message option named "message" you can use in commands.
* Used in "tableflip" or "shrug"
* @see {@link RequiredMessageOption}
*/
export let OptionalMessageOption: Option = OptPlaceholder;
/**
* Required message option named "message" you can use in commands.
* Used in "me"
* @see {@link OptionalMessageOption}
*/
export let RequiredMessageOption: Option = ReqPlaceholder;
export const _init = function (cmds: Command[]) {
try {
BUILT_IN = cmds;
OptionalMessageOption = cmds.find(c => c.name === "shrug")!.options![0];
RequiredMessageOption = cmds.find(c => c.name === "me")!.options![0];
} catch (e) {
console.error("Failed to load CommandsApi");
}
return cmds;
} as never;
export const _handleCommand = function (cmd: Command, args: Argument[], ctx: CommandContext) {
if (!cmd.isVencordCommand)
return cmd.execute(args, ctx);
const handleError = (err: any) => {
// TODO: cancel send if cmd.inputType === BUILT_IN_TEXT
const msg = `An Error occurred while executing command "${cmd.name}"`;
const reason = err instanceof Error ? err.stack || err.message : String(err);
console.error(msg, err);
sendBotMessage(ctx.channel.id, {
content: `${msg}:\n${makeCodeblock(reason)}`,
author: {
username: "Vencord"
}
});
};
try {
const res = cmd.execute(args, ctx);
return res instanceof Promise ? res.catch(handleError) : res;
} catch (err) {
return handleError(err);
}
} as never;
function modifyOpt(opt: Option | Command) {
opt.displayName ||= opt.name;
opt.displayDescription ||= opt.description;
opt.options?.forEach((opt, i, opts) => {
// See comment above Placeholders
if (opt === OptPlaceholder) opts[i] = OptionalMessageOption;
else if (opt === ReqPlaceholder) opts[i] = RequiredMessageOption;
opt.choices?.forEach(x => x.displayName ||= x.name);
modifyOpt(opts[i]);
});
}
export function registerCommand(command: Command, plugin: string) {
if (!BUILT_IN) {
console.warn(
"[CommandsAPI]",
`Not registering ${command.name} as the CommandsAPI hasn't been initialised.`,
"Please restart to use commands"
);
return;
}
if (BUILT_IN.some(c => c.name === command.name))
throw new Error(`Command '${command.name}' already exists.`);
command.isVencordCommand = true;
command.id ??= generateId();
command.applicationId ??= "-1"; // BUILT_IN;
command.type ??= ApplicationCommandType.CHAT_INPUT;
command.inputType ??= ApplicationCommandInputType.BUILT_IN_TEXT;
command.plugin ||= plugin;
modifyOpt(command);
commands[command.name] = command;
BUILT_IN.push(command);
}
export function unregisterCommand(name: string) {
const idx = BUILT_IN.findIndex(c => c.name === name);
if (idx === -1)
return false;
BUILT_IN.splice(idx, 1);
delete commands[name];
return true;
}

View File

@ -1,103 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Channel, Guild } from "discord-types/general";
import { Promisable } from "type-fest";
export interface CommandContext {
channel: Channel;
guild?: Guild;
}
export enum ApplicationCommandOptionType {
SUB_COMMAND = 1,
SUB_COMMAND_GROUP = 2,
STRING = 3,
INTEGER = 4,
BOOLEAN = 5,
USER = 6,
CHANNEL = 7,
ROLE = 8,
MENTIONABLE = 9,
NUMBER = 10,
ATTACHMENT = 11,
}
export enum ApplicationCommandInputType {
BUILT_IN = 0,
BUILT_IN_TEXT = 1,
BUILT_IN_INTEGRATION = 2,
BOT = 3,
PLACEHOLDER = 4,
}
export interface Option {
name: string;
displayName?: string;
type: ApplicationCommandOptionType;
description: string;
displayDescription?: string;
required?: boolean;
options?: Option[];
choices?: Array<ChoicesOption>;
}
export interface ChoicesOption {
label: string;
value: string;
name: string;
displayName?: string;
}
export enum ApplicationCommandType {
CHAT_INPUT = 1,
USER = 2,
MESSAGE = 3,
}
export interface CommandReturnValue {
content: string;
/** TODO: implement */
cancel?: boolean;
}
export interface Argument {
type: ApplicationCommandOptionType;
name: string;
value: string;
focused: undefined;
}
export interface Command {
id?: string;
applicationId?: string;
type?: ApplicationCommandType;
inputType?: ApplicationCommandInputType;
plugin?: string;
isVencordCommand?: boolean;
name: string;
displayName?: string;
description: string;
displayDescription?: string;
options?: Option[];
predicate?(ctx: CommandContext): boolean;
execute(args: Argument[], ctx: CommandContext): Promisable<void | CommandReturnValue>;
}

View File

@ -1,202 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -1,279 +0,0 @@
/* eslint-disable header/header */
/*!
* idb-keyval v6.2.0
* Copyright 2016, Jake Archibald
* Copyright 2022, Vendicated
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export function promisifyRequest<T = undefined>(
request: IDBRequest<T> | IDBTransaction,
): Promise<T> {
return new Promise<T>((resolve, reject) => {
// @ts-ignore - file size hacks
request.oncomplete = request.onsuccess = () => resolve(request.result);
// @ts-ignore - file size hacks
request.onabort = request.onerror = () => reject(request.error);
});
}
export function createStore(dbName: string, storeName: string): UseStore {
const request = indexedDB.open(dbName);
request.onupgradeneeded = () => request.result.createObjectStore(storeName);
const dbp = promisifyRequest(request);
return (txMode, callback) =>
dbp.then(db =>
callback(db.transaction(storeName, txMode).objectStore(storeName)),
);
}
export type UseStore = <T>(
txMode: IDBTransactionMode,
callback: (store: IDBObjectStore) => T | PromiseLike<T>,
) => Promise<T>;
let defaultGetStoreFunc: UseStore | undefined;
function defaultGetStore() {
if (!defaultGetStoreFunc) {
defaultGetStoreFunc = createStore("VencordData", "VencordStore");
}
return defaultGetStoreFunc;
}
/**
* Get a value by its key.
*
* @param key
* @param customStore Method to get a custom store. Use with caution (see the docs).
*/
export function get<T = any>(
key: IDBValidKey,
customStore = defaultGetStore(),
): Promise<T | undefined> {
return customStore("readonly", store => promisifyRequest(store.get(key)));
}
/**
* Set a value with a key.
*
* @param key
* @param value
* @param customStore Method to get a custom store. Use with caution (see the docs).
*/
export function set(
key: IDBValidKey,
value: any,
customStore = defaultGetStore(),
): Promise<void> {
return customStore("readwrite", store => {
store.put(value, key);
return promisifyRequest(store.transaction);
});
}
/**
* Set multiple values at once. This is faster than calling set() multiple times.
* It's also atomic if one of the pairs can't be added, none will be added.
*
* @param entries Array of entries, where each entry is an array of `[key, value]`.
* @param customStore Method to get a custom store. Use with caution (see the docs).
*/
export function setMany(
entries: [IDBValidKey, any][],
customStore = defaultGetStore(),
): Promise<void> {
return customStore("readwrite", store => {
entries.forEach(entry => store.put(entry[1], entry[0]));
return promisifyRequest(store.transaction);
});
}
/**
* Get multiple values by their keys
*
* @param keys
* @param customStore Method to get a custom store. Use with caution (see the docs).
*/
export function getMany<T = any>(
keys: IDBValidKey[],
customStore = defaultGetStore(),
): Promise<T[]> {
return customStore("readonly", store =>
Promise.all(keys.map(key => promisifyRequest(store.get(key)))),
);
}
/**
* Update a value. This lets you see the old value and update it as an atomic operation.
*
* @param key
* @param updater A callback that takes the old value and returns a new value.
* @param customStore Method to get a custom store. Use with caution (see the docs).
*/
export function update<T = any>(
key: IDBValidKey,
updater: (oldValue: T | undefined) => T,
customStore = defaultGetStore(),
): Promise<void> {
return customStore(
"readwrite",
store =>
// Need to create the promise manually.
// If I try to chain promises, the transaction closes in browsers
// that use a promise polyfill (IE10/11).
new Promise((resolve, reject) => {
store.get(key).onsuccess = function () {
try {
store.put(updater(this.result), key);
resolve(promisifyRequest(store.transaction));
} catch (err) {
reject(err);
}
};
}),
);
}
/**
* Delete a particular key from the store.
*
* @param key
* @param customStore Method to get a custom store. Use with caution (see the docs).
*/
export function del(
key: IDBValidKey,
customStore = defaultGetStore(),
): Promise<void> {
return customStore("readwrite", store => {
store.delete(key);
return promisifyRequest(store.transaction);
});
}
/**
* Delete multiple keys at once.
*
* @param keys List of keys to delete.
* @param customStore Method to get a custom store. Use with caution (see the docs).
*/
export function delMany(
keys: IDBValidKey[],
customStore = defaultGetStore(),
): Promise<void> {
return customStore("readwrite", (store: IDBObjectStore) => {
keys.forEach((key: IDBValidKey) => store.delete(key));
return promisifyRequest(store.transaction);
});
}
/**
* Clear all values in the store.
*
* @param customStore Method to get a custom store. Use with caution (see the docs).
*/
export function clear(customStore = defaultGetStore()): Promise<void> {
return customStore("readwrite", store => {
store.clear();
return promisifyRequest(store.transaction);
});
}
function eachCursor(
store: IDBObjectStore,
callback: (cursor: IDBCursorWithValue) => void,
): Promise<void> {
store.openCursor().onsuccess = function () {
if (!this.result) return;
callback(this.result);
this.result.continue();
};
return promisifyRequest(store.transaction);
}
/**
* Get all keys in the store.
*
* @param customStore Method to get a custom store. Use with caution (see the docs).
*/
export function keys<KeyType extends IDBValidKey>(
customStore = defaultGetStore(),
): Promise<KeyType[]> {
return customStore("readonly", store => {
// Fast path for modern browsers
if (store.getAllKeys) {
return promisifyRequest(
store.getAllKeys() as unknown as IDBRequest<KeyType[]>,
);
}
const items: KeyType[] = [];
return eachCursor(store, cursor =>
items.push(cursor.key as KeyType),
).then(() => items);
});
}
/**
* Get all values in the store.
*
* @param customStore Method to get a custom store. Use with caution (see the docs).
*/
export function values<T = any>(customStore = defaultGetStore()): Promise<T[]> {
return customStore("readonly", store => {
// Fast path for modern browsers
if (store.getAll) {
return promisifyRequest(store.getAll() as IDBRequest<T[]>);
}
const items: T[] = [];
return eachCursor(store, cursor => items.push(cursor.value as T)).then(
() => items,
);
});
}
/**
* Get all entries in the store. Each entry is an array of `[key, value]`.
*
* @param customStore Method to get a custom store. Use with caution (see the docs).
*/
export function entries<KeyType extends IDBValidKey, ValueType = any>(
customStore = defaultGetStore(),
): Promise<[KeyType, ValueType][]> {
return customStore("readonly", store => {
// Fast path for modern browsers
// (although, hopefully we'll get a simpler path some day)
if (store.getAll && store.getAllKeys) {
return Promise.all([
promisifyRequest(
store.getAllKeys() as unknown as IDBRequest<KeyType[]>,
),
promisifyRequest(store.getAll() as IDBRequest<ValueType[]>),
]).then(([keys, values]) => keys.map((key, i) => [key, values[i]]));
}
const items: [KeyType, ValueType][] = [];
return customStore("readonly", store =>
eachCursor(store, cursor =>
items.push([cursor.key as KeyType, cursor.value]),
).then(() => items),
);
});
}

View File

@ -1,59 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export type AccessoryCallback = (props: Record<string, any>) => JSX.Element;
export type Accessory = {
callback: AccessoryCallback;
position?: number;
};
export const accessories = new Map<String, Accessory>();
export function addAccessory(
identifier: string,
callback: AccessoryCallback,
position?: number
) {
accessories.set(identifier, {
callback,
position,
});
}
export function removeAccessory(identifier: string) {
accessories.delete(identifier);
}
export function _modifyAccessories(
elements: JSX.Element[],
props: Record<string, any>
) {
for (const accessory of accessories.values()) {
elements.splice(
accessory.position != null
? accessory.position < 0
? elements.length + accessory.position
: accessory.position
: elements.length,
0,
accessory.callback(props)
);
}
return elements;
}

View File

@ -1,24 +1,5 @@
/*
* 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 type { Channel,Message } from "discord-types/general";
import Logger from "../utils/logger";
import type { Message, Channel } from 'discord-types/general';
import Logger from '../utils/logger';
const MessageEventsLogger = new Logger("MessageEvents", "#e5c890");
@ -86,7 +67,7 @@ type ClickListener = (message: Message, channel: Channel, event: MouseEvent) =>
const listeners = new Set<ClickListener>();
export function _handleClick(message: Message, channel: Channel, event: MouseEvent) {
export function _handleClick(message, channel, event) {
for (const listener of listeners) {
try {
listener(message, channel, event);

View File

@ -1,21 +1,3 @@
/*
* 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 { waitFor } from "../webpack";
let NoticesModule: any;

View File

@ -1,61 +1,2 @@
/*
* 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 * as $Commands from "./Commands";
import * as $DataStore from "./DataStore";
import * as $MessageAccessories from "./MessageAccessories";
import * as $MessageEventsAPI from "./MessageEvents";
import * as $Notices from "./Notices";
/**
* An API allowing you to listen to Message Clicks or run your own logic
* before a message is sent
*
* If your plugin uses this, you must add MessageEventsAPI to its dependencies
*/
const MessageEvents = $MessageEventsAPI;
/**
* An API allowing you to create custom notices
* (snackbars on the top, like the Update prompt)
*/
const Notices = $Notices;
/**
* An API allowing you to register custom commands
*/
const Commands = $Commands;
/**
* A wrapper around IndexedDB. This can store arbitrarily
* large data and supports a lot of datatypes (Blob, Map, ...).
* For a full list, see the mdn link below
*
* This should always be preferred over the Settings API if possible, as
* localstorage has very strict size restrictions and blocks the event loop
*
* Make sure your keys are unique (tip: prefix them with ur plugin name)
* and please clean up no longer needed entries.
*
* This is actually just idb-keyval, so if you're familiar with that, you're golden!
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm#supported_types}
*/
const DataStore = $DataStore;
/**
* An API allowing you to add custom components as message accessories
*/
const MessageAccessories = $MessageAccessories;
export { Commands,DataStore, MessageAccessories, MessageEvents, Notices };
export * as MessageEvents from "./MessageEvents";
export * as Notices from "./Notices";

View File

@ -1,32 +1,12 @@
/*
* 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 plugins from "~plugins";
import plugins from "plugins";
import IpcEvents from "../utils/IpcEvents";
import { mergeDefaults } from "../utils/misc";
import { OptionType } from "../utils/types";
import { React } from "../webpack/common";
import { mergeDefaults } from '../utils/misc';
export interface Settings {
interface Settings {
notifyAboutUpdates: boolean;
unsafeRequire: boolean;
useQuickCss: boolean;
enableReactDevtools: boolean;
plugins: {
[plugin: string]: {
enabled: boolean;
@ -37,8 +17,8 @@ export interface Settings {
const DefaultSettings: Settings = {
notifyAboutUpdates: true,
unsafeRequire: false,
useQuickCss: true,
enableReactDevtools: false,
plugins: {}
};
@ -50,6 +30,9 @@ for (const plugin in plugins) {
try {
var settings = JSON.parse(VencordNative.ipc.sendSync(IpcEvents.GET_SETTINGS)) as Settings;
for (const key in DefaultSettings) {
settings[key] ??= DefaultSettings[key];
}
mergeDefaults(settings, DefaultSettings);
} catch (err) {
console.error("Corrupt settings file. ", err);
@ -59,71 +42,33 @@ try {
type SubscriptionCallback = ((newValue: any, path: string) => void) & { _path?: string; };
const subscriptions = new Set<SubscriptionCallback>();
// Wraps the passed settings object in a Proxy to nicely handle change listeners and default values
function makeProxy(settings: Settings, root = settings, path = ""): Settings {
return new Proxy(settings, {
get(target, p: string) {
const v = target[p];
// using "in" is important in the following cases to properly handle falsy or nullish values
if (!(p in target)) {
// Since the property is not set, check if this is a plugin's setting and if so, try to resolve
// the default value.
if (path.startsWith("plugins.")) {
const plugin = path.slice("plugins.".length);
if (plugin in plugins) {
const setting = plugins[plugin].options?.[p];
if (!setting) return v;
if ("default" in setting)
// normal setting with a default value
return setting.default;
if (setting.type === OptionType.SELECT)
return setting.options.find(o => o.default)?.value;
}
}
return v;
}
// Recursively proxy Objects with the updated property path
if (typeof v === "object" && !Array.isArray(v) && v !== null)
if (typeof v === "object" && !Array.isArray(v))
return makeProxy(v, root, `${path}${path && "."}${p}`);
// primitive or similar, no need to proxy further
return v;
},
set(target, p: string, v) {
// avoid unnecessary updates to React Components and other listeners
if (target[p] === v) return true;
target[p] = v;
// Call any listeners that are listening to a setting of this path
const setPath = `${path}${path && "."}${p}`;
for (const subscription of subscriptions) {
if (!subscription._path || subscription._path === setPath) {
subscription(v, setPath);
}
}
// And don't forget to persist the settings!
VencordNative.ipc.invoke(IpcEvents.SET_SETTINGS, JSON.stringify(root, null, 4));
return true;
}
});
}
/**
* Same as {@link Settings} but unproxied. You should treat this as readonly,
* as modifying properties on this will not save to disk or call settings
* listeners.
* WARNING: default values specified in plugin.options will not be ensured here. In other words,
* settings for which you specified a default value may be uninitialised. If you need proper
* handling for default values, use {@link Settings}
*/
export const PlainSettings = settings;
/**
* A smart settings object. Altering props automagically saves
* the updated settings to disk.
* This recursively proxies objects. If you need the object non proxied, use {@link PlainSettings}
*/
export const Settings = makeProxy(settings);

View File

@ -1,29 +1,10 @@
/*
* 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 Logger from "../utils/logger";
import { Margins, React } from "../webpack/common";
import { Card, React } from "../webpack/common";
import { ErrorCard } from "./ErrorCard";
interface Props {
fallback?: React.ComponentType<React.PropsWithChildren<{ error: any; message: string; stack: string; }>>;
fallback?: React.ComponentType<React.PropsWithChildren<{ error: any; }>>;
onError?(error: Error, errorInfo: React.ErrorInfo): void;
message?: string;
}
const color = "#e78284";
@ -34,7 +15,7 @@ const NO_ERROR = {};
export default class ErrorBoundary extends React.Component<React.PropsWithChildren<Props>> {
static wrap<T = any>(Component: React.ComponentType<T>): (props: T) => React.ReactElement {
return props => (
return (props) => (
<ErrorBoundary>
<Component {...props as any/* I hate react typings ??? */} />
</ErrorBoundary>
@ -43,23 +24,14 @@ export default class ErrorBoundary extends React.Component<React.PropsWithChildr
state = {
error: NO_ERROR as any,
stack: "",
message: ""
};
static getDerivedStateFromError(error: any) {
let stack = error?.stack ?? "";
let message = error?.message || String(error);
if (error instanceof Error && stack) {
const eolIdx = stack.indexOf("\n");
if (eolIdx !== -1) {
message = stack.slice(0, eolIdx);
stack = stack.slice(eolIdx + 1).replace(/https:\/\/\S+\/assets\//g, "");
}
}
return { error, stack, message };
return {
error: error?.stack?.replace(/https:\/\/\S+\/assets\//g, "") || error?.message || String(error)
};
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
@ -74,24 +46,22 @@ export default class ErrorBoundary extends React.Component<React.PropsWithChildr
if (this.props.fallback)
return <this.props.fallback
children={this.props.children}
{...this.state}
error={this.state.error}
/>;
const msg = this.props.message || "An error occurred while rendering this Component. More info can be found below and in your console.";
return (
<ErrorCard style={{
overflow: "hidden",
}}>
<h1>Oh no!</h1>
<p>{msg}</p>
<p>
An error occurred while rendering this Component. More info can be found below
and in your console.
</p>
<code>
{this.state.message}
{!!this.state.stack && (
<pre className={Margins.marginTop8}>
{this.state.stack}
<pre>
{this.state.error}
</pre>
)}
</code>
</ErrorCard>
);

View File

@ -1,21 +1,3 @@
/*
* 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 { Card } from "../webpack/common";
interface Props {

View File

@ -1,28 +1,11 @@
/*
* 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 type { React } from "../webpack/common";
import { PropsWithChildren } from "react";
import type { React } from '../webpack/common';
export function Flex(props: React.PropsWithChildren<{
flexDirection?: React.CSSProperties["flexDirection"];
style?: React.CSSProperties;
className?: string;
} & React.HTMLProps<HTMLDivElement>>) {
}>) {
props.style ??= {};
props.style.flexDirection ||= props.flexDirection;
props.style.gap ??= "1em";

View File

@ -1,21 +1,3 @@
/*
* 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 { React } from "../webpack/common";
interface Props {

View File

@ -1,42 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import monacoHtml from "~fileContent/monacoWin.html";
import { IpcEvents } from "../utils";
import { debounce } from "../utils/debounce";
import { Queue } from "../utils/Queue";
import { find } from "../webpack/webpack";
const queue = new Queue();
const setCss = debounce((css: string) => {
queue.add(() => VencordNative.ipc.invoke(IpcEvents.SET_QUICK_CSS, css));
});
export async function launchMonacoEditor() {
const win = open("about:blank", void 0, "popup,width=1000,height=1000")!;
win.setCss = setCss;
win.getCurrentCss = () => VencordNative.ipc.invoke(IpcEvents.GET_QUICK_CSS);
win.getTheme = () => find(m => m.ProtoClass?.typeName.endsWith("PreloadedUserSettings"))
.getCurrentValue().appearance.theme === 1
? "vs-dark"
: "vs-light";
win.document.write(monacoHtml);
}

View File

@ -1,225 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Forms } from "@components";
import { User } from "discord-types/general";
import { Constructor } from "type-fest";
import { generateId } from "../../api/Commands";
import { useSettings } from "../../api/settings";
import { lazyWebpack, proxyLazy } from "../../utils";
import { ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize } from "../../utils/modal";
import { OptionType, Plugin } from "../../utils/types";
import { filters } from "../../webpack";
import { Button, FluxDispatcher, React, Text, Tooltip, UserStore, UserUtils } from "../../webpack/common";
import ErrorBoundary from "../ErrorBoundary";
import { Flex } from "../Flex";
import {
SettingBooleanComponent,
SettingInputComponent,
SettingNumericComponent,
SettingSelectComponent,
SettingSliderComponent
} from "./components";
const UserSummaryItem = lazyWebpack(filters.byCode("defaultRenderUser", "showDefaultAvatarsForNullUsers"));
const AvatarStyles = lazyWebpack(filters.byProps(["moreUsers", "emptyUser", "avatarContainer", "clickableAvatar"]));
const UserRecord: Constructor<Partial<User>> = proxyLazy(() => UserStore.getCurrentUser().constructor) as any;
interface PluginModalProps extends ModalProps {
plugin: Plugin;
onRestartNeeded(): void;
}
/** To stop discord making unwanted requests... */
function makeDummyUser(user: { name: string, id: BigInt; }) {
const newUser = new UserRecord({
username: user.name,
id: generateId(),
bot: true,
});
FluxDispatcher.dispatch({
type: "USER_UPDATE",
user: newUser,
});
return newUser;
}
export default function PluginModal({ plugin, onRestartNeeded, onClose, transitionState }: PluginModalProps) {
const [authors, setAuthors] = React.useState<Partial<User>[]>([]);
const pluginSettings = useSettings().plugins[plugin.name];
const [tempSettings, setTempSettings] = React.useState<Record<string, any>>({});
const [errors, setErrors] = React.useState<Record<string, boolean>>({});
const canSubmit = () => Object.values(errors).every(e => !e);
React.useEffect(() => {
(async () => {
for (const user of plugin.authors.slice(0, 6)) {
const author = user.id ? await UserUtils.fetchUser(`${user.id}`).catch(() => null) : makeDummyUser(user);
setAuthors(a => [...a, author || makeDummyUser(user)]);
}
})();
}, []);
function saveAndClose() {
if (!plugin.options) {
onClose();
return;
}
let restartNeeded = false;
for (const [key, value] of Object.entries(tempSettings)) {
const option = plugin.options[key];
pluginSettings[key] = value;
option?.onChange?.(value);
if (option?.restartNeeded) restartNeeded = true;
}
if (restartNeeded) onRestartNeeded();
onClose();
}
function renderSettings() {
if (!pluginSettings || !plugin.options) {
return <Forms.FormText>There are no settings for this plugin.</Forms.FormText>;
}
const options: JSX.Element[] = [];
for (const [key, setting] of Object.entries(plugin.options)) {
function onChange(newValue) {
setTempSettings(s => ({ ...s, [key]: newValue }));
}
function onError(hasError: boolean) {
setErrors(e => ({ ...e, [key]: hasError }));
}
const props = { onChange, pluginSettings, id: key, onError };
switch (setting.type) {
case OptionType.SELECT: {
options.push(<SettingSelectComponent key={key} option={setting} {...props} />);
break;
}
case OptionType.STRING: {
options.push(<SettingInputComponent key={key} option={setting} {...props} />);
break;
}
case OptionType.NUMBER:
case OptionType.BIGINT: {
options.push(<SettingNumericComponent key={key} option={setting} {...props} />);
break;
}
case OptionType.BOOLEAN: {
options.push(<SettingBooleanComponent key={key} option={setting} {...props} />);
break;
}
case OptionType.SLIDER: {
options.push(<SettingSliderComponent key={key} option={setting} {...props} />);
break;
}
}
}
return <Flex flexDirection="column" style={{ gap: 12 }}>{options}</Flex>;
}
function renderMoreUsers(_label: string, count: number) {
const sliceCount = plugin.authors.length - count;
const sliceStart = plugin.authors.length - sliceCount;
const sliceEnd = sliceStart + plugin.authors.length - count;
return (
<Tooltip text={plugin.authors.slice(sliceStart, sliceEnd).map(u => u.name).join(", ")}>
{({ onMouseEnter, onMouseLeave }) => (
<div
className={AvatarStyles.moreUsers}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
+{sliceCount}
</div>
)}
</Tooltip>
);
}
return (
<ModalRoot transitionState={transitionState} size={ModalSize.MEDIUM}>
<ModalHeader>
<Text variant="heading-md/bold">{plugin.name}</Text>
</ModalHeader>
<ModalContent style={{ marginBottom: 8, marginTop: 8 }}>
<Forms.FormSection>
<Forms.FormTitle tag="h3">About {plugin.name}</Forms.FormTitle>
<Forms.FormText>{plugin.description}</Forms.FormText>
<div style={{ marginTop: 8, marginBottom: 8, width: "fit-content" }}>
<UserSummaryItem
users={authors}
count={plugin.authors.length}
guildId={undefined}
renderIcon={false}
max={6}
showDefaultAvatarsForNullUsers
showUserPopout
renderMoreUsers={renderMoreUsers}
/>
</div>
</Forms.FormSection>
{!!plugin.settingsAboutComponent && (
<div style={{ marginBottom: 8 }}>
<Forms.FormSection>
<ErrorBoundary message="An error occurred while rendering this plugin's custom InfoComponent">
<plugin.settingsAboutComponent />
</ErrorBoundary>
</Forms.FormSection>
</div>
)}
<Forms.FormSection>
<Forms.FormTitle tag="h3">Settings</Forms.FormTitle>
{renderSettings()}
</Forms.FormSection>
</ModalContent>
<ModalFooter>
<Flex>
<Button
onClick={onClose}
size={Button.Sizes.SMALL}
color={Button.Colors.RED}
>
Exit Without Saving
</Button>
<Tooltip text="You must fix all errors before saving" shouldShow={!canSubmit()}>
{({ onMouseEnter, onMouseLeave }) => (
<Button
size={Button.Sizes.SMALL}
color={Button.Colors.BRAND}
onClick={saveAndClose}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
disabled={!canSubmit()}
>
Save & Exit
</Button>
)}
</Tooltip>
</Flex>
</ModalFooter>
</ModalRoot>
);
}

View File

@ -1,69 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Forms } from "@components";
import { PluginOptionBoolean } from "../../../utils/types";
import { React, Select } from "../../../webpack/common";
import { ISettingElementProps } from ".";
export function SettingBooleanComponent({ option, pluginSettings, id, onChange, onError }: ISettingElementProps<PluginOptionBoolean>) {
const def = pluginSettings[id] ?? option.default;
const [state, setState] = React.useState(def ?? false);
const [error, setError] = React.useState<string | null>(null);
React.useEffect(() => {
onError(error !== null);
}, [error]);
const options = [
{ label: "Enabled", value: true, default: def === true },
{ label: "Disabled", value: false, default: typeof def === "undefined" || def === false },
];
function handleChange(newValue: boolean): void {
const isValid = (option.isValid && option.isValid(newValue)) ?? true;
if (typeof isValid === "string") setError(isValid);
else if (!isValid) setError("Invalid input provided.");
else {
setError(null);
setState(newValue);
onChange(newValue);
}
}
return (
<Forms.FormSection>
<Forms.FormTitle>{option.description}</Forms.FormTitle>
<Select
isDisabled={option.disabled?.() ?? false}
options={options}
placeholder={option.placeholder ?? "Select an option"}
maxVisibleItems={5}
closeOnSelect={true}
select={handleChange}
isSelected={v => v === state}
serialize={v => String(v)}
{...option.componentProps}
/>
{error && <Forms.FormText style={{ color: "var(--text-danger)" }}>{error}</Forms.FormText>}
</Forms.FormSection>
);
}

View File

@ -1,68 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Forms } from "@components";
import { OptionType, PluginOptionNumber } from "../../../utils/types";
import { React, TextInput } from "../../../webpack/common";
import { ISettingElementProps } from ".";
const MAX_SAFE_NUMBER = BigInt(Number.MAX_SAFE_INTEGER);
export function SettingNumericComponent({ option, pluginSettings, id, onChange, onError }: ISettingElementProps<PluginOptionNumber>) {
function serialize(value: any) {
if (option.type === OptionType.BIGINT) return BigInt(value);
return Number(value);
}
const [state, setState] = React.useState<any>(`${pluginSettings[id] ?? option.default ?? 0}`);
const [error, setError] = React.useState<string | null>(null);
React.useEffect(() => {
onError(error !== null);
}, [error]);
function handleChange(newValue) {
const isValid = (option.isValid && option.isValid(newValue)) ?? true;
if (typeof isValid === "string") setError(isValid);
else if (!isValid) setError("Invalid input provided.");
else if (option.type === OptionType.NUMBER && BigInt(newValue) >= MAX_SAFE_NUMBER) {
setState(`${Number.MAX_SAFE_INTEGER}`);
onChange(serialize(newValue));
} else {
setState(newValue);
onChange(serialize(newValue));
}
}
return (
<Forms.FormSection>
<Forms.FormTitle>{option.description}</Forms.FormTitle>
<TextInput
type="number"
pattern="-?[0-9]+"
value={state}
onChange={handleChange}
placeholder={option.placeholder ?? "Enter a number"}
disabled={option.disabled?.() ?? false}
{...option.componentProps}
/>
{error && <Forms.FormText style={{ color: "var(--text-danger)" }}>{error}</Forms.FormText>}
</Forms.FormSection>
);
}

View File

@ -1,63 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { FormSection, FormText, FormTitle } from "@components/Forms";
import Select from "@components/Select";
import { PluginOptionSelect } from "../../../utils/types";
import { React } from "../../../webpack/common";
import { ISettingElementProps } from ".";
export function SettingSelectComponent({ option, pluginSettings, onChange, onError, id }: ISettingElementProps<PluginOptionSelect>) {
const def = pluginSettings[id] ?? option.options?.find(o => o.default)?.value;
const [state, setState] = React.useState<any>(def ?? null);
const [error, setError] = React.useState<string | null>(null);
React.useEffect(() => {
onError(error !== null);
}, [error]);
function handleChange(newValue) {
const isValid = (option.isValid && option.isValid(newValue)) ?? true;
if (typeof isValid === "string") setError(isValid);
else if (!isValid) setError("Invalid input provided.");
else {
setState(newValue);
onChange(newValue);
}
}
return (
<FormSection>
<FormTitle>{option.description}</FormTitle>
<Select
isDisabled={option.disabled?.() ?? false}
options={option.options}
placeholder={option.placeholder ?? "Select an option"}
maxVisibleItems={5}
closeOnSelect={true}
select={handleChange}
isSelected={v => v === state}
serialize={v => String(v)}
{...option.componentProps}
/>
{error && <FormText style={{ color: "var(--text-danger)" }}>{error}</FormText>}
</FormSection>
);
}

View File

@ -1,69 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Forms } from "@components";
import { PluginOptionSlider } from "../../../utils/types";
import { React, Slider } from "../../../webpack/common";
import { ISettingElementProps } from ".";
export function makeRange(start: number, end: number, step = 1) {
const ranges: number[] = [];
for (let value = start; value <= end; value += step) {
ranges.push(Math.round(value * 100) / 100);
}
return ranges;
}
export function SettingSliderComponent({ option, pluginSettings, id, onChange, onError }: ISettingElementProps<PluginOptionSlider>) {
const def = pluginSettings[id] ?? option.default;
const [error, setError] = React.useState<string | null>(null);
React.useEffect(() => {
onError(error !== null);
}, [error]);
function handleChange(newValue: number): void {
const isValid = (option.isValid && option.isValid(newValue)) ?? true;
if (typeof isValid === "string") setError(isValid);
else if (!isValid) setError("Invalid input provided.");
else {
setError(null);
onChange(newValue);
}
}
return (
<Forms.FormSection>
<Forms.FormTitle>{option.description}</Forms.FormTitle>
<Slider
disabled={option.disabled?.() ?? false}
markers={option.markers}
minValue={option.markers[0]}
maxValue={option.markers[option.markers.length - 1]}
initialValue={def}
onValueChange={handleChange}
onValueRender={(v: number) => String(v.toFixed(2))}
stickToMarkers={option.stickToMarkers ?? true}
{...option.componentProps}
/>
</Forms.FormSection>
);
}

View File

@ -1,57 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Forms } from "@components";
import { PluginOptionString } from "../../../utils/types";
import { React, TextInput } from "../../../webpack/common";
import { ISettingElementProps } from ".";
export function SettingInputComponent({ option, pluginSettings, id, onChange, onError }: ISettingElementProps<PluginOptionString>) {
const [state, setState] = React.useState(pluginSettings[id] ?? option.default ?? null);
const [error, setError] = React.useState<string | null>(null);
React.useEffect(() => {
onError(error !== null);
}, [error]);
function handleChange(newValue) {
const isValid = (option.isValid && option.isValid(newValue)) ?? true;
if (typeof isValid === "string") setError(isValid);
else if (!isValid) setError("Invalid input provided.");
else {
setState(newValue);
onChange(newValue);
}
}
return (
<Forms.FormSection>
<Forms.FormTitle>{option.description}</Forms.FormTitle>
<TextInput
type="text"
value={state}
onChange={handleChange}
placeholder={option.placeholder ?? "Enter a value"}
disabled={option.disabled?.() ?? false}
{...option.componentProps}
/>
{error && <Forms.FormText style={{ color: "var(--text-danger)" }}>{error}</Forms.FormText>}
</Forms.FormSection>
);
}

View File

@ -1,36 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { PluginOptionBase } from "../../../utils/types";
export interface ISettingElementProps<T extends PluginOptionBase> {
option: T;
onChange(newValue: any): void;
pluginSettings: {
[setting: string]: any;
enabled: boolean;
};
id: string;
onError(hasError: boolean): void;
}
export * from "./SettingBooleanComponent";
export * from "./SettingNumericComponent";
export * from "./SettingSelectComponent";
export * from "./SettingSliderComponent";
export * from "./SettingTextComponent";

View File

@ -1,323 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { FormDivider, FormSection, FormText, FormTitle } from "@components/Forms";
import Plugins from "~plugins";
import { showNotice } from "../../api/Notices";
import { Settings, useSettings } from "../../api/settings";
import { startDependenciesRecursive, startPlugin, stopPlugin } from "../../plugins";
import { Logger, Modals } from "../../utils";
import { ChangeList } from "../../utils/ChangeList";
import { classes, lazyWebpack } from "../../utils/misc";
import { Plugin } from "../../utils/types";
import { filters } from "../../webpack";
import { Alerts, Button, Margins, Parser, React, Switch, Text, TextInput, Toasts, Tooltip } from "../../webpack/common";
import ErrorBoundary from "../ErrorBoundary";
import { ErrorCard } from "../ErrorCard";
import { Flex } from "../Flex";
import PluginModal from "./PluginModal";
import * as styles from "./styles";
const logger = new Logger("PluginSettings", "#a6d189");
const Select = lazyWebpack(filters.byCode("optionClassName", "popoutPosition", "autoFocus", "maxVisibleItems"));
const InputStyles = lazyWebpack(filters.byProps(["inputDefault", "inputWrapper"]));
const CogWheel = lazyWebpack(filters.byCode("18.564C15.797 19.099 14.932 19.498 14 19.738V22H10V19.738C9.069"));
const InfoIcon = lazyWebpack(filters.byCode("4.4408921e-16 C4.4771525,-1.77635684e-15 4.4408921e-16"));
function showErrorToast(message: string) {
Toasts.show({
message,
type: Toasts.Type.FAILURE,
id: Toasts.genId(),
options: {
position: Toasts.Position.BOTTOM
}
});
}
interface ReloadRequiredCardProps extends React.HTMLProps<HTMLDivElement> {
plugins: string[];
}
function ReloadRequiredCard({ plugins, ...props }: ReloadRequiredCardProps) {
if (plugins.length === 0) return null;
const pluginPrefix = plugins.length === 1 ? "The plugin" : "The following plugins require a reload to apply changes:";
const pluginSuffix = plugins.length === 1 ? " requires a reload to apply changes." : ".";
return (
<ErrorCard {...props} style={{ padding: "1em", display: "grid", gridTemplateColumns: "1fr auto", gap: 8, ...props.style }}>
<span style={{ margin: "auto 0" }}>
{pluginPrefix} <code>{plugins.join(", ")}</code>{pluginSuffix}
</span>
<Button look={Button.Looks.INVERTED} onClick={() => location.reload()}>Reload</Button>
</ErrorCard>
);
}
interface PluginCardProps extends React.HTMLProps<HTMLDivElement> {
plugin: Plugin;
disabled: boolean;
onRestartNeeded(name: string): void;
}
function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLeave }: PluginCardProps) {
const settings = useSettings();
const pluginSettings = settings.plugins[plugin.name];
const [iconHover, setIconHover] = React.useState(false);
function isEnabled() {
return pluginSettings?.enabled || plugin.started;
}
function openModal() {
Modals.openModalLazy(async () => {
return modalProps => {
return <PluginModal {...modalProps} plugin={plugin} onRestartNeeded={() => onRestartNeeded(plugin.name)} />;
};
});
}
function toggleEnabled() {
const wasEnabled = isEnabled();
// If we're enabling a plugin, make sure all deps are enabled recursively.
if (!wasEnabled) {
const { restartNeeded, failures } = startDependenciesRecursive(plugin);
if (failures.length) {
logger.error(`Failed to start dependencies for ${plugin.name}: ${failures.join(", ")}`);
showNotice("Failed to start dependencies: " + failures.join(", "), "Close", () => null);
return;
} else if (restartNeeded) {
// If any dependencies have patches, don't start the plugin yet.
pluginSettings.enabled = true;
onRestartNeeded(plugin.name);
return;
}
}
// if the plugin has patches, dont use stopPlugin/startPlugin. Wait for restart to apply changes.
if (plugin.patches) {
pluginSettings.enabled = !wasEnabled;
onRestartNeeded(plugin.name);
return;
}
// If the plugin is enabled, but hasn't been started, then we can just toggle it off.
if (wasEnabled && !plugin.started) {
pluginSettings.enabled = !wasEnabled;
return;
}
const result = wasEnabled ? stopPlugin(plugin) : startPlugin(plugin);
const action = wasEnabled ? "stop" : "start";
if (!result) {
logger.error(`Failed to ${action} plugin ${plugin.name}`);
showErrorToast(`Failed to ${action} plugin: ${plugin.name}`);
return;
}
pluginSettings.enabled = !wasEnabled;
}
return (
<Flex style={styles.PluginsGridItem} flexDirection="column" onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
<Switch
onChange={toggleEnabled}
disabled={disabled}
value={isEnabled()}
note={<Text variant="text-md/normal" style={{ height: 40, overflow: "hidden" }}>{plugin.description}</Text>}
hideBorder={true}
>
<Flex style={{ marginTop: "auto", width: "100%", height: "100%", alignItems: "center" }}>
<Text variant="text-md/bold" style={{ flexGrow: "1" }}>{plugin.name}</Text>
<button role="switch" onClick={() => openModal()} style={styles.SettingsIcon} className="button-12Fmur">
{plugin.options
? <CogWheel
style={{ color: iconHover ? "" : "var(--text-muted)" }}
onMouseEnter={() => setIconHover(true)}
onMouseLeave={() => setIconHover(false)}
/>
: <InfoIcon
width="24" height="24"
style={{ color: iconHover ? "" : "var(--text-muted)" }}
onMouseEnter={() => setIconHover(true)}
onMouseLeave={() => setIconHover(false)}
/>}
</button>
</Flex>
</Switch>
</Flex>
);
}
export default ErrorBoundary.wrap(function Settings() {
const settings = useSettings();
const changes = React.useMemo(() => new ChangeList<string>(), []);
React.useEffect(() => {
return () => void (changes.hasChanges && Alerts.show({
title: "Restart required",
body: (
<>
<p>The following plugins require a restart:</p>
<div>{changes.map((s, i) => (
<>
{i > 0 && ", "}
{Parser.parse("`" + s + "`")}
</>
))}</div>
</>
),
confirmText: "Restart now",
cancelText: "Later!",
onConfirm: () => location.reload()
}));
}, []);
const depMap = React.useMemo(() => {
const o = {} as Record<string, string[]>;
for (const plugin in Plugins) {
const deps = Plugins[plugin].dependencies;
if (deps) {
for (const dep of deps) {
o[dep] ??= [];
o[dep].push(plugin);
}
}
}
return o;
}, []);
function hasDependents(plugin: Plugin) {
const enabledDependants = depMap[plugin.name]?.filter(d => settings.plugins[d].enabled);
return !!enabledDependants?.length;
}
const sortedPlugins = React.useMemo(() => Object.values(Plugins)
.sort((a, b) => a.name.localeCompare(b.name)), []);
const [searchValue, setSearchValue] = React.useState({ value: "", status: "all" });
const onSearch = (query: string) => setSearchValue(prev => ({ ...prev, value: query }));
const onStatusChange = (status: string) => setSearchValue(prev => ({ ...prev, status }));
const pluginFilter = (plugin: typeof Plugins[keyof typeof Plugins]) => {
const showEnabled = searchValue.status === "enabled" || searchValue.status === "all";
const showDisabled = searchValue.status === "disabled" || searchValue.status === "all";
const enabled = settings.plugins[plugin.name]?.enabled || plugin.started;
return (
((showEnabled && enabled) || (showDisabled && !enabled)) &&
(
plugin.name.toLowerCase().includes(searchValue.value.toLowerCase()) ||
plugin.description.toLowerCase().includes(searchValue.value.toLowerCase())
)
);
};
return (
<FormSection tag="h1" title="Vencord">
<FormTitle tag="h5" className={classes(Margins.marginTop20, Margins.marginBottom8)}>
Plugins
</FormTitle>
<ReloadRequiredCard plugins={[...changes.getChanges()]} style={{ marginBottom: 16 }} />
<div style={styles.FiltersBar}>
<TextInput value={searchValue.value} placeholder={"Search for a plugin..."} onChange={onSearch} style={{ marginBottom: 24 }} />
<div className={InputStyles.inputWrapper}>
<Select
className={InputStyles.inputDefault}
options={[
{ label: "Show All", value: "all", default: true },
{ label: "Show Enabled", value: "enabled" },
{ label: "Show Disabled", value: "disabled" }
]}
serialize={v => String(v)}
select={onStatusChange}
isSelected={v => v === searchValue.status}
closeOnSelect={true}
/>
</div>
</div>
<div style={styles.PluginsGrid}>
{sortedPlugins?.length ? sortedPlugins
.filter(a => !a.required && !dependencyCheck(a.name, depMap).length && pluginFilter(a))
.map(plugin => {
const enabledDependants = depMap[plugin.name]?.filter(d => settings.plugins[d].enabled);
const dependency = enabledDependants?.length;
return <PluginCard
onRestartNeeded={name => changes.add(name)}
disabled={plugin.required || !!dependency}
plugin={plugin}
/>;
})
: <Text variant="text-md/normal">No plugins meet search criteria.</Text>
}
</div>
<FormDivider />
<FormTitle tag="h5" className={classes(Margins.marginTop20, Margins.marginBottom8)}>
Required Plugins
</FormTitle>
<div style={styles.PluginsGrid}>
{sortedPlugins?.length ? sortedPlugins
.filter(a => a.required || dependencyCheck(a.name, depMap).length && pluginFilter(a))
.map(plugin => {
const enabledDependants = depMap[plugin.name]?.filter(d => settings.plugins[d].enabled);
const dependency = enabledDependants?.length;
const tooltipText = plugin.required
? "This plugin is required for Vencord to function."
: makeDependencyList(dependencyCheck(plugin.name, depMap));
return <Tooltip text={tooltipText}>
{({ onMouseLeave, onMouseEnter }) => (
<PluginCard
onMouseLeave={onMouseLeave}
onMouseEnter={onMouseEnter}
onRestartNeeded={name => changes.add(name)}
disabled={plugin.required || !!dependency}
plugin={plugin}
/>
)}
</Tooltip>;
})
: <Text variant="text-md/normal">No plugins meet search criteria.</Text>
}
</div>
</FormSection>
);
});
function makeDependencyList(deps: string[]) {
return (
<React.Fragment>
<FormText>This plugin is required by:</FormText>
{deps.map((dep: string) => <FormText style={{ margin: "0 auto" }}>{dep}</FormText>)}
</React.Fragment>
);
}
function dependencyCheck(pluginName: string, depMap: Record<string, string[]>): string[] {
return depMap[pluginName]?.filter(d => Settings.plugins[d].enabled) || [];
}

View File

@ -1,50 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export const PluginsGrid: React.CSSProperties = {
marginTop: 16,
display: "grid",
gridGap: 16,
gridTemplateColumns: "repeat(auto-fill, minmax(250px, 1fr))",
};
export const PluginsGridItem: React.CSSProperties = {
backgroundColor: "var(--background-modifier-selected)",
color: "var(--interactive-active)",
borderRadius: 3,
cursor: "pointer",
display: "block",
height: "min-content",
padding: 10,
width: "100%",
};
export const FiltersBar: React.CSSProperties = {
gap: 10,
height: 40,
gridTemplateColumns: "1fr 150px",
display: "grid"
};
export const SettingsIcon: React.CSSProperties = {
height: "24px",
width: "24px",
padding: "0",
background: "transparent",
marginRight: 8
};

View File

@ -1,36 +1,30 @@
/*
* 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 { FormDivider, FormSection, FormText, FormTitle } from "@components/Forms";
import { classes, humanFriendlyJoin, useAwaiter } from "../utils/misc";
import Plugins from 'plugins';
import { useSettings } from "../api/settings";
import { ChangeList } from "../utils/ChangeList";
import IpcEvents from "../utils/IpcEvents";
import { useAwaiter } from "../utils/misc";
import { Alerts, Button, Margins, Parser, React, Switch } from "../webpack/common";
import { Button, Switch, Forms, React, Margins, Toasts, Alerts, Parser } from "../webpack/common";
import ErrorBoundary from "./ErrorBoundary";
import { Flex } from "./Flex";
import { launchMonacoEditor } from "./Monaco";
import { startPlugin } from "../plugins";
import { stopPlugin } from '../plugins/index';
import { Flex } from './Flex';
import { ChangeList } from '../utils/ChangeList';
function showErrorToast(message: string) {
Toasts.show({
message,
type: Toasts.Type.FAILURE,
id: Toasts.genId(),
options: {
position: Toasts.Position.BOTTOM
}
});
}
export default ErrorBoundary.wrap(function Settings() {
const [settingsDir, , settingsDirPending] = useAwaiter(() => VencordNative.ipc.invoke<string>(IpcEvents.GET_SETTINGS_DIR), "Loading...");
const settings = useSettings();
const changes = React.useMemo(() => new ChangeList<string>(), []);
const changes = React.useMemo(() => new ChangeList<string>, []);
React.useEffect(() => {
return () => void (changes.hasChanges && Alerts.show({
@ -41,7 +35,7 @@ export default ErrorBoundary.wrap(function Settings() {
<div>{changes.map((s, i) => (
<>
{i > 0 && ", "}
{Parser.parse("`" + s + "`")}
{Parser.parse('`' + s + '`')}
</>
))}</div>
</>
@ -52,17 +46,33 @@ export default ErrorBoundary.wrap(function Settings() {
}));
}, []);
const depMap = React.useMemo(() => {
const o = {} as Record<string, string[]>;
for (const plugin in Plugins) {
const deps = Plugins[plugin].dependencies;
if (deps) {
for (const dep of deps) {
o[dep] ??= [];
o[dep].push(plugin);
}
}
}
return o;
}, []);
const sortedPlugins = React.useMemo(() => Object.values(Plugins).sort((a, b) => a.name.localeCompare(b.name)), []);
return (
<FormSection tag="h1" title="Vencord">
<FormTitle tag="h5">
<Forms.FormSection tag="h1" title="Vencord">
<Forms.FormTitle tag="h5">
Settings
</FormTitle>
</Forms.FormTitle>
<FormText>
Settings Directory: <code style={{ userSelect: "text", cursor: "text" }}>{settingsDir}</code>
</FormText>
<Forms.FormText>
SettingsDir: <code style={{ userSelect: 'text', cursor: 'text' }}>{settingsDir}</code>
</Forms.FormText>
{!IS_WEB && <Flex className={Margins.marginBottom20} style={{ marginTop: 8 }}>
<Flex className={classes(Margins.marginBottom20)}>
<Button
onClick={() => window.DiscordNative.app.relaunch()}
size={Button.Sizes.SMALL}
@ -78,44 +88,86 @@ export default ErrorBoundary.wrap(function Settings() {
Launch Directory
</Button>
<Button
onClick={() => VencordNative.ipc.invoke(IpcEvents.OPEN_MONACO_EDITOR)}
onClick={() => VencordNative.ipc.invoke(IpcEvents.OPEN_PATH, settingsDir, "quickCss.css")}
size={Button.Sizes.SMALL}
disabled={settingsDir === "Loading..."}
>
Open QuickCSS File
</Button>
</Flex>}
{IS_WEB && <Button
onClick={launchMonacoEditor}
size={Button.Sizes.SMALL}
disabled={settingsDir === "Loading..."}
>
Open QuickCSS File
</Button>}
<FormDivider />
</Flex>
<Forms.FormDivider />
<Forms.FormTitle tag="h5">Settings</Forms.FormTitle>
<Switch
value={settings.useQuickCss}
onChange={(v: boolean) => settings.useQuickCss = v}
note="Loads styles from your QuickCss file"
note="Enable QuickCSS"
>
Use QuickCss
</Switch>
{!IS_WEB && <Switch
value={settings.enableReactDevtools}
onChange={(v: boolean) => settings.enableReactDevtools = v}
note="Requires a full restart"
>
Enable React Developer Tools
</Switch>}
{!IS_WEB && <Switch
<Switch
value={settings.notifyAboutUpdates}
onChange={(v: boolean) => settings.notifyAboutUpdates = v}
note="Shows a Toast on StartUp"
>
Get notified about new Updates
</Switch>}
</FormSection>
</Switch>
<Switch
value={settings.unsafeRequire}
onChange={(v: boolean) => settings.unsafeRequire = v}
note="Enables VencordNative.require. Useful for testing, very bad for security. Leave this off unless you need it."
>
Enable Unsafe Require
</Switch>
<Forms.FormDivider />
<Forms.FormTitle tag="h5" className={classes(Margins.marginTop20, Margins.marginBottom8)}>
Plugins
</Forms.FormTitle>
{sortedPlugins.map(p => {
const enabledDependants = depMap[p.name]?.filter(d => settings.plugins[d].enabled);
const dependency = enabledDependants?.length;
return (
<Switch
disabled={p.required || dependency}
key={p.name}
value={settings.plugins[p.name].enabled || p.required || dependency}
onChange={v => {
settings.plugins[p.name].enabled = v;
if (v) {
p.dependencies?.forEach(d => {
settings.plugins[d].enabled = true;
if (!Plugins[d].started && !stopPlugin) {
settings.plugins[p.name].enabled = false;
showErrorToast(`Failed to start dependency ${d}. Check the console for more info.`);
}
});
if (!p.started && !startPlugin(p)) {
showErrorToast(`Failed to start plugin ${p.name}. Check the console for more info.`);
}
} else {
if (p.started && !stopPlugin(p)) {
showErrorToast(`Failed to stop plugin ${p.name}. Check the console for more info.`);
}
}
if (p.patches) changes.handleChange(p.name);
}}
note={p.description}
tooltipNote={
p.required ?
"This plugin is required. Thus you cannot disable it."
: dependency ?
`${humanFriendlyJoin(enabledDependants)} ${enabledDependants.length === 1 ? "depends" : "depend"} on this plugin. Thus you cannot disable it.`
: null
}
>
{p.name}
</Switch>
);
})
}
</Forms.FormSection >
);
});

View File

@ -1,32 +1,12 @@
/*
* 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 { Forms } from "@components";
import gitHash from "~git-hash";
import { classes, useAwaiter } from "../utils/misc";
import { changes, checkForUpdates, getRepo, isNewer, rebuild, update, updateError, UpdateLogger } from "../utils/updater";
import { Alerts, Button, Card, Margins, Parser, React, Toasts } from "../webpack/common";
import gitHash from "git-hash";
import { changes, checkForUpdates, getRepo, rebuild, update, UpdateLogger, updateError } from '../utils/updater';
import { React, Forms, Button, Margins, Alerts, Card, Parser, Toasts } from '../webpack/common';
import { Flex } from "./Flex";
import { useAwaiter } from '../utils/misc';
import { Link } from "./Link";
import ErrorBoundary from "./ErrorBoundary";
import { ErrorCard } from "./ErrorCard";
import { Flex } from "./Flex";
import { Link } from "./Link";
function withDispatcher(dispatcher: React.Dispatch<React.SetStateAction<boolean>>, action: () => any) {
return async () => {
@ -63,15 +43,51 @@ function withDispatcher(dispatcher: React.Dispatch<React.SetStateAction<boolean>
dispatcher(false);
}
};
}
};
interface CommonProps {
repo: string;
repoPending: boolean;
}
export default ErrorBoundary.wrap(function Updater() {
const [repo, err, repoPending] = useAwaiter(getRepo, "Loading...");
const [isChecking, setIsChecking] = React.useState(false);
const [isUpdating, setIsUpdating] = React.useState(false);
const [updates, setUpdates] = React.useState(changes);
React.useEffect(() => {
if (err)
UpdateLogger.error("Failed to retrieve repo", err);
}, [err]);
const isOutdated = updates?.length > 0;
function Changes({ updates, repo, repoPending }: CommonProps & { updates: typeof changes; }) {
return (
<Forms.FormSection tag="h1" title="Vencord Updater">
<Forms.FormTitle tag="h5">Repo</Forms.FormTitle>
<Forms.FormText>{repoPending ? repo : err ? "Failed to retrieve - check console" : (
<Link href={repo}>
{repo.split("/").slice(-2).join("/")}
</Link>
)} ({gitHash})</Forms.FormText>
<Forms.FormDivider />
<Forms.FormTitle tag="h5">Updates</Forms.FormTitle>
{!updates && updateError ? (
<>
<Forms.FormText>Failed to check updates. Check the console for more info</Forms.FormText>
<ErrorCard style={{ padding: "1em" }}>
<p>{updateError.stderr || updateError.stdout || "An unknown error occurred"}</p>
</ErrorCard>
</>
) :
(
<Forms.FormText className={Margins.marginBottom8}>
{isOutdated ? `There are ${updates.length} Updates` : "Up to Date!"}
</Forms.FormText>
)
}
{isOutdated && (
<Card style={{ padding: ".5em" }}>
{updates.map(({ hash, author, message }) => (
<div>
@ -85,34 +101,9 @@ function Changes({ updates, repo, repoPending }: CommonProps & { updates: typeof
</div>
))}
</Card>
);
}
function Updatable(props: CommonProps) {
const [updates, setUpdates] = React.useState(changes);
const [isChecking, setIsChecking] = React.useState(false);
const [isUpdating, setIsUpdating] = React.useState(false);
const isOutdated = (updates?.length ?? 0) > 0;
return (
<>
{!updates && updateError ? (
<>
<Forms.FormText>Failed to check updates. Check the console for more info</Forms.FormText>
<ErrorCard style={{ padding: "1em" }}>
<p>{updateError.stderr || updateError.stdout || "An unknown error occurred"}</p>
</ErrorCard>
</>
) : (
<Forms.FormText className={Margins.marginBottom8}>
{isOutdated ? `There are ${updates.length} Updates` : "Up to Date!"}
</Forms.FormText>
)}
{isOutdated && <Changes updates={updates} {...props} />}
<Flex className={classes(Margins.marginBottom8, Margins.marginTop8)}>
<Flex className={`${Margins.marginBottom8} ${Margins.marginTop8}`}>
{isOutdated && <Button
size={Button.Sizes.SMALL}
disabled={isUpdating || isChecking}
@ -164,51 +155,6 @@ function Updatable(props: CommonProps) {
Check for Updates
</Button>
</Flex>
</>
);
}
function Newer(props: CommonProps) {
return (
<>
<Forms.FormText className={Margins.marginBottom8}>
Your local copy has more recent commits. Please stash or reset them.
</Forms.FormText>
<Changes {...props} updates={changes} />
</>
);
}
function Updater() {
const [repo, err, repoPending] = useAwaiter(getRepo, "Loading...");
React.useEffect(() => {
if (err)
UpdateLogger.error("Failed to retrieve repo", err);
}, [err]);
const commonProps: CommonProps = {
repo,
repoPending
};
return (
<Forms.FormSection tag="h1" title="Vencord Updater">
<Forms.FormTitle tag="h5">Repo</Forms.FormTitle>
<Forms.FormText>{repoPending ? repo : err ? "Failed to retrieve - check console" : (
<Link href={repo}>
{repo.split("/").slice(-2).join("/")}
</Link>
)} ({gitHash})</Forms.FormText>
<Forms.FormDivider />
<Forms.FormTitle tag="h5">Updates</Forms.FormTitle>
{isNewer ? <Newer {...commonProps} /> : <Updatable {...commonProps} />}
</Forms.FormSection >
);
}
export default IS_WEB ? null : ErrorBoundary.wrap(Updater);
});

View File

@ -1,21 +1,2 @@
/*
* 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/>.
*/
export { default as PluginSettings } from "./PluginSettings";
export { default as Settings } from "./Settings";
export { default as Updater } from "./Updater";

View File

@ -1,52 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>QuickCss Editor</title>
<link rel="stylesheet" data-name="vs/editor/editor.main"
href="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.34.0/min/vs/editor/editor.main.min.css">
<style>
html,
body,
#container {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
}
</style>
</head>
<body>
<div id="container"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.34.0/min/vs/loader.min.js"></script>
<script>
require.config({ paths: { 'vs': 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.34.0/min/vs' } });
require(["vs/editor/editor.main"], () => {
getCurrentCss().then(css => {
var editor = monaco.editor.create(document.getElementById('container'), {
value: css,
language: 'css',
theme: getTheme(),
});
editor.onDidChangeModelContent(() =>
setCss(editor.getValue())
);
window.addEventListener("resize", () => {
// make monaco re-layout
editor.layout();
});
});
});
</script>
</body>
</html>

44
src/globals.d.ts vendored
View File

@ -1,58 +1,16 @@
/*
* 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 {
/**
* This exists only at build time, so references to it in patches should insert it
* via String interpolation OR use different replacement code based on this
* but NEVER refrence it inside the patched code
*
* @example
* // BAD
* replace: "IS_WEB?foo:bar"
* // GOOD
* replace: IS_WEB ? "foo" : "bar"
* // also good
* replace: `${IS_WEB}?foo:bar`
*/
export var IS_WEB: boolean;
export var IS_STANDALONE: boolean;
export var VencordNative: typeof import("./VencordNative").default;
export var Vencord: typeof import("./Vencord");
export var appSettings: {
set(setting: string, v: any): void;
};
/**
* Only available when running in Electron, undefined on web.
* Thus, avoid using this or only use it inside an {@link IS_WEB} guard.
*
* If you really must use it, mark your plugin as Desktop App only via
* `target: "DESKTOP"`
*/
export var DiscordNative: any;
interface Window {
webpackChunkdiscord_app: {
push(chunk: any): any;
pop(): any;
};
[k: string]: any;
[k: PropertyKey]: any;
}
}

View File

@ -1,35 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { app } from "electron";
import { join } from "path";
export const DATA_DIR = process.env.VENCORD_USER_DATA_DIR ?? (
process.env.DISCORD_USER_DATA_DIR
? join(process.env.DISCORD_USER_DATA_DIR, "..", "VencordData")
: join(app.getPath("userData"), "..", "Vencord")
);
export const SETTINGS_DIR = join(DATA_DIR, "settings");
export const QUICKCSS_PATH = join(SETTINGS_DIR, "quickCss.css");
export const SETTINGS_FILE = join(SETTINGS_DIR, "settings.json");
export const ALLOWED_PROTOCOLS = [
"https:",
"http:",
"steam:",
"spotify:"
];

View File

@ -1,57 +0,0 @@
/* eslint-disable header/header */
/*!
* crxToZip
* Copyright (c) 2013 Rob Wu <rob@robwu.nl>
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
export function crxToZip(buf: Buffer) {
function calcLength(a: number, b: number, c: number, d: number) {
let length = 0;
length += a << 0;
length += b << 8;
length += c << 16;
length += d << 24 >>> 0;
return length;
}
// 50 4b 03 04
// This is actually a zip file
if (buf[0] === 80 && buf[1] === 75 && buf[2] === 3 && buf[3] === 4) {
return buf;
}
// 43 72 32 34 (Cr24)
if (buf[0] !== 67 || buf[1] !== 114 || buf[2] !== 50 || buf[3] !== 52) {
throw new Error("Invalid header: Does not start with Cr24");
}
// 02 00 00 00
// or
// 03 00 00 00
const isV3 = buf[4] === 3;
const isV2 = buf[4] === 2;
if ((!isV2 && !isV3) || buf[5] || buf[6] || buf[7]) {
throw new Error("Unexpected crx format version number.");
}
if (isV2) {
const publicKeyLength = calcLength(buf[8], buf[9], buf[10], buf[11]);
const signatureLength = calcLength(buf[12], buf[13], buf[14], buf[15]);
// 16 = Magic number (4), CRX format version (4), lengths (2x4)
const zipStartOffset = 16 + publicKeyLength + signatureLength;
return buf.subarray(zipStartOffset, buf.length);
}
// v3 format has header size and then header
const headerSize = calcLength(buf[8], buf[9], buf[10], buf[11]);
const zipStartOffset = 12 + headerSize;
return buf.subarray(zipStartOffset, buf.length);
}

View File

@ -1,76 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { session } from "electron";
import { unzip } from "fflate";
import { constants as fsConstants } from "fs";
import { access, mkdir, rm, writeFile } from "fs/promises";
import { join } from "path";
import { DATA_DIR } from "./constants";
import { crxToZip } from "./crxToZip";
import { get } from "./simpleGet";
const extensionCacheDir = join(DATA_DIR, "ExtensionCache");
async function extract(data: Buffer, outDir: string) {
await mkdir(outDir, { recursive: true });
return new Promise<void>((resolve, reject) => {
unzip(data, (err, files) => {
if (err) return void reject(err);
Promise.all(Object.keys(files).map(async f => {
// Signature stuff
// 'Cannot load extension with file or directory name
// _metadata. Filenames starting with "_" are reserved for use by the system.';
if (f.startsWith("_metadata/")) return;
if (f.endsWith("/")) return void mkdir(join(outDir, f), { recursive: true });
const pathElements = f.split("/");
const name = pathElements.pop()!;
const directories = pathElements.join("/");
const dir = join(outDir, directories);
if (directories) {
await mkdir(dir, { recursive: true });
}
await writeFile(join(dir, name), files[f]);
}))
.then(() => resolve())
.catch(err => {
rm(outDir, { recursive: true, force: true });
reject(err);
});
});
});
}
export async function installExt(id: string) {
const extDir = join(extensionCacheDir, `${id}`);
try {
await access(extDir, fsConstants.F_OK);
} catch (err) {
const url = `https://clients2.google.com/service/update2/crx?response=redirect&acceptformat=crx2,crx3&x=id%3D${id}%26uc&prodversion=32`;
const buf = await get(url);
await extract(crxToZip(buf), extDir);
}
session.defaultSession.loadExtension(extDir);
}

View File

@ -1,34 +1,16 @@
/*
* 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 { app, BrowserWindow, desktopCapturer, ipcMain, shell } from "electron";
import { mkdirSync, readFileSync, watch } from "fs";
import { open, readFile, writeFile } from "fs/promises";
import { join } from 'path';
import { debounce } from "../utils/debounce";
import IpcEvents from '../utils/IpcEvents';
import "./updater";
import { BrowserWindow, desktopCapturer, ipcMain, shell } from "electron";
import { mkdirSync, readFileSync, watch } from "fs";
import { open, readFile, writeFile } from "fs/promises";
import { join } from "path";
import monacoHtml from "~fileContent/../components/monacoWin.html;base64";
import { debounce } from "../utils/debounce";
import IpcEvents from "../utils/IpcEvents";
import { Queue } from "../utils/Queue";
import { ALLOWED_PROTOCOLS, QUICKCSS_PATH, SETTINGS_DIR, SETTINGS_FILE } from "./constants";
const DATA_DIR = join(app.getPath("userData"), "..", "Vencord");
const SETTINGS_DIR = join(DATA_DIR, "settings");
const QUICKCSS_PATH = join(SETTINGS_DIR, "quickCss.css");
const SETTINGS_FILE = join(SETTINGS_DIR, "settings.json");
mkdirSync(SETTINGS_DIR, { recursive: true });
@ -36,7 +18,7 @@ function readCss() {
return readFile(QUICKCSS_PATH, "utf-8").catch(() => "");
}
export function readSettings() {
function readSettings() {
try {
return readFileSync(SETTINGS_FILE, "utf-8");
} catch {
@ -47,33 +29,18 @@ export function readSettings() {
// Fix for screensharing in Electron >= 17
ipcMain.handle(IpcEvents.GET_DESKTOP_CAPTURE_SOURCES, (_, opts) => desktopCapturer.getSources(opts));
ipcMain.handle(IpcEvents.OPEN_QUICKCSS, () => shell.openPath(QUICKCSS_PATH));
ipcMain.handle(IpcEvents.OPEN_EXTERNAL, (_, url) => {
try {
var { protocol } = new URL(url);
} catch {
throw "Malformed URL";
}
if (!ALLOWED_PROTOCOLS.includes(protocol))
throw "Disallowed protocol.";
shell.openExternal(url);
});
const cssWriteQueue = new Queue();
const settingsWriteQueue = new Queue();
ipcMain.handle(IpcEvents.OPEN_PATH, (_, ...pathElements) => shell.openPath(join(...pathElements)));
ipcMain.handle(IpcEvents.OPEN_EXTERNAL, (_, url) => shell.openExternal(url));
ipcMain.handle(IpcEvents.GET_QUICK_CSS, () => readCss());
ipcMain.handle(IpcEvents.SET_QUICK_CSS, (_, css) =>
cssWriteQueue.add(() => writeFile(QUICKCSS_PATH, css))
);
ipcMain.handle(IpcEvents.GET_SETTINGS_DIR, () => SETTINGS_DIR);
ipcMain.on(IpcEvents.GET_SETTINGS, e => e.returnValue = readSettings());
ipcMain.on(IpcEvents.GET_SETTINGS, (e) => e.returnValue = readSettings());
ipcMain.handle(IpcEvents.GET_SWC_WASM, () => readFile(join(__dirname, "swc.wasm")));
let settingsWriteQueue = Promise.resolve();
ipcMain.handle(IpcEvents.SET_SETTINGS, (_, s) => {
settingsWriteQueue.add(() => writeFile(SETTINGS_FILE, s));
settingsWriteQueue = settingsWriteQueue.then(() => writeFile(SETTINGS_FILE, s));
});
@ -85,13 +52,3 @@ export function initIpc(mainWindow: BrowserWindow) {
}, 50));
});
}
ipcMain.handle(IpcEvents.OPEN_MONACO_EDITOR, async () => {
const win = new BrowserWindow({
title: "QuickCss Editor",
webPreferences: {
preload: join(__dirname, "preload.js"),
}
});
await win.loadURL(`data:text/html;base64,${monacoHtml}`);
});

View File

@ -1,37 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import https from "https";
export function get(url: string, options: https.RequestOptions = {}) {
return new Promise<Buffer>((resolve, reject) => {
https.get(url, options, res => {
const { statusCode, statusMessage, headers } = res;
if (statusCode! >= 400)
return void reject(`${statusCode}: ${statusMessage} - ${url}`);
if (statusCode! >= 300)
return void resolve(get(headers.location!, options));
const chunks = [] as Buffer[];
res.on("error", reject);
res.on("data", chunk => chunks.push(chunk));
res.once("end", () => resolve(Buffer.concat(chunks)));
});
});
}

94
src/ipcMain/updater.ts Normal file
View File

@ -0,0 +1,94 @@
import { ipcMain } from 'electron';
import { promisify } from "util";
import IpcEvents from "../utils/IpcEvents";
import { execFile as cpExecFile } from 'child_process';
import { join } from 'path';
import { createReadStream } from 'fs';
import { createHash } from 'crypto';
const VENCORD_SRC_DIR = join(__dirname, "..");
const execFile = promisify(cpExecFile);
function git(...args: string[]) {
return execFile("git", args, {
cwd: VENCORD_SRC_DIR
});
}
async function calculateHashes() {
const hashes = {} as Record<string, string>;
await Promise.all(
["patcher.js", "preload.js", "renderer.js"].map(file => new Promise<void>(r => {
const fis = createReadStream(join(__dirname, file));
const hash = createHash("sha1", { encoding: "hex" });
fis.once("end", () => {
hash.end();
hashes[file] = hash.read();
r();
});
fis.pipe(hash);
}))
);
return hashes;
}
function serializeErrors(func: (...args: any[]) => any) {
return async function () {
try {
return {
ok: true,
value: await func(...arguments)
};
} catch (e: any) {
return {
ok: false,
error: e instanceof Error ? {
// prototypes get lost, so turn error into plain object
...e
} : e
};
}
};
}
async function getRepo() {
const res = await git("remote", "get-url", "origin");
return res.stdout.trim()
.replace(/git@(.+):/, "https://$1/")
.replace(/\.git$/, "");
}
async function calculateGitChanges() {
await git("fetch");
const res = await git("log", `HEAD...origin/main`, "--pretty=format:%an/%h/%s");
const commits = res.stdout.trim();
return commits ? commits.split("\n").map(line => {
const [author, hash, ...rest] = line.split("/");
return {
hash, author, message: rest.join("/")
};
}) : [];
}
async function pull() {
const res = await git("pull");
return res.stdout.includes("Fast-forward");
}
async function build() {
const res = await execFile("node", ["build.mjs"], {
cwd: VENCORD_SRC_DIR
});
return !res.stderr.includes("Build failed");
}
ipcMain.handle(IpcEvents.GET_HASHES, serializeErrors(calculateHashes));
ipcMain.handle(IpcEvents.GET_REPO, serializeErrors(getRepo));
ipcMain.handle(IpcEvents.GET_UPDATES, serializeErrors(calculateGitChanges));
ipcMain.handle(IpcEvents.UPDATE, serializeErrors(pull));
ipcMain.handle(IpcEvents.BUILD, serializeErrors(build));

View File

@ -1,59 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { createHash } from "crypto";
import { createReadStream } from "fs";
import { join } from "path";
export async function calculateHashes() {
const hashes = {} as Record<string, string>;
await Promise.all(
["patcher.js", "preload.js", "renderer.js"].map(file => new Promise<void>(r => {
const fis = createReadStream(join(__dirname, file));
const hash = createHash("sha1", { encoding: "hex" });
fis.once("end", () => {
hash.end();
hashes[file] = hash.read();
r();
});
fis.pipe(hash);
}))
);
return hashes;
}
export function serializeErrors(func: (...args: any[]) => any) {
return async function () {
try {
return {
ok: true,
value: await func(...arguments)
};
} catch (e: any) {
return {
ok: false,
error: e instanceof Error ? {
// prototypes get lost, so turn error into plain object
...e
} : e
};
}
};
}

View File

@ -1,74 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { execFile as cpExecFile } from "child_process";
import { ipcMain } from "electron";
import { join } from "path";
import { promisify } from "util";
import IpcEvents from "../../utils/IpcEvents";
import { calculateHashes, serializeErrors } from "./common";
const VENCORD_SRC_DIR = join(__dirname, "..");
const execFile = promisify(cpExecFile);
function git(...args: string[]) {
return execFile("git", args, {
cwd: VENCORD_SRC_DIR
});
}
async function getRepo() {
const res = await git("remote", "get-url", "origin");
return res.stdout.trim()
.replace(/git@(.+):/, "https://$1/")
.replace(/\.git$/, "");
}
async function calculateGitChanges() {
await git("fetch");
const res = await git("log", "HEAD...origin/main", "--pretty=format:%an/%h/%s");
const commits = res.stdout.trim();
return commits ? commits.split("\n").map(line => {
const [author, hash, ...rest] = line.split("/");
return {
hash, author, message: rest.join("/")
};
}) : [];
}
async function pull() {
const res = await git("pull");
return res.stdout.includes("Fast-forward");
}
async function build() {
const res = await execFile("node", ["scripts/build/build.mjs"], {
cwd: VENCORD_SRC_DIR
});
return !res.stderr.includes("Build failed");
}
ipcMain.handle(IpcEvents.GET_HASHES, serializeErrors(calculateHashes));
ipcMain.handle(IpcEvents.GET_REPO, serializeErrors(getRepo));
ipcMain.handle(IpcEvents.GET_UPDATES, serializeErrors(calculateGitChanges));
ipcMain.handle(IpcEvents.UPDATE, serializeErrors(pull));
ipcMain.handle(IpcEvents.BUILD, serializeErrors(build));

View File

@ -1,86 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { ipcMain } from "electron";
import { writeFile } from "fs/promises";
import { join } from "path";
import gitHash from "~git-hash";
import gitRemote from "~git-remote";
import { VENCORD_USER_AGENT } from "../../utils/constants";
import IpcEvents from "../../utils/IpcEvents";
import { get } from "../simpleGet";
import { calculateHashes, serializeErrors } from "./common";
const API_BASE = `https://api.github.com/repos/${gitRemote}`;
let PendingUpdates = [] as [string, Buffer][];
async function githubGet(endpoint: string) {
return get(API_BASE + endpoint, {
headers: {
Accept: "application/vnd.github+json",
// "All API requests MUST include a valid User-Agent header.
// Requests with no User-Agent header will be rejected."
"User-Agent": VENCORD_USER_AGENT,
// todo: perhaps add support for (optional) api token?
// unauthorised rate limit is 60 reqs/h
// https://github.com/settings/tokens/new?description=Vencord%20Updater
}
});
}
async function calculateGitChanges() {
const res = await githubGet(`/compare/${gitHash}...HEAD`);
const data = JSON.parse(res.toString("utf-8"));
return data.commits.map(c => ({
// github api only sends the long sha
hash: c.sha.slice(0, 7),
author: c.author.login,
message: c.commit.message
}));
}
async function fetchUpdates() {
const release = await githubGet("/releases/latest");
const data = JSON.parse(release.toString());
const hash = data.name.slice(data.name.lastIndexOf(" ") + 1);
if (hash === gitHash)
return true;
await Promise.all(data.assets.map(async ({ name, browser_download_url }) => {
if (["patcher.js", "preload.js", "renderer.js"].some(s => name.startsWith(s))) {
PendingUpdates.push([name, await get(browser_download_url)]);
}
}));
return true;
}
async function applyUpdates() {
await Promise.all(PendingUpdates.map(([name, data]) => writeFile(join(__dirname, name), data)));
PendingUpdates = [];
return true;
}
ipcMain.handle(IpcEvents.GET_HASHES, serializeErrors(calculateHashes));
ipcMain.handle(IpcEvents.GET_REPO, serializeErrors(() => `https://github.com/${gitRemote}`));
ipcMain.handle(IpcEvents.GET_UPDATES, serializeErrors(calculateGitChanges));
ipcMain.handle(IpcEvents.UPDATE, serializeErrors(fetchUpdates));
ipcMain.handle(IpcEvents.BUILD, serializeErrors(applyUpdates));

View File

@ -1,19 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import(IS_STANDALONE ? "./http" : "./git");

39
src/modules.d.ts vendored
View File

@ -1,39 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// eslint-disable-next-line spaced-comment
/// <reference types="standalone-electron-types"/>
declare module "~plugins" {
const plugins: Record<string, import("./utils/types").Plugin>;
export default plugins;
}
declare module "~git-hash" {
const hash: string;
export default hash;
}
declare module "~git-remote" {
const remote: string;
export default remote;
}
declare module "~fileContent/*" {
const content: string;
export default content;
}

View File

@ -1,107 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { app, autoUpdater } from "electron";
import { existsSync, mkdirSync, readdirSync, writeFileSync } from "fs";
import { basename, dirname, join } from "path";
const { setAppUserModelId } = app;
// Apparently requiring Discords updater too early leads into issues,
// copied this workaround from powerCord
app.setAppUserModelId = function (id: string) {
app.setAppUserModelId = setAppUserModelId;
setAppUserModelId.call(this, id);
patchUpdater();
};
function isNewer($new: string, old: string) {
const newParts = $new.slice(4).split(".").map(Number);
const oldParts = old.slice(4).split(".").map(Number);
for (let i = 0; i < oldParts.length; i++) {
if (newParts[i] > oldParts[i]) return true;
if (newParts[i] < oldParts[i]) return false;
}
return false;
}
function patchLatest() {
const currentAppPath = dirname(process.execPath);
const currentVersion = basename(currentAppPath);
const discordPath = join(currentAppPath, "..");
const latestVersion = readdirSync(discordPath).reduce((prev, curr) => {
return (curr.startsWith("app-") && isNewer(curr, prev))
? curr
: prev;
}, currentVersion as string);
if (latestVersion === currentVersion) return;
const app = join(discordPath, latestVersion, "resources", "app");
if (existsSync(app)) return;
console.info("[Vencord] Detected Host Update. Repatching...");
const patcherPath = join(__dirname, "patcher.js");
mkdirSync(app);
writeFileSync(join(app, "package.json"), JSON.stringify({
name: "discord",
main: "index.js"
}));
writeFileSync(join(app, "index.js"), `require(${JSON.stringify(patcherPath)});`);
}
// Windows Host Updates install to a new folder app-{HOST_VERSION}, so we
// need to reinject
function patchUpdater() {
const main = require.main!;
const buildInfo = require(join(process.resourcesPath, "build_info.json"));
try {
if (buildInfo?.newUpdater) {
const autoStartScript = join(main.filename, "..", "autoStart", "win32.js");
const { update } = require(autoStartScript);
// New Updater Injection
require.cache[autoStartScript]!.exports.update = function () {
patchLatest();
update.apply(this, arguments);
};
} else {
const hostUpdaterScript = join(main.filename, "..", "hostUpdater.js");
const { quitAndInstall } = require(hostUpdaterScript);
// Old Updater Injection
require.cache[hostUpdaterScript]!.exports.quitAndInstall = function () {
patchLatest();
quitAndInstall.apply(this, arguments);
};
}
} catch {
// OpenAsar uses electrons autoUpdater on Windows
const { quitAndInstall } = autoUpdater;
autoUpdater.quitAndInstall = function () {
patchLatest();
quitAndInstall.call(this);
};
}
}

View File

@ -1,50 +1,10 @@
/*
* 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 electron, { app, BrowserWindowConstructorOptions } from "electron";
import { readFileSync } from "fs";
import { dirname, join } from "path";
import { initIpc } from "./ipcMain";
import { installExt } from "./ipcMain/extensions";
import { readSettings } from "./ipcMain/index";
import installExt, { REACT_DEVELOPER_TOOLS } from "electron-devtools-installer";
import { join } from "path";
import { initIpc } from './ipcMain';
console.log("[Vencord] Starting up...");
// Our injector file at app/index.js
const injectorPath = require.main!.filename;
// special discord_arch_electron injection method
const asarName = injectorPath.endsWith("app.asar/index.js") ? "_app.asar" : "app.asar";
// The original app.asar
const asarPath = join(dirname(injectorPath), "..", asarName);
const discordPkg = require(join(asarPath, "package.json"));
require.main!.filename = join(asarPath, discordPkg.main);
// @ts-ignore Untyped method? Dies from cringe
app.setAppPath(asarPath);
// Repatch after host updates on Windows
if (process.platform === "win32")
require("./patchWin32Updater");
class BrowserWindow extends electron.BrowserWindow {
constructor(options: BrowserWindowConstructorOptions) {
if (options?.webPreferences?.preload && options.title) {
@ -87,29 +47,9 @@ Object.defineProperty(global, "appSettings", {
process.env.DATA_DIR = join(app.getPath("userData"), "..", "Vencord");
electron.app.whenReady().then(() => {
// Source Maps! Maybe there's a better way but since the renderer is executed
// from a string I don't think any other form of sourcemaps would work
electron.protocol.registerFileProtocol("vencord", ({ url: unsafeUrl }, cb) => {
let url = unsafeUrl.slice("vencord://".length);
if (url.endsWith("/")) url = url.slice(0, -1);
switch (url) {
case "renderer.js.map":
case "preload.js.map":
case "patcher.js.map": // doubt
cb(join(__dirname, url));
break;
default:
cb({ statusCode: 403 });
}
});
try {
const settings = JSON.parse(readSettings());
if (settings.enableReactDevtools)
installExt("fmkadmapgofadopljbjfkapdkoienihi")
.then(() => console.info("[Vencord] Installed React Developer Tools"))
.catch(err => console.error("[Vencord] Failed to install React Developer Tools", err));
} catch { }
installExt(REACT_DEVELOPER_TOOLS)
.then(() => console.info("Installed React DevTools"))
.catch((err) => console.error("Failed to install React DevTools", err));
// Remove CSP
electron.session.defaultSession.webRequest.onHeadersReceived(({ responseHeaders, url }, cb) => {
@ -124,25 +64,10 @@ electron.app.whenReady().then(() => {
}
cb({ cancel: false, responseHeaders });
});
});
console.log("[Vencord] Loading original Discord app.asar");
// Legacy Vencord Injector requires "../app.asar". However, because we
// restore the require.main above this is messed up, so monkey patch Module._load to
// redirect such requires
// FIXME: remove this eventually
if (readFileSync(injectorPath, "utf-8").includes('require("../app.asar")')) {
console.warn("[Vencord] [--> WARNING <--] You have a legacy Vencord install. Please reinject");
const Module = require("module");
const loadModule = Module._load;
Module._load = function (path: string) {
if (path === "../app.asar") {
Module._load = loadModule;
arguments[0] = require.main!.filename;
}
return loadModule.apply(this, arguments);
};
} else {
console.log(require.main!.filename);
require(require.main!.filename);
}
// Drop science and sentry requests
electron.session.defaultSession.webRequest.onBeforeRequest(
{ urls: ["https://*/api/v*/science", "https://sentry.io/*"] },
(_, callback) => callback({ cancel: true })
);
});

View File

@ -1,23 +1,5 @@
/*
* 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";
import { Devs } from '../utils/constants';
export default definePlugin({
name: "STFU",
@ -26,8 +8,8 @@ export default definePlugin({
patches: [{
find: "setDevtoolsCallbacks",
replacement: {
match: /if\(.{0,10}\|\|"0.0.0"!==.{0,2}\.remoteApp\.getVersion\(\)\)/,
replace: "if(false)"
match: /\.setDevtoolsCallbacks\(.+?else/,
replace: ".setDevtoolsCallbacks(null,null);else"
}
}]
});

View File

@ -1,89 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Devs } from "../utils/constants";
import definePlugin, { OptionType } from "../utils/types";
import { Settings } from "../Vencord";
enum Methods {
Random,
Consistent,
Timestamp,
}
export default definePlugin({
name: "AnonymiseFileNames",
authors: [Devs.obscurity],
description: "Anonymise uploaded file names",
patches: [
{
find: "instantBatchUpload:function",
replacement: {
match: /uploadFiles:(.{1,2}),/,
replace:
"uploadFiles:(...args)=>(args[0].uploads.forEach(f=>f.filename=Vencord.Plugins.plugins.AnonymiseFileNames.anonymise(f.filename)),$1(...args)),",
},
},
],
options: {
method: {
description: "Anonymising method",
type: OptionType.SELECT,
options: [
{ label: "Random Characters", value: Methods.Random, default: true },
{ label: "Consistent", value: Methods.Consistent },
{ label: "Timestamp (4chan-like)", value: Methods.Timestamp },
],
},
randomisedLength: {
description: "Random characters length",
type: OptionType.NUMBER,
default: 7,
disabled: () => Settings.plugins.AnonymiseFileNames.method !== Methods.Random,
},
consistent: {
description: "Consistent filename",
type: OptionType.STRING,
default: "image",
disabled: () => Settings.plugins.AnonymiseFileNames.method !== Methods.Consistent,
},
},
anonymise(file: string) {
let name = "image";
const ext = file.match(/\..+$/g)?.[0] ?? "";
switch (Settings.plugins.AnonymiseFileNames.method) {
case Methods.Random:
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
name = Array.from(
{ length: Settings.plugins.AnonymiseFileNames.randomisedLength },
() => chars[Math.floor(Math.random() * chars.length)]
).join("");
break;
case Methods.Consistent:
name = Settings.plugins.AnonymiseFileNames.consistent;
break;
case Methods.Timestamp:
// UNIX timestamp in nanos, i could not find a better dependency-less way
name = `${Math.floor(Date.now() / 1000)}${Math.floor(window.performance.now())}`;
break;
}
return name + ext;
},
});

View File

@ -1,52 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Devs } from "../utils/constants";
import definePlugin from "../utils/types";
export default definePlugin({
name: "CommandsAPI",
authors: [Devs.Arjix],
description: "Api required by anything that uses commands",
patches: [
// obtain BUILT_IN_COMMANDS instance
{
find: '"giphy","tenor"',
replacement: [
{
// Matches BUILT_IN_COMMANDS. This is not exported so this is
// the only way. _init() just returns the same object to make the
// patch simpler
// textCommands = builtInCommands.filter(...)
match: /(?<=\w=)(\w)(\.filter\(.{0,30}giphy)/,
replace: "Vencord.Api.Commands._init($1)$2",
}
],
},
// command error handling
{
find: "Unexpected value for option",
replacement: {
// return [2, cmd.execute(args, ctx)]
match: /,(.{1,2})\.execute\((.{1,2}),(.{1,2})\)]/,
replace: (_, cmd, args, ctx) => `,Vencord.Api.Commands._handleCommand(${cmd}, ${args}, ${ctx})]`
}
}
],
});

View File

@ -1,36 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Devs } from "../utils/constants";
import definePlugin from "../utils/types";
export default definePlugin({
name: "MessageAccessoriesAPI",
description: "API to add message accessories.",
authors: [Devs.Cyn],
patches: [
{
find: "_messageAttachmentToEmbedMedia",
replacement: {
match: /\(\)\.container\)},(.+?)\)};return/,
replace: (_, accessories) =>
`().container)},Vencord.Api.MessageAccessories._modifyAccessories([${accessories}],this.props))};return`,
},
},
],
});

View File

@ -1,21 +1,3 @@
/*
* 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";

View File

@ -1,21 +1,3 @@
/*
* 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";
@ -31,7 +13,7 @@ export default definePlugin({
{
match: /;(.{1,2}=null;)(?=.{0,50}updateNotice)/g,
replace:
";if(Vencord.Api.Notices.currentNotice)return !1;$1"
';if(Vencord.Api.Notices.currentNotice)return !1;$1'
},
{
match: /(?<=NOTICE_DISMISS:function.+?){(?=if\(null==(.+?)\))/,

View File

@ -1,49 +1,19 @@
/*
* 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, { OptionType } from "../utils/types";
import definePlugin from "../utils/types";
export default definePlugin({
name: "BANger",
description: "Replaces the GIF in the ban dialogue with a custom one.",
authors: [
{
authors: [{
name: "Xinto",
id: 423915768191647755n
},
Devs.Glitch
],
}],
patches: [
{
find: "BAN_CONFIRM_TITLE.",
find: "BanConfirm",
replacement: {
match: /src:\w\(\d+\)/g,
replace: "src: Vencord.Settings.plugins.BANger.source"
replace: 'src: "https://i.imgur.com/wp5q52C.mp4"'
}
}
],
options: {
source: {
description: "Source to replace ban GIF with (Video or Gif)",
type: OptionType.STRING,
default: "https://i.imgur.com/wp5q52C.mp4",
restartNeeded: true,
}
}
});

View File

@ -1,21 +1,3 @@
/*
* 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";
@ -44,7 +26,7 @@ export default definePlugin({
],
altify(props: any) {
if (props.alt !== "GIF") return props.alt;
if (props.alt !== "GIF") return;
let url: string = props.original || props.src;
try {

View File

@ -1,36 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Devs } from "../utils/constants";
import definePlugin from "../utils/types";
export default definePlugin({
name: "BetterUploadButton",
authors: [Devs.obscurity],
description: "Upload with a single click, open menu with right click",
patches: [
{
find: "Messages.CHAT_ATTACH_UPLOAD_OR_INVITE",
replacement: {
match: /CHAT_ATTACH_UPLOAD_OR_INVITE,onDoubleClick:([^,]+),onClick:([^,]+)}}/,
replace:
"CHAT_ATTACH_UPLOAD_OR_INVITE,onClick:$1,onContextMenu:$2}}",
},
},
],
});

View File

@ -1,138 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export const defaultRules = [
"action_object_map",
"action_type_map",
"action_ref_map",
"spm@*.aliexpress.com",
"scm@*.aliexpress.com",
"aff_platform",
"aff_trace_key",
"algo_expid@*.aliexpress.*",
"algo_pvid@*.aliexpress.*",
"btsid",
"ws_ab_test",
"pd_rd_*@amazon.*",
"_encoding@amazon.*",
"psc@amazon.*",
"tag@amazon.*",
"ref_@amazon.*",
"pf_rd_*@amazon.*",
"pf@amazon.*",
"crid@amazon.*",
"keywords@amazon.*",
"sprefix@amazon.*",
"sr@amazon.*",
"ie@amazon.*",
"node@amazon.*",
"qid@amazon.*",
"callback@bilibili.com",
"cvid@bing.com",
"form@bing.com",
"sk@bing.com",
"sp@bing.com",
"sc@bing.com",
"qs@bing.com",
"pq@bing.com",
"sc_cid",
"mkt_tok",
"trk",
"trkCampaign",
"ga_*",
"gclid",
"gclsrc",
"hmb_campaign",
"hmb_medium",
"hmb_source",
"spReportId",
"spJobID",
"spUserID",
"spMailingID",
"itm_*",
"s_cid",
"elqTrackId",
"elqTrack",
"assetType",
"assetId",
"recipientId",
"campaignId",
"siteId",
"mc_cid",
"mc_eid",
"pk_*",
"sc_campaign",
"sc_channel",
"sc_content",
"sc_medium",
"sc_outcome",
"sc_geo",
"sc_country",
"nr_email_referer",
"vero_conv",
"vero_id",
"yclid",
"_openstat",
"mbid",
"cmpid",
"cid",
"c_id",
"campaign_id",
"Campaign",
"hash@ebay.*",
"fb_action_ids",
"fb_action_types",
"fb_ref",
"fb_source",
"fbclid",
"refsrc@facebook.com",
"hrc@facebook.com",
"gs_l",
"gs_lcp@google.*",
"ved@google.*",
"ei@google.*",
"sei@google.*",
"gws_rd@google.*",
"gs_gbg@google.*",
"gs_mss@google.*",
"gs_rn@google.*",
"_hsenc",
"_hsmi",
"__hssc",
"__hstc",
"hsCtaTracking",
"source@sourceforge.net",
"position@sourceforge.net",
"t@*.twitter.com",
"s@*.twitter.com",
"ref_*@*.twitter.com",
"tt_medium",
"tt_content",
"lr@yandex.*",
"redircnt@yandex.*",
"feature@youtube.com",
"kw@youtube.com",
"wt_zmc",
"utm_source",
"utm_content",
"utm_medium",
"utm_campaign",
"utm_term",
"si@open.spotify.com",
];

View File

@ -1,152 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {
addPreEditListener,
addPreSendListener,
MessageObject,
removePreEditListener,
removePreSendListener,
} from "../../api/MessageEvents";
import definePlugin from "../../utils/types";
import { defaultRules } from "./defaultRules";
// From lodash
const reRegExpChar = /[\\^$.*+?()[\]{}|]/g;
const reHasRegExpChar = RegExp(reRegExpChar.source);
export default definePlugin({
name: "clearURLs",
description: "Removes tracking garbage from URLs",
authors: [
{
name: "adryd",
id: 0n,
},
],
dependencies: ["MessageEventsAPI"],
escapeRegExp(str: string) {
return (str && reHasRegExpChar.test(str))
? str.replace(reRegExpChar, "\\$&")
: (str || "");
},
createRules() {
// Can be extended upon once user configs are available
// Eg. (useDefaultRules: boolean, customRules: Array[string])
const rules = defaultRules;
this.universalRules = new Set();
this.rulesByHost = new Map();
this.hostRules = new Map();
for (const rule of rules) {
const splitRule = rule.split("@");
const paramRule = new RegExp(
"^" +
this.escapeRegExp(splitRule[0]).replace(/\\\*/, ".+?") +
"$"
);
if (!splitRule[1]) {
this.universalRules.add(paramRule);
continue;
}
const hostRule = new RegExp(
"^(www\\.)?" +
this.escapeRegExp(splitRule[1])
.replace(/\\\./, "\\.")
.replace(/^\\\*\\\./, "(.+?\\.)?")
.replace(/\\\*/, ".+?") +
"$"
);
const hostRuleIndex = hostRule.toString();
this.hostRules.set(hostRuleIndex, hostRule);
if (this.rulesByHost.get(hostRuleIndex) == null) {
this.rulesByHost.set(hostRuleIndex, new Set());
}
this.rulesByHost.get(hostRuleIndex).add(paramRule);
}
},
removeParam(rule: string | RegExp, param: string, parent: URLSearchParams) {
if (param === rule || rule instanceof RegExp && rule.test(param)) {
parent.delete(param);
}
},
replacer(match: string) {
// Parse URL without throwing errors
try {
var url = new URL(match);
} catch (error) {
// Don't modify anything if we can't parse the URL
return match;
}
// Cheap way to check if there are any search params
if (url.searchParams.entries().next().done) {
// If there are none, we don't need to modify anything
return match;
}
// Check all universal rules
this.universalRules.forEach(rule => {
url.searchParams.forEach((_value, param, parent) => {
this.removeParam(rule, param, parent);
});
});
// Check rules for each hosts that match
this.hostRules.forEach((regex, hostRuleName) => {
if (!regex.test(url.hostname)) return;
this.rulesByHost.get(hostRuleName).forEach(rule => {
url.searchParams.forEach((_value, param, parent) => {
this.removeParam(rule, param, parent);
});
});
});
return url.toString();
},
onSend(msg: MessageObject) {
// Only run on messages that contain URLs
if (msg.content.match(/http(s)?:\/\//)) {
msg.content = msg.content.replace(
/(https?:\/\/[^\s<]+[^<.,:;"'>)|\]\s])/g,
match => this.replacer(match)
);
}
},
start() {
this.createRules();
this.preSend = addPreSendListener((_, msg) => this.onSend(msg));
this.preEdit = addPreEditListener((_cid, _mid, msg) =>
this.onSend(msg)
);
},
stop() {
removePreSendListener(this.preSend);
removePreEditListener(this.preEdit);
},
});

View File

@ -1,24 +1,6 @@
/*
* 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";
import { Toasts } from "../webpack/common";
import { Toasts } from '../webpack/common';
export default definePlugin({
name: "ClickableRoleDot",
@ -36,16 +18,7 @@ export default definePlugin({
],
copyToClipBoard(color: string) {
if (IS_WEB) {
navigator.clipboard.writeText(color)
.then(() => this.notifySuccess);
} else {
DiscordNative.clipboard.copy(color);
this.notifySuccess();
}
},
notifySuccess() {
window.DiscordNative.clipboard.copy(color);
Toasts.show({
message: "Copied to Clipboard!",
type: Toasts.Type.SUCCESS,

View File

@ -1,28 +1,6 @@
/*
* 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";
const WEB_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.",
@ -30,8 +8,8 @@ export default definePlugin({
getShortcuts() {
return {
toClip: IS_WEB ? WEB_ONLY("toClip") : window.DiscordNative.clipboard.copy,
fromClip: IS_WEB ? WEB_ONLY("fromClip") : window.DiscordNative.clipboard.read,
toClip: window.DiscordNative.clipboard.copy,
fromClip: window.DiscordNative.clipboard.read,
wp: Vencord.Webpack,
wpc: Vencord.Webpack.wreq.c,
wreq: Vencord.Webpack.wreq,
@ -44,7 +22,7 @@ export default definePlugin({
Settings: Vencord.Settings,
Api: Vencord.Api,
reload: () => location.reload(),
restart: IS_WEB ? WEB_ONLY("restart") : window.DiscordNative.app.relaunch
restart: () => window.DiscordNative.app.relaunch()
};
},

View File

@ -0,0 +1,21 @@
import { Devs } from "../utils/constants";
import definePlugin from '../utils/types';
export default definePlugin({
name: "Experiments",
authors: [Devs.Ven, Devs.Megu],
description: "Enable Experiments",
patches: [{
find: "Object.defineProperties(this,{isDeveloper",
replacement: {
match: /(?<={isDeveloper:\{[^}]+,get:function\(\)\{return )\w/,
replace: "true"
}
}, {
find: 'type:"user",revision',
replacement: {
match: /(\w)\|\|"CONNECTION_OPEN".+?;/g,
replace: "$1=!0;"
}
}]
});

View File

@ -1,94 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Forms } from "@components";
import { lazyWebpack } from "../utils";
import { Devs } from "../utils/constants";
import definePlugin, { OptionType } from "../utils/types";
import { Settings } from "../Vencord";
import { filters } from "../webpack";
import { React } from "../webpack/common";
const KbdStyles = lazyWebpack(filters.byProps(["key", "removeBuildOverride"]));
export default definePlugin({
name: "Experiments",
authors: [
Devs.Megu,
Devs.Ven,
{ name: "Nickyux", id: 427146305651998721n },
{ name: "BanTheNons", id: 460478012794863637n },
],
description: "Enable Access to Experiments in Discord!",
patches: [{
find: "Object.defineProperties(this,{isDeveloper",
replacement: {
match: /(?<={isDeveloper:\{[^}]+,get:function\(\)\{return )\w/,
replace: "true"
},
}, {
find: 'type:"user",revision',
replacement: {
match: /(\w)\|\|"CONNECTION_OPEN".+?;/g,
replace: "$1=!0;"
},
}, {
find: ".isStaff=function(){",
predicate: () => Settings.plugins.Experiments.enableIsStaff === true,
replacement: [
{
match: /return\s*(\w+)\.hasFlag\((.+?)\.STAFF\)}/,
replace: "return Vencord.Webpack.Common.UserStore.getCurrentUser().id===$1.id||$1.hasFlag($2.STAFF)}"
},
{
match: /hasFreePremium=function\(\){return this.isStaff\(\)\s*\|\|/,
replace: "hasFreePremium=function(){return ",
},
],
}],
options: {
enableIsStaff: {
description: "Enable isStaff (requires restart)",
type: OptionType.BOOLEAN,
default: false,
restartNeeded: true,
}
},
settingsAboutComponent: () => {
const isMacOS = navigator.platform.includes("Mac");
const modKey = isMacOS ? "cmd" : "ctrl";
const altKey = isMacOS ? "opt" : "alt";
return (
<React.Fragment>
<Forms.FormTitle tag="h3">More Information</Forms.FormTitle>
<Forms.FormText>
You can enable client DevTools{" "}
<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.
</Forms.FormText>
</React.Fragment>
);
}
});

View File

@ -1,61 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { ApplicationCommandOptionType } from "../api/Commands";
import { makeRange } from "../components/PluginSettings/components";
import { Devs } from "../utils/constants";
import definePlugin, { OptionType } from "../utils/types";
import { Settings } from "../Vencord";
export default definePlugin({
name: "Fart2",
authors: [Devs.Animal],
description: "Enable farting v2, a slash command that allows you to perform or request that someone perform a little toot.",
dependencies: ["CommandsAPI"],
commands: [{
name: "fart",
description: "A simple command in which you may either request that a user do a little toot for you, or conduct one yourself.",
options: [
{
type: ApplicationCommandOptionType.USER,
name: "user",
description: "A Discord™ user of which you would humbly request a toot from.",
required: false
}
],
execute(args) {
const fart = new Audio("https://raw.githubusercontent.com/ItzOnlyAnimal/AliuPlugins/main/fart.mp3");
fart.volume = Settings.plugins.Fart2.volume;
fart.play();
return {
content: (args[0]) ? `<@${args[0].value}> fart` : "fart"
};
},
}],
options: {
volume: {
description: "how loud you wanna fart (aka volume)",
type: OptionType.SLIDER,
markers: makeRange(0, 1, 0.1),
default: 0.5,
stickToMarkers: false,
}
}
});

View File

@ -1,42 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Samu
*
* 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 { addPreSendListener, MessageObject, removePreSendListener } from "../api/MessageEvents";
import { Devs } from "../utils/constants";
import definePlugin from "../utils/types";
const re = /https?:\/\/twitter\.com(?=\/\w+?\/status\/)/g;
export default definePlugin({
name: "FxTwitter",
description: "Uses FxTwitter to improve embeds from twitter on send",
authors: [Devs.Samu],
dependencies: ["MessageEventsAPI"],
addPrefix(msg: MessageObject) {
msg.content = msg.content.replace(re, "https://fxtwitter.com");
},
start() {
this.preSend = addPreSendListener((_, msg) => this.addPrefix(msg));
},
stop() {
removePreSendListener(this.preSend);
}
});

View File

@ -1,21 +1,3 @@
/*
* 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";
@ -24,7 +6,10 @@ export default definePlugin({
description: "Do not hide messages from 'likely spammers'",
authors: [
Devs.botato,
Devs.Animal,
{
name: "Iryis",
id: 118437263754395652n,
},
],
patches: [
{

View File

@ -1,67 +1,21 @@
/*
* 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, { OptionType } from "../utils/types";
import { Settings } from "../Vencord";
import definePlugin from "../utils/types";
export default definePlugin({
name: "Ify",
description: "Disables Spotify auto-pausing, allows activity to continue playing when idling and bypasses premium checks, allowing you to listen along with others.",
authors: [
Devs.Cyn,
Devs.Nuckyz
],
patches: [{
find: 'dispatch({type:"SPOTIFY_PROFILE_UPDATE"',
description: "Disabes Spotify auto-pausing and premium checks",
authors: [Devs.Cyn],
patches: [
{
find: '.displayName="SpotifyStore"',
replacement: [{
match: /(function\((.{1,2})\){)(.{1,6}dispatch\({type:"SPOTIFY_PROFILE_UPDATE")/,
replace: (_, functionStart, data, functionBody) => `${functionStart}${data}.body.product="premium";${functionBody}`
}],
}, {
find: '.displayName="SpotifyStore"',
predicate: () => Settings.plugins.Ify.noSpotifyAutoPause,
replacement: {
match: /function (.{1,2})\(\).{0,200}SPOTIFY_AUTO_PAUSED\);.{0,}}}}/,
replace: "function $1(){}"
}
}, {
find: '.displayName="SpotifyStore"',
predicate: () => Settings.plugins.Ify.keepSpotifyActivityOnIdle,
replacement: {
match: /(shouldShowActivity=function\(\){.{1,50})&&!.{1,6}\.isIdle\(\)(.{0,}?})/,
replace: (_, functionDeclarationAndExpression, restOfFunction) => `${functionDeclarationAndExpression}${restOfFunction}`
}
}],
options: {
noSpotifyAutoPause: {
description: "Disable Spotify auto-pause",
type: OptionType.BOOLEAN,
default: true,
restartNeeded: true,
match: /\.isPremium=.;/,
replace: ".isPremium=true;",
}, ...["SPEAKING", "VOICE_STATE_UPDATES", "MEDIA_ENGINE_SET_DESKTOP_SOURCE"].map(event => ({
match: new RegExp(`${event}:function\\(.\\){.+?}(,|}\\))`),
replace: (_, ending) => `${event}:function(){}${ending}`,
})),
],
},
keepSpotifyActivityOnIdle: {
description: "Keep Spotify activity playing when idling",
type: OptionType.BOOLEAN,
default: false,
restartNeeded: true,
}
}
]
});

View File

@ -1,24 +1,4 @@
/*
* 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 Plugins from "~plugins";
import { registerCommand, unregisterCommand } from "../api/Commands";
import Plugins from "plugins";
import { Settings } from "../api/settings";
import Logger from "../utils/logger";
import { Patch, Plugin } from "../utils/types";
@ -37,90 +17,44 @@ for (const plugin of Object.values(Plugins)) if (plugin.patches && Settings.plug
}
export function startAllPlugins() {
for (const name in Plugins) if (Settings.plugins[name].enabled) {
startPlugin(Plugins[name]);
for (const plugin in Plugins) if (Settings.plugins[plugin].enabled) {
startPlugin(Plugins[plugin]);
}
}
export function startDependenciesRecursive(p: Plugin) {
let restartNeeded = false;
const failures: string[] = [];
if (p.dependencies) for (const dep of p.dependencies) {
if (!Settings.plugins[dep].enabled) {
startDependenciesRecursive(Plugins[dep]);
// If the plugin has patches, don't start the plugin, just enable it.
if (Plugins[dep].patches) {
logger.warn(`Enabling dependency ${dep} requires restart.`);
Settings.plugins[dep].enabled = true;
restartNeeded = true;
continue;
}
const result = startPlugin(Plugins[dep]);
if (!result) failures.push(dep);
}
}
return { restartNeeded, failures };
}
export function startPlugin(p: Plugin) {
if (p.start) {
if (!p.start) return true;
logger.info("Starting plugin", p.name);
if (p.started) {
logger.warn(`${p.name} already started`);
return false;
}
try {
p.start();
p.started = true;
} catch (e) {
logger.error(`Failed to start ${p.name}\n`, e);
return false;
}
}
if (p.commands?.length) {
logger.info("Registering commands of plugin", p.name);
for (const cmd of p.commands) {
try {
registerCommand(cmd, p.name);
} catch (e) {
logger.error(`Failed to register command ${cmd.name}\n`, e);
return false;
}
}
}
return true;
} catch (err: any) {
logger.error(`Failed to start ${p.name}\n`, err);
return false;
}
}
export function stopPlugin(p: Plugin) {
if (p.stop) {
if (!p.stop) return true;
logger.info("Stopping plugin", p.name);
if (!p.started) {
logger.warn(`${p.name} already stopped`);
logger.warn(`${p.name} already stopped / never started`);
return false;
}
try {
p.stop();
p.started = false;
} catch (e) {
logger.error(`Failed to stop ${p.name}\n`, e);
return false;
}
}
if (p.commands?.length) {
logger.info("Unregistering commands of plugin", p.name);
for (const cmd of p.commands) {
try {
unregisterCommand(cmd.name);
} catch (e) {
logger.error(`Failed to unregister command ${cmd.name}\n`, e);
return false;
}
}
}
return true;
} catch (err: any) {
logger.error(`Failed to stop ${p.name}\n`, err);
return false;
}
}

View File

@ -1,153 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Message } from "discord-types/general";
import { Devs } from "../utils/constants";
import { lazyWebpack } from "../utils/misc";
import definePlugin from "../utils/types";
import { filters } from "../webpack";
import { ChannelStore, FluxDispatcher as Dispatcher, SelectedChannelStore, UserStore } from "../webpack/common";
const MessageStore = lazyWebpack(filters.byProps(["getRawMessages"]));
const isMac = navigator.platform.includes("Mac"); // bruh
let replyIdx = -1;
let editIdx = -1;
export default definePlugin({
name: "InteractionKeybinds",
authors: [Devs.obscurity, Devs.Ven],
description: "Reply to (ctrl + up/down) and edit (ctrl + shift + up/down) messages via keybinds",
start() {
Dispatcher.subscribe("DELETE_PENDING_REPLY", onDeletePendingReply);
Dispatcher.subscribe("MESSAGE_END_EDIT", onEndEdit);
Dispatcher.subscribe("MESSAGE_START_EDIT", onStartEdit);
Dispatcher.subscribe("CREATE_PENDING_REPLY", onCreatePendingReply);
document.addEventListener("keydown", onKeydown);
},
stop() {
Dispatcher.unsubscribe("DELETE_PENDING_REPLY", onDeletePendingReply);
Dispatcher.unsubscribe("MESSAGE_END_EDIT", onEndEdit);
Dispatcher.unsubscribe("MESSAGE_START_EDIT", onStartEdit);
Dispatcher.unsubscribe("CREATE_PENDING_REPLY", onCreatePendingReply);
document.removeEventListener("keydown", onKeydown);
},
});
const onDeletePendingReply = () => replyIdx = -1;
const onEndEdit = () => editIdx = -1;
function calculateIdx(messages: Message[], id: string) {
const idx = messages.findIndex(m => m.id === id);
return idx === -1
? idx
: messages.length - idx - 1;
}
function onStartEdit({ channelId, messageId, _isQuickEdit }: any) {
if (_isQuickEdit) return;
const meId = UserStore.getCurrentUser().id;
const messages = MessageStore.getMessages(channelId)._array.filter(m => m.author.id === meId);
editIdx = calculateIdx(messages, messageId);
}
function onCreatePendingReply({ message, _isQuickReply }: { message: Message; _isQuickReply: boolean; }) {
if (_isQuickReply) return;
replyIdx = calculateIdx(MessageStore.getMessages(message.channel_id)._array, message.id);
}
const isCtrl = (e: KeyboardEvent) => isMac ? e.metaKey : e.ctrlKey;
const isAltOrMeta = (e: KeyboardEvent) => e.altKey || (!isMac && e.metaKey);
function onKeydown(e: KeyboardEvent) {
const isUp = e.key === "ArrowUp";
if (!isUp && e.key !== "ArrowDown") return;
if (!isCtrl(e) || isAltOrMeta(e)) return;
if (e.shiftKey)
nextEdit(isUp);
else
nextReply(isUp);
}
function getNextMessage(isUp: boolean, isReply: boolean) {
let messages: Message[] = MessageStore.getMessages(SelectedChannelStore.getChannelId())._array;
if (!isReply) { // we are editing so only include own
const meId = UserStore.getCurrentUser().id;
messages = messages.filter(m => m.author.id === meId);
}
const mutate = (i: number) => isUp
? Math.min(messages.length - 1, i + 1)
: Math.max(-1, i - 1);
let i: number;
if (isReply)
replyIdx = i = mutate(replyIdx);
else
editIdx = i = mutate(editIdx);
return i === - 1 ? undefined : messages[messages.length - i - 1];
}
// handle next/prev reply
function nextReply(isUp: boolean) {
const message = getNextMessage(isUp, true);
if (!message)
return void Dispatcher.dispatch({
type: "DELETE_PENDING_REPLY",
channelId: SelectedChannelStore.getChannelId(),
});
const channel = ChannelStore.getChannel(message.channel_id);
const meId = UserStore.getCurrentUser().id;
Dispatcher.dispatch({
type: "CREATE_PENDING_REPLY",
channel,
message,
shouldMention: true,
showMentionToggle: channel.guild_id !== null && message.author.id !== meId,
_isQuickReply: true
});
}
// handle next/prev edit
function nextEdit(isUp: boolean) {
const message = getNextMessage(isUp, false);
if (!message)
Dispatcher.dispatch({
type: "MESSAGE_END_EDIT",
channelId: SelectedChannelStore.getChannelId()
});
else
Dispatcher.dispatch({
type: "MESSAGE_START_EDIT",
channelId: message.channel_id,
messageId: message.id,
content: message.content,
_isQuickEdit: true
});
}

28
src/plugins/isStaff.ts Normal file
View File

@ -0,0 +1,28 @@
import { Devs } from "../utils/constants";
import definePlugin from "../utils/types";
export default definePlugin({
name: "isStaff",
description:
"Gives access to client devtools & other things locked behind isStaff",
authors: [Devs.Megu],
patches: [
{
find: ".isStaff=function(){",
replacement: [
{
match: /(\w+)\.isStaff=function\(\){return\s*!1};/,
replace: "$1.isStaff=function(){return true};",
},
{
match: /return\s*\w+\.hasFlag\(.+?STAFF\)}/,
replace: "return true}",
},
{
match: /hasFreePremium=function\(\){return this.isStaff\(\)\s*\|\|/,
replace: "hasFreePremium=function(){return ",
},
],
},
],
});

View File

@ -1,22 +1,4 @@
/*
* 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 { addClickListener, removeClickListener } from "../api/MessageEvents";
import { addClickListener, removeClickListener } from '../api/MessageEvents';
import { Devs } from "../utils/constants";
import definePlugin from "../utils/types";
import { find, findByProps } from "../webpack";
@ -33,10 +15,10 @@ export default definePlugin({
dependencies: ["MessageEventsAPI"],
start() {
const { deleteMessage, startEditMessage } = findByProps("deleteMessage", "startEditMessage");
const { deleteMessage, startEditMessage } = findByProps("deleteMessage");
const { can } = findByProps("can", "initialize");
const { MANAGE_MESSAGES } = find(m => typeof m.MANAGE_MESSAGES === "bigint");
const { isEditing } = findByProps("isEditing", "isEditingAny");
const { isEditing } = findByProps("isEditing");
document.addEventListener("keydown", keydown);
document.addEventListener("keyup", keyup);

View File

@ -1,73 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated, Samu 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 { ApplicationCommandInputType, findOption, OptionalMessageOption, RequiredMessageOption, sendBotMessage } from "../api/Commands";
import { Devs } from "../utils/constants";
import definePlugin from "../utils/types";
function mock(input: string): string {
let output = "";
for (let i = 0; i < input.length; i++) {
output += i % 2 ? input[i].toUpperCase() : input[i].toLowerCase();
}
return output;
}
export default definePlugin({
name: "MoreCommands",
description: "echo, lenny, mock",
authors: [
Devs.Arjix,
Devs.echo,
{
name: "ICodeInAssembly",
id: 702973430449832038n
}
],
dependencies: ["CommandsAPI"],
commands: [
{
name: "echo",
description: "Sends a message as Clyde (locally)",
options: [OptionalMessageOption],
inputType: ApplicationCommandInputType.BOT,
execute: (opts, ctx) => {
const content = findOption(opts, "message", "");
sendBotMessage(ctx.channel.id, { content });
},
},
{
name: "lenny",
description: "Sends a lenny face",
options: [OptionalMessageOption],
execute: opts => ({
content: findOption(opts, "message", "") + " ( ͡° ͜ʖ ͡°)"
}),
},
{
name: "mock",
description: "mOcK PeOpLe",
options: [RequiredMessageOption],
execute: opts => ({
content: mock(findOption(opts, "message", ""))
}),
},
]
});

View File

@ -1,148 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Message, ReactionEmoji } from "discord-types/general";
import { makeRange } from "../components/PluginSettings/components/SettingSliderComponent";
import { Devs } from "../utils/constants";
import { sleep } from "../utils/misc";
import definePlugin, { OptionType } from "../utils/types";
import { Settings } from "../Vencord";
import { FluxDispatcher, SelectedChannelStore, UserStore } from "../webpack/common";
interface IMessageCreate {
type: "MESSAGE_CREATE";
optimistic: boolean;
isPushNotification: boolean;
channelId: string;
message: Message;
}
interface IReactionAdd {
type: "MESSAGE_REACTION_ADD";
optimistic: boolean;
channelId: string;
messageId: string;
userId: "195136840355807232";
emoji: ReactionEmoji;
}
const MOYAI = "🗿";
const MOYAI_URL =
"https://raw.githubusercontent.com/MeguminSama/VencordPlugins/main/plugins/moyai/moyai.mp3";
export default definePlugin({
name: "Moyai",
authors: [Devs.Megu, Devs.Nuckyz],
description: "🗿🗿🗿🗿🗿🗿🗿🗿",
async onMessage(e: IMessageCreate) {
if (e.optimistic || e.type !== "MESSAGE_CREATE") return;
if (e.message.state === "SENDING") return;
if (Settings.plugins.Moyai.ignoreBots && e.message.author?.bot) return;
if (!e.message.content) return;
if (e.channelId !== SelectedChannelStore.getChannelId()) return;
const moyaiCount = getMoyaiCount(e.message.content);
for (let i = 0; i < moyaiCount; i++) {
boom();
await sleep(300);
}
},
onReaction(e: IReactionAdd) {
if (e.optimistic || e.type !== "MESSAGE_REACTION_ADD") return;
if (Settings.plugins.Moyai.ignoreBots && UserStore.getUser(e.userId)?.bot) return;
if (e.channelId !== SelectedChannelStore.getChannelId()) return;
const name = e.emoji.name.toLowerCase();
if (name !== MOYAI && !name.includes("moyai") && !name.includes("moai")) return;
boom();
},
start() {
FluxDispatcher.subscribe("MESSAGE_CREATE", this.onMessage);
FluxDispatcher.subscribe("MESSAGE_REACTION_ADD", this.onReaction);
},
stop() {
FluxDispatcher.unsubscribe("MESSAGE_CREATE", this.onMessage);
FluxDispatcher.unsubscribe("MESSAGE_REACTION_ADD", this.onReaction);
},
options: {
volume: {
description: "Volume of the 🗿🗿🗿",
type: OptionType.SLIDER,
markers: makeRange(0, 1, 0.1),
default: 0.5,
stickToMarkers: false,
},
triggerWhenUnfocused: {
description: "Trigger the 🗿 even when the window is unfocused",
type: OptionType.BOOLEAN,
default: true,
restartNeeded: false,
},
ignoreBots: {
description: "Ignore bots",
type: OptionType.BOOLEAN,
default: true,
restartNeeded: false,
}
}
});
function countOccurrences(sourceString: string, subString: string) {
let i = 0;
let lastIdx = 0;
while ((lastIdx = sourceString.indexOf(subString, lastIdx) + 1) !== 0)
i++;
return i;
}
function countMatches(sourceString: string, pattern: RegExp) {
if (!pattern.global)
throw new Error("pattern must be global");
let i = 0;
while (pattern.test(sourceString))
i++;
return i;
}
const customMoyaiRe = /<a?:\w*moy?ai\w*:\d{17,20}>/gi;
function getMoyaiCount(message: string) {
const count = countOccurrences(message, MOYAI)
+ countMatches(message, customMoyaiRe);
return Math.min(count, 10);
}
function boom() {
if (!Settings.plugins.Moyai.triggerWhenUnfocused && !document.hasFocus()) return;
const audioElement = document.createElement("audio");
audioElement.src = MOYAI_URL;
audioElement.volume = Settings.plugins.Moyai.volume;
audioElement.play();
}

View File

@ -1,28 +1,12 @@
/*
* 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: "MuteNewGuild",
description: "Mutes newly joined guilds",
authors: [Devs.Glitch],
authors: [{
name:"Glitchy",
id: 269567451199569920n
}],
patches: [
{
find: ",acceptInvite:function",
@ -32,4 +16,4 @@ export default definePlugin({
}
}
],
});
})

View File

@ -1,27 +1,8 @@
/*
* 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 { addPreEditListener, addPreSendListener, removePreEditListener,removePreSendListener } from "../api/MessageEvents";
import { Devs } from "../utils/constants";
import definePlugin, { OptionType } from "../utils/types";
import { Settings } from "../Vencord";
import { addPreSendListener, addPreEditListener, SendListener, removePreSendListener, removePreEditListener } from '../api/MessageEvents';
import { findByProps } from "../webpack";
import { UserStore } from "../webpack/common";
import definePlugin from "../utils/types";
import { Devs } from '../utils/constants';
import { UserStore } from '../webpack/common';
export default definePlugin({
name: "NitroBypass",
@ -30,25 +11,11 @@ export default definePlugin({
dependencies: ["MessageEventsAPI"],
patches: [
{
find: "canUseAnimatedEmojis:function",
predicate: () => Settings.plugins.NitroBypass.enableEmojiBypass === true,
find: `canUseAnimatedEmojis:function`,
replacement: [
"canUseAnimatedEmojis",
"canUseEmojisEverywhere"
].map(func => {
return {
match: new RegExp(`${func}:function\\(.+?}`),
replace: `${func}:function (e) { return true; }`
};
})
},
{
find: "canUseAnimatedEmojis:function",
predicate: () => Settings.plugins.NitroBypass.enableStreamQualityBypass === true,
replacement: [
"canUseHighVideoUploadQuality",
"canStreamHighQuality",
"canStreamMidQuality"
"canUseEmojisEverywhere",
"canUseHigherFramerate"
].map(func => {
return {
match: new RegExp(`${func}:function\\(.+?}`),
@ -58,27 +25,12 @@ export default definePlugin({
},
{
find: "STREAM_FPS_OPTION.format",
predicate: () => Settings.plugins.NitroBypass.enableStreamQualityBypass === true,
replacement: {
match: /(userPremiumType|guildPremiumTier):.{0,10}TIER_\d,?/g,
replace: ""
}
}
],
options: {
enableEmojiBypass: {
description: "Allow sending fake emojis",
type: OptionType.BOOLEAN,
default: true,
restartNeeded: true,
},
enableStreamQualityBypass: {
description: "Allow streaming in nitro quality",
type: OptionType.BOOLEAN,
default: true,
restartNeeded: true,
}
},
get guildId() {
return window.location.href.split("channels/")[1].split("/")[0];
@ -89,10 +41,6 @@ export default definePlugin({
},
start() {
if (!Settings.plugins.NitroBypass.enableEmojiBypass) {
return;
}
if (this.canUseEmotes) {
console.info("[NitroBypass] Skipping start because you have nitro");
return;
@ -100,36 +48,27 @@ export default definePlugin({
const { getCustomEmojiById } = findByProps("getCustomEmojiById");
function getWordBoundary(origStr, offset) {
return (!origStr[offset] || /\s/.test(origStr[offset])) ? "" : " ";
}
this.preSend = addPreSendListener((_, messageObj) => {
const { guildId } = this;
const guildId = this.guildId;
for (const emoji of messageObj.validNonShortcutEmojis) {
if (!emoji.require_colons) continue;
if (emoji.guildId === guildId && !emoji.animated) continue;
const emojiString = `<${emoji.animated ? "a" : ""}:${emoji.originalName || emoji.name}:${emoji.id}>`;
const url = emoji.url.replace(/\?size=[0-9]+/, "?size=48");
messageObj.content = messageObj.content.replace(emojiString, (match, offset, origStr) => {
return `${getWordBoundary(origStr, offset - 1)}${url}${getWordBoundary(origStr, offset + match.length)}`;
});
const emojiString = `<${emoji.animated ? 'a' : ''}:${emoji.originalName || emoji.name}:${emoji.id}>`;
const url = emoji.url.replace(/\?size=[0-9]+/, `?size=48`);
messageObj.content = messageObj.content.replace(emojiString, ` ${url} `);
}
});
this.preEdit = addPreEditListener((_, __, messageObj) => {
const { guildId } = this;
const guildId = this.guildId;
for (const [emojiStr, _, emojiId] of messageObj.content.matchAll(/(?<!\\)<a?:(\w+):(\d+)>/ig)) {
const emoji = getCustomEmojiById(emojiId);
if (emoji == null || (emoji.guildId === guildId && !emoji.animated)) continue;
if (!emoji.require_colons) continue;
const url = emoji.url.replace(/\?size=[0-9]+/, "?size=48");
messageObj.content = messageObj.content.replace(emojiStr, (match, offset, origStr) => {
return `${getWordBoundary(origStr, offset - 1)}${url}${getWordBoundary(origStr, offset + match.length)}`;
});
const url = emoji.url.replace(/\?size=[0-9]+/, `?size=48`);
messageObj.content = messageObj.content.replace(emojiStr, ` ${url} `);
}
});
},

View File

@ -1,37 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Devs } from "../utils/constants";
import definePlugin from "../utils/types";
export default definePlugin({
name: "NoBlockedMessages",
description: "Hides all blocked messages from chat completely.",
authors: [Devs.rushii],
patches: [
{
find: 'safety_prompt:"DMSpamExperiment",response:"show_redacted_messages"',
replacement: [
{
match: /collapsedReason;return (?=\w{1,2}.createElement)/,
replace: "collapsedReason; return null;"
}
]
}
]
});

View File

@ -1,62 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Samu and Linnea Gräf
*
* 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 { addPreSendListener, MessageObject, removePreSendListener } from "../api/MessageEvents";
import { Devs } from "../utils/constants";
import definePlugin, { OptionType } from "../utils/types";
import { Settings } from "../Vencord";
export default definePlugin({
name: "NoCanaryMessageLinks",
description: "Allows you to change/remove the subdomain of discord message and channel links",
authors: [
Devs.Samu,
Devs.nea,
],
options: {
linkPrefix: {
description: "The subdomain for your discord message links",
type: OptionType.STRING,
default: "",
restartNeeded: false,
},
alwaysUseDiscordHost: {
description: "Always use discord.com host (replace discordapp.com)",
type: OptionType.BOOLEAN,
default: false,
restartNeeded: false,
},
},
dependencies: ["MessageEventsAPI"],
removeBetas(msg: MessageObject) {
const settings = Settings.plugins.NoCanaryMessageLinks;
msg.content = msg.content.replace(
/https:\/\/(?:canary\.|ptb\.)?(discord(?:app)?\.com)(\/channels\/(?:\d{17,20}|@me)\/\d{17,20}(?:\/\d{17,20})?)/g,
(_, host, path) => "https://" + (settings.linkPrefix ? settings.linkPrefix + "." : "") + (settings.alwaysUseDiscordHost ? "discord.com" : host) + path
);
},
start() {
this.preSend = addPreSendListener((_, msg) => this.removeBetas(msg));
},
stop() {
removePreSendListener(this.preSend);
}
});

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