This guide will give you a general idea of
- How to interact with Discord's modules (aka how webpack works and how to find things)
- How to actually find the right functions for your job
Instead of just telling you how to do things without you understanding why it works this way, I will instead try to make you understand how things work and then how to utilise this knowledge!
Note: While being about Discord, this guide more or less applies to any app using Electron/webpack so it might even be helpful for other apps!
You should be familiar with:
- Javascript (especially the Browser side of things), HTML and CSS
- React
- Chrome's DevTools
In case you aren't familiar with some of these, I highly recommend you try them out in a normal app first. For example, if you never had any experience with Web Development, you could make a personal homepage with React.
While you can of course learn these things while modding, you will work with minified Code with all documentation stripped out, so making a normal project will be way easier for someone new to this!
Now that you're ready to get started, install React Developer Tools. The process of doing this will differ per mod and environment, in Vencord it's as easy as enabling them in the settings, if you're using something else, figure it out yourself (or ask me :D)!
When making a Javascript app, you obviously don't want all your code in one single file. This is where Node's require comes into play, an API allowing you to import different files. File 1 specifies what it wants to export:
module.exports = {
hello() {
console.log("Hello was called!")
}
}
and now file 2 can import this function:
require("./file1").hello();
But require only exists in Node, the Browser doesn't know require!
In comes webpack, a tool that bundles a codebase using require to a web consumable build!
Webpack also has a require function, webpackRequire.
Except instead of filenames, it uses automatically generated ids for each module: require("./foo")
-> require(17271)
. What a bummer!
Here's an example from a random Discord module:
But as you probably already realised, we have no way of knowing what id a module has, so we can't just use webpack's require. We have to find a different method!
Imagine a module like
let count = 0;
function increment() {
count++
}
module.exports = {
count,
increment
}
If you imported this module from multiple files, you would expect all files to use the same counter. And that's exactly why require caches all imported modules. The first time you require a module, it's loaded and initialised and cached, then if you require it again you get the cached version.
You can actually view this cache via require.cache
! In our case above it would look something like this:
{
foo: {
loaded: true,
id: "foo",
exports: {
count: 0,
increment() {
count++
}
}
}
}
Now if you loop over this cache, you can access the exports of arbitrary modules without knowing their id as long as they have been imported before. You can probably already guess where this is going?
Note: The following code examples are only very basic to explain the basics and don't cover all cases. For a better implementation, check out the Vencord webpack source code!
We can now find the counter module without knowing its name:
function findCounter() {
// Iterate over the cache
for (const id in require.cache) {
// Check if the current module has an increment export
if (require.cache[id].exports.increment) {
// yippee! We found the counter module
return require.cache[id].exports;
}
}
// nothing found >:(
return null;
}
const Counter = findCounter();
Counter.increment();
But we obviously don't want to only find the counter, we want to find way more modules.
So let's make the method generic!
function findModuleByProperty(property: string) {
for (const id in require.cache) {
// Check if the current module has the specified property
if (property in require.cache[id].exports) {
// yippee! We found a matching module
return require.cache[id].exports;
}
}
// nothing found >:(
return null;
}
const Counter = findModuleByProperty("increment");
Counter.increment();
Sadly it's not always that simple! A lot of Discord modules don't have friendly exports, so it's more like
export.s = function() {
return "read if cute"
}
findModuleByProperties("s")
is not really viable... 😕 There will be many modules with that same export name and it will change every update
But surely Discord will only have one function returning "read if cute"
so we can find the function by its code!!
function findModuleByCode(code: string) {
for (const id in require.cache) {
const currentModule = require.cache[id];
// since the export names are also unusable, we need a second inner loop to test every export
for (const name in currentModule) {
// Check only the current export, in our case `exports.s`
const export = currentModule[name];
// Since we're searching a function by code, we first need to make sure this is actually a function!
if (typeof export === "function") {
// Thankfully, we're in Javascript which allows you to get the code of
// any function as string!
const functionCode = export.toString();
if (functionCode.includes(code)) {
// woah! This function includes the code we searched for, let's return it
return export;
// Note that now we only returned the single function we found, because
// otherwise we'd be in the same dilemma of not knowing the name again!
// If you wanted the full module, you could also return both
// the module and the name we found, so the user has more control!
return [currentModule, name];
}
}
}
}
// nothing found >:(
return null;
}
const getMessage = findModuleByCode("read if cute");
console.log(getMessage()) // read if cute
// or if you use the alternative return
const [module, getMessageName] = findModuleByCode("read if cute");
console.log(module[getMessageName]()) // read if cute
Usually, findByProps or findByCode will be all you need! However, in some rarer cases, you will need a more specific filter, for example a filter that finds a module whose USER
export is a function.
function findModuleByFilter(filter: (currentModule: any) => boolean) {
for (const id in require.cache) {
const currentModule = require.cache[id];
// Call the filter with the current module
if (filter(currentModule)) {
// The filter function matched this module!
return currentModule;
}
}
// nothing found >:(
return null;
}
const ModuleWithUserFunction = findModuleByFilter(m => typeof m.USER === "function")
ModuleWithUserFunction.USER()
I hope these examples helped you understand how finding modules works. It's nothing more than checking every single module in the cache until one matches your search.
Depending on your use case, you'll use one of those three methods of finding modules. Also different mods may have more find methods, like findByRegex, findByName, and so on, but they're all basically the same just with different methods of finding things!
Quick Summary:
- Use findByProps to find a module by export name, e.g. findByProps("foo") to find
exports.foo
- Use findByCode to find a single function by its code signature
- Use find to find a module with a custom filter if the other two won't suffice
Btw: You don't actually have to worry about the implementation of the find methods, you only really need to know how to use them. I just showed you basic examples so you understand how it works! Feel free to forget about them again :P
Now you know how to find modules once you know what you're looking for. But...
This is where Chrome and React DevTools come into play.
Chrome DevTools allow you to search Discord's code, put breakpoints and run code
React Devtools allow you to to inspect UI elements like Buttons and see all their internals, like the callback for when you click on it
I usually use the following approaches:
- Is there a UI element that already does what I want? For example, if I wanted to make an Emote Cloner plugin, to figure out how to create emotes, my first instinct would be to use React Devtools to inspect the Create Emote button in the Server Settings. Now inspect the onClick and similar and see what happens. Don't hesitate to put breakpoints and follow along each step!
- If React Devtools weren't of help, use the regular document inspector to find the html classes of the desired element. Now choose the
class that seems the most specific, for example
avatar-2e8lTP
. Remove the junk part and search the code (see below) for().CLASS
, like().avatar
. Now you will find all components that use this class (or another class with the same name)! - Search Discord's code for keywords related to your feature. In the same example as above, I would search for "createEmoji", "createCustomEmoji", "createEmote", "uploadCustomEmoji" ... until I find something promising. You can search all Discord files at once by pressing
ctrl shift f
while having DevTools open. Make sure to enable pretty print btw! (DevTools Settings -> Experimental -> Always pretty print). Check all results until you find something that looks right, then put a breakpoint and do whatever would trigger this method to be called. Now you can look at all the arguments passed to it and so on
Found the right method?
For this part, it's important that you understand how a module looks.
Essentially, webpack creates a bunch of chunks (multiple big bundles), where each chunk contains multiple modules. Think of it like chunk = folder, module = file in folder
Each module will have a similar syntax:
MODULE_ID: (module, exports, require) => {
addExports(exports, {
exportOne: () => theExportedThing,
exportTwo: () => theExportedThing
});
var import1 = require(MODULE_1_ID),
import2 = require(MODULE_2_ID)
// code
}
So once you found the right function, the first step would be to check the exports of the module its in to see if it's even exported. Scroll up until you see the familiar pattern of
12172: (e, t, n) => {
}
Check the exports to see which locals are exported and if the one you want is part of it.
You can also now require this module by id using webpackRequire to get its exports in your console.
In Vencord this would be Vencord.Webpack.wreq(12172)
(wreq stands for webpack require). If you're using a different mod, this might be different or not possible (if so, bug its dev to expose it, it's really useful!)
As you can see in the screenshot above, Discord's export names are sadly all short and unusable. But here's some advice on the names:
export.Z = export.default
with no more exportsexport.ZP = export.default
with more named exports
Now how you would write the filter depends on what you're trying to find:
- Object: findByProps
- single function: findByCode
- class:
find(m => m.prototype?.someInstanceMethod)
Since you already required the module by id, you now have the exports in your console. Experiment with the above approaches until you find a working filter.
IMPORTANT: Once you found a working filter, make sure this filter is actually unique! This means, feed the filter into findAll
to find all matches. If there's more than one match, you have to make your filter more specific until only exactly the right module matches!
- Make sure your FILTERS are UNIQUE! You don't want it to randomly find a different module all of a sudden
- When searching by code, NEVER RELY ON MINIFIED VARIABLE NAMES. They will change in the future and break your plugin
- Webpack search is EXTREMELY EXPENSIVE. Only do it once, and never do it inside React components, this will cause horrible lag. Also consider making Webpack search lazy (only done on demand). Vencord for example provides the
lazyWebpack
utility - React was not covered in this document, however, if you can, wrap your React UI into an ErrorBoundary to avoid nasty crashes. On Vencord for example,
ErrorBoundary.wrap(MyComponent)
or<ErrorBoundary><MyComponent /></ErrorBoundary>
- Some modules like context menus are lazily loaded (only loaded once the user opens a context menu), so you will have to get those in a different way. Vencord for example provides the
waitFor
utility
Webpack creates a global on the Window which is an Array with overwritten push function. In Discord' case, this is webpackChunkdiscord_app
.
Every time a new chunk is loaded, it pushes itself into this Array in the following form:
webpackChunkdiscord_app.push([
[CHUNK_ID],
{
MODULE_1_ID: (module, exports, require) => {
exports.foo = "12"
}
},
optionalCallback: require => {}
])
Once this chunk has been pushed, you can now require its MODULE_1_ID module via require(MODULE_1_ID)
Yes, you can register your own modules in Discord's webpack with this! Funky, huh?
const webpackRequire = webpackChunkdiscord_app.push([
[Symbol()], // Make sure our "chunk" id is unique
{} // No modules,
require => require // the return value of this function will be the return value of push
]);
const requireCache = webpackRequire.c;
requireCache[1234].exports.foo
const registeredModules = webpackRequire.m; // Includes all registered modules, even those that haven't been loaded
const requireMain = webpackRequire.s; // The entry point