@ -18,66 +18,120 @@
import { WEBPACK_CHUNK } from "@utils/constants" ;
import { Logger } from "@utils/Logger" ;
import { canonicalize Replacement } from "@utils/patches" ;
import { canonicalize Match, canonicalize Replacement } from "@utils/patches" ;
import { PatchReplacement } from "@utils/types" ;
import { WebpackInstance } from "discord-types/other" ;
import { traceFunction } from "../debug/Tracer" ;
import { _initWebpack } from "." ;
import { patches } from "../plugins" ;
import { _initWebpack , beforeInitListeners , factoryListeners , module Listeners , subscriptions , wreq } from "." ;
const logger = new Logger ( "WebpackInterceptor" , "#8caaee" ) ;
const initCallbackRegex = canonicalizeMatch ( /{return \i\(".+?"\)}/ ) ;
let webpackChunk : any [ ] ;
const logger = new Logger ( "WebpackInterceptor" , "#8caaee" ) ;
// Patch the window webpack chunk setter to monkey patch the push method before any chunks are pushed
// This way we can patch the factory of everything being pushed to the modules array
Object . defineProperty ( window , WEBPACK_CHUNK , {
configurable : true ,
get : ( ) = > webpackChunk ,
set : v = > {
if ( v ? . push ) {
if ( ! v . push . $ $vencordOriginal ) {
logger . info ( ` Patching ${ WEBPACK_CHUNK } .push ` ) ;
patchPush ( v ) ;
// @ts-ignore
delete window [ WEBPACK_CHUNK ] ;
window [ WEBPACK_CHUNK ] = v ;
}
}
if ( window [ WEBPACK_CHUNK ] ) {
logger . info ( ` Patching ${ WEBPACK_CHUNK } .push (was already existent, likely from cache!) ` ) ;
_initWebpack ( window [ WEBPACK_CHUNK ] ) ;
patchPush ( window [ WEBPACK_CHUNK ] ) ;
} else {
Object . defineProperty ( window , WEBPACK_CHUNK , {
get : ( ) = > webpackChunk ,
set : v = > {
if ( v ? . push ) {
if ( ! v . push . $ $vencordOriginal ) {
logger . info ( ` Patching ${ WEBPACK_CHUNK } .push ` ) ;
patchPush ( v ) ;
}
webpackChunk = v ;
}
} ) ;
// wreq.O is the webpack onChunksLoaded function
// Discord uses it to await for all the chunks to be loaded before initializing the app
// We monkey patch it to also monkey patch the initialize app callback to get immediate access to the webpack require and run our listeners before doing it
Object . defineProperty ( Function . prototype , "O" , {
configurable : true ,
set ( onChunksLoaded : any ) {
// When using react devtools or other extensions, or even when discord loads the sentry, we may also catch their webpack here.
// This ensures we actually got the right one
// this.e (wreq.e) is the method for loading a chunk, and only the main webpack has it
if ( new Error ( ) . stack ? . includes ( "discord.com" ) && String ( this . e ) . includes ( "Promise.all" ) ) {
logger . info ( "Found main WebpackRequire.onChunksLoaded" ) ;
delete ( Function . prototype as any ) . O ;
const originalOnChunksLoaded = onChunksLoaded ;
onChunksLoaded = function ( this : unknown , result : any , chunkIds : string [ ] , callback : ( ) = > any , priority : number ) {
if ( callback != null && initCallbackRegex . test ( callback . toString ( ) ) ) {
Object . defineProperty ( this , "O" , {
value : originalOnChunksLoaded ,
configurable : true
} ) ;
const wreq = this as WebpackInstance ;
const originalCallback = callback ;
callback = function ( this : unknown ) {
logger . info ( "Patched initialize app callback invoked, initializing our internal references to WebpackRequire and running beforeInitListeners" ) ;
_initWebpack ( wreq ) ;
for ( const beforeInitListener of beforeInitListeners ) {
beforeInitListener ( wreq ) ;
}
if ( _initWebpack ( v ) ) {
logger . info ( "Successfully initialised Vencord webpack" ) ;
// @ts-ignore
delete window [ WEBPACK_CHUNK ] ;
window [ WEBPACK_CHUNK ] = v ;
originalCallback . apply ( this , arguments as any ) ;
} ;
callback . toString = originalCallback . toString . bind ( originalCallback ) ;
arguments [ 2 ] = callback ;
}
}
webpackChunk = v ;
} ,
configurable : true
} ) ;
// wreq.m is the webpack module factory.
// normally, this is populated via webpackGlobal.push, which we patch below.
// However, Discord has their .m prepopulated.
// Thus, we use this hack to immediately access their wreq.m and patch all already existing factories
//
// Update: Discord now has TWO webpack instances. Their normal one and sentry
// Sentry does not push chunks to the global at all, so this same patch now also handles their sentry modules
Object . defineProperty ( Function . prototype , "m" , {
set ( v : any ) {
// When using react devtools or other extensions, we may also catch their webpack here.
// This ensures we actually got the right one
if ( new Error ( ) . stack ? . includes ( "discord.com" ) ) {
logger . info ( "Found webpack module factory" ) ;
patchFactories ( v ) ;
}
originalOnChunksLoaded . apply ( this , arguments as any ) ;
} ;
Object . defineProperty ( this , "m" , {
value : v ,
configurable : true ,
} ) ;
} ,
configurable : true
} ) ;
}
onChunksLoaded . toString = originalOnChunksLoaded . toString . bind ( originalOnChunksLoaded ) ;
}
Object . defineProperty ( this , "O" , {
value : onChunksLoaded ,
configurable : true
} ) ;
}
} ) ;
// wreq.m is the webpack module factory.
// normally, this is populated via webpackGlobal.push, which we patch below.
// However, Discord has their .m prepopulated.
// Thus, we use this hack to immediately access their wreq.m and patch all already existing factories
//
// Update: Discord now has TWO webpack instances. Their normal one and sentry
// Sentry does not push chunks to the global at all, so this same patch now also handles their sentry modules
Object . defineProperty ( Function . prototype , "m" , {
configurable : true ,
set ( v : any ) {
// When using react devtools or other extensions, we may also catch their webpack here.
// This ensures we actually got the right one
const error = new Error ( ) ;
if ( error . stack ? . includes ( "discord.com" ) ) {
logger . info ( "Found Webpack module factory" , error . stack . match ( /\/assets\/(.+?\.js)/ ) ? . [ 1 ] ? ? "" ) ;
patchFactories ( v ) ;
}
Object . defineProperty ( this , "m" , {
value : v ,
configurable : true
} ) ;
}
} ) ;
function patchPush ( webpackGlobal : any ) {
function handlePush ( chunk : any ) {
@ -91,6 +145,7 @@ function patchPush(webpackGlobal: any) {
}
handlePush . $ $vencordOriginal = webpackGlobal . push ;
handlePush . toString = handlePush . $ $vencordOriginal . toString . bind ( handlePush . $ $vencordOriginal ) ;
// Webpack overwrites .push with its own push like so: `d.push = n.bind(null, d.push.bind(d));`
// it wraps the old push (`d.push.bind(d)`). this old push is in this case our handlePush.
// If we then repatched the new push, we would end up with recursive patching, which leads to our patches
@ -99,41 +154,41 @@ function patchPush(webpackGlobal: any) {
handlePush . bind = ( . . . args : unknown [ ] ) = > handlePush . $ $vencordOriginal . bind ( . . . args ) ;
Object . defineProperty ( webpackGlobal , "push" , {
configurable : true ,
get : ( ) = > handlePush ,
set ( v ) {
handlePush . $ $vencordOriginal = v ;
} ,
configurable : true
}
} ) ;
}
function patchFactories ( factories : Record < string | number , ( module : { exports : any ; } , exports : any , require : any ) = > void > ) {
const { subscriptions , listeners } = Vencord . Webpack ;
const { patches } = Vencord . Plugins ;
let webpackNotInitializedLogged = false ;
function patchFactories ( factories : Record < string , ( module : any , exports : any , require : WebpackInstance ) = > void > ) {
for ( const id in factories ) {
let mod = factories [ 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.
//
// 0, prefix is to turn it into an expression: 0,function(){} would be invalid syntax without the 0,
let code : string = "0," + mod . toString ( ) . replaceAll ( "\n" , "" ) ;
const originalMod = mod ;
const patchedBy = new Set ( ) ;
const factory = factories [ id ] = function ( module , exports , require ) {
const factory = factories [ id ] = function ( module : any , exports : any , require : WebpackInstance ) {
if ( wreq == null && IS_DEV ) {
if ( ! webpackNotInitializedLogged ) {
webpackNotInitializedLogged = true ;
logger . error ( "WebpackRequire was not initialized, running modules without patches instead." ) ;
}
return void originalMod ( 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 ) ;
logger . error ( "Error in patched module ", err ) ;
return void originalMod ( module , exports , require ) ;
}
@ -153,11 +208,11 @@ function patchFactories(factories: Record<string | number, (module: { exports: a
return ;
}
for ( const callback of l isteners) {
for ( const callback of module L isteners) {
try {
callback ( exports , id ) ;
} catch ( err ) {
logger . error ( "Error in webpack listener", err ) ;
logger . error ( "Error in Webpack module listener:\n", err , callback ) ;
}
}
@ -171,107 +226,127 @@ function patchFactories(factories: Record<string | number, (module: { exports: a
callback ( exports . default , id ) ;
}
} catch ( err ) {
logger . error ( "Error while firing callback for webpack chunk", err ) ;
logger . error ( "Error while firing callback for Webpack subscription:\n", err , filter , callback ) ;
}
}
} as any as { toString : ( ) = > string , original : any , ( . . . args : any [ ] ) : void ; } ;
// for some reason throws some error on which calling .toString() leads to infinite recursion
// when you force load all chunks???
factory . toString = ( ) = > mod . toString ( ) ;
factory . toString = originalMod . toString . bind ( originalMod ) ;
factory . original = originalMod ;
for ( const factoryListener of factoryListeners ) {
try {
factoryListener ( originalMod ) ;
} catch ( err ) {
logger . error ( "Error in Webpack factory listener:\n" , err , factoryListener ) ;
}
}
// 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.
//
// 0, prefix is to turn it into an expression: 0,function(){} would be invalid syntax without the 0,
let code : string = "0," + mod . toString ( ) . replaceAll ( "\n" , "" ) ;
for ( let i = 0 ; i < patches . length ; i ++ ) {
const patch = patches [ i ] ;
const executePatch = traceFunction ( ` patch by ${ patch . plugin } ` , ( match : string | RegExp , replace : string ) = > code . replace ( match , replace ) ) ;
if ( patch . predicate && ! patch . predicate ( ) ) continue ;
if ( ! code . includes ( patch . find ) ) continue ;
if ( code . includes ( patch . find ) ) {
patchedBy . add ( patch . plugin ) ;
patchedBy . add ( patch . plugin ) ;
const previousMod = mod ;
const previousCode = code ;
const executePatch = traceFunction ( ` patch by ${ patch . plugin } ` , ( match : string | RegExp , replace : string ) = > code . replace ( match , replace ) ) ;
const previousMod = mod ;
const previousCode = code ;
// we change all patch.replacement to array in plugins/index
for ( const replacement of patch . replacement as PatchReplacement [ ] ) {
if ( replacement . predicate && ! replacement . predicate ( ) ) continue ;
const lastMod = mod ;
const lastCode = code ;
// We change all patch.replacement to array in plugins/index
for ( const replacement of patch . replacement as PatchReplacement [ ] ) {
if ( replacement . predicate && ! replacement . predicate ( ) ) continue ;
canonicalizeReplacement ( replacement , patch . plugin ) ;
const lastMod = mod ;
const lastCode = code ;
try {
const newCode = executePatch ( replacement . match , replacement . replace as string ) ;
if ( newCode === code ) {
if ( ! patch . noWarn ) {
logger . warn ( ` Patch by ${ patch . plugin } had no effect (Module id is ${ id } ): ${ replacement . match } ` ) ;
if ( IS_DEV ) {
logger . debug ( "Function Source:\n" , code ) ;
}
}
canonicalizeReplacement ( replacement , patch . plugin ) ;
if ( patch . group ) {
logger . warn ( ` Undoing patch group ${ patch . find } by ${ patch . plugin } because replacement ${ replacement . match } had no effect ` ) ;
code = previousCode ;
mod = previousMod ;
patchedBy . delete ( patch . plugin ) ;
break ;
}
} else {
code = newCode ;
mod = ( 0 , eval ) ( ` // Webpack Module ${ id } - Patched by ${ [ . . . patchedBy ] . join ( ", " ) } \ n ${ newCode } \ n//# sourceURL=WebpackModule ${ id } ` ) ;
}
} catch ( err ) {
logger . error ( ` Patch by ${ patch . plugin } errored (Module id is ${ id } ): ${ replacement . match } \ n ` , err ) ;
if ( IS_DEV ) {
const changeSize = code . length - lastCode . length ;
const match = lastCode . match ( replacement . match ) ! ;
// Use 200 surrounding characters of context
const start = Math . max ( 0 , match . index ! - 200 ) ;
const end = Math . min ( lastCode . length , match . index ! + match [ 0 ] . length + 200 ) ;
// (changeSize may be negative)
const endPatched = end + changeSize ;
const context = lastCode . slice ( start , end ) ;
const patchedContext = code . slice ( start , endPatched ) ;
// inline require to avoid including it in !IS_DEV builds
const diff = ( require ( "diff" ) as typeof import ( "diff" ) ) . diffWordsWithSpace ( context , patchedContext ) ;
let fmt = "%c %s " ;
const elements = [ ] as string [ ] ;
for ( const d of diff ) {
const color = d . removed
? "red"
: d . added
? "lime"
: "grey" ;
fmt += "%c%s" ;
elements . push ( "color:" + color , d . value ) ;
try {
const newCode = executePatch ( replacement . match , replacement . replace as string ) ;
if ( newCode === code ) {
if ( ! patch . noWarn ) {
logger . warn ( ` Patch by ${ patch . plugin } had no effect (Module id is ${ id } ): ${ replacement . match } ` ) ;
if ( IS_DEV ) {
logger . debug ( "Function Source:\n" , code ) ;
}
logger . errorCustomFmt ( . . . Logger . makeTitle ( "white" , "Before" ) , context ) ;
logger . errorCustomFmt ( . . . Logger . makeTitle ( "white" , "After" ) , patchedContext ) ;
const [ titleFmt , . . . titleElements ] = Logger . makeTitle ( "white" , "Diff" ) ;
logger . errorCustomFmt ( titleFmt + fmt , . . . titleElements , . . . elements ) ;
}
patchedBy . delete ( patch . plugin ) ;
if ( patch . group ) {
logger . warn ( ` Undoing patch group ${ patch . find } by ${ patch . plugin } because replacement ${ replacement . match } errored ` ) ;
code = previousCode ;
logger . warn ( ` Undoing patch group ${ patch . find } by ${ patch . plugin } because replacement ${ replacement . match } had no effect ` ) ;
mod = previousMod ;
code = previousCode ;
patchedBy . delete ( patch . plugin ) ;
break ;
}
code = lastCode ;
mod = lastMod ;
continue ;
}
}
if ( ! patch . all ) patches . splice ( i -- , 1 ) ;
code = newCode ;
mod = ( 0 , eval ) ( ` // Webpack Module ${ id } - Patched by ${ [ . . . patchedBy ] . join ( ", " ) } \ n ${ newCode } \ n//# sourceURL=WebpackModule ${ id } ` ) ;
} catch ( err ) {
logger . error ( ` Patch by ${ patch . plugin } errored (Module id is ${ id } ): ${ replacement . match } \ n ` , err ) ;
if ( IS_DEV ) {
const changeSize = code . length - lastCode . length ;
const match = lastCode . match ( replacement . match ) ! ;
// Use 200 surrounding characters of context
const start = Math . max ( 0 , match . index ! - 200 ) ;
const end = Math . min ( lastCode . length , match . index ! + match [ 0 ] . length + 200 ) ;
// (changeSize may be negative)
const endPatched = end + changeSize ;
const context = lastCode . slice ( start , end ) ;
const patchedContext = code . slice ( start , endPatched ) ;
// inline require to avoid including it in !IS_DEV builds
const diff = ( require ( "diff" ) as typeof import ( "diff" ) ) . diffWordsWithSpace ( context , patchedContext ) ;
let fmt = "%c %s " ;
const elements = [ ] as string [ ] ;
for ( const d of diff ) {
const color = d . removed
? "red"
: d . added
? "lime"
: "grey" ;
fmt += "%c%s" ;
elements . push ( "color:" + color , d . value ) ;
}
logger . errorCustomFmt ( . . . Logger . makeTitle ( "white" , "Before" ) , context ) ;
logger . errorCustomFmt ( . . . Logger . makeTitle ( "white" , "After" ) , patchedContext ) ;
const [ titleFmt , . . . titleElements ] = Logger . makeTitle ( "white" , "Diff" ) ;
logger . errorCustomFmt ( titleFmt + fmt , . . . titleElements , . . . elements ) ;
}
patchedBy . delete ( patch . plugin ) ;
if ( patch . group ) {
logger . warn ( ` Undoing patch group ${ patch . find } by ${ patch . plugin } because replacement ${ replacement . match } errored ` ) ;
mod = previousMod ;
code = previousCode ;
break ;
}
mod = lastMod ;
code = lastCode ;
}
}
if ( ! patch . all ) patches . splice ( i -- , 1 ) ;
}
}
}