Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Patcher API #11

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
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 {
Davilarek marked this conversation as resolved.
Show resolved Hide 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);
Davilarek marked this conversation as resolved.
Show resolved Hide 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
Loading