diff --git a/.github/workflows/reportBrokenPlugins.yml b/.github/workflows/reportBrokenPlugins.yml index 677ae3c5e..fe041023a 100644 --- a/.github/workflows/reportBrokenPlugins.yml +++ b/.github/workflows/reportBrokenPlugins.yml @@ -29,9 +29,12 @@ jobs: - name: Install dependencies run: | 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 run: pnpm buildWeb --standalone --dev @@ -42,6 +45,12 @@ jobs: export PATH="$PWD/node_modules/.bin:$PATH" export CHROMIUM_BIN=$(which chromium-browser) + - name: Create Report + timeout-minutes: 10 + run: | + export PATH="$PWD/node_modules/.bin:$PATH" + export CHROMIUM_BIN=${{ steps.setup-chrome.outputs.chrome-path }} + esbuild scripts/generateReport.ts > dist/report.mjs node dist/report.mjs >> $GITHUB_STEP_SUMMARY env: @@ -53,7 +62,7 @@ jobs: if: success() || failure() # even run if previous one failed run: | 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 esbuild scripts/generateReport.ts > dist/report.mjs diff --git a/src/plugins/_api/badges/fixBadgeOverflow.css b/src/plugins/_api/badges/fixBadgeOverflow.css new file mode 100644 index 000000000..348d0ff38 --- /dev/null +++ b/src/plugins/_api/badges/fixBadgeOverflow.css @@ -0,0 +1,3 @@ +[class*="profileBadges"] { + flex: none; +} diff --git a/src/plugins/_api/badges.tsx b/src/plugins/_api/badges/index.tsx similarity index 99% rename from src/plugins/_api/badges.tsx rename to src/plugins/_api/badges/index.tsx index 054649464..b7d1692f6 100644 --- a/src/plugins/_api/badges.tsx +++ b/src/plugins/_api/badges/index.tsx @@ -16,6 +16,8 @@ * along with this program. If not, see . */ +import "./fixBadgeOverflow.css"; + import { BadgePosition, BadgeUserArgs, ProfileBadge } from "@api/Badges"; import DonateButton from "@components/DonateButton"; import ErrorBoundary from "@components/ErrorBoundary"; @@ -99,7 +101,7 @@ export default definePlugin({ }, // 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) :" }, // conditionally override their onClick with badge.onClick if it exists diff --git a/src/plugins/imageLink/README.md b/src/plugins/imageLink/README.md new file mode 100644 index 000000000..add657948 --- /dev/null +++ b/src/plugins/imageLink/README.md @@ -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. diff --git a/src/plugins/imageLink/index.ts b/src/plugins/imageLink/index.ts new file mode 100644 index 000000000..73a977fa9 --- /dev/null +++ b/src/plugins/imageLink/index.ts @@ -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], + + patches: [ + { + find: "isEmbedInline:function()", + replacement: { + match: /(?<=isEmbedInline:function\(\)\{return )\i(?=\})/, + replace: "()=>false", + }, + }, + ], +}); diff --git a/src/plugins/implicitRelationships/index.ts b/src/plugins/implicitRelationships/index.ts index be17e0fe4..15165d64a 100644 --- a/src/plugins/implicitRelationships/index.ts +++ b/src/plugins/implicitRelationships/index.ts @@ -81,8 +81,8 @@ export default definePlugin({ find: "getRelationshipCounts(){", replacement: { predicate: () => Settings.plugins.ImplicitRelationships.sortByAffinity, - match: /\.sortBy\(\i=>\i\.comparator\)/, - replace: "$&.sortBy((row) => $self.sortList(row))" + match: /\}\)\.sortBy\((.+?)\)\.value\(\)/, + replace: "}).sortBy(row => $self.wrapSort(($1), row)).value()" } }, @@ -120,10 +120,10 @@ export default definePlugin({ } ), - sortList(row: any) { + wrapSort(comparator: Function, row: any) { return row.type === 5 ? -UserAffinitiesStore.getUserAffinity(row.user.id)?.affinity ?? 0 - : row.comparator; + : comparator(row); }, async fetchImplicitRelationships() { diff --git a/src/plugins/index.ts b/src/plugins/index.ts index 7092001ee..2d5e3e5a4 100644 --- a/src/plugins/index.ts +++ b/src/plugins/index.ts @@ -34,6 +34,9 @@ export const PMLogger = logger; export const plugins = Plugins; 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 settings = Settings.plugins; export function isPluginEnabled(p: string) { @@ -119,6 +122,33 @@ export function startDependenciesRecursive(p: Plugin) { return { restartNeeded, failures }; } +export function subscribePluginFluxEvents(p: Plugin, fluxDispatcher: typeof FluxDispatcher) { + if (p.flux) { + 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) { + 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) { const { name, commands, flux, contextMenus } = p; @@ -138,7 +168,7 @@ export const startPlugin = traceFunction("startPlugin", function startPlugin(p: } if (commands?.length) { - logger.info("Registering commands of plugin", name); + logger.debug("Registering commands of plugin", name); for (const cmd of commands) { try { registerCommand(cmd, name); @@ -149,13 +179,13 @@ export const startPlugin = traceFunction("startPlugin", function startPlugin(p: } } - if (flux) { - for (const event in flux) { - FluxDispatcher.subscribe(event as FluxEvents, flux[event]); - } + if (enabledPluginsSubscribedFlux) { + subscribePluginFluxEvents(p, FluxDispatcher); } + if (contextMenus) { + logger.debug("Adding context menus patches of plugin", name); for (const navId in contextMenus) { addContextMenuPatch(navId, contextMenus[navId]); } @@ -182,7 +212,7 @@ export const stopPlugin = traceFunction("stopPlugin", function stopPlugin(p: Plu } if (commands?.length) { - logger.info("Unregistering commands of plugin", name); + logger.debug("Unregistering commands of plugin", name); for (const cmd of commands) { try { unregisterCommand(cmd.name); @@ -193,13 +223,10 @@ export const stopPlugin = traceFunction("stopPlugin", function stopPlugin(p: Plu } } - if (flux) { - for (const event in flux) { - FluxDispatcher.unsubscribe(event as FluxEvents, flux[event]); - } - } + unsubscribePluginFluxEvents(p, FluxDispatcher); if (contextMenus) { + logger.debug("Removing context menus patches of plugin", name); for (const navId in contextMenus) { removeContextMenuPatch(navId, contextMenus[navId]); } diff --git a/src/plugins/messageClickActions/index.ts b/src/plugins/messageClickActions/index.ts index 052c33f61..14899c367 100644 --- a/src/plugins/messageClickActions/index.ts +++ b/src/plugins/messageClickActions/index.ts @@ -17,17 +17,19 @@ */ import { addClickListener, removeClickListener } from "@api/MessageEvents"; -import { definePluginSettings, Settings } from "@api/Settings"; +import { definePluginSettings } from "@api/Settings"; import { Devs } from "@utils/constants"; import definePlugin, { OptionType } from "@utils/types"; import { findByPropsLazy } from "@webpack"; import { FluxDispatcher, PermissionsBits, PermissionStore, UserStore } from "@webpack/common"; +const MessageActions = findByPropsLazy("deleteMessage", "startEditMessage"); +const EditStore = findByPropsLazy("isEditing", "isEditingAny"); + let isDeletePressed = false; const keydown = (e: KeyboardEvent) => e.key === "Backspace" && (isDeletePressed = true); const keyup = (e: KeyboardEvent) => e.key === "Backspace" && (isDeletePressed = false); - const settings = definePluginSettings({ enableDeleteOnClick: { type: OptionType.BOOLEAN, @@ -60,9 +62,6 @@ export default definePlugin({ settings, start() { - const MessageActions = findByPropsLazy("deleteMessage", "startEditMessage"); - const EditStore = findByPropsLazy("isEditing", "isEditingAny"); - document.addEventListener("keydown", keydown); document.addEventListener("keyup", keyup); @@ -85,11 +84,17 @@ export default definePlugin({ const EPHEMERAL = 64; 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({ type: "CREATE_PENDING_REPLY", channel, message: msg, - shouldMention: !Settings.plugins.NoReplyMention.enabled, + shouldMention, showMentionToggle: channel.guild_id !== null }); } diff --git a/src/plugins/messageLogger/index.tsx b/src/plugins/messageLogger/index.tsx index 731e4b84a..95234d5ac 100644 --- a/src/plugins/messageLogger/index.tsx +++ b/src/plugins/messageLogger/index.tsx @@ -255,7 +255,7 @@ export default definePlugin({ replace: "$1" + ".update($3,m =>" + " (($2.message.flags & 64) === 64 || $self.shouldIgnore($2.message, true)) ? m :" + - " $2.message.content !== m.editHistory?.[0]?.content && $2.message.content !== m.content ?" + + " $2.message.content !== m.content ?" + " m.set('editHistory',[...(m.editHistory || []), $self.makeEdit($2.message, m)]) :" + " m" + ")" + diff --git a/src/plugins/moreUserTags/index.tsx b/src/plugins/moreUserTags/index.tsx index 4ac542216..df47e5456 100644 --- a/src/plugins/moreUserTags/index.tsx +++ b/src/plugins/moreUserTags/index.tsx @@ -198,8 +198,7 @@ export default definePlugin({ replacement: [ // 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|BOT_TAG_BOT)/, + match: /(switch\((\i)\){.+?)case (\i(?:\.\i)?)\.BOT:default:(\i)=.{0,40}(\i\.\i\.Messages)\.APP_TAG/, replace: (_, origSwitch, variant, tags, displayedText, 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`]), - // FIXME: Remove the BOT_TAG_BOT variants from strings when the change arrives in stable getTagText(passedTagName: string, strings: Record) { - if (!passedTagName) return strings.APP_TAG ?? strings.BOT_TAG_BOT; + if (!passedTagName) return strings.APP_TAG; const [tagName, variant] = passedTagName.split("-"); const tag = tags.find(({ name }) => tagName === name); - if (!tag) return strings.APP_TAG ?? strings.BOT_TAG_BOT; - if (variant === "BOT" && tagName !== "WEBHOOK" && this.settings.store.dontShowForBots) 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; const tagText = settings.store.tagSettings?.[tag.name]?.text || tag.displayName; switch (variant) { case "OP": return `${strings.BOT_TAG_FORUM_ORIGINAL_POSTER} • ${tagText}`; case "BOT": - return `${strings.APP_TAG ?? strings.BOT_TAG_BOT} • ${tagText}`; + return `${strings.APP_TAG} • ${tagText}`; default: return tagText; } diff --git a/src/plugins/newGuildSettings/index.tsx b/src/plugins/newGuildSettings/index.tsx index 5c916cc77..d5c3d9206 100644 --- a/src/plugins/newGuildSettings/index.tsx +++ b/src/plugins/newGuildSettings/index.tsx @@ -31,6 +31,16 @@ const settings = definePluginSettings({ type: OptionType.BOOLEAN, 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: { description: "Suppress @everyone and @here", type: OptionType.BOOLEAN, @@ -41,6 +51,16 @@ const settings = definePluginSettings({ type: OptionType.BOOLEAN, default: true }, + highlights: { + description: "Suppress Highlights automatically", + type: OptionType.BOOLEAN, + default: true + }, + events: { + description: "Mute New Events automatically", + type: OptionType.BOOLEAN, + default: true + }, showAllChannels: { description: "Show all channels automatically", type: OptionType.BOOLEAN, @@ -53,7 +73,7 @@ export default definePlugin({ name: "NewGuildSettings", description: "Automatically mute new servers and change various other settings upon joining", 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: [ { find: ",acceptInvite(", @@ -78,8 +98,16 @@ export default definePlugin({ { muted: settings.store.guild, 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)) { toggleShowAllChannels(guildId); } diff --git a/src/plugins/pauseInvitesForever/README.md b/src/plugins/pauseInvitesForever/README.md new file mode 100644 index 000000000..7ccf0b72f --- /dev/null +++ b/src/plugins/pauseInvitesForever/README.md @@ -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) diff --git a/src/plugins/pauseInvitesForever/index.tsx b/src/plugins/pauseInvitesForever/index.tsx new file mode 100644 index 000000000..81f18fd6e --- /dev/null +++ b/src/plugins/pauseInvitesForever/index.tsx @@ -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 . +*/ + +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 ( +
+ {Messages.GUILD_INVITE_DISABLE_ACTION_SHEET_DESCRIPTION} + {this.showDisableInvites(guildId) && { + setChecked(true); + this.disableInvites(guildId); + }}> Pause Indefinitely.} +
+ ); + } +}); diff --git a/src/plugins/reviewDB/index.tsx b/src/plugins/reviewDB/index.tsx index 0c3578b07..bcd0f163f 100644 --- a/src/plugins/reviewDB/index.tsx +++ b/src/plugins/reviewDB/index.tsx @@ -35,13 +35,25 @@ import { getCurrentUserInfo, readNotification } from "./reviewDbApi"; import { settings } from "./settings"; import { showToast } from "./utils"; -const guildPopoutPatch: NavContextMenuPatchCallback = (children, props: { guild: Guild, onClose(): void; }) => { +const guildPopoutPatch: NavContextMenuPatchCallback = (children, { guild }: { guild: Guild, onClose(): void; }) => { children.push( 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( + openReviewsModal(user.id, user.username)} /> ); }; @@ -53,7 +65,10 @@ export default definePlugin({ settings, contextMenus: { - "guild-header-popout": guildPopoutPatch + "guild-header-popout": guildPopoutPatch, + "guild-context": guildPopoutPatch, + "user-context": userContextPatch, + "user-profile-actions": userContextPatch }, patches: [ diff --git a/src/plugins/secretRingTone/index.ts b/src/plugins/secretRingTone/index.ts index 8f0176f88..9c3956a80 100644 --- a/src/plugins/secretRingTone/index.ts +++ b/src/plugins/secretRingTone/index.ts @@ -14,10 +14,11 @@ export default definePlugin({ authors: [Devs.AndrewDLO, Devs.FieryFlames], patches: [ { - find: "call_ringing_beat\"", + find: '"call_ringing_beat"', replacement: { - match: /500===\i\(\)\.random\(1,1e3\)/, - replace: "true" + // FIXME Remove === alternative when it hits stable + match: /500(!==|===)\i\(\)\.random\(1,1e3\)/, + replace: (_, predicate) => predicate === "!==" ? "false" : "true", } }, ], diff --git a/src/plugins/showHiddenChannels/index.tsx b/src/plugins/showHiddenChannels/index.tsx index ce4a47ade..09aa2302a 100644 --- a/src/plugins/showHiddenChannels/index.tsx +++ b/src/plugins/showHiddenChannels/index.tsx @@ -36,6 +36,8 @@ const enum ShowMode { HiddenIconWithMutedStyle } +const CONNECT = 1n << 20n; + export const settings = definePluginSettings({ hideUnreads: { description: "Hide Unreads", @@ -273,12 +275,12 @@ export default definePlugin({ { // Change the role permission check to CONNECT if the channel is locked 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 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 diff --git a/src/plugins/sortFriendRequests/index.tsx b/src/plugins/sortFriendRequests/index.tsx index 32579a803..7033591bd 100644 --- a/src/plugins/sortFriendRequests/index.tsx +++ b/src/plugins/sortFriendRequests/index.tsx @@ -41,8 +41,8 @@ export default definePlugin({ patches: [{ find: "getRelationshipCounts(){", replacement: { - match: /\.sortBy\(\i=>\i\.comparator\)/, - replace: "$&.sortBy((row) => $self.sortList(row))" + match: /\}\)\.sortBy\((.+?)\)\.value\(\)/, + replace: "}).sortBy(row => $self.wrapSort(($1), row)).value()" } }, { 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 ? -this.getSince(row.user) - : row.comparator; + : comparator(row); }, getSince(user: User) { diff --git a/src/plugins/usrbg/index.tsx b/src/plugins/usrbg/index.tsx index eeccbc360..52b3ea7d5 100644 --- a/src/plugins/usrbg/index.tsx +++ b/src/plugins/usrbg/index.tsx @@ -27,10 +27,10 @@ import style from "./index.css?managed"; const API_URL = "https://usrbg.is-hardly.online/users"; interface UsrbgApiReturn { - endpoint: string; - bucket: string; - prefix: string; - users: { [id: string]: string; }; + endpoint: string + bucket: string + prefix: string + users: Record } const settings = definePluginSettings({ @@ -53,7 +53,7 @@ const settings = definePluginSettings({ export default definePlugin({ name: "USRBG", description: "Displays user banners from USRBG, allowing anyone to get a banner without Nitro", - authors: [Devs.AutumnVN, Devs.pylix, Devs.TheKodeToad, Devs.katlyn], + authors: [Devs.AutumnVN, Devs.katlyn, Devs.pylix, Devs.TheKodeToad], settings, patches: [ { @@ -120,21 +120,14 @@ export default definePlugin({ }, userHasBackground(userId: string) { - if (this.data === null) return false; - return this.data.users[userId] !== undefined; + return !!this.data?.users[userId]; }, getImageUrl(userId: string): string | null { - if (this.data == null) { - return null; - } + if (!this.userHasBackground(userId)) return null; - const etag = this.data.users[userId]; - if (etag === undefined) { - return null; - } - - const { endpoint, bucket, prefix } = this.data; + // 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}`; }, diff --git a/src/plugins/validUser/index.tsx b/src/plugins/validUser/index.tsx index 2fce693e8..7a21ac86b 100644 --- a/src/plugins/validUser/index.tsx +++ b/src/plugins/validUser/index.tsx @@ -21,12 +21,37 @@ import { Devs } from "@utils/constants"; import { sleep } from "@utils/misc"; import { Queue } from "@utils/Queue"; 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"; +// LYING to the type checker here +const UserFlags = Constants.UserFlags as Record; +const badges: Record = { + "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(); const queue = new Queue(5); +interface ProfileBadge { + id: string; + description: string; + icon: string; + link?: string; +} + interface MentionProps { data: { userId?: string; @@ -43,6 +68,45 @@ interface MentionProps { UserMention: ComponentType; } +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) { const [userId, setUserId] = useState(data.userId); @@ -85,14 +149,14 @@ function MentionWrapper({ data, UserMention, RoleMention, parse, props }: Mentio fetching.add(id); queue.unshift(() => - UserUtils.getUser(id) + getUser(id) .then(() => { setUserId(id); fetching.delete(id); }) .catch(e => { if (e?.status === 429) { - queue.unshift(() => sleep(1000).then(fetch)); + queue.unshift(() => sleep(e?.body?.retry_after ?? 1000).then(fetch)); fetching.delete(id); } }) @@ -112,7 +176,7 @@ function MentionWrapper({ data, UserMention, RoleMention, parse, props }: Mentio export default definePlugin({ name: "ValidUser", 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"], patches: [ diff --git a/src/utils/constants.ts b/src/utils/constants.ts index be1f9277c..97d1bf1ee 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -451,6 +451,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({ PolisanTheEasyNick: { name: "Oleh Polisan", id: 242305263313485825n + }, + GabiRP: { + name: "GabiRP", + id: 507955112027750401n } } satisfies Record); diff --git a/src/webpack/common/components.ts b/src/webpack/common/components.ts index 5bb0c4f67..de0c53a9f 100644 --- a/src/webpack/common/components.ts +++ b/src/webpack/common/components.ts @@ -35,7 +35,7 @@ export let Tooltip: t.Tooltip; export let TextInput: t.TextInput; export let TextArea: t.TextArea; export let Text: t.Text; -export let Heading: t.HeadingTag; +export let Heading: t.Heading; export let Select: t.Select; export let SearchableSelect: t.SearchableSelect; export let Slider: t.Slider; diff --git a/src/webpack/common/utils.ts b/src/webpack/common/utils.ts index 2e41dccec..fd41fd3e0 100644 --- a/src/webpack/common/utils.ts +++ b/src/webpack/common/utils.ts @@ -22,9 +22,11 @@ import { _resolveReady, filters, findByCodeLazy, findByProps, findByPropsLazy, f 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 + Vencord.Plugins.subscribeAllPluginsFluxEvents(m); + const cb = () => { m.unsubscribe("CONNECTION_OPEN", cb); _resolveReady(); @@ -36,6 +38,8 @@ export let ComponentDispatch; waitFor(["ComponentDispatch", "ComponentDispatcher"], m => ComponentDispatch = m.ComponentDispatch); +export const Constants = findByPropsLazy("Endpoints"); + export const RestAPI: t.RestAPI = proxyLazyWebpack(() => { const mod = findByProps("getAPIBaseURL"); return mod.HTTP ?? mod; diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index a871bd5ad..2edc8bc3e 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -62,38 +62,42 @@ Object.defineProperty(Function.prototype, "O", { 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 - if (new Error().stack?.includes("discord.com") && this.e.toString().includes("Promise.all")) { - logger.info("Found main Webpack onChunksLoaded"); + // this.e (wreq.e) is the method for loading a chunk, and only the main webpack has it + if (new Error().stack?.includes("discord.com") && String(this.e).includes("Promise.all")) { + logger.info("Found main WebpackRequire.onChunksLoaded"); delete (Function.prototype as any).O; - const wreq = this; - - const originalOnChunksLoaded = onChunksLoaded.bind(this); - onChunksLoaded = function (result: any, chunkIds: string[], callback: () => any, priority: number) { + const originalOnChunksLoaded = onChunksLoaded; + onChunksLoaded = function (this: unknown, result: any, chunkIds: string[], callback: () => any, priority: number) { if (callback != null && initCallbackRegex.test(callback.toString())) { - Object.defineProperty(wreq, "O", { + Object.defineProperty(this, "O", { value: originalOnChunksLoaded, configurable: true }); + const wreq = this as WebpackInstance; + const originalCallback = callback; - callback = function () { + callback = function (this: unknown) { + logger.info("Patched initialize app callback invoked, initializing our internal references to WebpackRequire and running beforeInitListeners"); _initWebpack(wreq); for (const beforeInitListener of beforeInitListeners) { beforeInitListener(wreq); } - originalCallback(); + originalCallback.apply(this, arguments as any); }; callback.toString = originalCallback.toString.bind(originalCallback); + arguments[2] = callback; } - originalOnChunksLoaded(result, chunkIds, callback, priority); + originalOnChunksLoaded.apply(this, arguments as any); }; + onChunksLoaded.toString = originalOnChunksLoaded.toString.bind(originalOnChunksLoaded); } Object.defineProperty(this, "O", { @@ -141,6 +145,7 @@ function patchPush(webpackGlobal: any) { } handlePush.$$vencordOriginal = webpackGlobal.push; + handlePush.toString = handlePush.$$vencordOriginal.toString.bind(handlePush.$$vencordOriginal); // Webpack overwrites .push with its own push like so: `d.push = n.bind(null, d.push.bind(d));` // it wraps the old push (`d.push.bind(d)`). this old push is in this case our handlePush. // If we then repatched the new push, we would end up with recursive patching, which leads to our patches @@ -169,10 +174,10 @@ function patchFactories(factories: Record string, original: any, (...args: any[]): void; }; - factory.toString = mod.toString.bind(mod); + factory.toString = originalMod.toString.bind(originalMod); factory.original = originalMod; for (const factoryListener of factoryListeners) { try { - factoryListener(factory); + factoryListener(originalMod); } catch (err) { - logger.error("Error in Webpack factory listener", err); + logger.error("Error in Webpack factory listener:\n", err, factoryListener); } } @@ -251,19 +256,19 @@ function patchFactories(factories: Record code.replace(match, replace)); if (patch.predicate && !patch.predicate()) continue; - if (!code.includes(patch.find)) continue; patchedBy.add(patch.plugin); + const executePatch = traceFunction(`patch by ${patch.plugin}`, (match: string | RegExp, replace: string) => code.replace(match, replace)); const previousMod = mod; 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[]) { if (replacement.predicate && !replacement.predicate()) continue; + const lastMod = mod; const lastCode = code; @@ -281,15 +286,17 @@ function patchFactories(factories: Record