Seaswimmer 2024-05-08 13:56:31 -04:00
commit 0d23fdeb41
62 changed files with 3911 additions and 2538 deletions

View file

@ -18,14 +18,14 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: pnpm/action-setup@v2 # Install pnpm using packageManager key in package.json - uses: pnpm/action-setup@v3 # Install pnpm using packageManager key in package.json
- name: Use Node.js 19 - name: Use Node.js 20
uses: actions/setup-node@v3 uses: actions/setup-node@v4
with: with:
node-version: 19 node-version: 20
cache: "pnpm" cache: "pnpm"
- name: Install dependencies - name: Install dependencies

View file

@ -13,7 +13,7 @@ jobs:
if: github.repository == 'Vendicated/Vencord' if: github.repository == 'Vendicated/Vencord'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- uses: pixta-dev/repository-mirroring-action@674e65a7d483ca28dafaacba0d07351bdcc8bd75 # v1.1.1 - uses: pixta-dev/repository-mirroring-action@674e65a7d483ca28dafaacba0d07351bdcc8bd75 # v1.1.1

View file

@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: check that tag matches package.json version - name: check that tag matches package.json version
run: | run: |
@ -20,12 +20,12 @@ jobs:
exit 1 exit 1
fi fi
- uses: pnpm/action-setup@v2 # Install pnpm using packageManager key in package.json - uses: pnpm/action-setup@v3 # Install pnpm using packageManager key in package.json
- name: Use Node.js 19 - name: Use Node.js 19
uses: actions/setup-node@v3 uses: actions/setup-node@v4
with: with:
node-version: 19 node-version: 20
cache: "pnpm" cache: "pnpm"
- name: Install dependencies - name: Install dependencies

View file

@ -11,28 +11,31 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
if: ${{ github.event_name == 'schedule' }} if: ${{ github.event_name == 'schedule' }}
with: with:
ref: dev ref: dev
- uses: actions/checkout@v3 - uses: actions/checkout@v4
if: ${{ github.event_name == 'workflow_dispatch' }} if: ${{ github.event_name == 'workflow_dispatch' }}
- uses: pnpm/action-setup@v2 # Install pnpm using packageManager key in package.json - uses: pnpm/action-setup@v3 # Install pnpm using packageManager key in package.json
- name: Use Node.js 19 - name: Use Node.js 20
uses: actions/setup-node@v3 uses: actions/setup-node@v4
with: with:
node-version: 19 node-version: 20
cache: "pnpm" cache: "pnpm"
- name: Install dependencies - name: Install dependencies
run: | run: |
pnpm install --frozen-lockfile pnpm install --frozen-lockfile
pnpm add puppeteer
sudo apt-get install -y chromium-browser - name: Install Google Chrome
id: setup-chrome
uses: browser-actions/setup-chrome@82b9ce628cc5595478a9ebadc480958a36457dc2
with:
chrome-version: stable
- name: Build web - name: Build web
run: pnpm buildWeb --standalone --dev run: pnpm buildWeb --standalone --dev
@ -41,7 +44,7 @@ jobs:
timeout-minutes: 10 timeout-minutes: 10
run: | run: |
export PATH="$PWD/node_modules/.bin:$PATH" export PATH="$PWD/node_modules/.bin:$PATH"
export CHROMIUM_BIN=$(which chromium-browser) export CHROMIUM_BIN=${{ steps.setup-chrome.outputs.chrome-path }}
esbuild scripts/generateReport.ts > dist/report.mjs esbuild scripts/generateReport.ts > dist/report.mjs
node dist/report.mjs >> $GITHUB_STEP_SUMMARY node dist/report.mjs >> $GITHUB_STEP_SUMMARY
@ -54,7 +57,7 @@ jobs:
if: success() || failure() # even run if previous one failed if: success() || failure() # even run if previous one failed
run: | run: |
export PATH="$PWD/node_modules/.bin:$PATH" export PATH="$PWD/node_modules/.bin:$PATH"
export CHROMIUM_BIN=$(which chromium-browser) export CHROMIUM_BIN=${{ steps.setup-chrome.outputs.chrome-path }}
export USE_CANARY=true export USE_CANARY=true
esbuild scripts/generateReport.ts > dist/report.mjs esbuild scripts/generateReport.ts > dist/report.mjs

View file

@ -10,13 +10,13 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: pnpm/action-setup@v2 # Install pnpm using packageManager key in package.json - uses: pnpm/action-setup@v3 # Install pnpm using packageManager key in package.json
- name: Use Node.js 18 - name: Use Node.js 20
uses: actions/setup-node@v3 uses: actions/setup-node@v4
with: with:
node-version: 18 node-version: 20
cache: "pnpm" cache: "pnpm"
- name: Install dependencies - name: Install dependencies

View file

@ -1,7 +1,7 @@
{ {
"name": "vencord", "name": "vencord",
"private": "true", "private": "true",
"version": "1.7.9", "version": "1.8.2",
"description": "The cutest Discord client mod", "description": "The cutest Discord client mod",
"homepage": "https://github.com/Vendicated/Vencord#readme", "homepage": "https://github.com/Vendicated/Vencord#readme",
"bugs": { "bugs": {
@ -71,7 +71,7 @@
"zip-local": "^0.3.5", "zip-local": "^0.3.5",
"zustand": "^3.7.2" "zustand": "^3.7.2"
}, },
"packageManager": "pnpm@8.10.2", "packageManager": "pnpm@9.1.0",
"pnpm": { "pnpm": {
"patchedDependencies": { "patchedDependencies": {
"eslint-plugin-path-alias@1.0.0": "patches/eslint-plugin-path-alias@1.0.0.patch", "eslint-plugin-path-alias@1.0.0": "patches/eslint-plugin-path-alias@1.0.0.patch",

File diff suppressed because it is too large Load diff

View file

@ -25,10 +25,11 @@ import { access, readdir, readFile } from "fs/promises";
import { join, relative } from "path"; import { join, relative } from "path";
import { promisify } from "util"; import { promisify } from "util";
// wtf is this assert syntax
import PackageJSON from "../../package.json" assert { type: "json" };
import { getPluginTarget } from "../utils.mjs"; import { getPluginTarget } from "../utils.mjs";
/** @type {import("../../package.json")} */
const PackageJSON = JSON.parse(readFileSync("package.json"));
export const VERSION = PackageJSON.version; export const VERSION = PackageJSON.version;
// https://reproducible-builds.org/docs/source-date-epoch/ // https://reproducible-builds.org/docs/source-date-epoch/
export const BUILD_TIMESTAMP = Number(process.env.SOURCE_DATE_EPOCH) || Date.now(); export const BUILD_TIMESTAMP = Number(process.env.SOURCE_DATE_EPOCH) || Date.now();

View file

@ -269,7 +269,7 @@ page.on("pageerror", e => console.error("[Page Error]", e));
await page.setBypassCSP(true); await page.setBypassCSP(true);
function runTime(token: string) { async function runtime(token: string) {
console.log("[PUP_DEBUG]", "Starting test..."); console.log("[PUP_DEBUG]", "Starting test...");
try { try {
@ -282,9 +282,13 @@ function runTime(token: string) {
// Monkey patch Logger to not log with custom css // Monkey patch Logger to not log with custom css
// @ts-ignore // @ts-ignore
const originalLog = Vencord.Util.Logger.prototype._log;
// @ts-ignore
Vencord.Util.Logger.prototype._log = function (level, levelColor, args) { Vencord.Util.Logger.prototype._log = function (level, levelColor, args) {
if (level === "warn" || level === "error") if (level === "warn" || level === "error")
console[level]("[Vencord]", this.name + ":", ...args); return console[level]("[Vencord]", this.name + ":", ...args);
return originalLog.call(this, level, levelColor, args);
}; };
// Force enable all plugins and patches // Force enable all plugins and patches
@ -310,45 +314,30 @@ function runTime(token: string) {
}); });
}); });
Vencord.Webpack.waitFor( let wreq: typeof Vencord.Webpack.wreq;
"loginToken",
m => {
console.log("[PUP_DEBUG]", "Logging in with token...");
m.loginToken(token);
}
);
// Force load all chunks const { canonicalizeMatch, Logger } = Vencord.Util;
Vencord.Webpack.onceReady.then(() => setTimeout(async () => {
console.log("[PUP_DEBUG]", "Webpack is ready!");
const { wreq } = Vencord.Webpack;
console.log("[PUP_DEBUG]", "Loading all chunks...");
let chunks = null as Record<number, string[]> | null;
const sym = Symbol("Vencord.chunksExtract");
Object.defineProperty(Object.prototype, sym, {
get() {
chunks = this;
},
set() { },
configurable: true,
});
await (wreq as any).el(sym);
delete Object.prototype[sym];
const validChunksEntryPoints = new Set<string>();
const validChunks = new Set<string>(); const validChunks = new Set<string>();
const invalidChunks = new Set<string>(); const invalidChunks = new Set<string>();
if (!chunks) throw new Error("Failed to get chunks"); let chunksSearchingResolve: (value: void | PromiseLike<void>) => void;
const chunksSearchingDone = new Promise<void>(r => chunksSearchingResolve = r);
for (const entryPoint in chunks) { // True if resolved, false otherwise
const chunkIds = chunks[entryPoint]; const chunksSearchPromises = [] as Array<() => boolean>;
let invalidEntryPoint = false; const lazyChunkRegex = canonicalizeMatch(/Promise\.all\((\[\i\.\i\(".+?"\).+?\])\).then\(\i\.bind\(\i,"(.+?)"\)\)/g);
const chunkIdsRegex = canonicalizeMatch(/\("(.+?)"\)/g);
async function searchAndLoadLazyChunks(factoryCode: string) {
const lazyChunks = factoryCode.matchAll(lazyChunkRegex);
const validChunkGroups = new Set<[chunkIds: string[], entryPoint: string]>();
await Promise.all(Array.from(lazyChunks).map(async ([, rawChunkIds, entryPoint]) => {
const chunkIds = Array.from(rawChunkIds.matchAll(chunkIdsRegex)).map(m => m[1]);
if (chunkIds.length === 0) return;
let invalidChunkGroup = false;
for (const id of chunkIds) { for (const id of chunkIds) {
if (wreq.u(id) == null || wreq.u(id) === "undefined.js") continue; if (wreq.u(id) == null || wreq.u(id) === "undefined.js") continue;
@ -359,56 +348,28 @@ function runTime(token: string) {
if (isWasm) { if (isWasm) {
invalidChunks.add(id); invalidChunks.add(id);
invalidEntryPoint = true; invalidChunkGroup = true;
continue; continue;
} }
validChunks.add(id); validChunks.add(id);
} }
if (!invalidEntryPoint) if (!invalidChunkGroup) {
validChunksEntryPoints.add(entryPoint); validChunkGroups.add([chunkIds, entryPoint]);
} }
}));
for (const entryPoint of validChunksEntryPoints) { // Loads all found valid chunk groups
try { await Promise.all(
// Loads all chunks required for an entry point Array.from(validChunkGroups)
await (wreq as any).el(entryPoint); .map(([chunkIds]) =>
} catch (err) { } Promise.all(chunkIds.map(id => wreq.e(id as any).catch(() => { })))
} )
);
// Matches "id" or id: // Requires the entry points for all valid chunk groups
const chunkIdRegex = /(?:"(\d+?)")|(?:(\d+?):)/g; for (const [, entryPoint] of validChunkGroups) {
const wreqU = wreq.u.toString();
const allChunks = [] as string[];
let currentMatch: RegExpExecArray | null;
while ((currentMatch = chunkIdRegex.exec(wreqU)) != null) {
const id = currentMatch[1] ?? currentMatch[2];
if (id == null) continue;
allChunks.push(id);
}
if (allChunks.length === 0) throw new Error("Failed to get all chunks");
const chunksLeft = allChunks.filter(id => {
return !(validChunks.has(id) || invalidChunks.has(id));
});
for (const id of chunksLeft) {
const isWasm = await fetch(wreq.p + wreq.u(id))
.then(r => r.text())
.then(t => t.includes(".module.wasm") || !t.includes("(this.webpackChunkdiscord_app=this.webpackChunkdiscord_app||[]).push"));
// Loads a chunk
if (!isWasm) await wreq.e(id as any);
}
// Make sure every chunk has finished loading
await new Promise(r => setTimeout(r, 1000));
for (const entryPoint of validChunksEntryPoints) {
try { try {
if (wreq.m[entryPoint]) wreq(entryPoint as any); if (wreq.m[entryPoint]) wreq(entryPoint as any);
} catch (err) { } catch (err) {
@ -416,11 +377,97 @@ function runTime(token: string) {
} }
} }
// setImmediate to only check if all chunks were loaded after this function resolves
// We check if all chunks were loaded every time a factory is loaded
// If we are still looking for chunks in the other factories, the array will have that factory's chunk search promise not resolved
// But, if all chunk search promises are resolved, this means we found every lazy chunk loaded by Discord code and manually loaded them
setTimeout(() => {
let allResolved = true;
for (let i = 0; i < chunksSearchPromises.length; i++) {
const isResolved = chunksSearchPromises[i]();
if (isResolved) {
// Remove finished promises to avoid having to iterate through a huge array everytime
chunksSearchPromises.splice(i--, 1);
} else {
allResolved = false;
}
}
if (allResolved) chunksSearchingResolve();
}, 0);
}
Vencord.Webpack.waitFor(
"loginToken",
m => {
console.log("[PUP_DEBUG]", "Logging in with token...");
m.loginToken(token);
}
);
Vencord.Webpack.beforeInitListeners.add(async webpackRequire => {
console.log("[PUP_DEBUG]", "Loading all chunks...");
wreq = webpackRequire;
Vencord.Webpack.factoryListeners.add(factory => {
let isResolved = false;
searchAndLoadLazyChunks(factory.toString()).then(() => isResolved = true);
chunksSearchPromises.push(() => isResolved);
});
// setImmediate to only search the initial factories after Discord initialized the app
// our beforeInitListeners are called before Discord initializes the app
setTimeout(() => {
for (const factoryId in wreq.m) {
let isResolved = false;
searchAndLoadLazyChunks(wreq.m[factoryId].toString()).then(() => isResolved = true);
chunksSearchPromises.push(() => isResolved);
}
}, 0);
});
await chunksSearchingDone;
// All chunks Discord has mapped to asset files, even if they are not used anymore
const allChunks = [] as string[];
// Matches "id" or id:
for (const currentMatch of wreq!.u.toString().matchAll(/(?:"(\d+?)")|(?:(\d+?):)/g)) {
const id = currentMatch[1] ?? currentMatch[2];
if (id == null) continue;
allChunks.push(id);
}
if (allChunks.length === 0) throw new Error("Failed to get all chunks");
// Chunks that are not loaded (not used) by Discord code anymore
const chunksLeft = allChunks.filter(id => {
return !(validChunks.has(id) || invalidChunks.has(id));
});
await Promise.all(chunksLeft.map(async id => {
const isWasm = await fetch(wreq.p + wreq.u(id))
.then(r => r.text())
.then(t => t.includes(".module.wasm") || !t.includes("(this.webpackChunkdiscord_app=this.webpackChunkdiscord_app||[]).push"));
// Loads and requires a chunk
if (!isWasm) {
await wreq.e(id as any);
if (wreq.m[id]) wreq(id as any);
}
}));
console.log("[PUP_DEBUG]", "Finished loading all chunks!"); console.log("[PUP_DEBUG]", "Finished loading all chunks!");
for (const patch of Vencord.Plugins.patches) { for (const patch of Vencord.Plugins.patches) {
if (!patch.all) { if (!patch.all) {
new Vencord.Util.Logger("WebpackInterceptor").warn(`Patch by ${patch.plugin} found no module (Module id is -): ${patch.find}`); new Logger("WebpackInterceptor").warn(`Patch by ${patch.plugin} found no module (Module id is -): ${patch.find}`);
} }
} }
@ -445,7 +492,7 @@ function runTime(token: string) {
const [code, matcher] = args; const [code, matcher] = args;
const module = Vencord.Webpack.findModuleFactory(...code); const module = Vencord.Webpack.findModuleFactory(...code);
if (module) result = module.toString().match(Vencord.Util.canonicalizeMatch(matcher)); if (module) result = module.toString().match(canonicalizeMatch(matcher));
} else { } else {
// @ts-ignore // @ts-ignore
result = Vencord.Webpack[method](...args); result = Vencord.Webpack[method](...args);
@ -463,7 +510,6 @@ function runTime(token: string) {
} }
setTimeout(() => console.log("[PUPPETEER_TEST_DONE_SIGNAL]"), 1000); setTimeout(() => console.log("[PUPPETEER_TEST_DONE_SIGNAL]"), 1000);
}, 1000));
} catch (e) { } catch (e) {
console.log("[PUP_DEBUG]", "A fatal error occurred:", e); console.log("[PUP_DEBUG]", "A fatal error occurred:", e);
process.exit(1); process.exit(1);
@ -473,7 +519,7 @@ function runTime(token: string) {
await page.evaluateOnNewDocument(` await page.evaluateOnNewDocument(`
${readFileSync("./dist/browser.js", "utf-8")} ${readFileSync("./dist/browser.js", "utf-8")}
;(${runTime.toString()})(${JSON.stringify(process.env.DISCORD_TOKEN)}); ;(${runtime.toString()})(${JSON.stringify(process.env.DISCORD_TOKEN)});
`); `);
await page.goto(CANARY ? "https://canary.discord.com/login" : "https://discord.com/login"); await page.goto(CANARY ? "https://canary.discord.com/login" : "https://discord.com/login");

View file

@ -36,7 +36,7 @@ export interface ProfileBadge {
image?: string; image?: string;
link?: string; link?: string;
/** Action to perform when you click the badge */ /** Action to perform when you click the badge */
onClick?(): void; onClick?(event: React.MouseEvent<HTMLButtonElement, MouseEvent>, props: BadgeUserArgs): void;
/** Should the user display this badge? */ /** Should the user display this badge? */
shouldShow?(userInfo: BadgeUserArgs): boolean; shouldShow?(userInfo: BadgeUserArgs): boolean;
/** Optional props (e.g. style) for the badge, ignored for component badges */ /** Optional props (e.g. style) for the badge, ignored for component badges */
@ -87,9 +87,7 @@ export function _getBadges(args: BadgeUserArgs) {
export interface BadgeUserArgs { export interface BadgeUserArgs {
user: User; user: User;
profile: Profile; guildId: string;
premiumSince: Date;
premiumGuildSince?: Date;
} }
interface ConnectedAccount { interface ConnectedAccount {

View file

@ -9,10 +9,12 @@ import "./contributorModal.css";
import { useSettings } from "@api/Settings"; import { useSettings } from "@api/Settings";
import { classNameFactory } from "@api/Styles"; import { classNameFactory } from "@api/Styles";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { Link } from "@components/Link";
import { DevsById } from "@utils/constants"; import { DevsById } from "@utils/constants";
import { fetchUserProfile, getTheme, Theme } from "@utils/discord"; import { fetchUserProfile, getTheme, Theme } from "@utils/discord";
import { pluralise } from "@utils/misc";
import { ModalContent, ModalRoot, openModal } from "@utils/modal"; import { ModalContent, ModalRoot, openModal } from "@utils/modal";
import { Forms, MaskedLink, showToast, useEffect, useMemo, UserProfileStore, useStateFromStores } from "@webpack/common"; import { Forms, MaskedLink, showToast, Tooltip, useEffect, useMemo, UserProfileStore, useStateFromStores } from "@webpack/common";
import { User } from "discord-types/general"; import { User } from "discord-types/general";
import Plugins from "~plugins"; import Plugins from "~plugins";
@ -72,6 +74,8 @@ function ContributorModal({ user }: { user: User; }) {
.sort((a, b) => Number(a.required ?? false) - Number(b.required ?? false)); .sort((a, b) => Number(a.required ?? false) - Number(b.required ?? false));
}, [user.id, user.username]); }, [user.id, user.username]);
const ContributedHyperLink = <Link href="https://vencord.dev/source">contributed</Link>;
return ( return (
<> <>
<div className={cl("header")}> <div className={cl("header")}>
@ -84,20 +88,37 @@ function ContributorModal({ user }: { user: User; }) {
<div className={cl("links")}> <div className={cl("links")}>
{website && ( {website && (
<MaskedLink <Tooltip text={website}>
href={"https://" + website} {props => (
> <MaskedLink {...props} href={"https://" + website}>
<WebsiteIcon /> <WebsiteIcon />
</MaskedLink> </MaskedLink>
)} )}
</Tooltip>
)}
{githubName && ( {githubName && (
<MaskedLink href={`https://github.com/${githubName}`}> <Tooltip text={githubName}>
{props => (
<MaskedLink {...props} href={`https://github.com/${githubName}`}>
<GithubIcon /> <GithubIcon />
</MaskedLink> </MaskedLink>
)} )}
</Tooltip>
)}
</div> </div>
</div> </div>
{plugins.length ? (
<Forms.FormText>
This person has {ContributedHyperLink} to {pluralise(plugins.length, "plugin")}!
</Forms.FormText>
) : (
<Forms.FormText>
This person has not made any plugins. They likely {ContributedHyperLink} to Vencord in other ways!
</Forms.FormText>
)}
{!!plugins.length && (
<div className={cl("plugins")}> <div className={cl("plugins")}>
{plugins.map(p => {plugins.map(p =>
<PluginCard <PluginCard
@ -108,6 +129,7 @@ function ContributorModal({ user }: { user: User; }) {
/> />
)} )}
</div> </div>
)}
</> </>
); );
} }

View file

@ -25,11 +25,13 @@
display: block; display: block;
position: absolute; position: absolute;
height: 100%; height: 100%;
width: 16px; width: 32px;
background: var(--background-tertiary); background: var(--background-tertiary);
z-index: -1; z-index: -1;
left: -16px; left: -32px;
top: 0; top: 0;
border-top-left-radius: 9999px;
border-bottom-left-radius: 9999px;
} }
.vc-author-modal-avatar { .vc-author-modal-avatar {
@ -55,4 +57,5 @@
.vc-author-modal-plugins { .vc-author-modal-plugins {
display: grid; display: grid;
gap: 0.5em; gap: 0.5em;
margin-top: 0.75em;
} }

View file

@ -27,6 +27,7 @@ import PluginModal from "@components/PluginSettings/PluginModal";
import { AddonCard } from "@components/VencordSettings/AddonCard"; import { AddonCard } from "@components/VencordSettings/AddonCard";
import { SettingsTab } from "@components/VencordSettings/shared"; import { SettingsTab } from "@components/VencordSettings/shared";
import { ChangeList } from "@utils/ChangeList"; import { ChangeList } from "@utils/ChangeList";
import { proxyLazy } from "@utils/lazy";
import { Logger } from "@utils/Logger"; import { Logger } from "@utils/Logger";
import { Margins } from "@utils/margins"; import { Margins } from "@utils/margins";
import { classes, isObjectEmpty } from "@utils/misc"; import { classes, isObjectEmpty } from "@utils/misc";
@ -38,8 +39,8 @@ import { Alerts, Button, Card, Forms, lodash, Parser, React, Select, Text, TextI
import Plugins from "~plugins"; import Plugins from "~plugins";
import { startDependenciesRecursive, startPlugin, stopPlugin } from "../../plugins"; // Avoid circular dependency
const { startDependenciesRecursive, startPlugin, stopPlugin } = proxyLazy(() => require("../../plugins"));
const cl = classNameFactory("vc-plugins-"); const cl = classNameFactory("vc-plugins-");
const logger = new Logger("PluginSettings", "#a6d189"); const logger = new Logger("PluginSettings", "#a6d189");

View file

@ -0,0 +1,3 @@
[class*="profileBadges"] {
flex: none;
}

View file

@ -16,11 +16,14 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import "./fixBadgeOverflow.css";
import { BadgePosition, BadgeUserArgs, ProfileBadge } from "@api/Badges"; import { BadgePosition, BadgeUserArgs, ProfileBadge } from "@api/Badges";
import DonateButton from "@components/DonateButton"; import DonateButton from "@components/DonateButton";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { Flex } from "@components/Flex"; import { Flex } from "@components/Flex";
import { Heart } from "@components/Heart"; import { Heart } from "@components/Heart";
import { openContributorModal } from "@components/PluginSettings/ContributorModal";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { Margins } from "@utils/margins"; import { Margins } from "@utils/margins";
import { isPluginDev } from "@utils/misc"; import { isPluginDev } from "@utils/misc";
@ -34,14 +37,8 @@ const ContributorBadge: ProfileBadge = {
description: "Vencord Contributor", description: "Vencord Contributor",
image: CONTRIBUTOR_BADGE, image: CONTRIBUTOR_BADGE,
position: BadgePosition.START, position: BadgePosition.START,
props: {
style: {
borderRadius: "50%",
transform: "scale(0.9)" // The image is a bit too big compared to default badges
}
},
shouldShow: ({ user }) => isPluginDev(user.id), shouldShow: ({ user }) => isPluginDev(user.id),
link: "https://github.com/Vendicated/Vencord" onClick: (_, { user }) => openContributorModal(user)
}; };
let DonorBadges = {} as Record<string, Array<Record<"tooltip" | "badge", string>>>; let DonorBadges = {} as Record<string, Array<Record<"tooltip" | "badge", string>>>;
@ -79,13 +76,13 @@ export default definePlugin({
}, },
// replace their component with ours if applicable // replace their component with ours if applicable
{ {
match: /(?<=text:(\i)\.description,spacing:12,)children:/, match: /(?<=text:(\i)\.description,spacing:12,.{0,50})children:/,
replace: "children:$1.component ? () => $self.renderBadgeComponent($1) :" replace: "children:$1.component ? () => $self.renderBadgeComponent($1) :"
}, },
// conditionally override their onClick with badge.onClick if it exists // conditionally override their onClick with badge.onClick if it exists
{ {
match: /href:(\i)\.link/, match: /href:(\i)\.link/,
replace: "...($1.onClick && { onClick: $1.onClick }),$&" replace: "...($1.onClick && { onClick: vcE => $1.onClick(vcE, arguments[0]) }),$&"
} }
] ]
} }

View file

@ -16,17 +16,31 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
const settings = definePluginSettings({
disableAnalytics: {
type: OptionType.BOOLEAN,
description: "Disable Discord's tracking (analytics/'science')",
default: true,
restartNeeded: true
}
});
export default definePlugin({ export default definePlugin({
name: "NoTrack", name: "NoTrack",
description: "Disable Discord's tracking ('science'), metrics and Sentry crash reporting", description: "Disable Discord's tracking (analytics/'science'), metrics and Sentry crash reporting",
authors: [Devs.Cyn, Devs.Ven, Devs.Nuckyz, Devs.Arrow], authors: [Devs.Cyn, Devs.Ven, Devs.Nuckyz, Devs.Arrow],
required: true, required: true,
settings,
patches: [ patches: [
{ {
find: "AnalyticsActionHandlers.handle", find: "AnalyticsActionHandlers.handle",
predicate: () => settings.store.disableAnalytics,
replacement: { replacement: {
match: /^.+$/, match: /^.+$/,
replace: "()=>{}", replace: "()=>{}",
@ -44,11 +58,11 @@ export default definePlugin({
replacement: [ replacement: [
{ {
match: /this\._intervalId=/, match: /this\._intervalId=/,
replace: "this._intervalId=undefined&&" replace: "this._intervalId=void 0&&"
}, },
{ {
match: /(increment\(\i\){)/, match: /(?:increment|distribution)\(\i(?:,\i)?\){/g,
replace: "$1return;" replace: "$&return;"
} }
] ]
}, },

View file

@ -17,6 +17,13 @@
*/ */
import { Settings } from "@api/Settings"; import { Settings } from "@api/Settings";
import BackupAndRestoreTab from "@components/VencordSettings/BackupAndRestoreTab";
import CloudTab from "@components/VencordSettings/CloudTab";
import PatchHelperTab from "@components/VencordSettings/PatchHelperTab";
import PluginsTab from "@components/VencordSettings/PluginsTab";
import ThemesTab from "@components/VencordSettings/ThemesTab";
import UpdaterTab from "@components/VencordSettings/UpdaterTab";
import VencordTab from "@components/VencordSettings/VencordTab";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { React } from "@webpack/common"; import { React } from "@webpack/common";
@ -36,7 +43,7 @@ export default definePlugin({
match: /\[\(0,.{1,3}\.jsxs?\)\((.{1,10}),(\{[^{}}]+\{.{0,20}.versionHash,.+?\})\)," "/, match: /\[\(0,.{1,3}\.jsxs?\)\((.{1,10}),(\{[^{}}]+\{.{0,20}.versionHash,.+?\})\)," "/,
replace: (m, component, props) => { replace: (m, component, props) => {
props = props.replace(/children:\[.+\]/, ""); props = props.replace(/children:\[.+\]/, "");
return `${m},Vencord.Plugins.plugins.Settings.makeInfoElements(${component}, ${props})`; return `${m},$self.makeInfoElements(${component}, ${props})`;
} }
} }
] ]
@ -77,43 +84,43 @@ export default definePlugin({
{ {
section: "VencordSettings", section: "VencordSettings",
label: "Vencord", label: "Vencord",
element: require("@components/VencordSettings/VencordTab").default, element: VencordTab,
className: "vc-settings" className: "vc-settings"
}, },
{ {
section: "VencordPlugins", section: "VencordPlugins",
label: "Plugins", label: "Plugins",
element: require("@components/VencordSettings/PluginsTab").default, element: PluginsTab,
className: "vc-plugins" className: "vc-plugins"
}, },
{ {
section: "VencordThemes", section: "VencordThemes",
label: "Themes", label: "Themes",
element: require("@components/VencordSettings/ThemesTab").default, element: ThemesTab,
className: "vc-themes" className: "vc-themes"
}, },
!IS_UPDATER_DISABLED && { !IS_UPDATER_DISABLED && {
section: "VencordUpdater", section: "VencordUpdater",
label: "Updater", label: "Updater",
element: require("@components/VencordSettings/UpdaterTab").default, element: UpdaterTab,
className: "vc-updater" className: "vc-updater"
}, },
{ {
section: "VencordCloud", section: "VencordCloud",
label: "Cloud", label: "Cloud",
element: require("@components/VencordSettings/CloudTab").default, element: CloudTab,
className: "vc-cloud" className: "vc-cloud"
}, },
{ {
section: "VencordSettingsSync", section: "VencordSettingsSync",
label: "Backup & Restore", label: "Backup & Restore",
element: require("@components/VencordSettings/BackupAndRestoreTab").default, element: BackupAndRestoreTab,
className: "vc-backup-restore" className: "vc-backup-restore"
}, },
IS_DEV && { IS_DEV && {
section: "VencordPatchHelper", section: "VencordPatchHelper",
label: "Patch Helper", label: "Patch Helper",
element: require("@components/VencordSettings/PatchHelperTab").default, element: PatchHelperTab,
className: "vc-patch-helper" className: "vc-patch-helper"
}, },
...this.customSections.map(func => func(SectionTypes)), ...this.customSections.map(func => func(SectionTypes)),

View file

@ -16,20 +16,24 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { DataStore } from "@api/index"; import ErrorBoundary from "@components/ErrorBoundary";
import { Link } from "@components/Link";
import { openUpdaterModal } from "@components/VencordSettings/UpdaterTab";
import { Devs, SUPPORT_CHANNEL_ID } from "@utils/constants"; import { Devs, SUPPORT_CHANNEL_ID } from "@utils/constants";
import { Margins } from "@utils/margins";
import { isPluginDev } from "@utils/misc"; import { isPluginDev } from "@utils/misc";
import { relaunch } from "@utils/native";
import { makeCodeblock } from "@utils/text"; import { makeCodeblock } from "@utils/text";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
import { isOutdated } from "@utils/updater"; import { isOutdated, update } from "@utils/updater";
import { Alerts, Forms, UserStore } from "@webpack/common"; import { Alerts, Card, ChannelStore, Forms, GuildMemberStore, NavigationRouter, Parser, RelationshipStore, UserStore } from "@webpack/common";
import gitHash from "~git-hash"; import gitHash from "~git-hash";
import plugins from "~plugins"; import plugins from "~plugins";
import settings from "./settings"; import settings from "./settings";
const REMEMBER_DISMISS_KEY = "Vencord-SupportHelper-Dismiss"; const VENCORD_GUILD_ID = "1015060230222131221";
const AllowedChannelIds = [ const AllowedChannelIds = [
SUPPORT_CHANNEL_ID, SUPPORT_CHANNEL_ID,
@ -37,6 +41,12 @@ const AllowedChannelIds = [
"1033680203433660458", // Vencord > #v "1033680203433660458", // Vencord > #v
]; ];
const TrustedRolesIds = [
"1026534353167208489", // contributor
"1026504932959977532", // regular
"1042507929485586532", // donor
];
export default definePlugin({ export default definePlugin({
name: "SupportHelper", name: "SupportHelper",
required: true, required: true,
@ -44,6 +54,14 @@ export default definePlugin({
authors: [Devs.Ven], authors: [Devs.Ven],
dependencies: ["CommandsAPI"], dependencies: ["CommandsAPI"],
patches: [{
find: ".BEGINNING_DM.format",
replacement: {
match: /BEGINNING_DM\.format\(\{.+?\}\),(?=.{0,100}userId:(\i\.getRecipientId\(\)))/,
replace: "$& $self.ContributorDmWarningCard({ userId: $1 }),"
}
}],
commands: [{ commands: [{
name: "vencord-debug", name: "vencord-debug",
description: "Send Vencord Debug info", description: "Send Vencord Debug info",
@ -64,15 +82,13 @@ export default definePlugin({
const isApiPlugin = (plugin: string) => plugin.endsWith("API") || plugins[plugin].required; const isApiPlugin = (plugin: string) => plugin.endsWith("API") || plugins[plugin].required;
const enabledPlugins = Object.keys(plugins).filter(p => Vencord.Plugins.isPluginEnabled(p) && !isApiPlugin(p)); const enabledPlugins = Object.keys(plugins).filter(p => Vencord.Plugins.isPluginEnabled(p) && !isApiPlugin(p));
const enabledApiPlugins = Object.keys(plugins).filter(p => Vencord.Plugins.isPluginEnabled(p) && isApiPlugin(p));
const info = { const info = {
Vencord: `v${VERSION}${gitHash}${settings.additionalInfo} - ${Intl.DateTimeFormat("en-GB", { dateStyle: "medium" }).format(BUILD_TIMESTAMP)}`, Vencord:
"Discord Branch": RELEASE_CHANNEL, `v${VERSION} • [${gitHash}](<https://github.com/Vendicated/Vencord/commit/${gitHash}>)` +
Client: client, `${settings.additionalInfo} - ${Intl.DateTimeFormat("en-GB", { dateStyle: "medium" }).format(BUILD_TIMESTAMP)}`,
Platform: window.navigator.platform, Client: `${RELEASE_CHANNEL} ~ ${client}`,
Outdated: isOutdated, Platform: window.navigator.platform
OpenAsar: "openasar" in window,
}; };
if (IS_DISCORD_DESKTOP) { if (IS_DISCORD_DESKTOP) {
@ -80,11 +96,10 @@ export default definePlugin({
} }
const debugInfo = ` const debugInfo = `
**Vencord Debug Info** >>> ${Object.entries(info).map(([k, v]) => `**${k}**: ${v}`).join("\n")}
>>> ${Object.entries(info).map(([k, v]) => `${k}: ${v}`).join("\n")}
Enabled Plugins (${enabledPlugins.length + enabledApiPlugins.length}): Enabled Plugins (${enabledPlugins.length}):
${makeCodeblock(enabledPlugins.join(", ") + "\n\n" + enabledApiPlugins.join(", "))} ${makeCodeblock(enabledPlugins.join(", "))}
`; `;
return { return {
@ -97,24 +112,75 @@ ${makeCodeblock(enabledPlugins.join(", ") + "\n\n" + enabledApiPlugins.join(", "
async CHANNEL_SELECT({ channelId }) { async CHANNEL_SELECT({ channelId }) {
if (channelId !== SUPPORT_CHANNEL_ID) return; if (channelId !== SUPPORT_CHANNEL_ID) return;
if (isPluginDev(UserStore.getCurrentUser().id)) return; const selfId = UserStore.getCurrentUser()?.id;
if (!selfId || isPluginDev(selfId)) return;
if (isOutdated && gitHash !== await DataStore.get(REMEMBER_DISMISS_KEY)) { if (isOutdated) {
const rememberDismiss = () => DataStore.set(REMEMBER_DISMISS_KEY, gitHash); return Alerts.show({
Alerts.show({
title: "Hold on!", title: "Hold on!",
body: <div> body: <div>
<Forms.FormText>You are using an outdated version of Vencord! Chances are, your issue is already fixed.</Forms.FormText> <Forms.FormText>You are using an outdated version of Vencord! Chances are, your issue is already fixed.</Forms.FormText>
<Forms.FormText> <Forms.FormText className={Margins.top8}>
Please first update using the Updater Page in Settings, or use the VencordInstaller (Update Vencord Button) Please first update before asking for support!
to do so, in case you can't access the Updater page.
</Forms.FormText> </Forms.FormText>
</div>, </div>,
onCancel: rememberDismiss, onCancel: () => openUpdaterModal!(),
onConfirm: rememberDismiss cancelText: "View Updates",
confirmText: "Update & Restart Now",
async onConfirm() {
await update();
relaunch();
},
secondaryConfirmText: "I know what I'm doing or I can't update"
});
}
// @ts-ignore outdated type
const roles = GuildMemberStore.getSelfMember(VENCORD_GUILD_ID)?.roles;
if (!roles || TrustedRolesIds.some(id => roles.includes(id))) return;
if (IS_UPDATER_DISABLED) {
return Alerts.show({
title: "Hold on!",
body: <div>
<Forms.FormText>You are using an externally updated Vencord version, which we do not provide support for!</Forms.FormText>
<Forms.FormText className={Margins.top8}>
Please either switch to an <Link href="https://vencord.dev/download">officially supported version of Vencord</Link>, or
contact your package maintainer for support instead.
</Forms.FormText>
</div>,
onCloseCallback: () => setTimeout(() => NavigationRouter.back(), 50)
});
}
const repo = await VencordNative.updater.getRepo();
if (repo.ok && !repo.value.includes("Vendicated/Vencord")) {
return Alerts.show({
title: "Hold on!",
body: <div>
<Forms.FormText>You are using a fork of Vencord, which we do not provide support for!</Forms.FormText>
<Forms.FormText className={Margins.top8}>
Please either switch to an <Link href="https://vencord.dev/download">officially supported version of Vencord</Link>, or
contact your package maintainer for support instead.
</Forms.FormText>
</div>,
onCloseCallback: () => setTimeout(() => NavigationRouter.back(), 50)
}); });
} }
} }
} },
ContributorDmWarningCard: ErrorBoundary.wrap(({ userId }) => {
if (!isPluginDev(userId)) return null;
if (RelationshipStore.isFriend(userId)) return null;
return (
<Card className={`vc-plugins-restart-card ${Margins.top8}`}>
Please do not private message Vencord plugin developers for support!
<br />
Instead, use the Vencord support channel: {Parser.parse("https://discord.com/channels/1015060230222131221/1026515880080842772")}
{!ChannelStore.getChannel(SUPPORT_CHANNEL_ID) && " (Click the link to join)"}
</Card>
);
}, { noop: true })
}); });

View file

@ -127,7 +127,7 @@ export default definePlugin({
}, },
// If we are rendering the Better Folders sidebar, we filter out everything but the scroller for the guild list from the GuildsBar Tree children // If we are rendering the Better Folders sidebar, we filter out everything but the scroller for the guild list from the GuildsBar Tree children
{ {
match: /unreadMentionsIndicatorBottom,barClassName.+?}\)\]/, match: /unreadMentionsIndicatorBottom,.+?}\)\]/,
replace: "$&.filter($self.makeGuildsBarTreeFilter(!!arguments[0].isBetterFolders))" replace: "$&.filter($self.makeGuildsBarTreeFilter(!!arguments[0].isBetterFolders))"
}, },
// Export the isBetterFolders variable to the folders component // Export the isBetterFolders variable to the folders component
@ -209,7 +209,7 @@ export default definePlugin({
predicate: () => settings.store.closeAllHomeButton, predicate: () => settings.store.closeAllHomeButton,
replacement: { replacement: {
// Close all folders when clicking the home button // Close all folders when clicking the home button
match: /(?<=onClick:\(\)=>{)(?=.{0,200}"discodo")/, match: /(?<=onClick:\(\)=>{)(?=.{0,300}"discodo")/,
replace: "$self.closeFolders();" replace: "$self.closeFolders();"
} }
} }

View file

@ -20,10 +20,12 @@ import { definePluginSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { ErrorCard } from "@components/ErrorCard"; import { ErrorCard } from "@components/ErrorCard";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { Logger } from "@utils/Logger";
import { Margins } from "@utils/margins"; import { Margins } from "@utils/margins";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy } from "@webpack"; import { findByPropsLazy } from "@webpack";
import { Forms, React } from "@webpack/common"; import { Forms, React, UserStore } from "@webpack/common";
import { User } from "discord-types/general";
const KbdStyles = findByPropsLazy("key", "removeBuildOverride"); const KbdStyles = findByPropsLazy("key", "removeBuildOverride");
@ -68,8 +70,8 @@ export default definePlugin({
predicate: () => settings.store.enableIsStaff, predicate: () => settings.store.enableIsStaff,
replacement: [ replacement: [
{ {
match: /=>*?(\i)\.hasFlag\((\i\.\i)\.STAFF\)}/, match: /(?<=>)(\i)\.hasFlag\((\i\.\i)\.STAFF\)(?=})/,
replace: (_, user, flags) => `=>Vencord.Webpack.Common.UserStore.getCurrentUser()?.id===${user}.id||${user}.hasFlag(${flags}.STAFF)}` replace: (_, user, flags) => `$self.isStaff(${user},${flags})`
}, },
{ {
match: /hasFreePremium\(\){return this.isStaff\(\)\s*?\|\|/, match: /hasFreePremium\(\){return this.isStaff\(\)\s*?\|\|/,
@ -86,6 +88,15 @@ export default definePlugin({
} }
], ],
isStaff(user: User, flags: any) {
try {
return UserStore.getCurrentUser()?.id === user.id || user.hasFlag(flags.STAFF);
} catch (err) {
new Logger("Experiments").error(err);
return user.hasFlag(flags.STAFF);
}
},
settingsAboutComponent: () => { settingsAboutComponent: () => {
const isMacOS = navigator.platform.includes("Mac"); const isMacOS = navigator.platform.includes("Mac");
const modKey = isMacOS ? "cmd" : "ctrl"; const modKey = isMacOS ? "cmd" : "ctrl";

View file

@ -24,7 +24,7 @@ import { getCurrentGuild } from "@utils/discord";
import { Logger } from "@utils/Logger"; import { Logger } from "@utils/Logger";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy, findStoreLazy, proxyLazyWebpack } from "@webpack"; import { findByPropsLazy, findStoreLazy, proxyLazyWebpack } from "@webpack";
import { Alerts, ChannelStore, EmojiStore, FluxDispatcher, Forms, lodash, Parser, PermissionsBits, PermissionStore, UploadHandler, UserSettingsActionCreators, UserStore } from "@webpack/common"; import { Alerts, ChannelStore, EmojiStore, FluxDispatcher, Forms, IconUtils, lodash, Parser, PermissionsBits, PermissionStore, UploadHandler, UserSettingsActionCreators, UserStore } from "@webpack/common";
import type { CustomEmoji } from "@webpack/types"; import type { CustomEmoji } from "@webpack/types";
import type { Message } from "discord-types/general"; import type { Message } from "discord-types/general";
import { applyPalette, GIFEncoder, quantize } from "gifenc"; import { applyPalette, GIFEncoder, quantize } from "gifenc";
@ -166,10 +166,13 @@ const settings = definePluginSettings({
description: "What text the hyperlink should use. {{NAME}} will be replaced with the emoji/sticker name.", description: "What text the hyperlink should use. {{NAME}} will be replaced with the emoji/sticker name.",
type: OptionType.STRING, type: OptionType.STRING,
default: "{{NAME}}" default: "{{NAME}}"
},
disableEmbedPermissionCheck: {
description: "Whether to disable the embed permission check when sending fake emojis and stickers",
type: OptionType.BOOLEAN,
default: false
} }
}).withPrivateSettings<{ });
disableEmbedPermissionCheck: boolean;
}>();
function hasPermission(channelId: string, permission: bigint) { function hasPermission(channelId: string, permission: bigint) {
const channel = ChannelStore.getChannel(channelId); const channel = ChannelStore.getChannel(channelId);
@ -397,6 +400,14 @@ export default definePlugin({
match: /(?<=type:"(?:SOUNDBOARD_SOUNDS_RECEIVED|GUILD_SOUNDBOARD_SOUND_CREATE|GUILD_SOUNDBOARD_SOUND_UPDATE|GUILD_SOUNDBOARD_SOUNDS_UPDATE)".+?available:)\i\.available/g, match: /(?<=type:"(?:SOUNDBOARD_SOUNDS_RECEIVED|GUILD_SOUNDBOARD_SOUND_CREATE|GUILD_SOUNDBOARD_SOUND_UPDATE|GUILD_SOUNDBOARD_SOUNDS_UPDATE)".+?available:)\i\.available/g,
replace: "true" replace: "true"
} }
},
// Allow using custom notification sounds
{
find: "canUseCustomNotificationSounds:function",
replacement: {
match: /canUseCustomNotificationSounds:function\(\i\){/,
replace: "$&return true;"
}
} }
], ],
@ -413,7 +424,8 @@ export default definePlugin({
}, },
handleProtoChange(proto: any, user: any) { handleProtoChange(proto: any, user: any) {
if (proto == null || typeof proto === "string" || !UserSettingsProtoStore || !PreloadedUserSettingsActionCreators || !AppearanceSettingsActionCreators || !ClientThemeSettingsActionsCreators) return; try {
if (proto == null || typeof proto === "string") return;
const premiumType: number = user?.premium_type ?? UserStore?.getCurrentUser()?.premiumType ?? 0; const premiumType: number = user?.premium_type ?? UserStore?.getCurrentUser()?.premiumType ?? 0;
@ -439,6 +451,9 @@ export default definePlugin({
proto.appearance.clientThemeSettings.backgroundGradientPresetId = clientThemeSettingsDummy.backgroundGradientPresetId; proto.appearance.clientThemeSettings.backgroundGradientPresetId = clientThemeSettingsDummy.backgroundGradientPresetId;
} }
} }
} catch (err) {
new Logger("FakeNitro").error(err);
}
}, },
handleGradientThemeSelect(backgroundGradientPresetId: number | undefined, theme: number, original: () => void) { handleGradientThemeSelect(backgroundGradientPresetId: number | undefined, theme: number, original: () => void) {
@ -900,7 +915,7 @@ export default definePlugin({
const emojiString = `<${emoji.animated ? "a" : ""}:${emoji.originalName || emoji.name}:${emoji.id}>`; const emojiString = `<${emoji.animated ? "a" : ""}:${emoji.originalName || emoji.name}:${emoji.id}>`;
const url = new URL(emoji.url); const url = new URL(IconUtils.getEmojiURL({ id: emoji.id, animated: emoji.animated, size: s.emojiSize }));
url.searchParams.set("size", s.emojiSize.toString()); url.searchParams.set("size", s.emojiSize.toString());
url.searchParams.set("name", emoji.name); url.searchParams.set("name", emoji.name);
@ -933,7 +948,7 @@ export default definePlugin({
hasBypass = true; hasBypass = true;
const url = new URL(emoji.url); const url = new URL(IconUtils.getEmojiURL({ id: emoji.id, animated: emoji.animated, size: s.emojiSize }));
url.searchParams.set("size", s.emojiSize.toString()); url.searchParams.set("size", s.emojiSize.toString());
url.searchParams.set("name", emoji.name); url.searchParams.set("name", emoji.name);

View file

@ -9,9 +9,8 @@ import { Devs } from "@utils/constants";
import { getCurrentChannel } from "@utils/discord"; import { getCurrentChannel } from "@utils/discord";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
import { findByPropsLazy } from "@webpack"; import { findByPropsLazy } from "@webpack";
import { React, RelationshipStore } from "@webpack/common"; import { Heading, React, RelationshipStore, Text } from "@webpack/common";
const { Heading, Text } = findByPropsLazy("Heading", "Text");
const container = findByPropsLazy("memberSinceWrapper"); const container = findByPropsLazy("memberSinceWrapper");
const { getCreatedAtDate } = findByPropsLazy("getCreatedAtDate"); const { getCreatedAtDate } = findByPropsLazy("getCreatedAtDate");
const clydeMoreInfo = findByPropsLazy("clydeMoreInfo"); const clydeMoreInfo = findByPropsLazy("clydeMoreInfo");

View file

@ -0,0 +1,3 @@
# ImageLink
If a message consists of only a link to an image, Discord hides the link and shows only the image embed. This plugin makes the link show regardless.

View file

@ -0,0 +1,24 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
export default definePlugin({
name: "ImageLink",
description: "Never hide image links in messages, even if it's the only content",
authors: [Devs.Kyuuhachi, Devs.Sqaaakoi],
patches: [
{
find: "unknownUserMentionPlaceholder:",
replacement: {
match: /\(0,\i\.isEmbedInline\)\(\i\)/,
replace: "false",
}
}
]
});

View file

@ -19,11 +19,12 @@
import { definePluginSettings } from "@api/Settings"; import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { findByProps, findStoreLazy } from "@webpack"; import { findByPropsLazy, findStoreLazy } from "@webpack";
import { ChannelStore, FluxDispatcher, GuildStore, RelationshipStore, SnowflakeUtils, UserStore } from "@webpack/common"; import { ChannelStore, FluxDispatcher, GuildStore, RelationshipStore, SnowflakeUtils, UserStore } from "@webpack/common";
import { Settings } from "Vencord"; import { Settings } from "Vencord";
const UserAffinitiesStore = findStoreLazy("UserAffinitiesStore"); const UserAffinitiesStore = findStoreLazy("UserAffinitiesStore");
const { FriendsSections } = findByPropsLazy("FriendsSections");
interface UserAffinity { interface UserAffinity {
user_id: string; user_id: string;
@ -81,8 +82,8 @@ export default definePlugin({
find: "getRelationshipCounts(){", find: "getRelationshipCounts(){",
replacement: { replacement: {
predicate: () => Settings.plugins.ImplicitRelationships.sortByAffinity, predicate: () => Settings.plugins.ImplicitRelationships.sortByAffinity,
match: /\.sortBy\(\i=>\i\.comparator\)/, match: /\}\)\.sortBy\((.+?)\)\.value\(\)/,
replace: "$&.sortBy((row) => $self.sortList(row))" replace: "}).sortBy(row => $self.wrapSort(($1), row)).value()"
} }
}, },
@ -120,10 +121,10 @@ export default definePlugin({
} }
), ),
sortList(row: any) { wrapSort(comparator: Function, row: any) {
return row.type === 5 return row.type === 5
? -UserAffinitiesStore.getUserAffinity(row.user.id)?.affinity ?? 0 ? -UserAffinitiesStore.getUserAffinity(row.user.id)?.affinity ?? 0
: row.comparator; : comparator(row);
}, },
async fetchImplicitRelationships() { async fetchImplicitRelationships() {
@ -151,7 +152,9 @@ export default definePlugin({
// OP 8 Request Guild Members allows 100 user IDs at a time // OP 8 Request Guild Members allows 100 user IDs at a time
const ignore = new Set(toRequest); const ignore = new Set(toRequest);
const relationships = RelationshipStore.getRelationships(); const relationships = RelationshipStore.getRelationships();
const callback = ({ nonce, members }) => { const callback = ({ chunks }) => {
for (const chunk of chunks) {
const { nonce, members } = chunk;
if (nonce !== sentNonce) return; if (nonce !== sentNonce) return;
members.forEach(member => { members.forEach(member => {
ignore.delete(member.user.id); ignore.delete(member.user.id);
@ -160,11 +163,14 @@ export default definePlugin({
nonFriendAffinities.map(id => UserStore.getUser(id)).filter(user => user && !ignore.has(user.id)).forEach(user => relationships[user.id] = 5); nonFriendAffinities.map(id => UserStore.getUser(id)).filter(user => user && !ignore.has(user.id)).forEach(user => relationships[user.id] = 5);
RelationshipStore.emitChange(); RelationshipStore.emitChange();
if (--count === 0) { if (--count === 0) {
FluxDispatcher.unsubscribe("GUILD_MEMBERS_CHUNK", callback); // @ts-ignore
FluxDispatcher.unsubscribe("GUILD_MEMBERS_CHUNK_BATCH", callback);
}
} }
}; };
FluxDispatcher.subscribe("GUILD_MEMBERS_CHUNK", callback); // @ts-ignore
FluxDispatcher.subscribe("GUILD_MEMBERS_CHUNK_BATCH", callback);
for (let i = 0; i < toRequest.length; i += 100) { for (let i = 0; i < toRequest.length; i += 100) {
FluxDispatcher.dispatch({ FluxDispatcher.dispatch({
type: "GUILD_MEMBERS_REQUEST", type: "GUILD_MEMBERS_REQUEST",
@ -176,7 +182,6 @@ export default definePlugin({
}, },
start() { start() {
const { FriendsSections } = findByProps("FriendsSections");
FriendsSections.IMPLICIT = "IMPLICIT"; FriendsSections.IMPLICIT = "IMPLICIT";
} }
}); });

View file

@ -34,6 +34,10 @@ export const PMLogger = logger;
export const plugins = Plugins; export const plugins = Plugins;
export const patches = [] as Patch[]; export const patches = [] as Patch[];
/** Whether we have subscribed to flux events of all the enabled plugins when FluxDispatcher was ready */
let enabledPluginsSubscribedFlux = false;
const subscribedFluxEventsPlugins = new Set<string>();
const settings = Settings.plugins; const settings = Settings.plugins;
export function isPluginEnabled(p: string) { export function isPluginEnabled(p: string) {
@ -119,6 +123,37 @@ export function startDependenciesRecursive(p: Plugin) {
return { restartNeeded, failures }; return { restartNeeded, failures };
} }
export function subscribePluginFluxEvents(p: Plugin, fluxDispatcher: typeof FluxDispatcher) {
if (p.flux && !subscribedFluxEventsPlugins.has(p.name)) {
subscribedFluxEventsPlugins.add(p.name);
logger.debug("Subscribing to flux events of plugin", p.name);
for (const [event, handler] of Object.entries(p.flux)) {
fluxDispatcher.subscribe(event as FluxEvents, handler);
}
}
}
export function unsubscribePluginFluxEvents(p: Plugin, fluxDispatcher: typeof FluxDispatcher) {
if (p.flux) {
subscribedFluxEventsPlugins.delete(p.name);
logger.debug("Unsubscribing from flux events of plugin", p.name);
for (const [event, handler] of Object.entries(p.flux)) {
fluxDispatcher.unsubscribe(event as FluxEvents, handler);
}
}
}
export function subscribeAllPluginsFluxEvents(fluxDispatcher: typeof FluxDispatcher) {
enabledPluginsSubscribedFlux = true;
for (const name in Plugins) {
if (!isPluginEnabled(name)) continue;
subscribePluginFluxEvents(Plugins[name], fluxDispatcher);
}
}
export const startPlugin = traceFunction("startPlugin", function startPlugin(p: Plugin) { export const startPlugin = traceFunction("startPlugin", function startPlugin(p: Plugin) {
const { name, commands, flux, contextMenus } = p; const { name, commands, flux, contextMenus } = p;
@ -138,7 +173,7 @@ export const startPlugin = traceFunction("startPlugin", function startPlugin(p:
} }
if (commands?.length) { if (commands?.length) {
logger.info("Registering commands of plugin", name); logger.debug("Registering commands of plugin", name);
for (const cmd of commands) { for (const cmd of commands) {
try { try {
registerCommand(cmd, name); registerCommand(cmd, name);
@ -149,13 +184,13 @@ export const startPlugin = traceFunction("startPlugin", function startPlugin(p:
} }
} }
if (flux) { if (enabledPluginsSubscribedFlux) {
for (const event in flux) { subscribePluginFluxEvents(p, FluxDispatcher);
FluxDispatcher.subscribe(event as FluxEvents, flux[event]);
}
} }
if (contextMenus) { if (contextMenus) {
logger.debug("Adding context menus patches of plugin", name);
for (const navId in contextMenus) { for (const navId in contextMenus) {
addContextMenuPatch(navId, contextMenus[navId]); addContextMenuPatch(navId, contextMenus[navId]);
} }
@ -182,7 +217,7 @@ export const stopPlugin = traceFunction("stopPlugin", function stopPlugin(p: Plu
} }
if (commands?.length) { if (commands?.length) {
logger.info("Unregistering commands of plugin", name); logger.debug("Unregistering commands of plugin", name);
for (const cmd of commands) { for (const cmd of commands) {
try { try {
unregisterCommand(cmd.name); unregisterCommand(cmd.name);
@ -193,13 +228,10 @@ export const stopPlugin = traceFunction("stopPlugin", function stopPlugin(p: Plu
} }
} }
if (flux) { unsubscribePluginFluxEvents(p, FluxDispatcher);
for (const event in flux) {
FluxDispatcher.unsubscribe(event as FluxEvents, flux[event]);
}
}
if (contextMenus) { if (contextMenus) {
logger.debug("Removing context menus patches of plugin", name);
for (const navId in contextMenus) { for (const navId in contextMenus) {
removeContextMenuPatch(navId, contextMenus[navId]); removeContextMenuPatch(navId, contextMenus[navId]);
} }

View file

@ -77,7 +77,8 @@ const enum NameFormat {
ArtistFirst = "artist-first", ArtistFirst = "artist-first",
SongFirst = "song-first", SongFirst = "song-first",
ArtistOnly = "artist", ArtistOnly = "artist",
SongOnly = "song" SongOnly = "song",
AlbumName = "album"
} }
const applicationId = "1108588077900898414"; const applicationId = "1108588077900898414";
@ -147,6 +148,10 @@ const settings = definePluginSettings({
{ {
label: "Use song name only", label: "Use song name only",
value: NameFormat.SongOnly value: NameFormat.SongOnly
},
{
label: "Use album name (falls back to custom status text if song has no album)",
value: NameFormat.AlbumName
} }
], ],
}, },
@ -313,6 +318,8 @@ export default definePlugin({
return trackData.artist; return trackData.artist;
case NameFormat.SongOnly: case NameFormat.SongOnly:
return trackData.name; return trackData.name;
case NameFormat.AlbumName:
return trackData.album || settings.store.statusName;
default: default:
return settings.store.statusName; return settings.store.statusName;
} }

View file

@ -17,17 +17,19 @@
*/ */
import { addClickListener, removeClickListener } from "@api/MessageEvents"; import { addClickListener, removeClickListener } from "@api/MessageEvents";
import { definePluginSettings, Settings } from "@api/Settings"; import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy } from "@webpack"; import { findByPropsLazy } from "@webpack";
import { FluxDispatcher, PermissionsBits, PermissionStore, UserStore } from "@webpack/common"; import { FluxDispatcher, PermissionsBits, PermissionStore, UserStore } from "@webpack/common";
const MessageActions = findByPropsLazy("deleteMessage", "startEditMessage");
const EditStore = findByPropsLazy("isEditing", "isEditingAny");
let isDeletePressed = false; let isDeletePressed = false;
const keydown = (e: KeyboardEvent) => e.key === "Backspace" && (isDeletePressed = true); const keydown = (e: KeyboardEvent) => e.key === "Backspace" && (isDeletePressed = true);
const keyup = (e: KeyboardEvent) => e.key === "Backspace" && (isDeletePressed = false); const keyup = (e: KeyboardEvent) => e.key === "Backspace" && (isDeletePressed = false);
const settings = definePluginSettings({ const settings = definePluginSettings({
enableDeleteOnClick: { enableDeleteOnClick: {
type: OptionType.BOOLEAN, type: OptionType.BOOLEAN,
@ -60,9 +62,6 @@ export default definePlugin({
settings, settings,
start() { start() {
const MessageActions = findByPropsLazy("deleteMessage", "startEditMessage");
const EditStore = findByPropsLazy("isEditing", "isEditingAny");
document.addEventListener("keydown", keydown); document.addEventListener("keydown", keydown);
document.addEventListener("keyup", keyup); document.addEventListener("keyup", keyup);
@ -85,11 +84,17 @@ export default definePlugin({
const EPHEMERAL = 64; const EPHEMERAL = 64;
if (msg.hasFlag(EPHEMERAL)) return; if (msg.hasFlag(EPHEMERAL)) return;
const isShiftPress = event.shiftKey && !settings.store.requireModifier;
const NoReplyMention = Vencord.Plugins.plugins.NoReplyMention as any as typeof import("../noReplyMention").default;
const shouldMention = Vencord.Plugins.isPluginEnabled("NoReplyMention")
? NoReplyMention.shouldMention(msg, isShiftPress)
: !isShiftPress;
FluxDispatcher.dispatch({ FluxDispatcher.dispatch({
type: "CREATE_PENDING_REPLY", type: "CREATE_PENDING_REPLY",
channel, channel,
message: msg, message: msg,
shouldMention: !Settings.plugins.NoReplyMention.enabled, shouldMention,
showMentionToggle: channel.guild_id !== null showMentionToggle: channel.guild_id !== null
}); });
} }

View file

@ -0,0 +1,31 @@
# MessageLatency
Displays an indicator for messages that took ≥n seconds to send.
> **NOTE**
>
> - This plugin only applies to messages received after opening the channel
> - False positives can exist if the user's system clock has drifted.
> - Grouped messages only display latency of the first message
## Demo
### Chat View
![chat-view](https://github.com/Vendicated/Vencord/assets/82430093/69430881-60b3-422f-aa3d-c62953837566)
### Clock -ve Drift
![pissbot-on-top](https://github.com/Vendicated/Vencord/assets/82430093/d9248b66-e761-4872-8829-e8bf4fea6ec8)
### Clock +ve Drift
![dumb-ai](https://github.com/Vendicated/Vencord/assets/82430093/0e9783cf-51d5-4559-ae10-42399e7d4099)
### Connection Delay
![who-this](https://github.com/Vendicated/Vencord/assets/82430093/fd68873d-8630-42cc-a166-e9063d2718b2)
### Icons
![icons](https://github.com/Vendicated/Vencord/assets/82430093/17630bd9-44ee-4967-bcdf-3315eb6eca85)

View file

@ -0,0 +1,147 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { definePluginSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import { isNonNullish } from "@utils/guards";
import definePlugin, { OptionType } from "@utils/types";
import { findExportedComponentLazy } from "@webpack";
import { SnowflakeUtils, Tooltip } from "@webpack/common";
import { Message } from "discord-types/general";
type FillValue = ("status-danger" | "status-warning" | "text-muted");
type Fill = [FillValue, FillValue, FillValue];
type DiffKey = keyof Diff;
interface Diff {
days: number,
hours: number,
minutes: number,
seconds: number;
}
const HiddenVisually = findExportedComponentLazy("HiddenVisually");
export default definePlugin({
name: "MessageLatency",
description: "Displays an indicator for messages that took ≥n seconds to send",
authors: [Devs.arHSM],
settings: definePluginSettings({
latency: {
type: OptionType.NUMBER,
description: "Threshold in seconds for latency indicator",
default: 2
}
}),
patches: [
{
find: "showCommunicationDisabledStyles",
replacement: {
match: /(message:(\i),avatar:\i,username:\(0,\i.jsxs\)\(\i.Fragment,\{children:\[)(\i&&)/,
replace: "$1$self.Tooltip()({ message: $2 }),$3"
}
}
],
stringDelta(delta: number) {
const diff: Diff = {
days: Math.round(delta / (60 * 60 * 24)),
hours: Math.round((delta / (60 * 60)) % 24),
minutes: Math.round((delta / (60)) % 60),
seconds: Math.round(delta % 60),
};
const str = (k: DiffKey) => diff[k] > 0 ? `${diff[k]} ${k}` : null;
const keys = Object.keys(diff) as DiffKey[];
return keys.map(str).filter(isNonNullish).join(" ") || "0 seconds";
},
latencyTooltipData(message: Message) {
const { id, nonce } = message;
// Message wasn't received through gateway
if (!isNonNullish(nonce)) return null;
const delta = Math.round((SnowflakeUtils.extractTimestamp(id) - SnowflakeUtils.extractTimestamp(nonce)) / 1000);
// Thanks dziurwa (I hate you)
// This is when the user's clock is ahead
// Can't do anything if the clock is behind
const abs = Math.abs(delta);
const ahead = abs !== delta;
const stringDelta = this.stringDelta(abs);
// Also thanks dziurwa
// 2 minutes
const TROLL_LIMIT = 2 * 60;
const { latency } = this.settings.store;
const fill: Fill = delta >= TROLL_LIMIT || ahead ? ["text-muted", "text-muted", "text-muted"] : delta >= (latency * 2) ? ["status-danger", "text-muted", "text-muted"] : ["status-warning", "status-warning", "text-muted"];
return abs >= latency ? { delta: stringDelta, ahead: abs !== delta, fill } : null;
},
Tooltip() {
return ErrorBoundary.wrap(({ message }: { message: Message; }) => {
const d = this.latencyTooltipData(message);
if (!isNonNullish(d)) return null;
return <Tooltip
text={d.ahead ? `This user's clock is ${d.delta} ahead` : `This message was sent with a delay of ${d.delta}.`}
position="top"
>
{
props => <>
{<this.Icon delta={d.delta} fill={d.fill} props={props} />}
{/* Time Out indicator uses this, I think this is for a11y */}
<HiddenVisually>Delayed Message</HiddenVisually>
</>
}
</Tooltip>;
});
},
Icon({ delta, fill, props }: {
delta: string;
fill: Fill,
props: {
onClick(): void;
onMouseEnter(): void;
onMouseLeave(): void;
onContextMenu(): void;
onFocus(): void;
onBlur(): void;
"aria-label"?: string;
};
}) {
return <svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
width="12"
height="12"
role="img"
fill="none"
style={{ marginRight: "8px", verticalAlign: -1 }}
aria-label={delta}
aria-hidden="false"
{...props}
>
<path
fill={`var(--${fill[0]})`}
d="M4.8001 12C4.8001 11.5576 4.51344 11.2 4.16023 11.2H2.23997C1.88676 11.2 1.6001 11.5576 1.6001 12V13.6C1.6001 14.0424 1.88676 14.4 2.23997 14.4H4.15959C4.5128 14.4 4.79946 14.0424 4.79946 13.6L4.8001 12Z"
/>
<path
fill={`var(--${fill[1]})`}
d="M9.6001 7.12724C9.6001 6.72504 9.31337 6.39998 8.9601 6.39998H7.0401C6.68684 6.39998 6.40011 6.72504 6.40011 7.12724V13.6727C6.40011 14.0749 6.68684 14.4 7.0401 14.4H8.9601C9.31337 14.4 9.6001 14.0749 9.6001 13.6727V7.12724Z"
/>
<path
fill={`var(--${fill[2]})`}
d="M14.4001 2.31109C14.4001 1.91784 14.1134 1.59998 13.7601 1.59998H11.8401C11.4868 1.59998 11.2001 1.91784 11.2001 2.31109V13.6888C11.2001 14.0821 11.4868 14.4 11.8401 14.4H13.7601C14.1134 14.4 14.4001 14.0821 14.4001 13.6888V2.31109Z"
/>
</svg>;
}
});

View file

@ -217,7 +217,9 @@ export default definePlugin({
ignoreChannels.includes(message.channel_id) || ignoreChannels.includes(message.channel_id) ||
ignoreChannels.includes(ChannelStore.getChannel(message.channel_id)?.parent_id) || ignoreChannels.includes(ChannelStore.getChannel(message.channel_id)?.parent_id) ||
(isEdit ? !logEdits : !logDeletes) || (isEdit ? !logEdits : !logDeletes) ||
ignoreGuilds.includes(ChannelStore.getChannel(message.channel_id)?.guild_id); ignoreGuilds.includes(ChannelStore.getChannel(message.channel_id)?.guild_id) ||
// Ignore Venbot in the support channel
(message.channel_id === "1026515880080842772" && message.author?.id === "1017176847865352332");
}, },
// Based on canary 63b8f1b4f2025213c5cf62f0966625bee3d53136 // Based on canary 63b8f1b4f2025213c5cf62f0966625bee3d53136

View file

@ -198,8 +198,7 @@ export default definePlugin({
replacement: [ replacement: [
// make the tag show the right text // make the tag show the right text
{ {
// FIXME: Remove the BOT_TAG_BOT variant when the change arrives in stable match: /(switch\((\i)\){.+?)case (\i(?:\.\i)?)\.BOT:default:(\i)=.{0,40}(\i\.\i\.Messages)\.APP_TAG/,
match: /(switch\((\i)\){.+?)case (\i(?:\.\i)?)\.BOT:default:(\i)=.{0,40}(\i\.\i\.Messages)\.(?:APP_TAG|BOT_TAG_BOT)/,
replace: (_, origSwitch, variant, tags, displayedText, strings) => replace: (_, origSwitch, variant, tags, displayedText, strings) =>
`${origSwitch}default:{${displayedText} = $self.getTagText(${tags}[${variant}], ${strings})}` `${origSwitch}default:{${displayedText} = $self.getTagText(${tags}[${variant}], ${strings})}`
}, },
@ -322,20 +321,19 @@ export default definePlugin({
isOPTag: (tag: number) => tag === Tag.Types.ORIGINAL_POSTER || tags.some(t => tag === Tag.Types[`${t.name}-OP`]), isOPTag: (tag: number) => tag === Tag.Types.ORIGINAL_POSTER || tags.some(t => tag === Tag.Types[`${t.name}-OP`]),
// FIXME: Remove the BOT_TAG_BOT variants from strings when the change arrives in stable
getTagText(passedTagName: string, strings: Record<string, string>) { getTagText(passedTagName: string, strings: Record<string, string>) {
if (!passedTagName) return strings.APP_TAG ?? strings.BOT_TAG_BOT; if (!passedTagName) return strings.APP_TAG;
const [tagName, variant] = passedTagName.split("-"); const [tagName, variant] = passedTagName.split("-");
const tag = tags.find(({ name }) => tagName === name); const tag = tags.find(({ name }) => tagName === name);
if (!tag) return strings.APP_TAG ?? strings.BOT_TAG_BOT; if (!tag) return strings.APP_TAG;
if (variant === "BOT" && tagName !== "WEBHOOK" && this.settings.store.dontShowForBots) return strings.APP_TAG ?? strings.BOT_TAG_BOT; if (variant === "BOT" && tagName !== "WEBHOOK" && this.settings.store.dontShowForBots) return strings.APP_TAG;
const tagText = settings.store.tagSettings?.[tag.name]?.text || tag.displayName; const tagText = settings.store.tagSettings?.[tag.name]?.text || tag.displayName;
switch (variant) { switch (variant) {
case "OP": case "OP":
return `${strings.BOT_TAG_FORUM_ORIGINAL_POSTER}${tagText}`; return `${strings.BOT_TAG_FORUM_ORIGINAL_POSTER}${tagText}`;
case "BOT": case "BOT":
return `${strings.APP_TAG ?? strings.BOT_TAG_BOT}${tagText}`; return `${strings.APP_TAG}${tagText}`;
default: default:
return tagText; return tagText;
} }

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { definePluginSettings,migratePluginSettings } from "@api/Settings"; import { definePluginSettings, migratePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy } from "@webpack"; import { findByPropsLazy } from "@webpack";
@ -31,6 +31,16 @@ const settings = definePluginSettings({
type: OptionType.BOOLEAN, type: OptionType.BOOLEAN,
default: true default: true
}, },
messages: {
description: "Server Notification Settings",
type: OptionType.SELECT,
options: [
{ label: "All messages", value: 0 },
{ label: "Only @mentions", value: 1 },
{ label: "Nothing", value: 2 },
{ label: "Server default", value: 3, default: true }
],
},
everyone: { everyone: {
description: "Suppress @everyone and @here", description: "Suppress @everyone and @here",
type: OptionType.BOOLEAN, type: OptionType.BOOLEAN,
@ -41,6 +51,16 @@ const settings = definePluginSettings({
type: OptionType.BOOLEAN, type: OptionType.BOOLEAN,
default: true default: true
}, },
highlights: {
description: "Suppress Highlights automatically",
type: OptionType.BOOLEAN,
default: true
},
events: {
description: "Mute New Events automatically",
type: OptionType.BOOLEAN,
default: true
},
showAllChannels: { showAllChannels: {
description: "Show all channels automatically", description: "Show all channels automatically",
type: OptionType.BOOLEAN, type: OptionType.BOOLEAN,
@ -53,7 +73,7 @@ export default definePlugin({
name: "NewGuildSettings", name: "NewGuildSettings",
description: "Automatically mute new servers and change various other settings upon joining", description: "Automatically mute new servers and change various other settings upon joining",
tags: ["MuteNewGuild", "mute", "server"], tags: ["MuteNewGuild", "mute", "server"],
authors: [Devs.Glitch, Devs.Nuckyz, Devs.carince, Devs.Mopi], authors: [Devs.Glitch, Devs.Nuckyz, Devs.carince, Devs.Mopi, Devs.GabiRP],
patches: [ patches: [
{ {
find: ",acceptInvite(", find: ",acceptInvite(",
@ -78,8 +98,16 @@ export default definePlugin({
{ {
muted: settings.store.guild, muted: settings.store.guild,
suppress_everyone: settings.store.everyone, suppress_everyone: settings.store.everyone,
suppress_roles: settings.store.role suppress_roles: settings.store.role,
mute_scheduled_events: settings.store.events,
notify_highlights: settings.store.highlights ? 1 : 0
}); });
if (settings.store.messages !== 3) {
updateGuildNotificationSettings(guildId,
{
message_notifications: settings.store.messages,
});
}
if (settings.store.showAllChannels && isOptInEnabledForGuild(guildId)) { if (settings.store.showAllChannels && isOptInEnabledForGuild(guildId)) {
toggleShowAllChannels(guildId); toggleShowAllChannels(guildId);
} }

View file

@ -0,0 +1,5 @@
# PauseInvitesForever
Adds a button to the Security Actions modal to to pause invites indefinitely.
![](https://github.com/Vendicated/Vencord/assets/47677887/e5ba40a3-cb08-462a-8615-fb74dd54c790)

View file

@ -0,0 +1,74 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { findByPropsLazy } from "@webpack";
import { GuildStore, RestAPI } from "@webpack/common";
const Messages = findByPropsLazy("GUILD_INVITE_DISABLE_ACTION_SHEET_DESCRIPTION");
const { InvitesDisabledExperiment } = findByPropsLazy("InvitesDisabledExperiment");
export default definePlugin({
name: "PauseInvitesForever",
tags: ["DisableInvitesForever"],
description: "Brings back the option to pause invites indefinitely that stupit Discord removed.",
authors: [Devs.Dolfies, Devs.amia],
patches: [
{
find: "Messages.GUILD_INVITE_DISABLE_ACTION_SHEET_DESCRIPTION",
replacement: [{
match: /children:\i\.\i\.\i\.GUILD_INVITE_DISABLE_ACTION_SHEET_DESCRIPTION/,
replace: "children: $self.renderInvitesLabel(arguments[0].guildId, setChecked)",
},
{
match: /(\i\.hasDMsDisabled\)\(\i\),\[\i,(\i)\]=\i\.useState\(\i\))/,
replace: "$1,setChecked=$2"
}]
}
],
showDisableInvites(guildId: string) {
// Once the experiment is removed, this should keep working
const { enableInvitesDisabled } = InvitesDisabledExperiment?.getCurrentConfig?.({ guildId }) ?? { enableInvitesDisabled: true };
// @ts-ignore
return enableInvitesDisabled && !GuildStore.getGuild(guildId).hasFeature("INVITES_DISABLED");
},
disableInvites(guildId: string) {
const guild = GuildStore.getGuild(guildId);
const features = [...guild.features, "INVITES_DISABLED"];
RestAPI.patch({
url: `/guilds/${guild.id}`,
body: { features },
});
},
renderInvitesLabel(guildId: string, setChecked: Function) {
return (
<div>
{Messages.GUILD_INVITE_DISABLE_ACTION_SHEET_DESCRIPTION}
{this.showDisableInvites(guildId) && <a role="button" onClick={() => {
setChecked(true);
this.disableInvites(guildId);
}}> Pause Indefinitely.</a>}
</div>
);
}
});

View file

@ -0,0 +1,5 @@
# ReplyTimestamp
Shows timestamps on the previews of replied-to messages. Pretty simple.
![](https://github.com/Vendicated/Vencord/assets/1547062/62e2b67a-e567-4c7a-884d-4640f897f7e0)

View file

@ -0,0 +1,77 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import "./style.css";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { findByPropsLazy } from "@webpack";
import { Timestamp } from "@webpack/common";
import type { Message } from "discord-types/general";
import type { HTMLAttributes } from "react";
const { getMessageTimestampId } = findByPropsLazy("getMessageTimestampId");
const { calendarFormat, dateFormat, isSameDay } = findByPropsLazy("calendarFormat", "dateFormat", "isSameDay", "accessibilityLabelCalendarFormat");
const MessageClasses = findByPropsLazy("separator", "latin24CompactTimeStamp");
function Sep(props: HTMLAttributes<HTMLElement>) {
return <i className={MessageClasses.separator} aria-hidden={true} {...props} />;
}
const enum ReferencedMessageState {
LOADED = 0,
NOT_LOADED = 1,
DELETED = 2,
}
type ReferencedMessage = { state: ReferencedMessageState.LOADED; message: Message; } | { state: ReferencedMessageState.NOT_LOADED | ReferencedMessageState.DELETED; };
function ReplyTimestamp({
referencedMessage,
baseMessage,
}: {
referencedMessage: ReferencedMessage,
baseMessage: Message;
}) {
if (referencedMessage.state !== ReferencedMessageState.LOADED) return null;
const refTimestamp = referencedMessage.message.timestamp as any;
const baseTimestamp = baseMessage.timestamp as any;
return (
<Timestamp
id={getMessageTimestampId(referencedMessage.message)}
className="vc-reply-timestamp"
compact={isSameDay(refTimestamp, baseTimestamp)}
timestamp={refTimestamp}
isInline={false}
>
<Sep>[</Sep>
{isSameDay(refTimestamp, baseTimestamp)
? dateFormat(refTimestamp, "LT")
: calendarFormat(refTimestamp)
}
<Sep>]</Sep>
</Timestamp>
);
}
export default definePlugin({
name: "ReplyTimestamp",
description: "Shows a timestamp on replied-message previews",
authors: [Devs.Kyuuhachi],
patches: [
{
find: "renderSingleLineMessage:function()",
replacement: {
match: /(?<="aria-label":\i,children:\[)(?=\i,\i,\i\])/,
replace: "$self.ReplyTimestamp(arguments[0]),"
}
}
],
ReplyTimestamp: ErrorBoundary.wrap(ReplyTimestamp, { noop: true }),
});

View file

@ -0,0 +1,3 @@
.vc-reply-timestamp {
margin-right: 0.25em;
}

View file

@ -91,7 +91,7 @@ const settings = definePluginSettings({
export default definePlugin({ export default definePlugin({
name: "ResurrectHome", name: "ResurrectHome",
description: "Re-enables the Server Home tab when there isn't a Server Guide. Also has an option to force the Server Home over the Server Guide, which is accessible through right-clicking the Server Guide.", description: "Re-enables the Server Home tab when there isn't a Server Guide. Also has an option to force the Server Home over the Server Guide, which is accessible through right-clicking a server.",
authors: [Devs.Dolfies, Devs.Nuckyz], authors: [Devs.Dolfies, Devs.Nuckyz],
settings, settings,
@ -151,7 +151,7 @@ export default definePlugin({
find: "487e85_1", find: "487e85_1",
replacement: { replacement: {
match: /(?<=text:(\i)\?\i\.\i\.Messages\.SERVER_GUIDE:\i\.\i\.Messages\.GUILD_HOME,)/, match: /(?<=text:(\i)\?\i\.\i\.Messages\.SERVER_GUIDE:\i\.\i\.Messages\.GUILD_HOME,)/,
replace: "badge:$self.ViewServerHomeButton({serverGuide:$1})," replace: "trailing:$self.ViewServerHomeButton({serverGuide:$1}),"
} }
}, },
// Disable view Server Home override when the Server Home is unmouted // Disable view Server Home override when the Server Home is unmouted

View file

@ -23,7 +23,6 @@ import ErrorBoundary from "@components/ErrorBoundary";
import ExpandableHeader from "@components/ExpandableHeader"; import ExpandableHeader from "@components/ExpandableHeader";
import { OpenExternalIcon } from "@components/Icons"; import { OpenExternalIcon } from "@components/Icons";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { Logger } from "@utils/Logger";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
import { Alerts, Menu, Parser, useState } from "@webpack/common"; import { Alerts, Menu, Parser, useState } from "@webpack/common";
import { Guild, User } from "discord-types/general"; import { Guild, User } from "discord-types/general";
@ -36,13 +35,26 @@ import { getCurrentUserInfo, readNotification } from "./reviewDbApi";
import { settings } from "./settings"; import { settings } from "./settings";
import { showToast } from "./utils"; import { showToast } from "./utils";
const guildPopoutPatch: NavContextMenuPatchCallback = (children, props: { guild: Guild, onClose(): void; }) => { const guildPopoutPatch: NavContextMenuPatchCallback = (children, { guild }: { guild: Guild, onClose(): void; }) => {
if (!guild) return;
children.push( children.push(
<Menu.MenuItem <Menu.MenuItem
label="View Reviews" label="View Reviews"
id="vc-rdb-server-reviews" id="vc-rdb-server-reviews"
icon={OpenExternalIcon} icon={OpenExternalIcon}
action={() => openReviewsModal(props.guild.id, props.guild.name)} action={() => openReviewsModal(guild.id, guild.name)}
/>
);
};
const userContextPatch: NavContextMenuPatchCallback = (children, { user }: { user?: User, onClose(): void; }) => {
if (!user) return;
children.push(
<Menu.MenuItem
label="View Reviews"
id="vc-rdb-user-reviews"
icon={OpenExternalIcon}
action={() => openReviewsModal(user.id, user.username)}
/> />
); );
}; };
@ -54,7 +66,10 @@ export default definePlugin({
settings, settings,
contextMenus: { contextMenus: {
"guild-header-popout": guildPopoutPatch "guild-header-popout": guildPopoutPatch,
"guild-context": guildPopoutPatch,
"user-context": userContextPatch,
"user-profile-actions": userContextPatch
}, },
patches: [ patches: [
@ -75,13 +90,6 @@ export default definePlugin({
const s = settings.store; const s = settings.store;
const { lastReviewId, notifyReviews } = s; const { lastReviewId, notifyReviews } = s;
const legacy = s as any as { token?: string; };
if (legacy.token) {
await updateAuth({ token: legacy.token });
legacy.token = undefined;
new Logger("ReviewDB").info("Migrated legacy settings");
}
await initAuth(); await initAuth();
setTimeout(async () => { setTimeout(async () => {

View file

@ -94,7 +94,7 @@ export default definePlugin({
find: "renderPrioritySpeaker", find: "renderPrioritySpeaker",
replacement: [ replacement: [
{ {
match: /renderName\(\).{0,100}speaking:.+?\.clanTag.+?"div",{/, match: /renderName\(\){.+?usernameSpeaking\]:.+?(?=children)/,
replace: "$&...$self.getVoiceProps(this.props)," replace: "$&...$self.getVoiceProps(this.props),"
} }
], ],

View file

@ -14,10 +14,11 @@ export default definePlugin({
authors: [Devs.AndrewDLO, Devs.FieryFlames], authors: [Devs.AndrewDLO, Devs.FieryFlames],
patches: [ patches: [
{ {
find: "call_ringing_beat\"", find: '"call_ringing_beat"',
replacement: { replacement: {
match: /500===\i\(\)\.random\(1,1e3\)/, // FIXME Remove === alternative when it hits stable
replace: "true" match: /500(!==|===)\i\(\)\.random\(1,1e3\)/,
replace: (_, predicate) => predicate === "!==" ? "false" : "true",
} }
}, },
], ],

View file

@ -36,6 +36,8 @@ const enum ShowMode {
HiddenIconWithMutedStyle HiddenIconWithMutedStyle
} }
const CONNECT = 1n << 20n;
export const settings = definePluginSettings({ export const settings = definePluginSettings({
hideUnreads: { hideUnreads: {
description: "Hide Unreads", description: "Hide Unreads",
@ -273,12 +275,12 @@ export default definePlugin({
{ {
// Change the role permission check to CONNECT if the channel is locked // Change the role permission check to CONNECT if the channel is locked
match: /ADMINISTRATOR\)\|\|(?<=context:(\i)}.+?)(?=(.+?)VIEW_CHANNEL)/, match: /ADMINISTRATOR\)\|\|(?<=context:(\i)}.+?)(?=(.+?)VIEW_CHANNEL)/,
replace: (m, channel, permCheck) => `${m}!Vencord.Webpack.Common.PermissionStore.can(${PermissionsBits.CONNECT}n,${channel})?${permCheck}CONNECT):` replace: (m, channel, permCheck) => `${m}!Vencord.Webpack.Common.PermissionStore.can(${CONNECT}n,${channel})?${permCheck}CONNECT):`
}, },
{ {
// Change the permissionOverwrite check to CONNECT if the channel is locked // Change the permissionOverwrite check to CONNECT if the channel is locked
match: /permissionOverwrites\[.+?\i=(?<=context:(\i)}.+?)(?=(.+?)VIEW_CHANNEL)/, match: /permissionOverwrites\[.+?\i=(?<=context:(\i)}.+?)(?=(.+?)VIEW_CHANNEL)/,
replace: (m, channel, permCheck) => `${m}!Vencord.Webpack.Common.PermissionStore.can(${PermissionsBits.CONNECT}n,${channel})?${permCheck}CONNECT):` replace: (m, channel, permCheck) => `${m}!Vencord.Webpack.Common.PermissionStore.can(${CONNECT}n,${channel})?${permCheck}CONNECT):`
}, },
{ {
// Include the @everyone role in the allowed roles list for Hidden Channels // Include the @everyone role in the allowed roles list for Hidden Channels

View file

@ -9,3 +9,9 @@ Displays various moderator-only elements regardless of permissions.
- Show the invites paused tooltip in the server list - Show the invites paused tooltip in the server list
![](https://github.com/Vendicated/Vencord/assets/47677887/b6a923d2-ac55-40d9-b4f8-fa6fc117148b) ![](https://github.com/Vendicated/Vencord/assets/47677887/b6a923d2-ac55-40d9-b4f8-fa6fc117148b)
- Show the member mod view context menu item in all servers
![](https://github.com/Vendicated/Vencord/assets/47677887/3dac95dd-841c-4c15-ad87-2db7bd1e4dab)
- Disable filters in Server Discovery search that hide servers that don't meet discovery criteria

View file

@ -31,12 +31,22 @@ const settings = definePluginSettings({
description: "Show the invites paused tooltip in the server list.", description: "Show the invites paused tooltip in the server list.",
default: true, default: true,
}, },
showModView: {
type: OptionType.BOOLEAN,
description: "Show the member mod view context menu item in all servers.",
default: true,
},
disableDiscoveryFilters: {
type: OptionType.BOOLEAN,
description: "Disable filters in Server Discovery search that hide servers that don't meet discovery criteria.",
default: true,
},
}); });
migratePluginSettings("ShowHiddenThings", "ShowTimeouts"); migratePluginSettings("ShowHiddenThings", "ShowTimeouts");
export default definePlugin({ export default definePlugin({
name: "ShowHiddenThings", name: "ShowHiddenThings",
tags: ["ShowTimeouts", "ShowInvitesPaused"], tags: ["ShowTimeouts", "ShowInvitesPaused", "ShowModView", "DisableDiscoveryFilters"],
description: "Displays various moderator-only elements regardless of permissions.", description: "Displays various moderator-only elements regardless of permissions.",
authors: [Devs.Dolfies], authors: [Devs.Dolfies],
patches: [ patches: [
@ -55,6 +65,22 @@ export default definePlugin({
match: /\i\.\i\.can\(\i\.Permissions.MANAGE_GUILD,\i\)/, match: /\i\.\i\.can\(\i\.Permissions.MANAGE_GUILD,\i\)/,
replace: "true", replace: "true",
}, },
},
{
find: "canAccessGuildMemberModViewWithExperiment:",
predicate: () => settings.store.showModView,
replacement: {
match: /return \i\.hasAny\(\i\.computePermissions\(\{user:\i,context:\i,checkElevated:!1\}\),\i\.MemberSafetyPagePermissions\)/,
replace: "return true",
}
},
{
find: "auto_removed:",
predicate: () => settings.store.disableDiscoveryFilters,
replacement: {
match: /filters:\i\.join\(" AND "\),facets:\[/,
replace: "facets:["
}
} }
], ],
settings, settings,

View file

@ -41,8 +41,8 @@ export default definePlugin({
patches: [{ patches: [{
find: "getRelationshipCounts(){", find: "getRelationshipCounts(){",
replacement: { replacement: {
match: /\.sortBy\(\i=>\i\.comparator\)/, match: /\}\)\.sortBy\((.+?)\)\.value\(\)/,
replace: "$&.sortBy((row) => $self.sortList(row))" replace: "}).sortBy(row => $self.wrapSort(($1), row)).value()"
} }
}, { }, {
find: ".Messages.FRIEND_REQUEST_CANCEL", find: ".Messages.FRIEND_REQUEST_CANCEL",
@ -53,10 +53,10 @@ export default definePlugin({
} }
}], }],
sortList(row: any) { wrapSort(comparator: Function, row: any) {
return row.type === 3 || row.type === 4 return row.type === 3 || row.type === 4
? -this.getSince(row.user) ? -this.getSince(row.user)
: row.comparator; : comparator(row);
}, },
getSince(user: User) { getSince(user: User) {

View file

@ -24,9 +24,14 @@ import definePlugin, { OptionType } from "@utils/types";
import style from "./index.css?managed"; import style from "./index.css?managed";
const BASE_URL = "https://raw.githubusercontent.com/AutumnVN/usrbg/main/usrbg.json"; const API_URL = "https://usrbg.is-hardly.online/users";
let data = {} as Record<string, string>; interface UsrbgApiReturn {
endpoint: string
bucket: string
prefix: string
users: Record<string, string>
}
const settings = definePluginSettings({ const settings = definePluginSettings({
nitroFirst: { nitroFirst: {
@ -48,7 +53,7 @@ const settings = definePluginSettings({
export default definePlugin({ export default definePlugin({
name: "USRBG", name: "USRBG",
description: "Displays user banners from USRBG, allowing anyone to get a banner without Nitro", description: "Displays user banners from USRBG, allowing anyone to get a banner without Nitro",
authors: [Devs.AutumnVN, Devs.pylix, Devs.TheKodeToad], authors: [Devs.AutumnVN, Devs.katlyn, Devs.pylix, Devs.TheKodeToad],
settings, settings,
patches: [ patches: [
{ {
@ -80,8 +85,7 @@ export default definePlugin({
} }
], ],
data: null as UsrbgApiReturn | null,
data,
settingsAboutComponent: () => { settingsAboutComponent: () => {
return ( return (
@ -91,9 +95,9 @@ export default definePlugin({
voiceBackgroundHook({ className, participantUserId }: any) { voiceBackgroundHook({ className, participantUserId }: any) {
if (className.includes("tile_")) { if (className.includes("tile_")) {
if (data[participantUserId]) { if (this.userHasBackground(participantUserId)) {
return { return {
backgroundImage: `url(${data[participantUserId]})`, backgroundImage: `url(${this.getImageUrl(participantUserId)})`,
backgroundSize: "cover", backgroundSize: "cover",
backgroundPosition: "center", backgroundPosition: "center",
backgroundRepeat: "no-repeat" backgroundRepeat: "no-repeat"
@ -104,24 +108,35 @@ export default definePlugin({
useBannerHook({ displayProfile, user }: any) { useBannerHook({ displayProfile, user }: any) {
if (displayProfile?.banner && settings.store.nitroFirst) return; if (displayProfile?.banner && settings.store.nitroFirst) return;
if (data[user.id]) return data[user.id]; if (this.userHasBackground(user.id)) return this.getImageUrl(user.id);
}, },
premiumHook({ userId }: any) { premiumHook({ userId }: any) {
if (data[userId]) return 2; if (this.userHasBackground(userId)) return 2;
}, },
shouldShowBadge({ displayProfile, user }: any) { shouldShowBadge({ displayProfile, user }: any) {
return displayProfile?.banner && (!data[user.id] || settings.store.nitroFirst); return displayProfile?.banner && (!this.userHasBackground(user.id) || settings.store.nitroFirst);
},
userHasBackground(userId: string) {
return !!this.data?.users[userId];
},
getImageUrl(userId: string): string|null {
if (!this.userHasBackground(userId)) return null;
// We can assert that data exists because userHasBackground returned true
const { endpoint, bucket, prefix, users: { [userId]: etag } } = this.data!;
return `${endpoint}/${bucket}/${prefix}${userId}?${etag}`;
}, },
async start() { async start() {
enableStyle(style); enableStyle(style);
const res = await fetch(BASE_URL); const res = await fetch(API_URL);
if (res.ok) { if (res.ok) {
data = await res.json(); this.data = await res.json();
this.data = data;
} }
} }
}); });

View file

@ -21,12 +21,37 @@ import { Devs } from "@utils/constants";
import { sleep } from "@utils/misc"; import { sleep } from "@utils/misc";
import { Queue } from "@utils/Queue"; import { Queue } from "@utils/Queue";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
import { UserStore, UserUtils, useState } from "@webpack/common"; import { Constants, FluxDispatcher, RestAPI, UserProfileStore, UserStore, useState } from "@webpack/common";
import type { ComponentType, ReactNode } from "react"; import type { ComponentType, ReactNode } from "react";
// LYING to the type checker here
const UserFlags = Constants.UserFlags as Record<string, number>;
const badges: Record<string, ProfileBadge> = {
"active_developer": { id: "active_developer", description: "Active Developer", icon: "6bdc42827a38498929a4920da12695d9", link: "https://support-dev.discord.com/hc/en-us/articles/10113997751447" },
"bug_hunter_level_1": { id: "bug_hunter_level_1", description: "Discord Bug Hunter", icon: "2717692c7dca7289b35297368a940dd0", link: "https://support.discord.com/hc/en-us/articles/360046057772-Discord-Bugs" },
"bug_hunter_level_2": { id: "bug_hunter_level_2", description: "Discord Bug Hunter", icon: "848f79194d4be5ff5f81505cbd0ce1e6", link: "https://support.discord.com/hc/en-us/articles/360046057772-Discord-Bugs" },
"certified_moderator": { id: "certified_moderator", description: "Moderator Programs Alumni", icon: "fee1624003e2fee35cb398e125dc479b", link: "https://discord.com/safety" },
"discord_employee": { id: "staff", description: "Discord Staff", icon: "5e74e9b61934fc1f67c65515d1f7e60d", link: "https://discord.com/company" },
"hypesquad": { id: "hypesquad", description: "HypeSquad Events", icon: "bf01d1073931f921909045f3a39fd264", link: "https://discord.com/hypesquad" },
"hypesquad_online_house_1": { id: "hypesquad_house_1", description: "HypeSquad Bravery", icon: "8a88d63823d8a71cd5e390baa45efa02", link: "https://discord.com/settings/hypesquad-online" },
"hypesquad_online_house_2": { id: "hypesquad_house_2", description: "HypeSquad Brilliance", icon: "011940fd013da3f7fb926e4a1cd2e618", link: "https://discord.com/settings/hypesquad-online" },
"hypesquad_online_house_3": { id: "hypesquad_house_3", description: "HypeSquad Balance", icon: "3aa41de486fa12454c3761e8e223442e", link: "https://discord.com/settings/hypesquad-online" },
"partner": { id: "partner", description: "Partnered Server Owner", icon: "3f9748e53446a137a052f3454e2de41e", link: "https://discord.com/partners" },
"premium": { id: "premium", description: "Subscriber", icon: "2ba85e8026a8614b640c2837bcdfe21b", link: "https://discord.com/settings/premium" },
"premium_early_supporter": { id: "early_supporter", description: "Early Supporter", icon: "7060786766c9c840eb3019e725d2b358", link: "https://discord.com/settings/premium" },
"verified_developer": { id: "verified_developer", description: "Early Verified Bot Developer", icon: "6df5892e0f35b051f8b61eace34f4967" },
};
const fetching = new Set<string>(); const fetching = new Set<string>();
const queue = new Queue(5); const queue = new Queue(5);
interface ProfileBadge {
id: string;
description: string;
icon: string;
link?: string;
}
interface MentionProps { interface MentionProps {
data: { data: {
userId?: string; userId?: string;
@ -43,6 +68,45 @@ interface MentionProps {
UserMention: ComponentType<any>; UserMention: ComponentType<any>;
} }
async function getUser(id: string) {
let userObj = UserStore.getUser(id);
if (userObj)
return userObj;
const user: any = await RestAPI.get({ url: `/users/${id}` }).then(response => {
FluxDispatcher.dispatch({
type: "USER_UPDATE",
user: response.body,
});
return response.body;
});
// Populate the profile
await FluxDispatcher.dispatch(
{
type: "USER_PROFILE_FETCH_FAILURE",
userId: id,
}
);
userObj = UserStore.getUser(id);
const fakeBadges: ProfileBadge[] = Object.entries(UserFlags)
.filter(([_, flag]) => !isNaN(flag) && userObj.hasFlag(flag))
.map(([key]) => badges[key.toLowerCase()]);
if (user.premium_type || !user.bot && (user.banner || user.avatar?.startsWith?.("a_")))
fakeBadges.push(badges.premium);
// Fill in what we can deduce
const profile = UserProfileStore.getUserProfile(id);
profile.accentColor = user.accent_color;
profile.badges = fakeBadges;
profile.banner = user.banner;
profile.premiumType = user.premium_type;
return userObj;
}
function MentionWrapper({ data, UserMention, RoleMention, parse, props }: MentionProps) { function MentionWrapper({ data, UserMention, RoleMention, parse, props }: MentionProps) {
const [userId, setUserId] = useState(data.userId); const [userId, setUserId] = useState(data.userId);
@ -85,14 +149,14 @@ function MentionWrapper({ data, UserMention, RoleMention, parse, props }: Mentio
fetching.add(id); fetching.add(id);
queue.unshift(() => queue.unshift(() =>
UserUtils.getUser(id) getUser(id)
.then(() => { .then(() => {
setUserId(id); setUserId(id);
fetching.delete(id); fetching.delete(id);
}) })
.catch(e => { .catch(e => {
if (e?.status === 429) { if (e?.status === 429) {
queue.unshift(() => sleep(1000).then(fetch)); queue.unshift(() => sleep(e?.body?.retry_after ?? 1000).then(fetch));
fetching.delete(id); fetching.delete(id);
} }
}) })
@ -112,7 +176,7 @@ function MentionWrapper({ data, UserMention, RoleMention, parse, props }: Mentio
export default definePlugin({ export default definePlugin({
name: "ValidUser", name: "ValidUser",
description: "Fix mentions for unknown users showing up as '@unknown-user' (hover over a mention to fix it)", description: "Fix mentions for unknown users showing up as '@unknown-user' (hover over a mention to fix it)",
authors: [Devs.Ven], authors: [Devs.Ven, Devs.Dolfies],
tags: ["MentionCacheFix"], tags: ["MentionCacheFix"],
patches: [ patches: [

View file

@ -0,0 +1,52 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import "./style.css";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
export default definePlugin({
name: "VoiceDownload",
description: "Adds a download to voice messages. (Opens a new browser tab)",
authors: [Devs.puv],
patches: [
{
find: "rippleContainer,children",
replacement: {
match: /\(0,\i\.jsx\).{0,150},children:.{0,50}\("source",{src:(\i)}\)}\)/,
replace: "[$&, $self.renderDownload($1)]"
}
}
],
renderDownload(src: string) {
return (
<a
className="vc-voice-download"
href={src}
download="voice-message.ogg"
onClick={e => e.stopPropagation()}
aria-label="Download voice message"
>
<this.Icon />
</a>
);
},
Icon: () => (
<svg
height="24"
width="24"
viewBox="0 0 24 24"
fill="currentColor"
>
<path
d="M12 2a1 1 0 0 1 1 1v10.59l3.3-3.3a1 1 0 1 1 1.4 1.42l-5 5a1 1 0 0 1-1.4 0l-5-5a1 1 0 1 1 1.4-1.42l3.3 3.3V3a1 1 0 0 1 1-1ZM3 20a1 1 0 1 0 0 2h18a1 1 0 1 0 0-2H3Z"
/>
</svg>
),
});

View file

@ -0,0 +1,12 @@
.vc-voice-download {
width: 24px;
height: 24px;
color: var(--interactive-normal);
margin-left: 12px;
cursor: pointer;
position: relative;
}
.vc-voice-download:hover {
color: var(--interactive-active);
}

View file

@ -0,0 +1,30 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
export default definePlugin({
name: "WebScreenShareFixes",
authors: [Devs.Kaitlyn],
description: "Removes 2500kbps bitrate cap on chromium and vesktop clients.",
enabledByDefault: true,
patches: [
{
find: "x-google-max-bitrate",
replacement: [
{
match: /"x-google-max-bitrate=".concat\(\i\)/,
replace: '"x-google-max-bitrate=".concat("80_000")'
},
{
match: /;level-asymmetry-allowed=1/,
replace: ";b=AS:800000;level-asymmetry-allowed=1"
}
]
}
]
});

View file

@ -266,6 +266,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({
name: "Dziurwa", name: "Dziurwa",
id: 1001086404203389018n id: 1001086404203389018n
}, },
arHSM: {
name: "arHSM",
id: 841509053422632990n
},
F53: { F53: {
name: "F53", name: "F53",
id: 280411966126948353n id: 280411966126948353n
@ -426,6 +430,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({
name: "newwares", name: "newwares",
id: 421405303951851520n id: 421405303951851520n
}, },
puv: {
name: "puv",
id: 469441552251355137n
},
Kodarru: { Kodarru: {
name: "Kodarru", name: "Kodarru",
id: 785227396218748949n id: 785227396218748949n
@ -449,6 +457,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({
PolisanTheEasyNick: { PolisanTheEasyNick: {
name: "Oleh Polisan", name: "Oleh Polisan",
id: 242305263313485825n id: 242305263313485825n
},
GabiRP: {
name: "GabiRP",
id: 507955112027750401n
} }
} satisfies Record<string, Dev>); } satisfies Record<string, Dev>);

View file

@ -114,3 +114,7 @@ export function identity<T>(value: T): T {
export const isMobile = navigator.userAgent.includes("Mobi"); export const isMobile = navigator.userAgent.includes("Mobi");
export const isPluginDev = (id: string) => Object.hasOwn(DevsById, id); export const isPluginDev = (id: string) => Object.hasOwn(DevsById, id);
export function pluralise(amount: number, singular: string, plural = singular + "s") {
return amount === 1 ? `${amount} ${singular}` : `${amount} ${plural}`;
}

View file

@ -18,20 +18,20 @@
import { PatchReplacement, ReplaceFn } from "./types"; import { PatchReplacement, ReplaceFn } from "./types";
export function canonicalizeMatch(match: RegExp | string) { export function canonicalizeMatch<T extends RegExp | string>(match: T): T {
if (typeof match === "string") return match; if (typeof match === "string") return match;
const canonSource = match.source const canonSource = match.source
.replaceAll("\\i", "[A-Za-z_$][\\w$]*"); .replaceAll("\\i", "[A-Za-z_$][\\w$]*");
return new RegExp(canonSource, match.flags); return new RegExp(canonSource, match.flags) as T;
} }
export function canonicalizeReplace(replace: string | ReplaceFn, pluginName: string): string | ReplaceFn { export function canonicalizeReplace<T extends string | ReplaceFn>(replace: T, pluginName: string): T {
const self = `Vencord.Plugins.plugins[${JSON.stringify(pluginName)}]`; const self = `Vencord.Plugins.plugins[${JSON.stringify(pluginName)}]`;
if (typeof replace !== "function") if (typeof replace !== "function")
return replace.replaceAll("$self", self); return replace.replaceAll("$self", self) as T;
return (...args) => replace(...args).replaceAll("$self", self); return ((...args) => replace(...args).replaceAll("$self", self)) as T;
} }
export function canonicalizeDescriptor<T>(descriptor: TypedPropertyDescriptor<T>, canonicalize: (value: T) => T) { export function canonicalizeDescriptor<T>(descriptor: TypedPropertyDescriptor<T>, canonicalize: (value: T) => T) {

View file

@ -36,6 +36,7 @@ export let Tooltip: t.Tooltip;
export let TextInput: t.TextInput; export let TextInput: t.TextInput;
export let TextArea: t.TextArea; export let TextArea: t.TextArea;
export let Text: t.Text; export let Text: t.Text;
export let Heading: t.Heading;
export let Select: t.Select; export let Select: t.Select;
export let SearchableSelect: t.SearchableSelect; export let SearchableSelect: t.SearchableSelect;
export let Slider: t.Slider; export let Slider: t.Slider;
@ -59,6 +60,28 @@ export const Flex = waitForComponent<t.Flex>("Flex", ["Justify", "Align", "Wrap"
export const { OAuth2AuthorizeModal } = findByPropsLazy("OAuth2AuthorizeModal"); export const { OAuth2AuthorizeModal } = findByPropsLazy("OAuth2AuthorizeModal");
waitFor(["FormItem", "Button"], m => { waitFor(["FormItem", "Button"], m => {
({ useToken, Card, Button, FormSwitch: Switch, Tooltip, TextInput, TextArea, Text, Select, SearchableSelect, Slider, ButtonLooks, TabBar, Popout, Dialog, Paginator, ScrollerThin, Clickable, Avatar, FocusLock } = m); ({
useToken,
Card,
Button,
FormSwitch: Switch,
Tooltip,
TextInput,
TextArea,
Text,
Select,
SearchableSelect,
Slider,
ButtonLooks,
TabBar,
Popout,
Dialog,
Paginator,
ScrollerThin,
Clickable,
Avatar,
FocusLock,
Heading
} = m);
Forms = m; Forms = m;
}); });

View file

@ -64,23 +64,15 @@ export let DraftStore: t.DraftStore;
/** /**
* React hook that returns stateful data for one or more stores * React hook that returns stateful data for one or more stores
* You might need a custom comparator (4th argument) if your store data is an object * You might need a custom comparator (4th argument) if your store data is an object
*
* @param stores The stores to listen to * @param stores The stores to listen to
* @param mapper A function that returns the data you need * @param mapper A function that returns the data you need
* @param idk some thing, idk just pass null * @param dependencies An array of reactive values which the hook depends on. Use this if your mapper or equality function depends on the value of another hook
* @param isEqual A custom comparator for the data returned by mapper * @param isEqual A custom comparator for the data returned by mapper
* *
* @example const user = useStateFromStores([UserStore], () => UserStore.getCurrentUser(), null, (old, current) => old.id === current.id); * @example const user = useStateFromStores([UserStore], () => UserStore.getCurrentUser(), null, (old, current) => old.id === current.id);
*/ */
export const { useStateFromStores }: { // eslint-disable-next-line prefer-destructuring
useStateFromStores: <T>( export const useStateFromStores: t.useStateFromStores = findByPropsLazy("useStateFromStores").useStateFromStores;
stores: t.FluxStore[],
mapper: () => T,
idk?: any,
isEqual?: (old: T, newer: T) => boolean
) => T;
}
= findByPropsLazy("useStateFromStores");
waitForStore("DraftStore", s => DraftStore = s); waitForStore("DraftStore", s => DraftStore = s);
waitForStore("UserStore", s => UserStore = s); waitForStore("UserStore", s => UserStore = s);

View file

@ -20,23 +20,24 @@ import type { ComponentType, CSSProperties, FunctionComponent, HtmlHTMLAttribute
export type TextVariant = "heading-sm/normal" | "heading-sm/medium" | "heading-sm/semibold" | "heading-sm/bold" | "heading-md/normal" | "heading-md/medium" | "heading-md/semibold" | "heading-md/bold" | "heading-lg/normal" | "heading-lg/medium" | "heading-lg/semibold" | "heading-lg/bold" | "heading-xl/normal" | "heading-xl/medium" | "heading-xl/bold" | "heading-xxl/normal" | "heading-xxl/medium" | "heading-xxl/bold" | "eyebrow" | "heading-deprecated-14/normal" | "heading-deprecated-14/medium" | "heading-deprecated-14/bold" | "text-xxs/normal" | "text-xxs/medium" | "text-xxs/semibold" | "text-xxs/bold" | "text-xs/normal" | "text-xs/medium" | "text-xs/semibold" | "text-xs/bold" | "text-sm/normal" | "text-sm/medium" | "text-sm/semibold" | "text-sm/bold" | "text-md/normal" | "text-md/medium" | "text-md/semibold" | "text-md/bold" | "text-lg/normal" | "text-lg/medium" | "text-lg/semibold" | "text-lg/bold" | "display-sm" | "display-md" | "display-lg" | "code"; export type TextVariant = "heading-sm/normal" | "heading-sm/medium" | "heading-sm/semibold" | "heading-sm/bold" | "heading-md/normal" | "heading-md/medium" | "heading-md/semibold" | "heading-md/bold" | "heading-lg/normal" | "heading-lg/medium" | "heading-lg/semibold" | "heading-lg/bold" | "heading-xl/normal" | "heading-xl/medium" | "heading-xl/bold" | "heading-xxl/normal" | "heading-xxl/medium" | "heading-xxl/bold" | "eyebrow" | "heading-deprecated-14/normal" | "heading-deprecated-14/medium" | "heading-deprecated-14/bold" | "text-xxs/normal" | "text-xxs/medium" | "text-xxs/semibold" | "text-xxs/bold" | "text-xs/normal" | "text-xs/medium" | "text-xs/semibold" | "text-xs/bold" | "text-sm/normal" | "text-sm/medium" | "text-sm/semibold" | "text-sm/bold" | "text-md/normal" | "text-md/medium" | "text-md/semibold" | "text-md/bold" | "text-lg/normal" | "text-lg/medium" | "text-lg/semibold" | "text-lg/bold" | "display-sm" | "display-md" | "display-lg" | "code";
export type FormTextTypes = Record<"DEFAULT" | "INPUT_PLACEHOLDER" | "DESCRIPTION" | "LABEL_BOLD" | "LABEL_SELECTED" | "LABEL_DESCRIPTOR" | "ERROR" | "SUCCESS", string>; export type FormTextTypes = Record<"DEFAULT" | "INPUT_PLACEHOLDER" | "DESCRIPTION" | "LABEL_BOLD" | "LABEL_SELECTED" | "LABEL_DESCRIPTOR" | "ERROR" | "SUCCESS", string>;
export type Heading = `h${1 | 2 | 3 | 4 | 5 | 6}`; export type HeadingTag = `h${1 | 2 | 3 | 4 | 5 | 6}`;
export type Margins = Record<"marginTop16" | "marginTop8" | "marginBottom8" | "marginTop20" | "marginBottom20", string>; export type Margins = Record<"marginTop16" | "marginTop8" | "marginBottom8" | "marginTop20" | "marginBottom20", string>;
export type ButtonLooks = Record<"FILLED" | "INVERTED" | "OUTLINED" | "LINK" | "BLANK", string>; export type ButtonLooks = Record<"FILLED" | "INVERTED" | "OUTLINED" | "LINK" | "BLANK", string>;
export type TextProps = PropsWithChildren<HtmlHTMLAttributes<HTMLDivElement> & { export type TextProps = PropsWithChildren<HtmlHTMLAttributes<HTMLDivElement> & {
variant?: TextVariant; variant?: TextVariant;
tag?: "div" | "span" | "p" | "strong" | Heading; tag?: "div" | "span" | "p" | "strong" | HeadingTag;
selectable?: boolean; selectable?: boolean;
lineClamp?: number; lineClamp?: number;
}>; }>;
export type Text = ComponentType<TextProps>; export type Text = ComponentType<TextProps>;
export type Heading = ComponentType<TextProps>;
export type FormTitle = ComponentType<HTMLProps<HTMLTitleElement> & PropsWithChildren<{ export type FormTitle = ComponentType<HTMLProps<HTMLTitleElement> & PropsWithChildren<{
/** default is h5 */ /** default is h5 */
tag?: Heading; tag?: HeadingTag;
faded?: boolean; faded?: boolean;
disabled?: boolean; disabled?: boolean;
required?: boolean; required?: boolean;
@ -45,7 +46,7 @@ export type FormTitle = ComponentType<HTMLProps<HTMLTitleElement> & PropsWithChi
export type FormSection = ComponentType<PropsWithChildren<{ export type FormSection = ComponentType<PropsWithChildren<{
/** default is h5 */ /** default is h5 */
tag?: Heading; tag?: HeadingTag;
className?: string; className?: string;
titleClassName?: string; titleClassName?: string;
titleId?: string; titleId?: string;
@ -455,5 +456,5 @@ export type Avatar = ComponentType<PropsWithChildren<{
}>>; }>>;
type FocusLock = ComponentType<PropsWithChildren<{ type FocusLock = ComponentType<PropsWithChildren<{
containerRef: RefObject<HTMLElement> containerRef: RefObject<HTMLElement>;
}>>; }>>;

View file

@ -182,3 +182,10 @@ export class GuildStore extends FluxStore {
getRoles(guildId: string): Record<string, Role>; getRoles(guildId: string): Record<string, Role>;
getAllGuildRoles(): Record<string, Record<string, Role>>; getAllGuildRoles(): Record<string, Record<string, Role>>;
} }
export type useStateFromStores = <T>(
stores: t.FluxStore[],
mapper: () => T,
dependencies?: any,
isEqual?: (old: T, newer: T) => boolean
) => T;

View file

@ -23,9 +23,11 @@ import { _resolveReady, filters, findByCodeLazy, findByProps, findByPropsLazy, f
import type * as t from "./types/utils"; import type * as t from "./types/utils";
export let FluxDispatcher: t.FluxDispatcher; export let FluxDispatcher: t.FluxDispatcher;
waitFor(["dispatch", "subscribe"], m => { waitFor(["dispatch", "subscribe"], m => {
FluxDispatcher = m; FluxDispatcher = m;
// Non import call to avoid circular dependency
Vencord.Plugins.subscribeAllPluginsFluxEvents(m);
const cb = () => { const cb = () => {
m.unsubscribe("CONNECTION_OPEN", cb); m.unsubscribe("CONNECTION_OPEN", cb);
_resolveReady(); _resolveReady();
@ -37,6 +39,8 @@ export let ComponentDispatch;
waitFor(["ComponentDispatch", "ComponentDispatcher"], m => ComponentDispatch = m.ComponentDispatch); waitFor(["ComponentDispatch", "ComponentDispatcher"], m => ComponentDispatch = m.ComponentDispatch);
export const Constants = findByPropsLazy("Endpoints");
export const RestAPI: t.RestAPI = proxyLazyWebpack(() => { export const RestAPI: t.RestAPI = proxyLazyWebpack(() => {
const mod = findByProps("getAPIBaseURL"); const mod = findByProps("getAPIBaseURL");
return mod.HTTP ?? mod; return mod.HTTP ?? mod;

View file

@ -18,66 +18,121 @@
import { WEBPACK_CHUNK } from "@utils/constants"; import { WEBPACK_CHUNK } from "@utils/constants";
import { Logger } from "@utils/Logger"; import { Logger } from "@utils/Logger";
import { canonicalizeReplacement } from "@utils/patches"; import { canonicalizeMatch, canonicalizeReplacement } from "@utils/patches";
import { PatchReplacement } from "@utils/types"; import { PatchReplacement } from "@utils/types";
import { WebpackInstance } from "discord-types/other";
import { traceFunction } from "../debug/Tracer"; import { traceFunction } from "../debug/Tracer";
import { _initWebpack } from "."; import { patches } from "../plugins";
import { _initWebpack, beforeInitListeners, factoryListeners, moduleListeners, subscriptions, wreq } from ".";
const logger = new Logger("WebpackInterceptor", "#8caaee");
const initCallbackRegex = canonicalizeMatch(/{return \i\(".+?"\)}/);
let webpackChunk: any[]; let webpackChunk: any[];
const logger = new Logger("WebpackInterceptor", "#8caaee"); // Patch the window webpack chunk setter to monkey patch the push method before any chunks are pushed
// This way we can patch the factory of everything being pushed to the modules array
Object.defineProperty(window, WEBPACK_CHUNK, {
configurable: true,
if (window[WEBPACK_CHUNK]) {
logger.info(`Patching ${WEBPACK_CHUNK}.push (was already existent, likely from cache!)`);
_initWebpack(window[WEBPACK_CHUNK]);
patchPush(window[WEBPACK_CHUNK]);
} else {
Object.defineProperty(window, WEBPACK_CHUNK, {
get: () => webpackChunk, get: () => webpackChunk,
set: v => { set: v => {
if (v?.push) { if (v?.push) {
if (!v.push.$$vencordOriginal) { if (!v.push.$$vencordOriginal) {
logger.info(`Patching ${WEBPACK_CHUNK}.push`); logger.info(`Patching ${WEBPACK_CHUNK}.push`);
patchPush(v); patchPush(v);
}
if (_initWebpack(v)) {
logger.info("Successfully initialised Vencord webpack");
// @ts-ignore // @ts-ignore
delete window[WEBPACK_CHUNK]; delete window[WEBPACK_CHUNK];
window[WEBPACK_CHUNK] = v; window[WEBPACK_CHUNK] = v;
} }
} }
webpackChunk = v; webpackChunk = v;
}, }
});
// wreq.O is the webpack onChunksLoaded function
// Discord uses it to await for all the chunks to be loaded before initializing the app
// We monkey patch it to also monkey patch the initialize app callback to get immediate access to the webpack require and run our listeners before doing it
Object.defineProperty(Function.prototype, "O", {
configurable: true,
set(onChunksLoaded: any) {
// When using react devtools or other extensions, or even when discord loads the sentry, we may also catch their webpack here.
// This ensures we actually got the right one
// this.e (wreq.e) is the method for loading a chunk, and only the main webpack has it
const { stack } = new Error();
if ((stack?.includes("discord.com") || stack?.includes("discordapp.com")) && String(this.e).includes("Promise.all")) {
logger.info("Found main WebpackRequire.onChunksLoaded");
delete (Function.prototype as any).O;
const originalOnChunksLoaded = onChunksLoaded;
onChunksLoaded = function (this: unknown, result: any, chunkIds: string[], callback: () => any, priority: number) {
if (callback != null && initCallbackRegex.test(callback.toString())) {
Object.defineProperty(this, "O", {
value: originalOnChunksLoaded,
configurable: true configurable: true
}); });
// wreq.m is the webpack module factory. const wreq = this as WebpackInstance;
// normally, this is populated via webpackGlobal.push, which we patch below.
// However, Discord has their .m prepopulated. const originalCallback = callback;
// Thus, we use this hack to immediately access their wreq.m and patch all already existing factories callback = function (this: unknown) {
// logger.info("Patched initialize app callback invoked, initializing our internal references to WebpackRequire and running beforeInitListeners");
// Update: Discord now has TWO webpack instances. Their normal one and sentry _initWebpack(wreq);
// Sentry does not push chunks to the global at all, so this same patch now also handles their sentry modules
Object.defineProperty(Function.prototype, "m", { for (const beforeInitListener of beforeInitListeners) {
beforeInitListener(wreq);
}
originalCallback.apply(this, arguments as any);
};
callback.toString = originalCallback.toString.bind(originalCallback);
arguments[2] = callback;
}
originalOnChunksLoaded.apply(this, arguments as any);
};
onChunksLoaded.toString = originalOnChunksLoaded.toString.bind(originalOnChunksLoaded);
}
Object.defineProperty(this, "O", {
value: onChunksLoaded,
configurable: true
});
}
});
// wreq.m is the webpack module factory.
// normally, this is populated via webpackGlobal.push, which we patch below.
// However, Discord has their .m prepopulated.
// Thus, we use this hack to immediately access their wreq.m and patch all already existing factories
//
// Update: Discord now has TWO webpack instances. Their normal one and sentry
// Sentry does not push chunks to the global at all, so this same patch now also handles their sentry modules
Object.defineProperty(Function.prototype, "m", {
configurable: true,
set(v: any) { set(v: any) {
// When using react devtools or other extensions, we may also catch their webpack here. // When using react devtools or other extensions, we may also catch their webpack here.
// This ensures we actually got the right one // This ensures we actually got the right one
if (new Error().stack?.includes("discord.com")) { const { stack } = new Error();
logger.info("Found webpack module factory"); if (stack?.includes("discord.com") || stack?.includes("discordapp.com")) {
logger.info("Found Webpack module factory", stack.match(/\/assets\/(.+?\.js)/)?.[1] ?? "");
patchFactories(v); patchFactories(v);
} }
Object.defineProperty(this, "m", { Object.defineProperty(this, "m", {
value: v, value: v,
configurable: true,
});
},
configurable: true configurable: true
}); });
} }
});
function patchPush(webpackGlobal: any) { function patchPush(webpackGlobal: any) {
function handlePush(chunk: any) { function handlePush(chunk: any) {
@ -91,6 +146,7 @@ function patchPush(webpackGlobal: any) {
} }
handlePush.$$vencordOriginal = webpackGlobal.push; handlePush.$$vencordOriginal = webpackGlobal.push;
handlePush.toString = handlePush.$$vencordOriginal.toString.bind(handlePush.$$vencordOriginal);
// Webpack overwrites .push with its own push like so: `d.push = n.bind(null, d.push.bind(d));` // Webpack overwrites .push with its own push like so: `d.push = n.bind(null, d.push.bind(d));`
// it wraps the old push (`d.push.bind(d)`). this old push is in this case our handlePush. // it wraps the old push (`d.push.bind(d)`). this old push is in this case our handlePush.
// If we then repatched the new push, we would end up with recursive patching, which leads to our patches // If we then repatched the new push, we would end up with recursive patching, which leads to our patches
@ -99,41 +155,41 @@ function patchPush(webpackGlobal: any) {
handlePush.bind = (...args: unknown[]) => handlePush.$$vencordOriginal.bind(...args); handlePush.bind = (...args: unknown[]) => handlePush.$$vencordOriginal.bind(...args);
Object.defineProperty(webpackGlobal, "push", { Object.defineProperty(webpackGlobal, "push", {
configurable: true,
get: () => handlePush, get: () => handlePush,
set(v) { set(v) {
handlePush.$$vencordOriginal = v; handlePush.$$vencordOriginal = v;
}, }
configurable: true
}); });
} }
function patchFactories(factories: Record<string | number, (module: { exports: any; }, exports: any, require: any) => void>) { let webpackNotInitializedLogged = false;
const { subscriptions, listeners } = Vencord.Webpack;
const { patches } = Vencord.Plugins;
function patchFactories(factories: Record<string, (module: any, exports: any, require: WebpackInstance) => void>) {
for (const id in factories) { for (const id in factories) {
let mod = factories[id]; let mod = factories[id];
// Discords Webpack chunks for some ungodly reason contain random
// newlines. Cyn recommended this workaround and it seems to work fine,
// however this could potentially break code, so if anything goes weird,
// this is probably why.
// Additionally, `[actual newline]` is one less char than "\n", so if Discord
// ever targets newer browsers, the minifier could potentially use this trick and
// cause issues.
//
// 0, prefix is to turn it into an expression: 0,function(){} would be invalid syntax without the 0,
let code: string = "0," + mod.toString().replaceAll("\n", "");
const originalMod = mod; const originalMod = mod;
const patchedBy = new Set(); const patchedBy = new Set();
const factory = factories[id] = function (module, exports, require) { const factory = factories[id] = function (module: any, exports: any, require: WebpackInstance) {
if (wreq == null && IS_DEV) {
if (!webpackNotInitializedLogged) {
webpackNotInitializedLogged = true;
logger.error("WebpackRequire was not initialized, running modules without patches instead.");
}
return void originalMod(module, exports, require);
}
try { try {
mod(module, exports, require); mod(module, exports, require);
} catch (err) { } catch (err) {
// Just rethrow discord errors // Just rethrow discord errors
if (mod === originalMod) throw err; if (mod === originalMod) throw err;
logger.error("Error in patched chunk", err); logger.error("Error in patched module", err);
return void originalMod(module, exports, require); return void originalMod(module, exports, require);
} }
@ -153,11 +209,11 @@ function patchFactories(factories: Record<string | number, (module: { exports: a
return; return;
} }
for (const callback of listeners) { for (const callback of moduleListeners) {
try { try {
callback(exports, id); callback(exports, id);
} catch (err) { } catch (err) {
logger.error("Error in webpack listener", err); logger.error("Error in Webpack module listener:\n", err, callback);
} }
} }
@ -171,30 +227,48 @@ function patchFactories(factories: Record<string | number, (module: { exports: a
callback(exports.default, id); callback(exports.default, id);
} }
} catch (err) { } catch (err) {
logger.error("Error while firing callback for webpack chunk", err); logger.error("Error while firing callback for Webpack subscription:\n", err, filter, callback);
} }
} }
} as any as { toString: () => string, original: any, (...args: any[]): void; }; } as any as { toString: () => string, original: any, (...args: any[]): void; };
// for some reason throws some error on which calling .toString() leads to infinite recursion factory.toString = originalMod.toString.bind(originalMod);
// when you force load all chunks???
factory.toString = () => mod.toString();
factory.original = originalMod; factory.original = originalMod;
for (const factoryListener of factoryListeners) {
try {
factoryListener(originalMod);
} catch (err) {
logger.error("Error in Webpack factory listener:\n", err, factoryListener);
}
}
// Discords Webpack chunks for some ungodly reason contain random
// newlines. Cyn recommended this workaround and it seems to work fine,
// however this could potentially break code, so if anything goes weird,
// this is probably why.
// Additionally, `[actual newline]` is one less char than "\n", so if Discord
// ever targets newer browsers, the minifier could potentially use this trick and
// cause issues.
//
// 0, prefix is to turn it into an expression: 0,function(){} would be invalid syntax without the 0,
let code: string = "0," + mod.toString().replaceAll("\n", "");
for (let i = 0; i < patches.length; i++) { for (let i = 0; i < patches.length; i++) {
const patch = patches[i]; const patch = patches[i];
const executePatch = traceFunction(`patch by ${patch.plugin}`, (match: string | RegExp, replace: string) => code.replace(match, replace));
if (patch.predicate && !patch.predicate()) continue; if (patch.predicate && !patch.predicate()) continue;
if (!code.includes(patch.find)) continue;
if (code.includes(patch.find)) {
patchedBy.add(patch.plugin); patchedBy.add(patch.plugin);
const executePatch = traceFunction(`patch by ${patch.plugin}`, (match: string | RegExp, replace: string) => code.replace(match, replace));
const previousMod = mod; const previousMod = mod;
const previousCode = code; const previousCode = code;
// we change all patch.replacement to array in plugins/index // We change all patch.replacement to array in plugins/index
for (const replacement of patch.replacement as PatchReplacement[]) { for (const replacement of patch.replacement as PatchReplacement[]) {
if (replacement.predicate && !replacement.predicate()) continue; if (replacement.predicate && !replacement.predicate()) continue;
const lastMod = mod; const lastMod = mod;
const lastCode = code; const lastCode = code;
@ -212,15 +286,17 @@ function patchFactories(factories: Record<string | number, (module: { exports: a
if (patch.group) { if (patch.group) {
logger.warn(`Undoing patch group ${patch.find} by ${patch.plugin} because replacement ${replacement.match} had no effect`); logger.warn(`Undoing patch group ${patch.find} by ${patch.plugin} because replacement ${replacement.match} had no effect`);
code = previousCode;
mod = previousMod; mod = previousMod;
code = previousCode;
patchedBy.delete(patch.plugin); patchedBy.delete(patch.plugin);
break; break;
} }
} else {
continue;
}
code = newCode; code = newCode;
mod = (0, eval)(`// Webpack Module ${id} - Patched by ${[...patchedBy].join(", ")}\n${newCode}\n//# sourceURL=WebpackModule${id}`); mod = (0, eval)(`// Webpack Module ${id} - Patched by ${[...patchedBy].join(", ")}\n${newCode}\n//# sourceURL=WebpackModule${id}`);
}
} catch (err) { } catch (err) {
logger.error(`Patch by ${patch.plugin} errored (Module id is ${id}): ${replacement.match}\n`, err); logger.error(`Patch by ${patch.plugin} errored (Module id is ${id}): ${replacement.match}\n`, err);
@ -258,20 +334,20 @@ function patchFactories(factories: Record<string | number, (module: { exports: a
} }
patchedBy.delete(patch.plugin); patchedBy.delete(patch.plugin);
if (patch.group) { if (patch.group) {
logger.warn(`Undoing patch group ${patch.find} by ${patch.plugin} because replacement ${replacement.match} errored`); logger.warn(`Undoing patch group ${patch.find} by ${patch.plugin} because replacement ${replacement.match} errored`);
code = previousCode;
mod = previousMod; mod = previousMod;
code = previousCode;
break; break;
} }
code = lastCode;
mod = lastMod; mod = lastMod;
code = lastCode;
} }
} }
if (!patch.all) patches.splice(i--, 1); if (!patch.all) patches.splice(i--, 1);
} }
} }
}
} }

View file

@ -68,20 +68,16 @@ export const filters = {
} }
}; };
export const subscriptions = new Map<FilterFn, CallbackFn>();
export const listeners = new Set<CallbackFn>();
export type CallbackFn = (mod: any, id: string) => void; export type CallbackFn = (mod: any, id: string) => void;
export function _initWebpack(instance: typeof window.webpackChunkdiscord_app) { export const subscriptions = new Map<FilterFn, CallbackFn>();
if (cache !== void 0) throw "no."; export const moduleListeners = new Set<CallbackFn>();
export const factoryListeners = new Set<(factory: (module: any, exports: any, require: WebpackInstance) => void) => void>();
export const beforeInitListeners = new Set<(wreq: WebpackInstance) => void>();
instance.push([[Symbol("Vencord")], {}, r => wreq = r]); export function _initWebpack(webpackRequire: WebpackInstance) {
instance.pop(); wreq = webpackRequire;
if (!wreq) return false; cache = webpackRequire.c;
cache = wreq.c;
return true;
} }
let devToolsOpen = false; let devToolsOpen = false;
@ -425,7 +421,7 @@ export async function extractAndLoadChunks(code: string[], matcher: RegExp = Def
const match = module.toString().match(canonicalizeMatch(matcher)); const match = module.toString().match(canonicalizeMatch(matcher));
if (!match) { if (!match) {
const err = new Error("extractAndLoadChunks: Couldn't find entry point id in module factory code"); const err = new Error("extractAndLoadChunks: Couldn't find chunk loading in module factory code");
logger.warn(err, "Code:", code, "Matcher:", matcher); logger.warn(err, "Code:", code, "Matcher:", matcher);
// Strict behaviour in DevBuilds to fail early and make sure the issue is found // Strict behaviour in DevBuilds to fail early and make sure the issue is found
@ -491,14 +487,6 @@ export function waitFor(filter: string | string[] | FilterFn, callback: Callback
subscriptions.set(filter, callback); subscriptions.set(filter, callback);
} }
export function addListener(callback: CallbackFn) {
listeners.add(callback);
}
export function removeListener(callback: CallbackFn) {
listeners.delete(callback);
}
/** /**
* Search modules by keyword. This searches the factory methods, * Search modules by keyword. This searches the factory methods,
* meaning you can search all sorts of things, displayName, methodName, strings somewhere in the code, etc * meaning you can search all sorts of things, displayName, methodName, strings somewhere in the code, etc

View file

@ -1,5 +1,6 @@
{ {
"compilerOptions": { "compilerOptions": {
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"esModuleInterop": true, "esModuleInterop": true,
"skipLibCheck": true, "skipLibCheck": true,