Slacked: Part 1

Posted on 26th April 2023 at 20:13

My journey into writing a Vencord like client mod for Slack

Contents

How Does Vencord Work?

All electron apps have a resources folder, inside which you’ll find an app.asar. This file contains the apps source code packaged into a single file. There’s nothing special about these files, they’re not encrypted or obfuscated, you can extract the content inside. Electron apps will also load an app.asar directory if extracted, it doesn’t need to be bundled into the archive.

What Vencord does is make a copy of the original asar and replaces the main app.asar with it’s own script. All this script does is load their injector script which is stored elsewhere.

1
require('/home/emily/.config/Vencord/dist/patcher.js');

But the patcher is where the magic happens.

Vencord’s Patcher

The first step to understanding this was, how does it actually load Discord? None of the fancy plugins/modding matters if I don’t even know how it eventually opens Discord from its patcher.

I’ve simplified some parts here for clarity.

1
import { app } from 'electron';
2
3
const injectorPath = require.main!.filename;
4
5
// The original app.asar
6
const asarPath = join(dirname(injectorPath), '..', '_app.asar');
7
8
const discordPkg = require(join(asarPath, 'package.json'));
9
require.main!.filename = join(asarPath, discordPkg.main);
10
11
app.setAppPath(asarPath);
12
13
// some code here we'll get to...
14
15
console.log('[Vencord] Loading original Discord app.asar');
16
require(require.main!.filename);

Interesting! So all it’s doing is replacing require.main!.filename with the path to Discord’s actual package.json and loading that at the end! But, does this work for Slack?

Uhhh kinda?

It started! Which is a good sign, the code runs but it seems to exit for some reason.

To debug this, I had to delve into Slack’s minified source code :D

Inside that mess I searched for all process.exit(0) as the exit code was 0, until I found the culprit!

1
b.app.requestSingleInstanceLock() || oe.app.exit(0), process.exit(0);

Inside of main.bundle.js requestSingleInstanceLock appears to fail, I suppose this is because technically I am running two instances? My script and then Slack’s actual code? So, let’s replace this part with sed.

Terminal window
1
sed 's/||oe.app.exit(0),process.exit(0)//g' -i main.bundle.js

It's alive!

This skeleton code now boots into slack via our script! This is where the fun starts.

1
import { dirname, join } from 'path';
2
import { app } from 'electron';
3
4
const injectorPath = require.main!.filename;
5
6
const asarPath = join(dirname(injectorPath), '..', '_app.asar');
7
8
const slackPackage = require(join(asarPath, 'package.json'));
9
require.main!.filename = join(asarPath, slackPackage.main);
10
11
// @ts-ignore
12
app.setAppPath(asarPath);
13
14
require(require.main!.filename);

Opening Chrome Dev Tools on Startup

Let’s start with something simple, how can we trigger the dev tools on startup?

Looking through the rest of Vencord’s patcher, I see that it creates a BrowserWindow class that extends electron.BrowserWindow. It only provides an implementation for the constructor and a lot of that isn’t relevant for us right now. The interesting part is this initIpc(this) function, but we’ll come back to that.

Let’s create our BrowserWindow class like so.

1
// @ts-ignore
2
app.setAppPath(asarPath);
3
4
const initIpc = (window: electron.BrowserWindow) => {};
5
6
class BrowserWindow extends electron.BrowserWindow {
7
constructor(options: BrowserWindowConstructorOptions) {
8
super(options);
9
initIpc(this);
10
}
11
}
12
13
require(require.main!.filename);

After which we copy over all of the methods and properties on the original electron.BrowserWindow to our new window class.

1
Object.assign(BrowserWindow, electron.BrowserWindow);

And then we also have to replace electrons BrowserWindow exports.

1
const electronPath = require.resolve('electron');
2
delete require.cache[electronPath]!.exports;
3
require.cache[electronPath]!.exports = {
4
...electron,
5
BrowserWindow
6
};

That’s it! Surprisingly simple when viewed from our perspective, but I would’ve never figured this out without some serious time and research, so thanks V.

Heading back to that initIpc function, we passed along our custom BrowserWindow instance to that function. With electron you can programmatically open devtools from that BrowserWindow instance, let’s try it.

1
const initIpc = (window: electron.BrowserWindow) => {
2
window.webContents.openDevTools();
3
};

Annnnnnnnd…

There he is! Mr. Devtools

Wowie, we did it! We can now execute custom code automatically within a Slack desktop client :D

But how does Vencord’s text replacement plugin system work? Can we implement it with our mod? What about loading custom themes? That’s for part 2.