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
18 changes: 18 additions & 0 deletions src/api/Patcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export interface IBasePatcherApi {
internalId: string | undefined; /* NOT ON REPLUGGED */
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.");
}
}

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
48 changes: 48 additions & 0 deletions src/common/PatcherApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
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);
},
};
},
}),
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);
},
}),
} as { [key: string]: FunctionImplementation };
export {
implementationStore,
};
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
53 changes: 49 additions & 4 deletions src/converter.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* 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, 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";
Expand Down Expand Up @@ -131,9 +131,54 @@ export default async function (ast: ParseResult<File>, targetedDiscordModApiLibr
*/
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 newExpressionPaths = findPathsToType({ obj: element, targetType: "NewExpression" });
for (let index2 = 0; index2 < newExpressionPaths.length; index2++) {
Davilarek marked this conversation as resolved.
Show resolved Hide resolved
const element2 = newExpressionPaths[index2];
Davilarek marked this conversation as resolved.
Show resolved Hide resolved
const trueObj = deepFind<NewExpression>(element, element2);
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 as unknown as Statement);
Davilarek marked this conversation as resolved.
Show resolved Hide resolved
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;
debugger;
Davilarek marked this conversation as resolved.
Show resolved Hide resolved
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);
continue;
Davilarek marked this conversation as resolved.
Show resolved Hide resolved
}
}
const memberExpressionPaths = findPathsToType({ obj: element, targetType: "MemberExpression" });
for (let index2 = 0; index2 < memberExpressionPaths.length; index2++) {
Davilarek marked this conversation as resolved.
Show resolved Hide resolved
const element2 = memberExpressionPaths[index2];
const trueObj = deepFind<MemberExpression>(element, element2);
console.log(trueObj);
if (trueObj != undefined && importAliasMap.find(x => x.codeName == (trueObj.object as Identifier).name) !== undefined) {
Expand Down
20 changes: 19 additions & 1 deletion src/converters/betterdiscord.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,37 @@
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("undefined", "undefined", "after");
Davilarek marked this conversation as resolved.
Show resolved Hide resolved
// return createFunctionWithWrapperNeeded("undefined", "undefined", "after");
// return createFunctionThatIsMissing();
return createFunctionWithWrapperNeeded("BdApi.Patcher", "after", "afterWrapper");
}
}

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

export default {
WebpackApi: new BDWebpackApi(),
PatcherApi: BDPatcherApi.prototype,
importsForbidden: true,
} as IModImplementation;
20 changes: 19 additions & 1 deletion src/converters/replugged.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,32 @@
import { ClassDeclaration, ClassMethod, ExportDefaultDeclaration, Identifier, Statement } from "@babel/types";
import { IModImplementation } from "../api/ModImplementation.js";
import { createFunctionWithWrapperNeeded } from "../api/RuntimeGenerators.js";
import { createFunctionFromObjectProperty, createFunctionWithWrapperNeeded } from "../api/RuntimeGenerators.js";
import { IBaseWebpackApi } from "../api/Webpack.js";
import { parse } from "@babel/parser";
import { IBasePatcherApi } from "../api/Patcher.js";

class RPWebpackApi implements IBaseWebpackApi {
get getModule() {
return createFunctionWithWrapperNeeded("replugged.webpack", "getModule", "getModuleRawToExportedWrapper");
}
}

class RPPatcherApi implements IBasePatcherApi {
get constructor_() {
return createFunctionFromObjectProperty("replugged", "Injector") as any;
}
constructor() {
return this.constructor_;
}
internalId: undefined;
Davilarek marked this conversation as resolved.
Show resolved Hide resolved
get unpatchAll() {
return createFunctionFromObjectProperty("replugged.Injector.constructor", "unpatchAllWrapper");
}
get after() {
return createFunctionFromObjectProperty("replugged.Injector.constructor", "afterWrapper");
}
}

export function convertFormat(ast: Statement[]) {
let targetClassName = undefined;
for (const astNode of ast) {
Expand Down Expand Up @@ -42,4 +59,5 @@ export function convertFormat(ast: Statement[]) {

export default {
WebpackApi: new RPWebpackApi(),
PatcherApi: RPPatcherApi.prototype,
} as IModImplementation;
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './api/Webpack.js';
export * from './api/Webpack.js';
export * from './api/Patcher.js';
Loading