Skip to content

Instantly share code, notes, and snippets.

@sunnniee
Last active May 2, 2024 23:06
Show Gist options
  • Save sunnniee/28bd595f8c07992f6d03289911289ba8 to your computer and use it in GitHub Desktop.
Save sunnniee/28bd595f8c07992f6d03289911289ba8 to your computer and use it in GitHub Desktop.

Unofficial Vencord Plugin Guide

Table of contents

Prerequisites

  • A git Vencord install
  • A fork of Vencord if you plan on making your plugin public
  • A code editor
  • Some very basic knowledge of JavaScript

Setting things up

First up, you should use a development Vencord build. Assuming you already injected your git install of Vencord (see Prerequisites), you can do it by running a single command

pnpm build --watch

After that, all you need to do is refresh Discord, and you should see (Dev) next to your Vencord version

image

Now you can start making your plugin. In Vencord's src directory, make a userplugins folder, and create a TypeScript file named however you'd like. The path should be [Vencord]/src/userplugins/yourPlugin.ts

If you want to use multiple files and/or add CSS, you can instead create a folder where the main file is called index.ts ([Vencord]/src/userplugins/yourPlugin/index.ts)

How a plugin works

Plugins have a patches array, and they are what separate Vencord from other client mods.

Vencord uses a different way of making mods than what you may be used to. Instead of monkeypatching webpack, it directly modifies the code before Discord loads it.

This is significantly more efficient than monkeypatching webpack, and is surprisingly easy, but it may be confusing at first. Here's an example:

If you wanted to make a patch that patches the isStaff method on a user, the part of code you want to patch would be simmilar to

user.isStaff = function () {
    return someCondition(user);
};

Though discord's code is minified, so it's actually closer to

e.isStaff=function(){return n(e)}

In settings, you'll notice a new "Patch Helper" tab in the Vencord category.

ℹ If you use VSCode, Vencord has a companion extension. It's not required, but can be useful for doing things directly in your code editor

Here's how a patch to make the function always return true would look like

{ 
    find: ".isStaff=function(){",
    replacement: [{
        match: /\(.isStaff=function\(\){)return \i\(\i\)}/,
        replace: "$1return true;}"
    }]
}
  • The find value is a unique string to find the module you need to patch. It only has to share the module, it doesn't have to be in the exact same function. In Patch Helper, there will be a field to check if a certain finder is unique or not

    • If it's unique, the module number should show at the bottom, which means it's safe to use as a finder. ⚠ Do not rely on minified variable names like e or n for your finders - those can change at any time and quickly break your patch
    • If it matches multiple modules, a "Multiple matches. Please refine your filter" error will be shown
    • If it doesn't have any matches, a "No match. Perhaps the module is lazy loaded?" error will be shown. A lazy loaded module is a module only loaded when needed, like the contents of context menus. If you get the error but are sure your patch works, load the code by doing whatever triggers your patch, and then go back to Patch Helper
  • Within replacement, match is a regex which matches the relevant part of discord's code that you want to replace, and replace is the value to replace your match with.

    • You might've noticed some special groups used in the match, like $1 and especially \i. You can see the meaning of those in the Cheat Sheet present in Patch Helper.

I've left a few regex resources in the Additional resources section

(todo: add info about native plugin functions)

Making a plugin

Let's start off with a template

import definePlugin from "@utils/types";

export default definePlugin({
    name: "Your Plugin",
    description: "This plugin does something cool",
    authors: [{
        name: "You!",
        id: 0n
    }],

    patches: [],
    start() {

    },
    stop() {

    },
});

name, description and authors are self explanatory, patches was explained earlier and start and stop are functions ran when the plugin is started and stopped from settings

⚠ As patches require a restart to be applied, plugins that have patches won't be started/stopped until you restart Discord, so the respective start/stop functions won't be ran instantly. If your plugin doesn't use patches, please remove the patches key from the plugin definiton

For this example, let's make a plugin which adds a button to the on click menu of a stock emoji to send that emoji in chat. Not the most useful but it's a good example

image

1. Adding the button

When adding something to the UI, the best way to find what component needs to be edited is using React Devtools. Enable "Settings -> Vencord -> Enable React Developer Tools" and fully restart Discord. Now if you open devtools and select the arrow at the top, there should be a new "⚛ Components" option.

In there, you can use the select tool (top left of devtools) to select the element you want to patch. Look for a component that looks useful, and go to the source. The JavaScript when returning a component (after selecting Pretty Print in the bottom left - it makes the code actually readable) looks something like this

return (0, r.jsx)(u.Z, {
  children: [(0, r.jsx)(n.Z, { /* some arguments */ }), (0, x.jsx)(n.Z, { /* some more arguments */ })]
})
  • The (0, functionName)(args) is minifier magic you can ignore - just think of it as functionName(args)
  • r.jsx (or r.jsxs) in this context is React.createElement
  • Since the child of the container is an array, you can add an extra component to it by adding to the array

I'll jump straight for the patch to add the button and then explain it

{
    find: ".EMOJI_POPOUT_STANDARD_EMOJI_DESCRIPTION",
    replacement: {
        match: /(?<=.primaryEmoji,src:(\i).{0,400}).Messages.EMOJI_POPOUT_STANDARD_EMOJI_DESCRIPTION}\)]/,
        replace: "$&.concat([$self.EmojiButton($1)])"
    }
}

The finder is the i18n string "EMOJI_POPOUT_STANDARD_EMOJI_DESCRIPTION". i18n strings and class names (ex. ().emojiName,) are often the best finders as they are really unlikely to change.

The match makes more sense when you see the original code

/* ... */ q = e => {
let {node: t} = e;
/* ... */
return (0, i.jsxs)(U.Z, {
    className: ee().truncatingText,
    children: [
        (0, l.jsx)(f.default, {
          emojiName: t.name,
          className: Z.primaryEmoji,
          src: t.src
        }),
        /* other children */
        (0, i.jsx)(f.Text, { 
            variant: "text-sm/normal",
            children: z.Z.Messages.EMOJI_POPOUT_STANDARD_EMOJI_DESCRIPTION
        })
    ]
})

First, the patch uses a lookbehind (?<=) to find the variable that stores the emoji, without actually matching code we don't care about and risk accidentally removing it. As the Cheat Sheet mentions, \i is a special regex identifier added by Vencord to match variables

ℹ If you saw any advice about using arguments for this before, it's no longer valid ever since the October client mod breakage

Then, after the match (as mentioned earlier, $& represents the entire match, so our code is added after it), we .concat to the array of components with our own component

2. Making the button

Back in your plugin definition, you can add the Button component

// ...
start() {},
stop() {},
EmojiButton(node) {
    return "placeholder";
},

Since we're going to be writing UI with React, you'll likely want to use a TSX file - changing the file extension to .tsx will allow you to write React components in your code.

For this, you can (and should, where possible) use Discord's built-in components. Since they're common enoguh, Vencord has it in webpack commons. You can import it at the top of the file, alongside the definePlugin import

import { Button } from "@webpack/common";

And you then you can return the button from your function

EmojiButton(node) {
   return <Button onClick={() => sendEmote(node)}>
        Send emote
    </Button>;
},

However, now we need to make the sendEmote function

3. Sending a message

The most convinient way to send a message is by using discord's function for it. To find it, you can use webpack finds.

ℹ Please enable the ConsoleShortcuts plugin, it's really useful.

The main two methods used for finding stuff will be findByProps and findByCode. In our case, an obvious starting point would be findByProps("sendMessage"). Really conveniently, there's only one match, which is the one we care about.

However, there sometimes may be multiple functions which share the same name, even when we only want one. In that case you can pass multiple arguments to the finder, like findByProps("sendMessage", "editMessage"). Don't make it too strict though, in case an update breaks it.

Let's add it to the plugin

import { findByPropsLazy } from "@webpack";
// ...
const { sendMessage } = findByPropsLazy("sendMessage", "editMessage");

Wait, why is this one lazy?

Normally, webpack searches cannot be ran at the top level, as it runs before webpack is initalized. By making it lazy, you make the find only run once the value is used, which is after Discord started, and the function is guaranteed to exist.

Now let's implement the sendEmote function

import { getCurrentChannel } from "@utils/discord";
// ...
interface EmojiNode {
    type: "emoji";
    name: string;
    surrogate: string;
}
function sendEmote(node: EmojiNode) {
    sendMessage(getCurrentChannel().id, {
        content: node.surrogate
    });
}

Quite straightforward. We're importing the getCurrentChannel function from utils, but other than that there's nothing else to explain.

4. Adding settings to a plugin

Let's add a setting that allows you to send the emoji's name alongside the emoji.

You can add settings by importing definePluginSettings

import { definePluginSettings } from "@api/Settings";
import { OptionType } from "@utils/types";
// ...
const settings = definePluginSettings({
    withName: {
        type: OptionType.BOOLEAN,
        description: "Include the emoji's name",
        default: false,
    }
});

and then adding it to the plugin object

export default definePlugin({
    // ...
    settings: settings,
    // or since in JavaScript { value } is treated as { value: value }, you can simply 
    settings,

This adds in the settings UI for us. After that, we can add a condition in our sendEmote function

  sendMessage(getCurrentChannel().id, {
-     content: node.surrogate
+     content: settings.store.withName ? `${node.surrogate} - ${node.name.replace(":", "\\:")}` : node.surrogate
  });

Settings save automatically, so changes are applied without a restart just fine

Congratulations, we made a basic plugin 🎉

Final code

If you directly jumped here from the start, take a quick peek back at writing a component to change the file extension

import { definePluginSettings } from "@api/Settings";
import { getCurrentChannel } from "@utils/discord";
import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy } from "@webpack";
import { Button } from "@webpack/common";

interface EmojiNode {
    type: "emoji";
    name: string;
    surrogate: string;
}

const { sendMessage } = findByPropsLazy("sendMessage", "editMessage");

function sendEmote(node: EmojiNode) {
    sendMessage(getCurrentChannel().id, {
        content: settings.store.withName ? `${node.surrogate} - ${node.name.replace(":", "\\:")}` : node.surrogate
    });
}

const settings = definePluginSettings({
    withName: {
        type: OptionType.BOOLEAN,
        description: "Include the emoji's name",
        default: false,
    }
});

export default definePlugin({
    name: "Your Plugin",
    description: "This plugin does something cool",
    authors: [{
        name: "You!",
        id: 0n
    }],

    patches: [{
        find: ".EMOJI_POPOUT_STANDARD_EMOJI_DESCRIPTION",
        replacement: {
            match: /(?<=.primaryEmoji,src:(\i).{0,400}).Messages.EMOJI_POPOUT_STANDARD_EMOJI_DESCRIPTION}\)]/,
            replace: "$&.concat([$self.EmojiButton($1)])"
        }
    }],

    settings,

    EmojiButton(node: EmojiNode) {
        return <Button onClick={() => sendEmote(node)}>
            Send emote
        </Button>;
    }
});

Additional resources

Plugin development

Regular expressions

@LotharieSlayer
Copy link

LotharieSlayer commented Jul 26, 2023

I always get this error, even by copying and pasting the "Final code" :

X [ERROR] Expected ">" but found "onClick"

    src/plugins/userplugins/index.ts:49:23:
      49 │         return <Button onClick={() => sendEmote(node)}>
         │                        ~~~~~~~
         ╵                        >

Can someone help me pls ?

@Rico040
Copy link

Rico040 commented Feb 11, 2024

I always get this error, even by copying and pasting the "Final code" :

X [ERROR] Expected ">" but found "onClick"

    src/plugins/userplugins/index.ts:49:23:
      49 │         return <Button onClick={() => sendEmote(node)}>
         │                        ~~~~~~~
         ╵                        >

Can someone help me pls ?

you forget to change file extenstion to tsx, i think

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment