From 58af3585490009bc17400784247a2ebe5f4b9363 Mon Sep 17 00:00:00 2001 From: Kofhisho Date: Tue, 19 May 2026 13:31:00 +0100 Subject: [PATCH] BlockKeywords: Add Regex Helper (#1087) * BlockKeywords: Add Regex Helper * [autofix.ci] apply automated fixes * BlockKeywords: Fix regex quantifiers * BlockKeywords: Refactor & Fixes typo and deadcode * BlockKeywords: Optimize and fix some more bugs * refactor: Replace `any` with `unknown` in catch block Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * fix: Wrap `RegexHelper` component in `ErrorBoundary` Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * fix: Subscribe to settings state in `RegexHelper` Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * bruh * refactor: no `any` Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * Update index.tsx * Apply suggestion from @greptile-apps[bot] Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * styles --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> Co-authored-by: thororen1234 <78185467+thororen1234@users.noreply.github.com> --- src/equicordplugins/blockKeywords/index.ts | 132 ------------- src/equicordplugins/blockKeywords/index.tsx | 193 +++++++++++++++++++ src/equicordplugins/blockKeywords/styles.css | 16 ++ 3 files changed, 209 insertions(+), 132 deletions(-) delete mode 100644 src/equicordplugins/blockKeywords/index.ts create mode 100644 src/equicordplugins/blockKeywords/index.tsx create mode 100644 src/equicordplugins/blockKeywords/styles.css diff --git a/src/equicordplugins/blockKeywords/index.ts b/src/equicordplugins/blockKeywords/index.ts deleted file mode 100644 index bbe146b46..000000000 --- a/src/equicordplugins/blockKeywords/index.ts +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2024 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { definePluginSettings } from "@api/Settings"; -import { EquicordDevs } from "@utils/constants"; -import definePlugin, { OptionType } from "@utils/types"; -import { Message } from "@vencord/discord-types"; - -let blockedKeywords: Array; - -const settings = definePluginSettings({ - blockedWords: { - type: OptionType.STRING, - description: "Comma-seperated list of words to block", - default: "", - restartNeeded: true - }, - useRegex: { - type: OptionType.BOOLEAN, - description: "Use each value as a regular expression when checking message content (advanced)", - default: false, - restartNeeded: true - }, - caseSensitive: { - type: OptionType.BOOLEAN, - description: "Whether to use a case sensitive search or not", - default: false, - restartNeeded: true - }, - ignoreBlockedMessages: { - description: "Completely ignores (recent) new messages bar", - type: OptionType.BOOLEAN, - default: true, - restartNeeded: true, - }, -}); - -export function containsBlockedKeywords(message: Message) { - if (!blockedKeywords) return false; - - // can't use forEach because we need to return from inside the loop - // message content loop - for (let wordIndex = 0; wordIndex < blockedKeywords.length; wordIndex++) { - if (blockedKeywords[wordIndex].test(message.content)) { - return true; - } - } - - // embed content loop (e.g. twitter embeds) - for (let embedIndex = 0; embedIndex < message.embeds.length; embedIndex++) { - const embed = message.embeds[embedIndex]; - for (let wordIndex = 0; wordIndex < blockedKeywords.length; wordIndex++) { - // doing this because undefined strings get converted to the string "undefined" in regex tests - // @ts-ignore - const descriptionHasKeywords = embed.rawDescription != null && blockedKeywords[wordIndex].test(embed.rawDescription); - // @ts-ignore - const titleHasKeywords = embed.rawTitle != null && blockedKeywords[wordIndex].test(embed.rawTitle); - if (descriptionHasKeywords || titleHasKeywords) { - return true; - } - } - } - - return false; -} - -export default definePlugin({ - name: "BlockKeywords", - description: "Blocks messages containing specific user-defined keywords, as if the user sending them was blocked.", - tags: ["Appearance", "Customisation", "Privacy"], - authors: [EquicordDevs.catcraft], - patches: [ - { - find: "_channelMessages={}", - predicate: () => settings.store.blockedWords !== "", - replacement: { - match: /static commit\((\i)\)\{/g, - replace: "$&$1=$self.blockMessagesWithKeywords($1);" - } - }, - { - find: '"MessageStore"', - predicate: () => settings.store.ignoreBlockedMessages && settings.store.blockedWords !== "", - replacement: [ - { - match: /(?<=MESSAGE_CREATE:function\((\i)\){)/, - replace: (_, props) => `if($self.containsBlockedKeywords(${props}.message))return;` - } - ] - }, - { - find: '"ReadStateStore"', - predicate: () => settings.store.ignoreBlockedMessages && settings.store.blockedWords !== "", - replacement: [ - { - match: /(?<=MESSAGE_CREATE:function\((\i)\){)/, - replace: (_, props) => `if($self.containsBlockedKeywords(${props}.message))return;` - } - ] - }, - ], - - settings, - containsBlockedKeywords, - - start() { - const blockedWordsList: Array = settings.store.blockedWords.split(","); - const caseSensitiveFlag = settings.store.caseSensitive ? "" : "i"; - - if (!blockedWordsList) return; - - if (settings.store.useRegex) { - blockedKeywords = blockedWordsList.map(word => { - return new RegExp(word, caseSensitiveFlag); - }); - } else { - blockedKeywords = blockedWordsList.map(word => { - // escape regex chars in word https://stackoverflow.com/a/6969486 - return new RegExp(`\\b${word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`, caseSensitiveFlag); - }); - } - }, - - blockMessagesWithKeywords(messageList: any) { - return messageList.reset(messageList.map( - message => message.set("blocked", message.blocked || this.containsBlockedKeywords(message)) - )); - } -}); diff --git a/src/equicordplugins/blockKeywords/index.tsx b/src/equicordplugins/blockKeywords/index.tsx new file mode 100644 index 000000000..d10bb808e --- /dev/null +++ b/src/equicordplugins/blockKeywords/index.tsx @@ -0,0 +1,193 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import "./styles.css"; + +import { definePluginSettings } from "@api/Settings"; +import { Card } from "@components/Card"; +import { HeadingTertiary } from "@components/Heading"; +import { ErrorBoundary } from "@components/index"; +import { Margins } from "@components/margins"; +import { EquicordDevs } from "@utils/constants"; +import { classNameFactory } from "@utils/css"; +import { classes } from "@utils/misc"; +import definePlugin, { OptionType } from "@utils/types"; +import { Message } from "@vencord/discord-types"; +import { React, TextInput } from "@webpack/common"; + +let blockedKeywords: Array; +const cl = classNameFactory("vc-block-keywords-"); + +function splitPatterns(input: string): string[] { + return input + .replace(/\[([^\]]*)\]/g, m => m.replace(/,/g, "\x00")) // protect commas in [...] + .replace(/\{(\d+,?\d*|,\d+)\}/g, m => m.replace(",", "\x00")) // protect commas in {n,m} + .split(",") + .map(s => s.replace(/\x00/g, ",").trim()) + .filter(Boolean); +} + +function RegexHelper() { + const [testInput, setTestInput] = React.useState(""); + const { blockedWords, caseSensitive, useRegex } = settings.use(["blockedWords", "caseSensitive", "useRegex"]); + + const results = React.useMemo(() => { + const caseSensitiveFlag = caseSensitive ? "" : "i"; + return splitPatterns(blockedWords) + .map(pattern => { + try { + const regex = useRegex + ? new RegExp(pattern, caseSensitiveFlag) + : new RegExp(`\\b${pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`, caseSensitiveFlag); + return { pattern, matches: regex.test(testInput) }; + } catch (e: unknown) { + return { pattern, matches: false, error: e instanceof Error ? e.message : String(e) }; + } + }); + }, [testInput, blockedWords, caseSensitive, useRegex]); + + return ( + + Regex Helper + + {results.length === 0 ? + + No patterns configured + : ( + results.map(({ pattern, matches, error }, i) => ( + + {pattern} + {error && {error}} + + )))} + + ); +} + +const settings = definePluginSettings({ + blockedWords: { + type: OptionType.STRING, + description: "Comma-separated list of words to block", + default: "", + restartNeeded: true + }, + useRegex: { + type: OptionType.BOOLEAN, + description: "Use each value as a regular expression when checking message content (advanced)", + default: false, + restartNeeded: true + }, + regexHelper: { + type: OptionType.COMPONENT, + description: "Test your regular expressions against a sample input", + component: () => , + }, + caseSensitive: { + type: OptionType.BOOLEAN, + description: "Whether to use a case sensitive search or not", + default: false, + restartNeeded: true + }, + ignoreBlockedMessages: { + description: "Completely ignores (recent) new messages bar", + type: OptionType.BOOLEAN, + default: true, + restartNeeded: true, + }, +}, { + regexHelper: { + hidden() { return !this.store.useRegex; } + } +}); + +export function containsBlockedKeywords(message: Message) { + if (!blockedKeywords) return false; + + // test a nullable string against all keywords + const testField = (text: string | null | undefined) => text != null && blockedKeywords.some(regex => regex.test(text)); + + return blockedKeywords.some(regex => + regex.test(message.content)) || message.embeds.some(embed => + testField(embed.rawDescription) || testField(embed.rawTitle)); +} + +export default definePlugin({ + name: "BlockKeywords", + description: "Blocks messages containing specific user-defined keywords, as if the user sending them was blocked.", + tags: ["Appearance", "Customisation", "Privacy"], + authors: [EquicordDevs.catcraft, EquicordDevs.secp192k1], + patches: [ + { + find: "_channelMessages={}", + predicate: () => settings.store.blockedWords !== "", + replacement: { + match: /static commit\((\i)\)\{/g, + replace: "$&$1=$self.blockMessagesWithKeywords($1);" + } + }, + { + find: '"MessageStore"', + predicate: () => settings.store.ignoreBlockedMessages && settings.store.blockedWords !== "", + replacement: [ + { + match: /(?<=MESSAGE_CREATE:function\((\i)\){)/, + replace: (_, props) => `if($self.containsBlockedKeywords(${props}.message))return;` + } + ] + }, + { + find: '"ReadStateStore"', + predicate: () => settings.store.ignoreBlockedMessages && settings.store.blockedWords !== "", + replacement: [ + { + match: /(?<=MESSAGE_CREATE:function\((\i)\){)/, + replace: (_, props) => `if($self.containsBlockedKeywords(${props}.message))return;` + } + ] + }, + ], + + settings, + containsBlockedKeywords, + + start() { + const blockedWordsList = splitPatterns(settings.store.blockedWords); + const caseSensitiveFlag = settings.store.caseSensitive ? "" : "i"; + + if (blockedWordsList.length === 0) return; + + if (settings.store.useRegex) { + blockedKeywords = blockedWordsList.map(word => { + return new RegExp(word, caseSensitiveFlag); + }); + } else { + blockedKeywords = blockedWordsList.map(word => { + // escape regex chars in word https://stackoverflow.com/a/6969486 + return new RegExp(`\\b${word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`, caseSensitiveFlag); + }); + } + }, + + blockMessagesWithKeywords(messageList) { + return messageList.reset(messageList.map( + message => message.set("blocked", message.blocked || this.containsBlockedKeywords(message)) + )); + } +}); diff --git a/src/equicordplugins/blockKeywords/styles.css b/src/equicordplugins/blockKeywords/styles.css new file mode 100644 index 000000000..9e89d078a --- /dev/null +++ b/src/equicordplugins/blockKeywords/styles.css @@ -0,0 +1,16 @@ +.vc-block-keywords-regex { + padding: 0.75em +} + +.vc-block-keywords-card { + padding: 0.4em 0.75em; + display: flex; + align-items: center; + justify-content: space-between; +} + +.vc-block-keywords-error { + font-size: 12px; + margin-left: 1em; + flex-shrink: 0; +} \ No newline at end of file