Merge branch 'dev' into strict-csp

This commit is contained in:
Nuckyz 2025-05-28 11:42:25 -03:00 committed by GitHub
commit 2d739f0dd5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
48 changed files with 296 additions and 623 deletions

View file

@ -17,9 +17,10 @@
*/
function parseHeaders(headers) {
const result = new Headers();
if (!headers)
return {};
const result = {};
return result;
const headersArr = headers.trim().split("\n");
for (var i = 0; i < headersArr.length; i++) {
var row = headersArr[i];
@ -27,13 +28,7 @@ function parseHeaders(headers) {
, key = row.slice(0, index).trim().toLowerCase()
, value = row.slice(index + 1).trim();
if (result[key] === undefined) {
result[key] = value;
} else if (Array.isArray(result[key])) {
result[key].push(value);
} else {
result[key] = [result[key], value];
}
result.append(key, value);
}
return result;
}

View file

@ -9,6 +9,7 @@
// @license GPL-3.0
// @match *://*.discord.com/*
// @grant GM_xmlhttpRequest
// @grant unsafeWindow
// @run-at document-start
// @compatible chrome Chrome + Tampermonkey or Violentmonkey
// @compatible firefox Firefox Tampermonkey

View file

@ -1,7 +1,7 @@
{
"name": "vencord",
"private": "true",
"version": "1.12.0",
"version": "1.12.2",
"description": "The cutest Discord client mod",
"homepage": "https://github.com/Vendicated/Vencord#readme",
"bugs": {

View file

@ -112,7 +112,7 @@ const buildConfigs = ([
...nodeCommonOpts,
entryPoints: ["src/main/index.ts"],
outfile: "dist/patcher.js",
footer: { js: "//# sourceURL=VencordPatcher\n" + sourceMapFooter("patcher") },
footer: { js: "//# sourceURL=file:///VencordPatcher\n" + sourceMapFooter("patcher") },
sourcemap,
plugins: [
// @ts-ignore this is never undefined
@ -131,7 +131,7 @@ const buildConfigs = ([
outfile: "dist/renderer.js",
format: "iife",
target: ["esnext"],
footer: { js: "//# sourceURL=VencordRenderer\n" + sourceMapFooter("renderer") },
footer: { js: "//# sourceURL=file:///VencordRenderer\n" + sourceMapFooter("renderer") },
globalName: "Vencord",
sourcemap,
plugins: [
@ -148,7 +148,7 @@ const buildConfigs = ([
...nodeCommonOpts,
entryPoints: ["src/preload.ts"],
outfile: "dist/preload.js",
footer: { js: "//# sourceURL=VencordPreload\n" + sourceMapFooter("preload") },
footer: { js: "//# sourceURL=file:///VencordPreload\n" + sourceMapFooter("preload") },
sourcemap,
define: {
...defines,
@ -162,7 +162,7 @@ const buildConfigs = ([
...nodeCommonOpts,
entryPoints: ["src/main/index.ts"],
outfile: "dist/vencordDesktopMain.js",
footer: { js: "//# sourceURL=VencordDesktopMain\n" + sourceMapFooter("vencordDesktopMain") },
footer: { js: "//# sourceURL=file:///VencordDesktopMain\n" + sourceMapFooter("vencordDesktopMain") },
sourcemap,
plugins: [
...nodeCommonOpts.plugins,
@ -180,7 +180,7 @@ const buildConfigs = ([
outfile: "dist/vencordDesktopRenderer.js",
format: "iife",
target: ["esnext"],
footer: { js: "//# sourceURL=VencordDesktopRenderer\n" + sourceMapFooter("vencordDesktopRenderer") },
footer: { js: "//# sourceURL=file:///VencordDesktopRenderer\n" + sourceMapFooter("vencordDesktopRenderer") },
globalName: "Vencord",
sourcemap,
plugins: [
@ -197,7 +197,7 @@ const buildConfigs = ([
...nodeCommonOpts,
entryPoints: ["src/preload.ts"],
outfile: "dist/vencordDesktopPreload.js",
footer: { js: "//# sourceURL=VencordPreload\n" + sourceMapFooter("vencordDesktopPreload") },
footer: { js: "//# sourceURL=file:///VencordPreload\n" + sourceMapFooter("vencordDesktopPreload") },
sourcemap,
define: {
...defines,

View file

@ -82,7 +82,7 @@ const buildConfigs = [
{
...commonOptions,
outfile: "dist/browser.js",
footer: { js: "//# sourceURL=VencordWeb" }
footer: { js: "//# sourceURL=file:///VencordWeb" }
},
{
...commonOptions,
@ -91,7 +91,7 @@ const buildConfigs = [
...commonOptions.define,
IS_EXTENSION: "true"
},
footer: { js: "//# sourceURL=VencordWeb" }
footer: { js: "//# sourceURL=file:///VencordWeb" }
},
{
...commonOptions,

View file

@ -182,7 +182,7 @@ export const globPlugins = kind => ({
const mod = `p${i}`;
code += `import ${mod} from "./${dir}/${fileName.replace(/\.tsx?$/, "")}";\n`;
pluginsCode += `[${mod}.name]:${mod},\n`;
metaCode += `[${mod}.name]:${JSON.stringify({ folderName, userPlugin })},\n`; // TODO: add excluded plugins to display in the UI?
metaCode += `[${mod}.name]:${JSON.stringify({ folderName, userPlugin })},\n`;
i++;
}
}

View file

@ -261,7 +261,7 @@ page.on("console", async e => {
const [, tag, message, otherMessage] = args as Array<string>;
switch (tag) {
case "WebpackInterceptor:":
case "WebpackPatcher:":
const patchFailMatch = message.match(/Patch by (.+?) (had no effect|errored|found no module) \(Module id is (.+?)\): (.+)/);
const patchSlowMatch = message.match(/Patch by (.+?) (took [\d.]+?ms) \(Module id is (.+?)\): (.+)/);
const match = patchFailMatch ?? patchSlowMatch;

View file

@ -16,10 +16,10 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { LazyComponent, LazyComponentWrapper } from "@utils/lazyReact";
import { Logger } from "@utils/Logger";
import { Margins } from "@utils/margins";
import { LazyComponent, LazyComponentWrapper } from "@utils/react";
import { React } from "@webpack/common";
import type { React } from "@webpack/common";
import { ErrorCard } from "./ErrorCard";
@ -46,7 +46,9 @@ const NO_ERROR = {};
// We might want to import this in a place where React isn't ready yet.
// Thus, wrap in a LazyComponent
const ErrorBoundary = LazyComponent(() => {
return class ErrorBoundary extends React.PureComponent<React.PropsWithChildren<Props>> {
// This component is used in a lot of files which end up importing other Webpack commons and causing circular imports.
// For this reason, use a non import access here.
return class ErrorBoundary extends Vencord.Webpack.Common.React.PureComponent<React.PropsWithChildren<Props>> {
state = {
error: NO_ERROR as any,
stack: "",

View file

@ -46,13 +46,13 @@ async function runReporter() {
for (const patch of patches) {
if (!patch.all) {
new Logger("WebpackInterceptor").warn(`Patch by ${patch.plugin} found no module (Module id is -): ${patch.find}`);
new Logger("WebpackPatcher").warn(`Patch by ${patch.plugin} found no module (Module id is -): ${patch.find}`);
}
}
for (const [plugin, moduleId, match, totalTime] of patchTimings) {
if (totalTime > 5) {
new Logger("WebpackInterceptor").warn(`Patch by ${plugin} took ${Math.round(totalTime * 100) / 100}ms (Module id is ${String(moduleId)}): ${match}`);
new Logger("WebpackPatcher").warn(`Patch by ${plugin} took ${Math.round(totalTime * 100) / 100}ms (Module id is ${String(moduleId)}): ${match}`);
}
}

View file

@ -27,7 +27,7 @@ import { openContributorModal } from "@components/PluginSettings/ContributorModa
import { Devs } from "@utils/constants";
import { Logger } from "@utils/Logger";
import { Margins } from "@utils/margins";
import { isPluginDev } from "@utils/misc";
import { shouldShowContributorBadge } from "@utils/misc";
import { closeModal, ModalContent, ModalFooter, ModalHeader, ModalRoot, openModal } from "@utils/modal";
import definePlugin from "@utils/types";
import { Forms, Toasts, UserStore } from "@webpack/common";
@ -39,7 +39,7 @@ const ContributorBadge: ProfileBadge = {
description: "Vencord Contributor",
image: CONTRIBUTOR_BADGE,
position: BadgePosition.START,
shouldShow: ({ userId }) => isPluginDev(userId),
shouldShow: ({ userId }) => shouldShowContributorBadge(userId),
onClick: (_, { userId }) => openContributorModal(UserStore.getUser(userId))
};

View file

@ -30,8 +30,8 @@ export default definePlugin({
replacement: [
// Main setting definition
{
match: /(?<=INFREQUENT_USER_ACTION.{0,20},)useSetting:/,
replace: "userSettingsAPIGroup:arguments[0],userSettingsAPIName:arguments[1],$&"
match: /\.updateAsync\(.+?(?=,useSetting:)/,
replace: "$&,userSettingsAPIGroup:arguments[0],userSettingsAPIName:arguments[1]"
},
// Selective wrapper
{

View file

@ -72,7 +72,7 @@ export default definePlugin({
group: true,
replacement: [
{
match: /let{speaking:\i/,
match: /let{ref:\i,speaking:\i/,
replace: "$self.useAccountPanelRef();$&"
},
{

View file

@ -21,12 +21,10 @@ import { definePluginSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { findByCodeLazy, findByPropsLazy } from "@webpack";
type AnonUpload = Upload & { anonymise?: boolean; };
import { findByCodeLazy } from "@webpack";
import { useState } from "@webpack/common";
const ActionBarIcon = findByCodeLazy(".actionBarIcon)");
const UploadDraft = findByPropsLazy("popFirstFile", "update");
const enum Methods {
Random,
@ -34,6 +32,7 @@ const enum Methods {
Timestamp,
}
const ANONYMISE_UPLOAD_SYMBOL = Symbol("vcAnonymise");
const tarExtMatcher = /\.tar\.\w+$/;
const settings = definePluginSettings({
@ -69,41 +68,44 @@ export default definePlugin({
name: "AnonymiseFileNames",
authors: [Devs.fawn],
description: "Anonymise uploaded file names",
settings,
patches: [
{
find: "instantBatchUpload:",
find: 'type:"UPLOAD_START"',
replacement: {
match: /uploadFiles:(\i),/,
replace:
"uploadFiles:(...args)=>(args[0].uploads.forEach(f=>f.filename=$self.anonymise(f)),$1(...args)),",
match: /await \i\.uploadFiles\((\i),/,
replace: "$1.forEach($self.anonymise),$&"
},
},
{
find: 'addFilesTo:"message.attachments"',
replacement: {
match: /(\i.uploadFiles\((\i),)/,
replace: "$2.forEach(f=>f.filename=$self.anonymise(f)),$1"
match: /\i.uploadFiles\((\i),/,
replace: "$1.forEach($self.anonymise),$&"
}
},
{
find: "#{intl::ATTACHMENT_UTILITIES_SPOILER}",
replacement: {
match: /(?<=children:\[)(?=.{10,80}tooltip:.{0,100}#{intl::ATTACHMENT_UTILITIES_SPOILER})/,
replace: "arguments[0].canEdit!==false?$self.renderIcon(arguments[0]):null,"
replace: "arguments[0].canEdit!==false?$self.AnonymiseUploadButton(arguments[0]):null,"
},
},
],
settings,
renderIcon: ErrorBoundary.wrap(({ upload, channelId, draftType }: { upload: AnonUpload; draftType: unknown; channelId: string; }) => {
const anonymise = upload.anonymise ?? settings.store.anonymiseByDefault;
AnonymiseUploadButton: ErrorBoundary.wrap(({ upload }: { upload: Upload; }) => {
const [anonymise, setAnonymise] = useState(upload[ANONYMISE_UPLOAD_SYMBOL] ?? settings.store.anonymiseByDefault);
function onToggleAnonymise() {
upload[ANONYMISE_UPLOAD_SYMBOL] = !anonymise;
setAnonymise(!anonymise);
}
return (
<ActionBarIcon
tooltip={anonymise ? "Using anonymous file name" : "Using normal file name"}
onClick={() => {
upload.anonymise = !anonymise;
UploadDraft.update(channelId, upload.id, draftType, {}); // dummy update so component rerenders
}}
onClick={onToggleAnonymise}
>
{anonymise
? <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M17.06 13C15.2 13 13.64 14.33 13.24 16.1C12.29 15.69 11.42 15.8 10.76 16.09C10.35 14.31 8.79 13 6.94 13C4.77 13 3 14.79 3 17C3 19.21 4.77 21 6.94 21C9 21 10.68 19.38 10.84 17.32C11.18 17.08 12.07 16.63 13.16 17.34C13.34 19.39 15 21 17.06 21C19.23 21 21 19.21 21 17C21 14.79 19.23 13 17.06 13M6.94 19.86C5.38 19.86 4.13 18.58 4.13 17S5.39 14.14 6.94 14.14C8.5 14.14 9.75 15.42 9.75 17S8.5 19.86 6.94 19.86M17.06 19.86C15.5 19.86 14.25 18.58 14.25 17S15.5 14.14 17.06 14.14C18.62 14.14 19.88 15.42 19.88 17S18.61 19.86 17.06 19.86M22 10.5H2V12H22V10.5M15.53 2.63C15.31 2.14 14.75 1.88 14.22 2.05L12 2.79L9.77 2.05L9.72 2.04C9.19 1.89 8.63 2.17 8.43 2.68L6 9H18L15.56 2.68L15.53 2.63Z" /></svg>
@ -113,25 +115,31 @@ export default definePlugin({
);
}, { noop: true }),
anonymise(upload: AnonUpload) {
if ((upload.anonymise ?? settings.store.anonymiseByDefault) === false) return upload.filename;
const file = upload.filename;
const tarMatch = tarExtMatcher.exec(file);
const extIdx = tarMatch?.index ?? file.lastIndexOf(".");
const ext = extIdx !== -1 ? file.slice(extIdx) : "";
switch (settings.store.method) {
case Methods.Random:
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
return Array.from(
{ length: settings.store.randomisedLength },
() => chars[Math.floor(Math.random() * chars.length)]
).join("") + ext;
case Methods.Consistent:
return settings.store.consistent + ext;
case Methods.Timestamp:
return Date.now() + ext;
anonymise(upload: Upload) {
if ((upload[ANONYMISE_UPLOAD_SYMBOL] ?? settings.store.anonymiseByDefault) === false) {
return;
}
},
const originalFileName = upload.filename;
const tarMatch = tarExtMatcher.exec(originalFileName);
const extIdx = tarMatch?.index ?? originalFileName.lastIndexOf(".");
const ext = extIdx !== -1 ? originalFileName.slice(extIdx) : "";
const newFilename = (() => {
switch (settings.store.method) {
case Methods.Random:
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
return Array.from(
{ length: settings.store.randomisedLength },
() => chars[Math.floor(Math.random() * chars.length)]
).join("") + ext;
case Methods.Consistent:
return settings.store.consistent + ext;
case Methods.Timestamp:
return Date.now() + ext;
}
})();
upload.filename = newFilename;
}
});

View file

@ -42,6 +42,7 @@ export default definePlugin({
description: "Client plugin for arRPC to enable RPC on Discord Web (experimental)",
authors: [Devs.Ducko],
reporterTestable: ReporterTestable.None,
hidden: IS_VESKTOP || "legcord" in window,
settingsAboutComponent: () => (
<>
@ -73,9 +74,6 @@ export default definePlugin({
},
async start() {
// Legcord comes with its own arRPC implementation, so this plugin just confuses users
if ("legcord" in window) return;
if (ws) ws.close();
ws = new WebSocket("ws://127.0.0.1:1337"); // try to open WebSocket

View file

@ -74,6 +74,22 @@ function makeShortcuts() {
};
}
function findStoreWrapper(findStore: typeof Webpack.findStore) {
const cache = new Map<string, unknown>();
return function (storeName: string) {
const cacheKey = String(storeName);
if (cache.has(cacheKey)) return cache.get(cacheKey);
let store: unknown;
try {
store = findStore(storeName);
} catch { }
if (store) cache.set(cacheKey, store);
return store;
};
}
let fakeRenderWin: WeakRef<Window> | undefined;
const find = newFindWrapper(f => f);
const findByProps = newFindWrapper(filters.byProps);
@ -98,7 +114,7 @@ function makeShortcuts() {
findComponentByCode: newFindWrapper(filters.componentByCode),
findAllComponentsByCode: (...code: string[]) => findAll(filters.componentByCode(...code)),
findExportedComponent: (...props: string[]) => findByProps(...props)[props[0]],
findStore: newFindWrapper(filters.byStoreName),
findStore: findStoreWrapper(Webpack.findStore),
PluginsApi: { getter: () => Vencord.Plugins },
plugins: { getter: () => Vencord.Plugins.plugins },
Settings: { getter: () => Vencord.Settings },

View file

@ -17,6 +17,7 @@
*/
import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
import { migratePluginSettings } from "@api/Settings";
import { CheckedTextInput } from "@components/CheckedTextInput";
import { Devs } from "@utils/constants";
import { Logger } from "@utils/Logger";
@ -165,7 +166,7 @@ async function doClone(guildId: string, data: Sticker | Emoji) {
message = JSON.parse(e.text).message;
} catch { }
new Logger("EmoteCloner").error("Failed to clone", data.name, "to", guildId, e);
new Logger("ExpressionCloner").error("Failed to clone", data.name, "to", guildId, e);
Toasts.show({
message: "Failed to clone: " + message,
type: Toasts.Type.FAILURE,
@ -364,10 +365,11 @@ const expressionPickerPatch: NavContextMenuPatchCallback = (children, props: { t
}
};
migratePluginSettings("ExpressionCloner", "EmoteCloner");
export default definePlugin({
name: "EmoteCloner",
name: "ExpressionCloner",
description: "Allows you to clone Emotes & Stickers to your own server (right click them)",
tags: ["StickerCloner"],
tags: ["StickerCloner", "EmoteCloner", "EmojiCloner"],
authors: [Devs.Ven, Devs.Nuckyz],
contextMenus: {
"message": messageContextMenuPatch,

View file

@ -110,7 +110,7 @@ export default definePlugin({
patches: [
{
// Indicator
find: "#{intl::MESSAGE_EDITED}",
find: ".SEND_FAILED,",
replacement: {
match: /let\{className:\i,message:\i[^}]*\}=(\i)/,
replace: "try {$1 && $self.INV_REGEX.test($1.message.content) ? $1.content.push($self.indicator()) : null } catch {};$&"

View file

@ -7,7 +7,7 @@
import { proxyLazy } from "@utils/lazy";
import { sleep } from "@utils/misc";
import { Queue } from "@utils/Queue";
import { Flux, FluxDispatcher, GuildChannelStore, PrivateChannelsStore } from "@webpack/common";
import { ChannelActionCreators, Flux, FluxDispatcher, GuildChannelStore } from "@webpack/common";
export const OnlineMemberCountStore = proxyLazy(() => {
const preloadQueue = new Queue();
@ -22,7 +22,7 @@ export const OnlineMemberCountStore = proxyLazy(() => {
async _ensureCount(guildId: string) {
if (onlineMemberMap.has(guildId)) return;
await PrivateChannelsStore.preload(guildId, GuildChannelStore.getDefaultChannel(guildId).id);
await ChannelActionCreators.preload(guildId, GuildChannelStore.getDefaultChannel(guildId).id);
}
ensureCount(guildId?: string) {

View file

@ -464,19 +464,21 @@ export default definePlugin({
{
// Message content renderer
find: ".SEND_FAILED,",
replacement: {
// Render editHistory behind the message content
match: /\.isFailed]:.+?children:\[/,
replace: "$&arguments[0]?.message?.editHistory?.length>0&&$self.renderEdits(arguments[0]),"
}
},
{
find: "#{intl::MESSAGE_EDITED}",
replacement: [
{
// Render editHistory in the deepest div for message content
match: /(\)\("div",\{id:.+?children:\[)/,
replace: "$1 (!!arguments[0].message.editHistory?.length && $self.renderEdits(arguments[0])),"
},
{
// Make edit marker clickable
match: /"span",\{(?=className:\i\.edited,)/,
replace: "$self.EditMarker,{message:arguments[0].message,"
}
]
replacement: {
// Make edit marker clickable
match: /"span",\{(?=className:\i\.edited,)/,
replace: "$self.EditMarker,{message:arguments[0].message,"
}
},
{

View file

@ -1,65 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated, Samu and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { ApplicationCommandInputType, findOption, OptionalMessageOption, RequiredMessageOption, sendBotMessage } from "@api/Commands";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
function mock(input: string): string {
let output = "";
for (let i = 0; i < input.length; i++) {
output += i % 2 ? input[i].toUpperCase() : input[i].toLowerCase();
}
return output;
}
export default definePlugin({
name: "MoreCommands",
description: "echo, lenny, mock",
authors: [Devs.Arjix, Devs.echo, Devs.Samu],
commands: [
{
name: "echo",
description: "Sends a message as Clyde (locally)",
options: [OptionalMessageOption],
inputType: ApplicationCommandInputType.BOT,
execute: (opts, ctx) => {
const content = findOption(opts, "message", "");
sendBotMessage(ctx.channel.id, { content });
},
},
{
name: "lenny",
description: "Sends a lenny face",
options: [OptionalMessageOption],
execute: opts => ({
content: findOption(opts, "message", "") + " ( ͡° ͜ʖ ͡°)"
}),
},
{
name: "mock",
description: "mOcK PeOpLe",
options: [RequiredMessageOption],
execute: opts => ({
content: mock(findOption(opts, "message", ""))
}),
},
]
});

View file

@ -1,47 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { findOption, OptionalMessageOption } from "@api/Commands";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
export default definePlugin({
name: "MoreKaomoji",
description: "Adds more Kaomoji to discord. ヽ(´▽`)/",
authors: [Devs.JacobTm],
commands: [
{ name: "dissatisfaction", description: " " },
{ name: "smug", description: "ಠ_ಠ" },
{ name: "happy", description: "ヽ(´▽`)/" },
{ name: "crying", description: "ಥ_ಥ" },
{ name: "angry", description: "ヽ(`Д´)ノ" },
{ name: "anger", description: "ヽ(`皿′o)ノ" },
{ name: "joy", description: "<( ̄︶ ̄)>" },
{ name: "blush", description: "૮ ˶ᵔ ᵕ ᵔ˶ ა" },
{ name: "confused", description: "(•ิ_•ิ)?" },
{ name: "sleeping", description: "(ᴗ_ᴗ)" },
{ name: "laughing", description: "o(≧▽≦)o" },
].map(data => ({
...data,
options: [OptionalMessageOption],
execute: opts => ({
content: findOption(opts, "message", "") + " " + data.description
})
}))
});

View file

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

View file

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

View file

@ -30,7 +30,7 @@ export default definePlugin({
{
find: "}searchWithoutFetchingLatest(",
replacement: {
match: /searchWithoutFetchingLatest.{20,300}get\((\i).{10,40}?reduce\(\((\i),(\i)\)=>\{/,
match: /\.get\((\i)\)\.nameMatchesChain\(\i\)\.reduce\(\((\i),(\i)\)=>\{/,
replace: "$& if ($self.shouldSkip($1, $3)) return $2;"
}
}

View file

@ -1,5 +1,5 @@
# No Unblock To Jump
Removes the popup preventing you to jump to a message from a blocked/ignored user (eg: in search results)
Removes the popup preventing you to jump to a message from a blocked/ignored user or likely spammer (eg: in search results)
![A modal popup telling you to unblock a user to jump their message](https://github.com/user-attachments/assets/0e4b859d-f3b3-4101-9a83-829afb473d1e)

View file

@ -21,7 +21,7 @@ import definePlugin from "@utils/types";
export default definePlugin({
name: "NoUnblockToJump",
description: "Allows you to jump to messages of blocked users without unblocking them",
description: "Allows you to jump to messages of blocked or ignored users and likely spammers without unblocking them",
authors: [Devs.dzshn],
patches: [
{

View file

@ -106,13 +106,15 @@ export default definePlugin({
}
]
},
{
find: ".CONNECTED_ACCOUNT_VIEWED,",
// User Profile Modal & User Profile Modal v2
...[".__invalid_connectedAccountOpenIconContainer", ".BLUESKY||"].map(find => ({
find,
replacement: {
match: /(?<=href:\i,onClick:(\i)=>\{)(?=.{0,10}\i=(\i)\.type,.{0,100}CONNECTED_ACCOUNT_VIEWED)/,
match: /(?<=onClick:(\i)=>\{)(?=.{0,100}\.CONNECTED_ACCOUNT_VIEWED)(?<==(\i)\.metadata.+?)/,
replace: "if($self.handleAccountView($1,$2.type,$2.id)) return;"
}
}
}))
],
async handleLink(data: { href: string; }, event?: MouseEvent) {

View file

@ -1,109 +0,0 @@
/*
* 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 { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType, ReporterTestable } from "@utils/types";
import { FluxDispatcher } from "@webpack/common";
const enum Intensity {
Normal,
Better,
ProjectX,
}
const settings = definePluginSettings({
superIntensePartyMode: {
description: "Party intensity",
type: OptionType.SELECT,
options: [
{ label: "Normal", value: Intensity.Normal, default: true },
{ label: "Better", value: Intensity.Better },
{ label: "Project X", value: Intensity.ProjectX },
],
restartNeeded: false,
onChange: setSettings
},
});
export default definePlugin({
name: "PartyMode",
description: "Allows you to use party mode cause the party never ends ✨",
authors: [Devs.UwUDev],
reporterTestable: ReporterTestable.None,
settings,
start() {
setPoggerState(true);
setSettings(settings.store.superIntensePartyMode);
},
stop() {
setPoggerState(false);
},
});
function setPoggerState(state: boolean) {
FluxDispatcher.dispatch({
type: "POGGERMODE_SETTINGS_UPDATE",
settings: {
enabled: state,
settingsVisible: state
}
});
}
function setSettings(intensity: Intensity) {
const state = {
screenshakeEnabledLocations: { 0: true, 1: true, 2: true },
shakeIntensity: 1,
confettiSize: 16,
confettiCount: 5,
combosRequiredCount: 1
};
switch (intensity) {
case Intensity.Normal: {
Object.assign(state, {
screenshakeEnabledLocations: { 0: true, 1: false, 2: false },
combosRequiredCount: 5
});
break;
}
case Intensity.Better: {
Object.assign(state, {
confettiSize: 12,
confettiCount: 8,
});
break;
}
case Intensity.ProjectX: {
Object.assign(state, {
shakeIntensity: 20,
confettiSize: 25,
confettiCount: 15,
});
break;
}
}
FluxDispatcher.dispatch({
type: "POGGERMODE_SETTINGS_UPDATE",
settings: state
});
}

View file

@ -26,7 +26,7 @@ import { Devs } from "@utils/constants";
import { classes } from "@utils/misc";
import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy } from "@webpack";
import { Button, ChannelStore, Dialog, GuildMemberStore, GuildStore, match, Menu, PermissionsBits, Popout, TooltipContainer, UserStore } from "@webpack/common";
import { Button, ChannelStore, Dialog, GuildMemberStore, GuildStore, match, Menu, PermissionsBits, Popout, TooltipContainer, useRef, UserStore } from "@webpack/common";
import type { Guild, GuildMember } from "discord-types/general";
import openRolesAndUsersPermissionsModal, { PermissionType, RoleOrUserPermission } from "./components/RolesAndUsersPermissions";
@ -173,32 +173,38 @@ export default definePlugin({
}
],
ViewPermissionsButton: ErrorBoundary.wrap(({ guild, guildMember }: { guild: Guild; guildMember: GuildMember; }) => (
<Popout
position="bottom"
align="center"
renderPopout={({ closePopout }) => (
<Dialog className={PopoutClasses.container} style={{ width: "500px" }}>
<UserPermissions guild={guild} guildMember={guildMember} closePopout={closePopout} />
</Dialog>
)}
>
{popoutProps => (
<TooltipContainer text="View Permissions">
<Button
{...popoutProps}
color={Button.Colors.CUSTOM}
look={Button.Looks.FILLED}
size={Button.Sizes.NONE}
innerClassName={classes(RoleButtonClasses.buttonInner, RoleButtonClasses.icon)}
className={classes(RoleButtonClasses.button, RoleButtonClasses.icon, "vc-permviewer-role-button")}
>
<SafetyIcon height="16" width="16" />
</Button>
</TooltipContainer>
)}
</Popout>
), { noop: true }),
ViewPermissionsButton: ErrorBoundary.wrap(({ guild, guildMember }: { guild: Guild; guildMember: GuildMember; }) => {
const buttonRef = useRef(null);
return (
<Popout
position="bottom"
align="center"
targetElementRef={buttonRef}
renderPopout={({ closePopout }) => (
<Dialog className={PopoutClasses.container} style={{ width: "500px" }}>
<UserPermissions guild={guild} guildMember={guildMember} closePopout={closePopout} />
</Dialog>
)}
>
{popoutProps => (
<TooltipContainer text="View Permissions">
<Button
{...popoutProps}
buttonRef={buttonRef}
color={Button.Colors.CUSTOM}
look={Button.Looks.FILLED}
size={Button.Sizes.NONE}
innerClassName={classes(RoleButtonClasses.buttonInner, RoleButtonClasses.icon)}
className={classes(RoleButtonClasses.button, RoleButtonClasses.icon, "vc-permviewer-role-button")}
>
<SafetyIcon height="16" width="16" />
</Button>
</TooltipContainer>
)}
</Popout>
);
}, { noop: true }),
contextMenus: {
"user-context": makeContextMenuPatch("roles", MenuItemParentType.User),

View file

@ -153,7 +153,7 @@ export default definePlugin({
},
// Messages
{
find: "#{intl::MESSAGE_EDITED}",
find: ".SEND_FAILED,",
replacement: {
match: /(?<=isUnsupported\]:(\i)\.isUnsupported\}\),)(?=children:\[)/,
replace: "style:$self.useMessageColorsStyle($1),"

View file

@ -19,7 +19,7 @@
import { ILanguageRegistration } from "@vap/shiki";
export const VPC_REPO = "Vap0r1ze/vapcord";
export const VPC_REPO_COMMIT = "88a7032a59cca40da170926651b08201ea3b965a";
export const VPC_REPO_COMMIT = "4d0e4b420fb1e4358852bbd18c804a6f5e54c0d7";
export const vpcRepoAssets = `https://raw.githubusercontent.com/${VPC_REPO}/${VPC_REPO_COMMIT}/assets/shiki-codeblocks`;
export const vpcRepoGrammar = (fileName: string) => `${vpcRepoAssets}/${fileName}`;
export const vpcRepoLanguages = `${vpcRepoAssets}/languages.json`;
@ -46,7 +46,7 @@ export interface LanguageJson {
export const languages: Record<string, Language> = {};
export const loadLanguages = async () => {
const langsJson: LanguageJson[] = await fetch(vpcRepoLanguages).then(res => res.json());
const langsJson: LanguageJson[] = await fetch(vpcRepoLanguages).then(res => res.ok ? res.json() : []);
const loadedLanguages = Object.fromEntries(
langsJson.map(lang => [lang.id, {
...lang,

View file

@ -356,15 +356,10 @@ export default definePlugin({
find: "#{intl::CHANNEL_CALL_CURRENT_SPEAKER}",
replacement: [
{
// Remove the divider and the open chat button for the HiddenChannelLockScreen
match: /"more-options-popout"\)\),(?<=channel:(\i).+?inCall:(\i).+?)/,
replace: (m, channel, inCall) => `${m}${inCall}||!$self.isHiddenChannel(${channel},true)&&`
},
{
// Remove invite users button for the HiddenChannelLockScreen
match: /"popup".{0,100}?if\((?<=channel:(\i).+?inCall:(\i).+?)/,
replace: (m, channel, inCall) => `${m}(${inCall}||!$self.isHiddenChannel(${channel},true))&&`
},
// Remove the open chat button for the HiddenChannelLockScreen
match: /(?<=&&)\i\.push\(.{0,120}"chat-spacer"/,
replace: "(arguments[0]?.inCall||!$self.isHiddenChannel(arguments[0]?.channel,true))&&$&"
}
]
},
{
@ -397,8 +392,8 @@ export default definePlugin({
replacement: [
{
// Render our HiddenChannelLockScreen component instead of the main stage channel component
match: /"124px".+?children:(?<=let \i,{channel:(\i).+?)(?=.{0,20}?}\)}function)/,
replace: (m, channel) => `${m}$self.isHiddenChannel(${channel})?$self.HiddenChannelLockScreen(${channel}):`
match: /screenMessage:(\i)\?.+?children:(?=!\1)(?<=let \i,{channel:(\i).+?)/,
replace: (m, _isPopoutOpen, channel) => `${m}$self.isHiddenChannel(${channel})?$self.HiddenChannelLockScreen(${channel}):`
},
{
// Disable useless components for the HiddenChannelLockScreen of stage channels
@ -427,8 +422,8 @@ export default definePlugin({
},
{
// Remove the open chat button for the HiddenChannelLockScreen
match: /"recents".+?&&(?=\(.+?channelId:(\i)\.id,showRequestToSpeakSidebar)/,
replace: (m, channel) => `${m}!$self.isHiddenChannel(${channel})&&`
match: /(?<=&&)\(0,\i\.jsxs?\).{0,180}\.buttonIcon/,
replace: "!$self.isHiddenChannel(arguments[0]?.channel,true)&&$&"
}
]
},

View file

@ -24,7 +24,7 @@ import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { findComponentByCodeLazy } from "@webpack";
import { Menu, Popout, useState } from "@webpack/common";
import { Menu, Popout, useRef, useState } from "@webpack/common";
import type { ReactNode } from "react";
const HeaderBarIcon = findComponentByCodeLazy(".HEADER_BAR_BADGE_TOP:", '.iconBadge,"top"');
@ -95,6 +95,7 @@ function VencordPopoutIcon(isShown: boolean) {
}
function VencordPopoutButton() {
const buttonRef = useRef(null);
const [show, setShow] = useState(false);
return (
@ -104,10 +105,12 @@ function VencordPopoutButton() {
animation={Popout.Animation.NONE}
shouldShow={show}
onRequestClose={() => setShow(false)}
targetElementRef={buttonRef}
renderPopout={() => VencordPopout(() => setShow(false))}
>
{(_, { isShown }) => (
<HeaderBarIcon
ref={buttonRef}
className="vc-toolbox-btn"
onClick={() => setShow(v => !v)}
tooltip={isShown ? null : "Vencord Toolbox"}

View file

@ -21,8 +21,12 @@ export default definePlugin({
replace: '"x-google-max-bitrate=".concat("80_000")'
},
{
match: /;level-asymmetry-allowed=1/,
match: ";level-asymmetry-allowed=1",
replace: ";b=AS:800000;level-asymmetry-allowed=1"
},
{
match: "useinbandfec=1",
replace: "useinbandfec=1;stereo=1;sprop-stereo=1"
}
]
}

View file

@ -17,7 +17,7 @@
*/
import { MessageObject } from "@api/MessageEvents";
import { ChannelStore, ComponentDispatch, Constants, FluxDispatcher, GuildStore, i18n, IconUtils, InviteActions, MessageActions, PrivateChannelsStore, RestAPI, SelectedChannelStore, SelectedGuildStore, UserProfileActions, UserProfileStore, UserSettingsActionCreators, UserUtils } from "@webpack/common";
import { ChannelActionCreators, ChannelStore, ComponentDispatch, Constants, FluxDispatcher, GuildStore, i18n, IconUtils, InviteActions, MessageActions, RestAPI, SelectedChannelStore, SelectedGuildStore, UserProfileActions, UserProfileStore, UserSettingsActionCreators, UserUtils } from "@webpack/common";
import { Channel, Guild, Message, User } from "discord-types/general";
import { Except } from "type-fest";
@ -91,7 +91,7 @@ export function getCurrentGuild(): Guild | undefined {
}
export function openPrivateChannel(userId: string) {
PrivateChannelsStore.openPrivateChannel(userId);
ChannelActionCreators.openPrivateChannel(userId);
}
export const enum Theme {

View file

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

View file

@ -19,7 +19,6 @@
import { Settings, SettingsStore } from "@api/Settings";
import { ThemeStore } from "@webpack/common";
let style: HTMLStyleElement;
let themesStyle: HTMLStyleElement;
@ -61,7 +60,10 @@ async function initThemes() {
const { themeLinks, enabledThemes } = Settings;
// "darker" and "midnight" both count as dark
const activeTheme = ThemeStore.theme === "light" ? "light" : "dark";
// This function is first called on DOMContentLoaded, so ThemeStore may not have been loaded yet
const activeTheme = ThemeStore == null
? undefined
: ThemeStore.theme === "light" ? "light" : "dark";
const links = themeLinks
.map(rawLink => {
@ -98,6 +100,14 @@ document.addEventListener("DOMContentLoaded", () => {
SettingsStore.addChangeListener("themeLinks", initThemes);
SettingsStore.addChangeListener("enabledThemes", initThemes);
if (!IS_WEB) {
VencordNative.quickCss.addThemeChangeListener(initThemes);
}
});
export function initQuickCssThemeStore() {
initThemes();
let currentTheme = ThemeStore.theme;
ThemeStore.addChangeListener(() => {
if (currentTheme === ThemeStore.theme) return;
@ -105,7 +115,4 @@ document.addEventListener("DOMContentLoaded", () => {
currentTheme = ThemeStore.theme;
initThemes();
});
if (!IS_WEB)
VencordNative.quickCss.addThemeChangeListener(initThemes);
});
}

View file

@ -17,9 +17,7 @@
*/
import { LazyComponent, LazyComponentWrapper } from "@utils/react";
// eslint-disable-next-line path-alias/no-relative
import { FilterFn, filters, lazyWebpackSearchHistory, waitFor } from "../webpack";
import { FilterFn, filters, lazyWebpackSearchHistory, waitFor } from "@webpack";
export function waitForComponent<T extends React.ComponentType<any> = React.ComponentType<any> & Record<string, any>>(name: string, filter: FilterFn | string | string[]) {
if (IS_REPORTER) lazyWebpackSearchHistory.push(["waitForComponent", Array.isArray(filter) ? filter : [filter]]);

View file

@ -16,8 +16,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// eslint-disable-next-line path-alias/no-relative
import { filters, mapMangledModuleLazy, waitFor, wreq } from "../webpack";
import { filters, mapMangledModuleLazy, waitFor, wreq } from "@webpack";
import type * as t from "./types/menu";
export const Menu = {} as t.Menu;

View file

@ -16,8 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// eslint-disable-next-line path-alias/no-relative
import { findByCodeLazy, findByPropsLazy, waitFor } from "../webpack";
import { findByCodeLazy, findByPropsLazy, waitFor } from "@webpack";
export let React: typeof import("react");
export let useState: typeof React.useState;

View file

@ -16,10 +16,9 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { findByCodeLazy, findByPropsLazy } from "@webpack";
import type * as Stores from "discord-types/stores";
// eslint-disable-next-line path-alias/no-relative
import { findByCodeLazy, findByPropsLazy } from "../webpack";
import { waitForStore } from "./internal";
import * as t from "./types/stores";
@ -33,7 +32,7 @@ export let MessageStore: Omit<Stores.MessageStore, "getMessages"> & GenericStore
getMessages(chanId: string): any;
};
// this is not actually a FluxStore
// TODO: The correct name for this is ChannelActionCreators and it has already been exported again from utils. Remove this export once enough time has passed
export const PrivateChannelsStore = findByPropsLazy("openPrivateChannel");
export let PermissionStore: GenericStore;
export let GuildChannelStore: GenericStore;
@ -86,4 +85,8 @@ waitForStore("GuildChannelStore", m => GuildChannelStore = m);
waitForStore("MessageStore", m => MessageStore = m);
waitForStore("WindowStore", m => WindowStore = m);
waitForStore("EmojiStore", m => EmojiStore = m);
waitForStore("ThemeStore", m => ThemeStore = m);
waitForStore("ThemeStore", m => {
ThemeStore = m;
// Importing this directly can easily cause circular imports. For this reason, use a non import access here.
Vencord.QuickCss.initQuickCssThemeStore();
});

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import type { ComponentClass, ComponentPropsWithRef, ComponentType, CSSProperties, FunctionComponent, HtmlHTMLAttributes, HTMLProps, JSX, KeyboardEvent, MouseEvent, PointerEvent, PropsWithChildren, ReactNode, Ref } from "react";
import type { ComponentClass, ComponentPropsWithRef, ComponentType, CSSProperties, FunctionComponent, HtmlHTMLAttributes, HTMLProps, JSX, KeyboardEvent, MouseEvent, PointerEvent, PropsWithChildren, ReactNode, Ref, RefObject } from "react";
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";
@ -426,6 +426,7 @@ export type Popout = ComponentType<{
}
): ReactNode;
shouldShow?: boolean;
targetElementRef: RefObject<any>;
renderPopout(args: {
closePopout(): void;
isPositioned: boolean;

View file

@ -16,16 +16,15 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { _resolveReady, filters, findByCodeLazy, findByPropsLazy, findLazy, mapMangledModuleLazy, waitFor } from "@webpack";
import type { Channel } from "discord-types/general";
// eslint-disable-next-line path-alias/no-relative
import { _resolveReady, filters, findByCodeLazy, findByPropsLazy, findLazy, mapMangledModuleLazy, waitFor } from "../webpack";
import type * as t from "./types/utils";
export let FluxDispatcher: t.FluxDispatcher;
waitFor(["dispatch", "subscribe"], m => {
FluxDispatcher = m;
// Non import call to avoid circular dependency
// Non import access to avoid circular dependency
Vencord.Plugins.subscribeAllPluginsFluxEvents(m);
const cb = () => {
@ -35,7 +34,7 @@ waitFor(["dispatch", "subscribe"], m => {
m.subscribe("CONNECTION_OPEN", cb);
});
export let ComponentDispatch;
export let ComponentDispatch: any;
waitFor(["dispatchToLastSubscribed"], m => ComponentDispatch = m);
export const Constants: t.Constants = mapMangledModuleLazy('ME:"/users/@me"', {
@ -177,6 +176,7 @@ export const MessageActions = findByPropsLazy("editMessage", "sendMessage");
export const MessageCache = findByPropsLazy("clearCache", "_channelMessages");
export const UserProfileActions = findByPropsLazy("openUserProfileModal", "closeUserProfileModal");
export const InviteActions = findByPropsLazy("resolveInvite");
export const ChannelActionCreators = findByPropsLazy("openPrivateChannel");
export const IconUtils: t.IconUtils = findByPropsLazy("getGuildBannerURL", "getUserAvatarURL");

View file

@ -60,7 +60,7 @@ export function getFactoryPatchedBy(moduleId: PropertyKey, webpackRequire = wreq
return webpackRequire.m[moduleId]?.[SYM_PATCHED_BY];
}
const logger = new Logger("WebpackInterceptor", "#8caaee");
const logger = new Logger("WebpackPatcher", "#8caaee");
/** Whether we tried to fallback to the WebpackRequire of the factory, or disabled patches */
let wreqFallbackApplied = false;
@ -106,6 +106,13 @@ define(Function.prototype, "m", {
const fileName = stack.match(/\/assets\/(.+?\.js)/)?.[1];
// Currently, sentry and libdiscore Webpack instances are not meant to be patched.
// As an extra measure, take advatange of the fact their files include the names and return early if it's one of them.
// Later down we also include other measures to avoid patching them.
if (["sentry", "libdiscore"].some(name => fileName?.toLowerCase()?.includes(name))) {
return;
}
// Define a setter for the bundlePath property of WebpackRequire. Only Webpack instances which include chunk loading functionality,
// like the main Discord Webpack, have this property.
// So if the setter is called with the Discord bundlePath, this means we should patch this instance and initialize the internal references to WebpackRequire.
@ -116,7 +123,10 @@ define(Function.prototype, "m", {
define(this, "p", { value: bundlePath });
clearTimeout(bundlePathTimeout);
if (bundlePath !== "/assets/") {
// libdiscore init Webpack instance always returns a constant string for the js filename of a chunk.
// In that case, avoid patching this instance,
// as it runs before the main Webpack instance and will make the WebpackRequire fallback not work properly, or init an wrongful main WebpackRequire.
if (bundlePath !== "/assets/" || /(?:=>|{return)"[^"]/.exec(String(this.u))) {
return;
}
@ -129,9 +139,9 @@ define(Function.prototype, "m", {
}
});
// In the past, the sentry Webpack instance which we also wanted to patch used to rely on chunks being loaded before initting sentry.
// In the past, the sentry Webpack instance which we also wanted to patch used to rely on chunks being loaded before initing sentry.
// This Webpack instance did not include actual chunk loading, and only awaited for them to be loaded, which means it did not include the bundlePath property.
// To keep backwards compability, in case this is ever the case again, and keep patching this type of instance, we explicity patch instances which include wreq.O and not wreq.p.
// To keep backwards compability, if this is ever the case again, and keep patching this type of instance, we explicity patch instances which include wreq.O and not wreq.p.
// Since we cannot check what is the bundlePath of the instance to filter for the Discord bundlePath, we only patch it if wreq.p is not included,
// which means the instance relies on another instance which does chunk loading, and that makes it very likely to only target Discord Webpack instances like the old sentry.
@ -436,12 +446,21 @@ function runFactoryWithWrap(patchedFactory: PatchedModuleFactory, thisArg: unkno
callback(exports, module.id);
continue;
}
} catch (err) {
logger.error(
"Error while filtering or firing callback for Webpack waitFor subscription:\n", err,
"\n\nModule exports:", exports,
"\n\nFilter:", filter,
"\n\nCallback:", callback
);
}
if (typeof exports !== "object") {
continue;
}
if (typeof exports !== "object") {
continue;
}
for (const exportKey in exports) {
for (const exportKey in exports) {
try {
// Some exports might have not been initialized yet due to circular imports, so try catch it.
try {
var exportValue = exports[exportKey];
@ -454,9 +473,14 @@ function runFactoryWithWrap(patchedFactory: PatchedModuleFactory, thisArg: unkno
callback(exportValue, module.id);
break;
}
} catch (err) {
logger.error(
"Error while filtering or firing callback for Webpack waitFor subscription:\n", err,
"\n\nExport value:", exports,
"\n\nFilter:", filter,
"\n\nCallback:", callback
);
}
} catch (err) {
logger.error("Error while firing callback for Webpack waitFor subscription:\n", err, filter, callback);
}
}
@ -567,7 +591,7 @@ function patchFactory(moduleId: PropertyKey, originalFactory: AnyModuleFactory):
}
code = newCode;
patchedSource = `// Webpack Module ${String(moduleId)} - Patched by ${pluginsList.join(", ")}\n${newCode}\n//# sourceURL=WebpackModule${String(moduleId)}`;
patchedSource = `// Webpack Module ${String(moduleId)} - Patched by ${pluginsList.join(", ")}\n${newCode}\n//# sourceURL=file:///WebpackModule${String(moduleId)}`;
patchedFactory = (0, eval)(patchedSource);
if (!patchedBy.has(patch.plugin)) {

View file

@ -145,9 +145,17 @@ function makePropertyNonEnumerable(target: Record<PropertyKey, any>, key: Proper
}
export function _blacklistBadModules(requireCache: NonNullable<AnyWebpackRequire["c"]>, exports: ModuleExports, moduleId: PropertyKey) {
if (shouldIgnoreValue(exports)) {
makePropertyNonEnumerable(requireCache, moduleId);
return true;
try {
if (shouldIgnoreValue(exports)) {
makePropertyNonEnumerable(requireCache, moduleId);
return true;
}
} catch (err) {
logger.error(
"Error while blacklisting module:\n", err,
"\n\nModule id:", moduleId,
"\n\nModule exports:", exports,
);
}
if (typeof exports !== "object") {
@ -156,17 +164,25 @@ export function _blacklistBadModules(requireCache: NonNullable<AnyWebpackRequire
let hasOnlyBadProperties = true;
for (const exportKey in exports) {
// Some exports might have not been initialized yet due to circular imports, so try catch it.
try {
var exportValue = exports[exportKey];
} catch {
continue;
}
// Some exports might have not been initialized yet due to circular imports, so try catch it.
try {
var exportValue = exports[exportKey];
} catch {
continue;
}
if (shouldIgnoreValue(exportValue)) {
makePropertyNonEnumerable(exports, exportKey);
} else {
hasOnlyBadProperties = false;
if (shouldIgnoreValue(exportValue)) {
makePropertyNonEnumerable(exports, exportKey);
} else {
hasOnlyBadProperties = false;
}
} catch (err) {
logger.error(
"Error while blacklistng module:\n", err,
"\n\nModule id:", moduleId,
"\n\nExport value:", exportValue,
);
}
}
@ -452,6 +468,29 @@ export function findStore(name: StoreNameFilter) {
}
}
try {
const getLibdiscore = findByCode("libdiscoreWasm is not initialized");
const libdiscoreExports = getLibdiscore();
for (const libdiscoreExportName in libdiscoreExports) {
if (!libdiscoreExportName.endsWith("Store")) {
continue;
}
const storeName = libdiscoreExportName;
const store = libdiscoreExports[storeName];
if (storeName === name) {
res = store;
}
if (fluxStores[storeName] == null) {
fluxStores[storeName] = store;
}
}
} catch { }
if (res == null) {
res = find(filters.byStoreName(name), { isIndirect: true });
}
@ -735,7 +774,7 @@ export function extract(id: string | number) {
// This module is NOT ACTUALLY USED! This means putting breakpoints will have NO EFFECT!!
0,${mod.toString()}
//# sourceURL=ExtractedWebpackModule${id}
//# sourceURL=file:///ExtractedWebpackModule${id}
`;
const extracted = (0, eval)(code);
return extracted as Function;