From 0863efa93eaa4f1325fb3daae324d0dfff650217 Mon Sep 17 00:00:00 2001 From: thororen <78185467+thororen1234@users.noreply.github.com> Date: Tue, 14 Jul 2026 15:42:06 -0400 Subject: [PATCH 1/8] ReviewDB: Fix DM Sidebar Patch (#4384) --- src/plugins/reviewDB/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/reviewDB/index.tsx b/src/plugins/reviewDB/index.tsx index 91f706d36..da3e17e69 100644 --- a/src/plugins/reviewDB/index.tsx +++ b/src/plugins/reviewDB/index.tsx @@ -87,7 +87,7 @@ export default definePlugin({ // DM profile sidebar find: ".SIDEBAR,disableToolbar:", replacement: { - match: /user:(\i),widgets:.{0,100}?\}\),(?=.{0,200}?#{intl::USER_PROFILE_WISHLIST})/, + match: /user:(\i),widgets:.{0,100}?\}\),(?=.{0,100}unownedWishlistItems:\i,wishlistId:\i)/, replace: "$&$self.renderProfileComponent({user:$1,isSideBar:true})," } }, From fd8a86ea0bb8b8e44b518e773d196953a7b2eaeb Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Tue, 14 Jul 2026 16:51:46 -0300 Subject: [PATCH 2/8] BetterSettings: Attempt to make patch faster --- src/plugins/betterSettings/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/betterSettings/index.tsx b/src/plugins/betterSettings/index.tsx index 23f3c1ec8..16a7bac47 100644 --- a/src/plugins/betterSettings/index.tsx +++ b/src/plugins/betterSettings/index.tsx @@ -95,8 +95,8 @@ export default definePlugin({ find: "this.renderArtisanalHack()", replacement: [ { - match: /class (\i)(?= extends \i\.PureComponent.+?static contextType=.+?jsx\)\(\1,\{mode:)/, - replace: "var $1=$self.Layer;class VencordPatchedOldFadeLayer", + match: /class (\i)( extends \i\.PureComponent.+?jsx\)\(\1,\{mode:)/, + replace: "var $1=$self.Layer;class VencordPatchedOldFadeLayer$2", predicate: () => settings.store.disableFade }, { // Lazy-load contents From fea94b79125d1ff6491b71c187baaa579aeffeb7 Mon Sep 17 00:00:00 2001 From: sadan <117494111+sadan4@users.noreply.github.com> Date: Tue, 14 Jul 2026 17:44:46 -0400 Subject: [PATCH 3/8] MessageLogger: Fix deleted attachments patches (#4342) Co-authored-by: thororen <78185467+thororen1234@users.noreply.github.com> Co-authored-by: Nuckyz <61953774+Nuckyz@users.noreply.github.com> --- src/plugins/messageLogger/index.tsx | 78 +++++++++++++-------- src/plugins/messageLogger/messageLogger.css | 7 +- src/webpack/common/utils.ts | 2 +- 3 files changed, 52 insertions(+), 35 deletions(-) diff --git a/src/plugins/messageLogger/index.tsx b/src/plugins/messageLogger/index.tsx index bb916b474..64382e371 100644 --- a/src/plugins/messageLogger/index.tsx +++ b/src/plugins/messageLogger/index.tsx @@ -28,7 +28,7 @@ import { getIntlMessage } from "@utils/discord"; import { Logger } from "@utils/Logger"; import { classes } from "@utils/misc"; import definePlugin, { OptionType } from "@utils/types"; -import { Message } from "@vencord/discord-types"; +import { Message, MessageAttachment } from "@vencord/discord-types"; import { findCssClassesLazy } from "@webpack"; import { ChannelStore, FluxDispatcher, Menu, MessageStore, Parser, SelectedChannelStore, Timestamp, UserStore, useStateFromStores } from "@webpack/common"; @@ -42,6 +42,15 @@ interface MLMessage extends Message { firstEditTimestamp?: Date; } +interface MLAttachment extends MessageAttachment { + /** + * if the attachment was deleted + * + * a non-deleted {@link MLMessage|Message} can have deleted attachments + */ + deleted?: boolean; +} + const MessageClasses = findCssClassesLazy("edited", "communicationDisabled", "isSystemMessage"); const settings = definePluginSettings({ @@ -207,7 +216,7 @@ export default definePlugin({ name: "MessageLogger", description: "Temporarily logs deleted and edited messages.", tags: ["Chat", "Utility"], - authors: [Devs.rushii, Devs.Ven, Devs.AutumnVN, Devs.Nickyux, Devs.Kyuuhachi], + authors: [Devs.rushii, Devs.Ven, Devs.AutumnVN, Devs.Nickyux, Devs.Kyuuhachi, Devs.sadan], dependencies: ["MessageUpdaterAPI"], settings, contextMenus: { @@ -255,6 +264,28 @@ export default definePlugin({ }; }, + handleUpdateAttachments(newMessage: MLMessage): MLAttachment[] { + const oldMessage = MessageStore.getMessage(newMessage.channel_id, newMessage.id) as MLMessage | undefined; + // if oldMessage is undefined, this is a new message and we shouldn't touch the attachments + if (!oldMessage || this.shouldIgnore(newMessage, true)) { + return newMessage.attachments; + } + // not sure if it's ever actually null after an edit but discord does a null check here + if (!newMessage.attachments?.length) { + return oldMessage.attachments.map((a): MLAttachment => ({ ...a, deleted: true })); + } + const attachments: MLAttachment[] = []; + for (const oldAttachment of oldMessage.attachments) { + const wasDeleted = newMessage.attachments.every(a => a.id !== oldAttachment.id); + if (wasDeleted) { + attachments.push({ ...oldAttachment, deleted: true }); + } else { + attachments.push(oldAttachment); + } + } + return attachments; + }, + handleDelete(cache: any, data: { ids: string[], id: string; mlDeleted?: boolean; }, isBulk: boolean) { try { if (cache == null || (!isBulk && !cache.has(data.id))) return cache; @@ -409,41 +440,19 @@ export default definePlugin({ }, { - // Updated message transformer(?) + // Updated message transformer find: ".PREMIUM_REFERRAL&&(", replacement: [ { - // Pass through editHistory & deleted & original attachments to the "edited message" transformer + // Pass through editHistory & deleted to the "edited message" transformer match: /(?<=null!=\i\.edited_timestamp\)return )\i\(\i,\{reactions:(\i)\.reactions.{0,50}\}\)/, replace: "Object.assign($&,{ deleted:$1.deleted, editHistory:$1.editHistory, firstEditTimestamp:$1.firstEditTimestamp })" }, - + // just mark deleted attachments as deleted on MESSAGE_UPDATE { - // Construct new edited message and add editHistory & deleted (ref above) - // Pass in custom data to attachment parser to mark attachments deleted as well - match: /attachments:(\i)\((\i)\)/, - replace: - "attachments: $1((() => {" + - " if ($self.shouldIgnore($2)) return $2;" + - " let old = arguments[1]?.attachments;" + - " if (!old) return $2;" + - " let new_ = $2.attachments?.map(a => a.id) ?? [];" + - " let diff = old.filter(a => !new_.includes(a.id));" + - " old.forEach(a => a.deleted = true);" + - " $2.attachments = [...diff, ...$2.attachments];" + - " return $2;" + - "})())," + - "deleted: arguments[1]?.deleted," + - "editHistory: arguments[1]?.editHistory," + - "firstEditTimestamp: new Date(arguments[1]?.firstEditTimestamp ?? $2.editedTimestamp ?? $2.timestamp)" - }, - { - // Preserve deleted attribute on attachments - match: /(\((\i)\){return null==\2\.attachments.+?)spoiler:/, - replace: - "$1deleted: arguments[0]?.deleted," + - "spoiler:" + match: /attachments:(\i)\.attachments\?\?\[\],/, + replace: "attachments: $self.handleUpdateAttachments($1)," } ] }, @@ -452,9 +461,16 @@ export default definePlugin({ // Attachment renderer find: "#{intl::REMOVE_ATTACHMENT_TOOLTIP_TEXT}", replacement: [ + // add deleted class to deleted attachments { - match: /\.SPOILER,(?=\[\i\.\i\]:)/, - replace: '$&"messagelogger-deleted-attachment":arguments[0]?.item?.originalItem?.deleted,' + // we can't use arguments[0] because we patch a nested **non-arrow** function + match: /\.SPOILER,(?=\[\i\.\i\]:)(?<=item:(\i),.{0,200}?)/, + replace: '$&"messagelogger-deleted-attachment": $1?.originalItem?.deleted,' + }, + // dont allow deleting attachments from deleted messages + { + match: /(?<=\{let\{[^}]*?item:(\i),autoPlayGif:\i,)canRemoveItem:(\i)(?=,onRemoveItem:)/, + replace: "_canRemoveItem:$2 = arguments[0].canRemoveItem && !$1?.originalItem?.deleted", } ] }, diff --git a/src/plugins/messageLogger/messageLogger.css b/src/plugins/messageLogger/messageLogger.css index 652fbc6e2..a65fd97ab 100644 --- a/src/plugins/messageLogger/messageLogger.css +++ b/src/plugins/messageLogger/messageLogger.css @@ -2,9 +2,10 @@ display: none; } -.messagelogger-deleted -:is( - .messagelogger-deleted-attachment, + +/* non-deleted messages can have deleted attachments */ +.messagelogger-deleted-attachment, +.messagelogger-deleted :is( .emoji, [data-type="sticker"], [class*="embedIframe"], diff --git a/src/webpack/common/utils.ts b/src/webpack/common/utils.ts index 49c764346..32eb73a71 100644 --- a/src/webpack/common/utils.ts +++ b/src/webpack/common/utils.ts @@ -190,7 +190,7 @@ export const ExpressionPickerStore: t.ExpressionPickerStore = mapMangledModuleLa openExpressionPicker: filters.byCode(/setState\({activeView:(?:(?!null)\i),activeViewType:/), closeExpressionPicker: filters.byCode("setState({activeView:null"), toggleMultiExpressionPicker: filters.byCode(".EMOJI,"), - toggleExpressionPicker: filters.byCode(/getState\(\)\.activeView===\i\?\i\(\):\i\(/), + toggleExpressionPicker: filters.byCode(/\i\.activeView===\i&&\i\.activeViewType===\i&&/), setExpressionPickerView: filters.byCode(/setState\({activeView:\i,lastActiveView:/), setSearchQuery: filters.byCode("searchQuery:"), useExpressionPickerStore: filters.byCode(/\(\i,\i=\i\)=>/) From 331f12e592c7aed0c1f8eb62229ecc35b6fc0d4b Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Tue, 14 Jul 2026 19:14:53 -0300 Subject: [PATCH 4/8] ReplyTimestamp: Fix overlapping with message author --- src/plugins/replyTimestamp/style.css | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/plugins/replyTimestamp/style.css b/src/plugins/replyTimestamp/style.css index f42371717..d910134e4 100644 --- a/src/plugins/replyTimestamp/style.css +++ b/src/plugins/replyTimestamp/style.css @@ -1,3 +1,4 @@ .vc-reply-timestamp { - margin-right: 0.25em; -} + margin-inline-end: 0.5rem; + width: unset !important; +} \ No newline at end of file From f1ed0f652d612d764d421e849038100d16942f72 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Tue, 14 Jul 2026 19:54:12 -0300 Subject: [PATCH 5/8] BetterFolders: Optimize patch --- src/plugins/betterFolders/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/betterFolders/index.tsx b/src/plugins/betterFolders/index.tsx index acffff289..33cfcfbb3 100644 --- a/src/plugins/betterFolders/index.tsx +++ b/src/plugins/betterFolders/index.tsx @@ -213,8 +213,8 @@ export default definePlugin({ // If we are rendering the normal GuildsBar sidebar, we make Discord think the folder is always collapsed to show better icons (the mini guild icons) and avoid transitions { predicate: () => settings.store.keepIcons, - match: /(?<=let ?(?:\i,)*?{folderNode:\i,setNodeRef:\i,.+?expanded:(\i),.+?;)(?=let)/, - replace: (_, isExpanded) => `${isExpanded}=!!arguments[0]?.isBetterFolders&&${isExpanded};` + match: /let ?(?:\i,)*?{folderNode:\i,setNodeRef:\i,.+?expanded:(\i),.+?;(?=let)/, + replace: (m, isExpanded) => `${m}${isExpanded}=!!arguments[0]?.isBetterFolders&&${isExpanded};` }, // Disable expanding and collapsing folders transition in the normal GuildsBar sidebar { From fc5466ebf854a82c8385c41742162c4b66f8a119 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Tue, 14 Jul 2026 19:59:28 -0300 Subject: [PATCH 6/8] Bump to 1.14.16 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0ee7fca89..2eff65bd1 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "vencord", "private": "true", - "version": "1.14.15", + "version": "1.14.16", "description": "The cutest Discord client mod", "homepage": "https://github.com/Vendicated/Vencord#readme", "bugs": { From e51821c740eaf0f8c91fe60539514cb498e41273 Mon Sep 17 00:00:00 2001 From: Lunascape <21148758+Lunascaped@users.noreply.github.com> Date: Wed, 15 Jul 2026 02:13:45 -0500 Subject: [PATCH 7/8] new plugin TenorGifSearch (#4332) Co-authored-by: V --- src/main/csp/index.ts | 6 +- src/plugins/tenorGifSearch/index.tsx | 249 +++++++++++++++++++++++++++ src/utils/constants.ts | 4 + 3 files changed, 258 insertions(+), 1 deletion(-) create mode 100644 src/plugins/tenorGifSearch/index.tsx diff --git a/src/main/csp/index.ts b/src/main/csp/index.ts index 887010b6d..bc5eb153c 100644 --- a/src/main/csp/index.ts +++ b/src/main/csp/index.ts @@ -12,6 +12,7 @@ type PolicyMap = Record; export const ConnectSrc = ["connect-src"]; export const ImageSrc = [...ConnectSrc, "img-src"]; export const CssSrc = ["style-src", "font-src"]; +export const ImageAndMediaSrc = [...ImageSrc, "media-src"]; export const ImageAndCssSrc = [...ImageSrc, ...CssSrc]; export const ImageScriptsAndCssSrc = [...ImageAndCssSrc, "script-src", "worker-src"]; @@ -40,7 +41,6 @@ export const CspPolicies: PolicyMap = { "i.imgur.com": ImageSrc, // Imgur, used by some themes "i.ibb.co": ImageSrc, // ImgBB, used by some themes "i.pinimg.com": ImageSrc, // Pinterest, used by some themes - "*.tenor.com": ImageSrc, // Tenor, used by some themes "files.catbox.moe": ImageAndCssSrc, // Catbox, used by some themes "cdn.discordapp.com": ImageAndCssSrc, // Discord CDN, used by Vencord and some themes to load media @@ -63,6 +63,10 @@ export const CspPolicies: PolicyMap = { "dearrow-thumb.ajay.app": ImageSrc, // Dearrow Thumbnail CDN "usrbg.is-hardly.online": ImageSrc, // USRBG API "icons.duckduckgo.com": ImageSrc, // DuckDuckGo Favicon API (Reverse Image Search) + + // Tenor, used by TenorSearch plugin and some themes + "*.tenor.com": ImageAndMediaSrc, + "*.tenor.co": ImageAndMediaSrc, }; const findHeader = (headers: PolicyMap, headerName: Lowercase) => { diff --git a/src/plugins/tenorGifSearch/index.tsx b/src/plugins/tenorGifSearch/index.tsx new file mode 100644 index 000000000..b691cedc4 --- /dev/null +++ b/src/plugins/tenorGifSearch/index.tsx @@ -0,0 +1,249 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2026 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { Devs } from "@utils/constants"; +import { isNonNullish } from "@utils/guards"; +import definePlugin from "@utils/types"; +import { FluxDispatcher, LocaleStore } from "@webpack/common"; + +// API key is taken from the GBoard app on iOS +const TENOR_KEY = "3Z0688EVWYKH"; + +let cachedCategories: TrendingCategories | null = null; + +interface TenorMedia { + url: string; + preview: string; + dims: [number, number]; +} +interface TenorResult { + id: string; + media: Array>; + itemurl: string; +} +interface TenorCategoryTag { + searchterm: string; + image: string; +} + +interface DiscordGif { + id: string; + title: string; + url: string; + src: string; + gif_src: string; + width: number; + height: number; + preview: string; +} + +interface TrendingCategories { + trendingCategories: Record<"name" | "src", string>[]; + trendingGIFPreview: { src: string; }; +} + +function toDiscordGif(item: TenorResult): DiscordGif | null { + const { gif, webm } = item.media[0]; + + return { + id: item.id, + title: "", // discord always returns a blank string + url: item.itemurl, + gif_src: gif.url, + src: webm.url, + width: webm.dims[0], + height: webm.dims[1], + preview: webm.preview + }; +} + +function mapToDiscordGifs(items: TenorResult[]) { + return items.map(toDiscordGif).filter(isNonNullish); +} + +async function tenorFetch(path: string, params: Record) { + const url = `https://api.tenor.com/v1${path}?` + new URLSearchParams({ + key: TENOR_KEY, + locale: LocaleStore.locale.replace("-", "_").toLowerCase(), + ...params + }); + + const res = await fetch(url); + if (!res.ok) + throw new Error(`GET ${path}: Tenor API request failed with status ${res.status}`); + + return res.json() as Promise; +} + +// function contributed by taep96 +async function fetchTenorResults(path: string, limit: number, extra: Record = {}) { + const pageSize = Math.min(limit, 50); + const items: TenorResult[] = []; + const seen = new Set(); + let pos = ""; + + while (items.length < limit) { + const params: Record = { + ...extra, + limit: String(Math.min(limit - items.length, pageSize)) + }; + if (pos) params.pos = pos; + + const { next, results: page } = await tenorFetch<{ next?: string; results: TenorResult[]; }>(path, params); + if (!page.length) break; + + const previousLength = items.length; + for (const item of page) { + if (seen.has(item.id)) continue; + seen.add(item.id); + + items.push(item); + if (items.length >= limit) break; + } + if (items.length === previousLength) break; + + if (!next || next === pos) break; + pos = next; + } + + return items; +} + +async function fetchCategories(): Promise { + return tenorFetch<{ tags?: TenorCategoryTag[]; }>("/categories", { type: "featured" }) + .then(({ tags }) => { + if (!tags?.length) return null; + + return { + trendingCategories: tags.map(t => ({ name: t.searchterm, src: t.image })), + trendingGIFPreview: { src: tags[0].image } + }; + }) + .catch(() => null); + +} + +export default definePlugin({ + name: "TenorGifSearch", + description: "Restore Tenor GIF search", + authors: [Devs.Lunascape], + + patches: [ + { + find: "renderHeaderContent()", + replacement: { + match: /placeholder:(\i),"aria-label":(\i)/, + replace: 'placeholder:$1?.replace(/Giphy|Klipy/gi,"Tenor"),"aria-label":$2?.replace(/Giphy|Klipy/gi,"Tenor")' + } + }, + { + find: '"GIF_PICKER_TRENDING_FETCH_SUCCESS",trendingCategories:', + replacement: [ + { + match: /let \i=Date\.now\(\);\i\([^)]+\),\i\.\i\.get\(\{url:\i\.\i\.GIFS_SEARCH,query:\{q:(\i),/, + replace: "return $self.handleSearchFetch($1);$&" + }, + { + match: /""!==(\i)&&null!=\1&&\i\.\i\.get\(\{url:\i\.\i\.GIFS_SUGGEST,/, + replace: "return $self.handleSuggestionsFetch($1);$&" + }, + { + match: /\i\.\i\.get\(\{url:\i\.\i\.GIFS_TRENDING,/, + replace: "return $self.handleTrendingFetch();$&" + }, + { + match: /let \i=Date\.now\(\);\i\([^)]+\),\i\.\i\.get\(\{url:\i\.\i\.GIFS_TRENDING_GIFS,/, + replace: "return $self.handleTrendingGifsFetch();$&" + }, + { + match: /\i\.\i\.post\(\{url:\i\.\i\.GIFS_SELECT,body:\{id:(\i),q:(\i),provider:\i\}/, + replace: "return $self.handleGifSelect($1,$2);$&" + } + ] + }, + { + find: '"IntegrationQueryStore"', + replacement: { + match: /(?<=search\((\i),(\i)\)\{)null==\i\.getResults\(\1,\2\)&&/, + replace: "return $self.tenorIntegrationSearch($1,$2);null==void 0&&" + } + } + ], + + async start() { + cachedCategories = await fetchCategories() ?? cachedCategories; + }, + + handleSearchFetch(query: string) { + // discord has a 100 result limit for normal search + fetchTenorResults("/search", 100, { q: query }) + .then(results => { + const items = mapToDiscordGifs(results); + FluxDispatcher.dispatch( + items.length + ? { type: "GIF_PICKER_QUERY_SUCCESS", query, items } + : { type: "GIF_PICKER_QUERY_FAILURE", query } + ); + }) + .catch(() => { + FluxDispatcher.dispatch({ type: "GIF_PICKER_QUERY_FAILURE", query }); + }); + }, + + async handleSuggestionsFetch(query: string) { + if (!query) return; + + const { results } = await tenorFetch<{ results?: string[]; }>("/search_suggestions", { q: query, limit: "5" }); + + FluxDispatcher.dispatch({ type: "GIF_PICKER_SUGGESTIONS_SUCCESS", query, items: results }); + }, + + async handleTrendingFetch() { + if (!cachedCategories) { + cachedCategories = await fetchCategories(); + + if (!cachedCategories) return; + } + + FluxDispatcher.dispatch({ type: "GIF_PICKER_TRENDING_FETCH_SUCCESS", ...cachedCategories }); + }, + + handleGifSelect(id: string, query: string) { + tenorFetch("/registershare", { id, q: query }); + }, + + handleTrendingGifsFetch() { + fetchTenorResults("/trending", 50) + .then(results => { + const items = mapToDiscordGifs(results); + FluxDispatcher.dispatch( + items.length + ? { type: "GIF_PICKER_QUERY_SUCCESS", items } + : { type: "GIF_PICKER_QUERY_FAILURE" } + ); + }) + .catch(() => { + FluxDispatcher.dispatch({ type: "GIF_PICKER_QUERY_FAILURE" }); + }); + }, + + tenorIntegrationSearch(integration: string, query: string) { + FluxDispatcher.dispatch({ type: "INTEGRATION_QUERY", integration, query }); + + fetchTenorResults("/search", 20, { q: query }) + .then(results => { + const items = mapToDiscordGifs(results); + FluxDispatcher.dispatch( + items.length + ? { type: "INTEGRATION_QUERY_SUCCESS", integration, query, results: items } + : { type: "INTEGRATION_QUERY_FAILURE", integration, query } + ); + }) + .catch(() => { + FluxDispatcher.dispatch({ type: "INTEGRATION_QUERY_FAILURE", integration, query, results: [] }); + }); + } +}); diff --git a/src/utils/constants.ts b/src/utils/constants.ts index a4ff9aa4e..85901da7f 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -646,6 +646,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({ name: "NightmareSan", id: 304239816466235392n }, + Lunascape: { + name: "Lunascape", + id: 383365021415243776n + } } satisfies Record); // iife so #__PURE__ works correctly From b8fbed9287842660f6b62ee36e9ef3bd59fe0bfa Mon Sep 17 00:00:00 2001 From: V Date: Wed, 15 Jul 2026 09:37:22 +0200 Subject: [PATCH 8/8] MessageLogger: fix "silent edit/delete" bypass (#4377) It is possible to replace a message in place by creating a new message with the same nonce as an existing one. This is not considered an edit since it's a new message. Thus it bypasses our edit logging and can be used to "delete" a message by replacing it with an empty one. This fixes that bypass --- src/plugins/messageLogger/index.tsx | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/plugins/messageLogger/index.tsx b/src/plugins/messageLogger/index.tsx index 64382e371..9686f5763 100644 --- a/src/plugins/messageLogger/index.tsx +++ b/src/plugins/messageLogger/index.tsx @@ -30,7 +30,7 @@ import { classes } from "@utils/misc"; import definePlugin, { OptionType } from "@utils/types"; import { Message, MessageAttachment } from "@vencord/discord-types"; import { findCssClassesLazy } from "@webpack"; -import { ChannelStore, FluxDispatcher, Menu, MessageStore, Parser, SelectedChannelStore, Timestamp, UserStore, useStateFromStores } from "@webpack/common"; +import { AuthenticationStore, ChannelStore, FluxDispatcher, Menu, MessageStore, Parser, SelectedChannelStore, Timestamp, UserStore, useStateFromStores } from "@webpack/common"; import overlayStyle from "./deleteStyleOverlay.css?managed"; import textStyle from "./deleteStyleText.css?managed"; @@ -338,6 +338,24 @@ export default definePlugin({ } }, + // It is possible to replace a message in place by creating a new message with the same nonce as an existing one. + // This is not considered an edit since it's a new message. Thus it bypasses our edit logging and can be used to "delete" a message by replacing it with an empty one. + // This fixes that bypass + normalizeNonce(msg: Message) { + try { + if (!msg.nonce || msg.author.id === AuthenticationStore.getId()) return; + + const prevMsg = MessageStore.getMessage(msg.channel_id, msg.nonce); + if (!prevMsg || prevMsg.state !== "SENT") return; + + if (prevMsg.id !== msg.id) { + delete msg.nonce; + } + } catch (e) { + console.error("[MessageLogger] Error normalizing nonce"); + } + }, + EditMarker({ message, className, children, ...props }: any) { return ( settings.store.collapseDeleted + }, + + { + find: "this.truncateTop", + replacement: { + match: /receiveMessage\((\i)\)\{/, + replace: "$& $self.normalizeNonce($1);" + } } ] });