Add monkeypatch utility function

This commit is contained in:
Samuel Elliott 2018-03-04 20:21:18 +00:00
parent 2bf1709dba
commit 88b063ca8e
No known key found for this signature in database
GPG Key ID: 8420C7CDE43DC4D6
8 changed files with 260 additions and 12 deletions

View File

@ -14,6 +14,7 @@ import ExtModuleManager from './extmodulemanager';
import PluginManager from './pluginmanager';
import ThemeManager from './thememanager';
import Events from './events';
import { SettingsSet, SettingsCategory, Setting, SettingsScheme } from 'structs';
import { Modals, DOM } from 'ui';
import SettingsModal from '../ui/components/bd/modals/SettingsModal.vue';
@ -80,6 +81,9 @@ export default class PluginApi {
get Utils() {
return {
overload: () => Utils.overload.apply(Utils, arguments),
monkeyPatch: () => Utils.monkeyPatch.apply(Utils, arguments),
monkeyPatchOnce: () => Utils.monkeyPatchOnce.apply(Utils, arguments),
compatibleMonkeyPatch: () => Utils.monkeyPatchOnce.apply(Utils, arguments),
tryParseJson: () => Utils.tryParseJson.apply(Utils, arguments),
toCamelCase: () => Utils.toCamelCase.apply(Utils, arguments),
compare: () => Utils.compare.apply(Utils, arguments),
@ -88,6 +92,27 @@ export default class PluginApi {
};
}
createSettingsSet(args, ...merge) {
return new SettingsSet(args, ...merge);
}
createSettingsCategory(args, ...merge) {
return new SettingsCategory(args, ...merge);
}
createSetting(args, ...merge) {
return new Setting(args, ...merge);
}
createSettingsScheme(args) {
return new SettingsScheme(args);
}
get Settings() {
return {
createSet: this.createSet.bind(this),
createCategory: this.createSettingsCategory.bind(this),
createSetting: this.createSetting.bind(this),
createScheme: this.createSettingsScheme.bind(this)
};
}
getInternalSetting(set, category, setting) {
return Settings.get(set, category, setting);
}

View File

@ -85,7 +85,7 @@ export default class SettingsCategory {
/**
* Returns the first setting where calling {function} returns true.
* @param {Function} function A function to call to filter setting
* @param {Function} function A function to call to filter settings
* @return {Setting}
*/
find(f) {
@ -112,7 +112,7 @@ export default class SettingsCategory {
/**
* Merges a category into this category without emitting events (and therefore synchronously).
* This only exists for use by SettingsSet.
* This only exists for use by the constructor and SettingsSet.
*/
_merge(newCategory) {
let updatedSettings = [];
@ -151,7 +151,7 @@ export default class SettingsCategory {
continue;
}
const updatedSetting = await setting._merge(newSetting, false);
const updatedSetting = await setting.merge(newSetting, false);
if (!updatedSetting) continue;
updatedSettings = updatedSettings.concat(updatedSetting.map(({ setting, value, old_value }) => ({
category: this, category_id: this.id,

View File

@ -191,7 +191,7 @@ export default class SettingsSet {
* Returns the value of the setting with the ID {id}.
* @param {String} categoryid The ID of the category to look in (optional)
* @param {String} id The ID of the setting to look for
* @return {SettingsCategory}
* @return {Any}
*/
get(cid, sid) {
const setting = this.getSetting(cid, sid);

View File

@ -103,7 +103,7 @@ export default class Setting {
/**
* Merges a setting into this setting without emitting events (and therefore synchronously).
* This only exists for use by SettingsCategory.
* This only exists for use by the constructor and SettingsCategory.
*/
_merge(newSetting) {
const value = newSetting.args ? newSetting.args.value : newSetting.value;

View File

@ -21,12 +21,11 @@ export default class {
const defer = setInterval(() => {
if (!this.profilePopupModule) return;
clearInterval(defer);
this.profilePopupModule.open = Utils.overload(this.profilePopupModule.open, userid => {
Events.emit('ui-event', {
event: 'profile-popup-open',
data: { userid }
});
});
Utils.monkeyPatch(this.profilePopupModule, 'open', 'after', (data, userid) => Events.emit('ui-event', {
event: 'profile-popup-open',
data: { userid }
}));
}, 100);
}

View File

@ -40,7 +40,7 @@ export default class extends EventListener {
setTimeout(() => {
let hasBadges = false;
let root = document.querySelector('[class*=profileBadges]');
let root = document.querySelector('[class*="profileBadges"]');
if (root) {
hasBadges = true;
} else {

View File

@ -0,0 +1,162 @@
/**
* BetterDiscord Monkeypatch
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import { ClientLogger as Logger } from './logger';
export class PatchedFunction {
constructor(object, methodName, replaceOriginal = true) {
if (object[methodName].__monkeyPatch)
return object[methodName].__monkeyPatch;
this.object = object;
this.methodName = methodName;
this.patches = [];
this.originalMethod = object[methodName];
this.replaced = false;
const patchedFunction = this;
this.replace = function(...args) {
patchedFunction.call(this, arguments);
};
this.replace.__monkeyPatch = this;
if (replaceOriginal)
this.replaceOriginal();
}
addPatch(patch) {
if (!this.patches.includes(patch))
this.patches.push(patch);
}
removePatch(patch, restoreOriginal = true) {
let i = 0;
while (this.patches[i]) {
if (this.patches[i] !== patch) i++;
else this.patches.splice(i, 1);
}
if (!this.patches.length && restoreOriginal)
this.restoreOriginal();
}
replaceOriginal() {
if (this.replaced) return;
this.object[this.methodName] = Object.assign(this.replace, this.object[this.methodName]);
this.replaced = true;
}
restoreOriginal() {
if (!this.replaced) return;
this.object[this.methodName] = Object.assign(this.object[this.methodName], this.replace);
this.replaced = false;
}
call(_this, args) {
const data = {
this: _this,
arguments: args,
return: undefined,
originalMethod: this.originalMethod,
callOriginalMethod: () => {
Logger.log('MonkeyPatch', [`Calling original method`, this, data]);
data.return = this.originalMethod.apply(data.this, data.arguments);
}
};
// Work through the patches calling each patch's hooks as if each patch had overridden the previous patch
for (let patch of this.patches) {
let callOriginalMethod = data.callOriginalMethod;
data.callOriginalMethod = () => {
const patch_data = Object.assign({}, data, {
callOriginalMethod, patch
});
patch.call(patch_data);
data.arguments = patch_data.arguments;
data.return = patch_data.return;
};
}
data.callOriginalMethod();
return data.return;
}
}
export class Patch {
constructor(patchedFunction, options, f) {
this.patchedFunction = patchedFunction;
if (options instanceof Function) {
f = options;
options = {
instead: data => {
f.call(this, data, ...data.arguments);
}
};
} else if (options === 'before') {
options = {
before: data => {
f.call(this, data, ...data.arguments);
}
};
} else if (options === 'after') {
options = {
after: data => {
f.call(this, data, ...data.arguments);
}
};
}
this.before = options.before || undefined;
this.instead = options.instead || undefined;
this.after = options.after || undefined;
this.once = options.once || false;
this.silent = options.silent || false;
this.suppressErrors = typeof options.suppressErrors === 'boolean' ? options.suppressErrors : true;
}
call(data) {
if (this.once)
this.cancel();
this.callBefore(data);
this.callInstead(data);
this.callAfter(data);
}
callBefore(data) {
if (this.before)
this.callHook('before', this.before, data);
}
callInstead(data) {
if (this.instead)
this.callHook('instead', this.instead, data);
else data.callOriginalMethod();
}
callAfter(data) {
if (this.after)
this.callHook('after', this.after, data);
}
callHook(hook, f, data) {
try {
f.call(this, data, ...data.arguments);
} catch (err) {
Logger.log('MonkeyPatch', [`Error thrown in ${hook} hook of`, this, '- :', err]);
if (!this.suppressErrors) throw err;
}
}
cancel() {
this.patchedFunction.removePatch(this);
}
}

View File

@ -13,6 +13,7 @@ const
fs = require('fs'),
_ = require('lodash');
import { PatchedFunction, Patch } from './monkeypatch';
import { Vendor } from 'modules';
import filetype from 'file-type';
@ -25,6 +26,67 @@ export class Utils {
}
}
/**
* Monkey-patches an object's method.
*/
static monkeyPatch(object, methodName, options, f) {
const patchedFunction = new PatchedFunction(object, methodName);
const patch = new Patch(patchedFunction, options, f);
patchedFunction.addPatch(patch);
return patch;
}
/**
* Monkey-patches an object's method and returns a promise that will be resolved with the data object when the method is called.
* You will have to call data.callOriginalMethod() if it wants the original method to be called.
*/
static monkeyPatchOnce(object, methodName) {
return new Promise((resolve, reject) => {
this.monkeyPatch(object, methodName, data => {
data.patch.cancel();
resolve(data);
});
});
}
/**
* Monkeypatch function that is compatible with samogot's Lib Discord Internals.
* Don't use this for writing new plugins as it will eventually be removed!
*/
static compatibleMonkeyPatch(what, methodName, options) {
const { before, instead, after, once = false, silent = false } = options;
const cancelPatch = () => patch.cancel();
const compatible_function = _function => data => {
const compatible_data = {
thisObject: data.this,
methodArguments: data.arguments,
returnValue: data.return,
cancelPatch,
originalMethod: data.originalMethod,
callOriginalMethod: () => data.callOriginalMethod()
};
try {
_function(compatible_data);
data.arguments = compatible_data.methodArguments;
data.return = compatible_data.returnValue;
} catch (err) {
data.arguments = compatible_data.methodArguments;
data.return = compatible_data.returnValue;
throw err;
}
};
const patch = this.monkeyPatch(what, methodName, {
before: before ? compatible_function(before) : undefined,
instead: instead ? compatible_function(instead) : undefined,
after: after ? compatible_function(after) : undefined,
once
});
return cancelPatch;
}
static async tryParseJson(jsonString) {
try {
return JSON.parse(jsonString);