aboutsummaryrefslogtreecommitdiff
path: root/src/utils/patchWebpack.ts
blob: ce29c82ddad2bbfb57ef0b86be6db3943d9ce1fe (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
import { WEBPACK_CHUNK } from './constants';
import Logger from "./logger";
import { _initWebpack } from "../webpack";

let webpackChunk: any[];

const logger = new Logger("WebpackInterceptor", "#8caaee");

Object.defineProperty(window, WEBPACK_CHUNK, {
    get: () => webpackChunk,
    set: (v) => {
        if (v?.push !== Array.prototype.push) {
            logger.info(`Patching ${WEBPACK_CHUNK}.push`);
            _initWebpack(v);
            patchPush();
            // @ts-ignore
            delete window[WEBPACK_CHUNK];
            window[WEBPACK_CHUNK] = v;
        }
        webpackChunk = v;
    },
    configurable: true
});

function patchPush() {
    function handlePush(chunk) {
        try {
            const modules = chunk[1];
            const { subscriptions, listeners } = Vencord.Webpack;
            const { patches } = Vencord.Plugins;

            for (const id in modules) {
                let mod = modules[id];
                // Discords Webpack chunks for some ungodly reason contain random
                // newlines. Cyn recommended this workaround and it seems to work fine,
                // however this could potentially break code, so if anything goes weird,
                // this is probably why.
                // Additionally, `[actual newline]` is one less char than "\n", so if Discord
                // ever targets newer browsers, the minifier could potentially use this trick and
                // cause issues.
                let code = mod.toString().replaceAll("\n", "");
                const originalMod = mod;
                const patchedBy = new Set();

                modules[id] = function (module, exports, require) {
                    try {
                        mod(module, exports, require);
                    } catch (err) {
                        // Just rethrow discord errors
                        if (mod === originalMod) throw err;

                        logger.error("Error in patched chunk", err);
                        return originalMod(module, exports, require);
                    }

                    for (const callback of listeners) {
                        try {
                            callback(exports);
                        } catch (err) {
                            logger.error("Error in webpack listener", err);
                        }
                    }
                    for (const [filter, callback] of subscriptions) {
                        try {
                            if (filter(exports)) {
                                subscriptions.delete(filter);
                                callback(exports);
                            } else for (const nested in exports) {
                                if (exports[nested] && filter(exports[nested])) {
                                    subscriptions.delete(filter);
                                    callback(exports[nested]);
                                }
                            }
                        } catch (err) {
                            logger.error("Error while firing callback for webpack chunk", err);
                        }
                    }
                };
                modules[id].toString = () => mod.toString();
                modules[id].original = originalMod;

                for (let i = 0; i < patches.length; i++) {
                    const patch = patches[i];
                    if (code.includes(patch.find)) {
                        patchedBy.add(patch.plugin);
                        // @ts-ignore we change all patch.replacement to array in plugins/index
                        for (const replacement of patch.replacement) {
                            const lastMod = mod;
                            const lastCode = code;
                            try {
                                const newCode = code.replace(replacement.match, replacement.replace);
                                if (newCode === code) {
                                    logger.warn(`Patch by ${patch.plugin} had no effect: ${replacement.match}`);
                                    logger.debug("Function Source:\n", code);
                                } else {
                                    code = newCode;
                                    mod = (0, eval)(`// Webpack Module ${id} - Patched by ${[...patchedBy].join(", ")}\n${newCode}\n//# sourceURL=WebpackModule${id}`);
                                }
                            } catch (err) {
                                // TODO - More meaningful errors. This probably means moving away from string.replace
                                // in favour of manual matching. Then cut out the context and log some sort of
                                // diff
                                logger.error("Failed to apply patch of", patch.plugin, err);
                                logger.debug("Original Source\n", lastCode);
                                logger.debug("Patched Source\n", code);
                                code = lastCode;
                                mod = lastMod;
                                patchedBy.delete(patch.plugin);
                            }
                        }
                        patches.splice(i--, 1);
                    }
                }
            }
        } catch (err) {
            logger.error("oopsie", err);
        }

        return handlePush.original.call(window[WEBPACK_CHUNK], chunk);
    }

    handlePush.original = window[WEBPACK_CHUNK].push;
    Object.defineProperty(window[WEBPACK_CHUNK], "push", {
        get: () => handlePush,
        set: (v) => (handlePush.original = v),
        configurable: true
    });
}