Message Formatter Plugin
An intermediate Plexcord plugin that uses patches to modify Discord's message rendering. Demonstrates the patch system with real regex replacements.
Example: Message Formatter Plugin
Difficulty: Intermediate | Concepts: Patches, regex replacements, Webpack module access
This plugin intercepts Discord's message parsing layer and transforms text before it renders. Specifically it auto-converts plain-text emoji codes like :smile: that Discord's native parser might miss, and optionally uppercases @everyone.
This is a practical introduction to the patch system, one of the most powerful tools in Plexcord.
Full Source
// File: src/plugins/messageFormatter/index.ts
import definePlugin, { OptionType } from "@utils/types";
import { definePluginSettings } from "@api/Settings";
import { Logger } from "@utils/Logger";
const logger = new Logger("MessageFormatter");
const settings = definePluginSettings({
emphasizeEveryone: {
type: OptionType.BOOLEAN,
description: 'Makes "@everyone" and "@here" visually bold in rendered messages',
default: false,
restartNeeded: true // Patch changes require restart
},
showWordCount: {
type: OptionType.BOOLEAN,
description: "Add a word count badge to messages longer than 100 words",
default: false,
restartNeeded: true
}
});
// A simple word counter utility
function countWords(text: string): number {
return text.trim().split(/\s+/).filter(Boolean).length;
}
export default definePlugin({
name: "MessageFormatter",
description: "Transforms message content with optional formatting enhancements",
authors: [
{
name: "YourName",
id: 0n
}
],
settings,
patches: [
// Patch 1: Emphasize @everyone and @here
{
find: "parseEveryone",
predicate: () => settings.store.emphasizeEveryone,
replacement: {
match: /parseEveryone\((\i)\)/,
replace: "parseEveryone($1).map(token => "
+ "token.type === 'mention' && (token.content === '@everyone' || token.content === '@here') "
+ "? { ...token, content: '**' + token.content + '**' } "
+ ": token)"
}
},
// Patch 2: Add word count to long messages
// This patches the MessageContent component render function
{
find: "messageContent",
predicate: () => settings.store.showWordCount,
replacement: {
match: /(\i)\.content,children:\[(\i),/,
replace: "$1.content,children:[$2,"
+ "Vencord.Webpack.Common.React.createElement('span', "
+ "{ style: { opacity: 0.4, fontSize: '10px', marginLeft: '4px' } },"
+ "$self.getWordCountBadge($1.content)),"
}
}
],
// Called by the patch above
getWordCountBadge(content: string): string | null {
if (!settings.store.showWordCount) return null;
const count = countWords(content);
if (count < 100) return null;
return `${count}w`;
}
});Breaking Down the Patches
Patch Anatomy
Every patch has three required properties:
{
find: "uniqueStringInModule", // Which module to patch
replacement: {
match: /regexPattern/, // What to find in the function's source
replace: "replacement" // What to replace it with
}
}predicate
The optional predicate function controls whether a patch is applied at all. Here we tie it to the plugin setting:
predicate: () => settings.store.emphasizeEveryoneIf the setting is false, the patch is skipped entirely. This avoids unnecessary monkey-patching.
$self in Replacements
Inside a replacement string, $self refers to the plugin object itself. This lets you call plugin methods from within patched code:
replace: "...$self.getWordCountBadge($1.content)..."Then define the method on the plugin:
export default definePlugin({
// ...
getWordCountBadge(content: string) { ... }
});Capture Groups
The regex (\i) captures an identifier (a minified variable name). In the replacement, $1 refers to the first captured group.
Debugging Patches
If a patch doesn't work, open DevTools and check:
// See all patches and whether they applied
Vencord.Plugins.plugins.MessageFormatter.patchesYou can also search for the module manually:
// Find what 'parseEveryone' belongs to
Object.values(Vencord.Webpack.wreq.c)
.filter(m => m.exports?.toString?.().includes("parseEveryone"))Important Notes
- Patches with
restartNeeded: trueonly take effect after Plexcord rebuilds and Discord restarts - When Discord updates its source, obfuscated variable names change; your
(\i)captures will still work but the surrounding context string may need to be updated - Always use the most unique
findstring possible to avoid accidentally patching the wrong module
Next Example
Keyword Alert Plugin
An intermediate Plexcord plugin that monitors messages for user-defined keywords and sends notifications. Demonstrates settings, Flux events, and stores.
Voice Logger Plugin
An advanced Plexcord plugin that tracks voice channel activity using DataStore for persistence. Demonstrates Flux events, DataStore, settings, and multi-event handling.