Skip to content
This repository was archived by the owner on Feb 20, 2025. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"cSpell.words": [
"replugged"
"replugged",
"unpatch"
]
}
2 changes: 2 additions & 0 deletions src/api/ModImplementation.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { WebpackApi } from "./Webpack.js";
import { PatcherApi } from "./Patcher.js";

export interface IModImplementation {
WebpackApi: typeof WebpackApi,
PatcherApi: typeof PatcherApi.prototype,
/**
* shall be true when a mod requires the Dev to bundle their code into single file
*/
Expand Down
26 changes: 26 additions & 0 deletions src/api/Patcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
export interface IBasePatcherApi {
internalId: string | undefined; /* NOT ON REPLUGGED, on BD it's the "caller" for Patcher functions */
unpatchAll(): void;
after<T, A = unknown[], R = unknown>(target: T, name: string, cb: (args: A, res: R, instance: T) => R): () => void;
}

class DummyPatcherApi implements IBasePatcherApi {
Comment thread
Davilarek marked this conversation as resolved.
internalId: string | undefined;
unpatchAll(): void {
throw new Error("Method not implemented. This is a dummy class.");
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
after<T, A = unknown[], R = unknown>(target: T, name: string, cb: (args: A, res: R, instance: T) => R): () => void {
throw new Error("Method not implemented. This is a dummy class.");
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
before<T, A = unknown[]>(target: T, name: string, cb: (args: A, instance: T) => A): () => void {
throw new Error("Method not implemented. This is a dummy class.");
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
instead<T, A extends unknown[] = unknown[], R = unknown>(target: T, name: string, cb: (args: A, orig: ((...args_: A) => R), instance: T) => R): () => void {
throw new Error("Method not implemented. This is a dummy class.");
}
}

export const PatcherApi = DummyPatcherApi;
9 changes: 8 additions & 1 deletion src/api/RuntimeGenerators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ export function createFunctionWithWrapperNeeded(objectName: string, property: st
Object.defineProperty(result, "wrapperName", { value: wrapperName });
return result;
}
export function createFunctionThatIsMissing() {
const result = new Function(`return () => {throw new Error("Missing");}`)();
Object.defineProperty(result, "missing", {
value: true,
});
return result;
}

import { FunctionImplementation, __requireInternal, doesImplement, implementationStores, initStores } from "../common/index.js";
import { createJavaScriptFromObject, getKeyValue } from "../utils.js";
Expand Down Expand Up @@ -120,7 +127,7 @@ export async function addCode(mod: IModImplementation) {
}
}
// const rawCode = "globalThis.implementationStores = {\n" + getMain(serializer).serialize(constructed) + "\n}";
const req = (target: any) => new Function("return {" + target.func + "}.func;")();
const req = (target: any) => new Function("return {" + target.func + "}.func" + (target.asImmediatelyInvokedFunctionExpression === "true" ? "();" : ";"))();
const rawCode =
`${IMPLEMENTATION_STORES_PATH_SOURCE}.${IMPLEMENTATION_STORES_PATH_VAR_NAME} = (${createJavaScriptFromObject(constructed, true)});
${IMPLEMENTATION_STORES_PATH_SOURCE}.${IMPLEMENTATION_STORES_PATH_REQ} = ${req.toString()};`;
Expand Down
76 changes: 76 additions & 0 deletions src/common/PatcherApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { FunctionImplementation, __requireInternal } from "./index.js";
import { IModImplementation } from "../api/ModImplementation.js";
import { IBasePatcherApi } from "../api/Patcher.js";
export let targetMod: IModImplementation;

const implementationStore = {
Patcher_constructor: new FunctionImplementation({
data: null,
depends: [],
supplies: "constructor_",
isWrapper: true,
asImmediatelyInvokedFunctionExpression: true,
func() {
return {
internalId: Date.now().toString(),
get after() {
return __requireInternal(targetMod, "PatcherApi", "after")?.bind(undefined, this);
},
get unpatchAll() {
return __requireInternal(targetMod, "PatcherApi", "unpatchAll")?.bind(undefined, this);
},
get before() {
return __requireInternal(targetMod, "PatcherApi", "after")?.bind(undefined, this);
},
get instead() {
return __requireInternal(targetMod, "PatcherApi", "instead")?.bind(undefined, this);
},
};
},
}),
afterWrapper: new FunctionImplementation({
data: null,
depends: [],
supplies: "after",
isWrapper: true,
func<T, A = unknown[], R = unknown>(thisObj: IBasePatcherApi, target: T, name: string, cb: (args: A, res: R, instance: T) => R): () => void {
return __requireInternal(targetMod, "PatcherApi", "after", true)!(thisObj.internalId, target, name, (instance_: T, args_: A, res_: R) => {
return cb(args_, res_, instance_);
});
},
}),
unpatchAllWrapper: new FunctionImplementation({
data: null,
depends: [],
supplies: "unpatchAll",
isWrapper: true,
func(thisObj: IBasePatcherApi) {
return __requireInternal(targetMod, "PatcherApi", "unpatchAll", true)!(thisObj.internalId);
},
}),
beforeWrapper: new FunctionImplementation({
data: null,
depends: [],
supplies: "before",
isWrapper: true,
func<T, A = unknown[]>(thisObj: IBasePatcherApi, target: T, name: string, cb: (args: A, instance: T) => A): () => void { // in replugged callback needs to return arguments. what happens in BD?
return __requireInternal(targetMod, "PatcherApi", "before", true)!(thisObj.internalId, target, name, (instance_: T, args_: A) => {
return cb(args_, instance_);
});
},
}),
insteadWrapper: new FunctionImplementation({
data: null,
depends: [],
supplies: "instead",
isWrapper: true,
func<T, A extends unknown[] = unknown[], R = unknown>(thisObj: IBasePatcherApi, target: T, name: string, cb: (args: A, orig: ((...args_: A) => R), instance: T) => A): () => void {
return __requireInternal(targetMod, "PatcherApi", "instead", true)!(thisObj.internalId, target, name, (instance_: T, args_: A, orig: ((...args__: A) => R)) => {
return cb(args_, orig, instance_);
});
},
}),
} as { [key: string]: FunctionImplementation };
export {
implementationStore,
};
11 changes: 0 additions & 11 deletions src/common/WebpackApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ const implementationStore = {
supplies: "getByStrings",
data: null,
func(...strings) {
/* __requireInternal(targetMod, "WebpackApi", "test")!(); */
const getModule = __requireInternal(targetMod, "WebpackApi", "getModule");
if (!getModule)
throw new Error("Unimplemented");
Expand All @@ -66,16 +65,6 @@ const implementationStore = {
});
},
}),
test: new FunctionImplementation({
data: null,
depends: ["getByStrings"],
supplies: "test",
// eslint-disable-next-line @typescript-eslint/no-unused-vars
func(...args: any[]) {
debugger;
return "the test worked";
},
}),
} as { [key: string]: FunctionImplementation };
export {
implementationStore,
Expand Down
11 changes: 9 additions & 2 deletions src/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,27 +7,30 @@ export interface IFunctionImplementation {
data: any,
func: (...args: any[]) => any,
isWrapper?: boolean,
asImmediatelyInvokedFunctionExpression?: boolean;
}
class FunctionImplementation implements IFunctionImplementation {
supplies: string;
depends: string[];
data: any;
func: (...args: any[]) => any;
isWrapper?: boolean | undefined;
asImmediatelyInvokedFunctionExpression?: boolean | undefined;
// constructor(supplies: string, depends: string[], data: any, func: (...args: any[]) => any) {
// this.supplies = supplies;
// this.depends = depends;
// this.data = data;
// this.func = func;
// }
constructor(options: IFunctionImplementation) {
const { supplies, depends, data, func, isWrapper } = options;
const { supplies, depends, data, func, isWrapper, asImmediatelyInvokedFunctionExpression } = options;
this.supplies = supplies!;
this.depends = depends!;
Object.defineProperty(this, "data", { value: data, enumerable: data !== null });
this.func = func!;
// this.isWrapper = isWrapper === true;
Object.defineProperty(this, "isWrapper", { value: isWrapper, enumerable: isWrapper === true });
Object.defineProperty(this, "asImmediatelyInvokedFunctionExpression", { value: asImmediatelyInvokedFunctionExpression, enumerable: asImmediatelyInvokedFunctionExpression === true });
}
}
export {
Expand Down Expand Up @@ -56,7 +59,11 @@ export async function initStores() {
}
export function doesImplement(mod: IModImplementation, category: string, method: string) {
const categoryObj = getKeyValue(mod, category as keyof IModImplementation);
return getKeyValue(categoryObj, method as never) != undefined;
const value = getKeyValue(categoryObj, method as never);
if (value === undefined || (value as ({ missing: boolean })).missing === true) {
return false;
}
return true;
}

export function __requireInternal(mod: IModImplementation, category: string, method: string, ignoreWrappers: boolean = false) {
Expand Down
61 changes: 50 additions & 11 deletions src/converter.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { ParseResult } from "@babel/parser";
import { File, Identifier, ImportDeclaration, ImportSpecifier, MemberExpression, Statement, callExpression, identifier, memberExpression, stringLiteral } from "@babel/types";
import { File, Identifier, ImportDeclaration, ImportSpecifier, MemberExpression, NewExpression, Standardized, Statement, callExpression, identifier, memberExpression, newExpression, stringLiteral } from "@babel/types";
import { NonFunctionType, getKeyValue, myPackageName } from "./utils.js";
import { IModImplementation } from "./api/ModImplementation";
import { addCode } from "./api/RuntimeGenerators.js";
import { IMPLEMENTATION_STORES_PATH_REQ, IMPLEMENTATION_STORES_PATH_SOURCE, IMPLEMENTATION_STORES_PATH_VAR_NAME } from "./constants.js";

function removeASTLocation(ast: Statement[] | Statement) {
function removeASTLocation(ast: Standardized[] | Standardized) {
if (Array.isArray(ast)) {
ast.forEach(a => removeASTLocation(a));
}
Expand Down Expand Up @@ -97,7 +97,7 @@ function deepFind<K>(obj: any, path: string): K | undefined {

export default async function (ast: ParseResult<File>, targetedDiscordModApiLibrary: { default: IModImplementation }): Promise<Statement[]> {
const parsedBody = ast.program.body;
const importStatements = parsedBody.filter(x => x.type == "ImportDeclaration") as Statement[];
const importStatements = parsedBody.filter(x => x.type == "ImportDeclaration") as ImportDeclaration[];
const importAliasMap = [] as { internalName: string, codeName: string }[];
removeASTLocation(importStatements);
const importsToRemove: number[] = [];
Expand All @@ -106,7 +106,6 @@ export default async function (ast: ParseResult<File>, targetedDiscordModApiLibr
// spec.local.name = "test_"; // alias
// // @ts-ignore
// spec.imported.name = "test"; // imported value
// debugger;
if (element.source.value == myPackageName) { // checking if it's the same module as we are
for (let index2 = 0; index2 < element.specifiers.length; index2++) {
const spec = element.specifiers[index2] as ImportSpecifier;
Expand All @@ -120,7 +119,7 @@ export default async function (ast: ParseResult<File>, targetedDiscordModApiLibr
}
const trueImportsToRemove =
importStatements.filter((_, index) => importsToRemove.includes(index));
const parsedBodyWithoutOurImports = parsedBody.filter((item, index) => !trueImportsToRemove.includes(parsedBody[index]));
const parsedBodyWithoutOurImports = parsedBody.filter((_, index) => !trueImportsToRemove.includes(parsedBody[index] as ImportDeclaration));
// parsedBodyWithoutOurImports.unshift(...await addCode(targetedDiscordModApiLibrary.default));
for (let index = 0; index < parsedBodyWithoutOurImports.length; index++) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
Expand All @@ -129,15 +128,55 @@ export default async function (ast: ParseResult<File>, targetedDiscordModApiLibr
* Example:
* WebpackApi.getModule(something) -> "WebpackApi" matches signature of an element from importsToBake array -> import WebpackApi ourselves -> find method getModule -> select replacement based on target client mod -> read target's object path and property name -> replace them
*/
debugger;
// console.log(findAllTypesWithPath(element, "MemberExpression"));
const paths = findPathsToType({ obj: element, targetType: "MemberExpression" });
for (let index2 = 0; index2 < paths.length; index2++) {
const element2 = paths[index2];
const trueObj = deepFind<MemberExpression>(element, element2);
const newExpressionPaths = findPathsToType({ obj: element, targetType: "NewExpression" });
for (const newExpressionPath of newExpressionPaths) {
const trueObj = deepFind<NewExpression>(element, newExpressionPath);
console.log(trueObj);
Comment thread
Davilarek marked this conversation as resolved.
if (trueObj != undefined && importAliasMap.find(x => x.codeName == (trueObj.callee as Identifier).name) !== undefined) {
removeASTLocation(trueObj);
const importedInternalName = importAliasMap.find(x => x.codeName == (trueObj.callee as Identifier).name)!.internalName;
const propDesc = Object.getOwnPropertyDescriptor(targetedDiscordModApiLibrary.default, importedInternalName as keyof IModImplementation);
if (!propDesc)
continue;
const result: IModImplementation[keyof IModImplementation] = propDesc.value ?? propDesc.get!();
if (result == undefined || typeof result === "boolean")
continue;
const { constructor } = result;
// @ts-expect-error This for sure is a constructor
const constructed = new constructor();
if (constructed.constructor_?.wrapperName) {
const originalObj = importedInternalName;
for (const prop of Object.getOwnPropertyNames(trueObj)) {
// @ts-expect-error well
delete trueObj[prop];
}
const newCallExpr = callExpression(memberExpression(identifier(IMPLEMENTATION_STORES_PATH_SOURCE), identifier(IMPLEMENTATION_STORES_PATH_REQ)), [
memberExpression(memberExpression(memberExpression(identifier(IMPLEMENTATION_STORES_PATH_SOURCE), identifier(IMPLEMENTATION_STORES_PATH_VAR_NAME)), identifier(originalObj)), identifier(constructed.constructor_.wrapperName)),
]);
Object.assign(trueObj, newCallExpr);
continue;
}
const newAst = newExpression(
memberExpression(
identifier(constructed.object),
identifier(constructed.property),
),
[],
);
for (const prop of Object.getOwnPropertyNames(trueObj)) {
// @ts-expect-error well
delete trueObj[prop];
}
Object.assign(trueObj, newAst);
}
}
const memberExpressionPaths = findPathsToType({ obj: element, targetType: "MemberExpression" });
for (const memberExpressionPath of memberExpressionPaths) {
const trueObj = deepFind<MemberExpression>(element, memberExpressionPath);
console.log(trueObj);
if (trueObj != undefined && importAliasMap.find(x => x.codeName == (trueObj.object as Identifier).name) !== undefined) {
removeASTLocation(trueObj as unknown as Statement);
removeASTLocation(trueObj);
const importedInternalName = importAliasMap.find(x => x.codeName == (trueObj.object as Identifier).name)!.internalName;
const propDesc = Object.getOwnPropertyDescriptor(targetedDiscordModApiLibrary.default, importedInternalName as keyof IModImplementation);
if (!propDesc)
Expand Down
23 changes: 22 additions & 1 deletion src/converters/betterdiscord.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,40 @@
import { Statement } from "@babel/types";
import { IModImplementation } from "../api/ModImplementation.js";
import { createFunctionFromObjectProperty } from "../api/RuntimeGenerators.js";
import { createFunctionFromObjectProperty, createFunctionWithWrapperNeeded } from "../api/RuntimeGenerators.js";
import { IBaseWebpackApi } from "../api/Webpack.js";
import { IBasePatcherApi } from "../api/Patcher.js";

class BDWebpackApi implements IBaseWebpackApi {
get getModule() {
return createFunctionFromObjectProperty("BdApi.Webpack", "getModule");
}
}

class BDPatcherApi implements IBasePatcherApi {
get constructor_() {
return createFunctionWithWrapperNeeded("undefined", "undefined", "Patcher_constructor") as any;
}
internalId!: string;
get unpatchAll() {
return createFunctionWithWrapperNeeded("BdApi.Patcher", "unpatchAll", "unpatchAllWrapper");
}
get after() {
return createFunctionWithWrapperNeeded("BdApi.Patcher", "after", "afterWrapper");
}
get before() {
return createFunctionWithWrapperNeeded("BdApi.Patcher", "before", "beforeWrapper");
}
get instead() {
return createFunctionWithWrapperNeeded("BdApi.Patcher", "instead", "insteadWrapper");
}
}

export function convertFormat(ast: Statement[]) {
return ast;
}

export default {
WebpackApi: new BDWebpackApi(),
PatcherApi: BDPatcherApi.prototype,
importsForbidden: true,
} as IModImplementation;
Loading