Rewrite a bit and provide patch wrapper. add patcher test plugin as example

This commit is contained in:
Jiiks 2018-03-17 12:48:08 -03:00
parent 86528a3335
commit b5fc88bc8e
9 changed files with 213 additions and 239 deletions

View File

@ -10,7 +10,7 @@
import { DOM, BdUI, Modals, Reflection } from 'ui';
import BdCss from './styles/index.scss';
import { Patcher, Vendor, Events, CssEditor, Globals, ExtModuleManager, PluginManager, ThemeManager, ModuleManager, WebpackModules, Settings, Database, ReactComponents, ReactAutoPatcher, DiscordApi } from 'modules';
import { Patcher, MonkeyPatch, Vendor, Events, CssEditor, Globals, ExtModuleManager, PluginManager, ThemeManager, ModuleManager, WebpackModules, Settings, Database, ReactComponents, ReactAutoPatcher, DiscordApi } from 'modules';
import { ClientLogger as Logger, ClientIPC, Utils } from 'common';
import { EmoteModule } from 'builtin';
const ignoreExternal = false;
@ -25,6 +25,7 @@ class BetterDiscord {
Modals,
Reflection,
Patcher,
MonkeyPatch,
Vendor,
Events,
CssEditor,

View File

@ -15,5 +15,5 @@ export { default as Permissions } from './permissionmanager';
export { default as Database } from './database';
export { default as EventsWrapper } from './eventswrapper';
export { default as DiscordApi } from './discordapi';
export { default as Patcher } from './patcher';
export * from './patcher';
export * from './reactcomponents';

View File

@ -9,31 +9,63 @@
*/
import { WebpackModules } from './webpackmodules';
import { ClientLogger as Logger } from 'common';
import { ClientLogger as Logger, Utils } from 'common';
export default class Patcher {
export class Patcher {
static get patches() { return this._patches || (this._patches = {}) }
static getPatchesByCaller(id) {
const patches = [];
for (const patch in this.patches) {
if (this.patches.hasOwnProperty(patch)) {
if (this.patches[patch].caller === id) patches.push(this.patches[patch]);
}
}
return patches;
}
static unpatchAll(patches) {
for (const patch of patches) {
for (const child of patch.children) {
child.unpatch();
}
}
}
static resolveModule(module) {
if (module instanceof Function || (module instanceof Object && !(module instanceof Array))) return module;
if ('string' === typeof module) return WebpackModules.getModuleByName(module);
if (module instanceof Array) return WebpackModules.getModuleByProps(module);
return null;
}
static overrideFn(patch) {
return function () {
for (const superPatch of patch.supers) {
let retVal = null;
if (!patch.children) return patch.originalFunction.apply(this, arguments);
for (const superPatch of patch.children.filter(c => c.type === 'before')) {
try {
superPatch.callback.apply(this, arguments);
superPatch.callback(this, arguments);
} catch (err) {
Logger.err('Patcher', err);
Logger.err(`Patcher:${patch.id}`, err);
}
}
const retVal = patch.originalFunction.apply(this, arguments);
for (const slavePatch of patch.slaves) {
const insteads = patch.children.filter(c => c.type === 'instead');
if (!insteads.length) {
retVal = patch.originalFunction.apply(this, arguments);
} else {
for (const insteadPatch of insteads) {
try {
retVal = insteadPatch.callback(this, arguments);
} catch (err) {
Logger.err(`Patcher:${patch.id}`, err);
}
}
}
for (const slavePatch of patch.children.filter(c => c.type === 'after')) {
try {
slavePatch.callback.apply(this, [arguments, { patch, retVal }]);
slavePatch.callback(this, arguments, retVal);
} catch (err) {
Logger.err('Patcher', err);
Logger.err(`Patcher:${patch.id}`, err);
}
}
return retVal;
@ -44,61 +76,56 @@ export default class Patcher {
patch.proxyFunction = patch.module[patch.functionName] = this.overrideFn(patch);
}
static pushPatch(id, module, functionName) {
static pushPatch(caller, id, module, functionName) {
const patch = {
caller,
id,
module,
functionName,
originalFunction: module[functionName],
proxyFunction: null,
revert: () => {
revert: () => { // Calling revert will destroy any patches added to the same module after this
patch.module[patch.functionName] = patch.originalFunction;
patch.proxyFunction = null;
patch.slaves = patch.supers = [];
},
supers: [],
slaves: []
counter: 0,
children: []
};
patch.proxyFunction = module[functionName] = this.overrideFn(patch);
return this.patches[id] = patch;
}
static get before() { return this.superpatch; }
static superpatch(unresolveModule, functionName, callback, displayName) {
const module = this.resolveModule(unresolveModule);
static before() { return this.pushChildPatch(...arguments, 'before') }
static after() { return this.pushChildPatch(...arguments, 'after') }
static instead() { return this.pushChildPatch(...arguments, 'instead') }
static pushChildPatch(caller, unresolvedModule, functionName, callback, displayName, type = 'after') {
const module = this.resolveModule(unresolvedModule);
if (!module || !module[functionName] || !(module[functionName] instanceof Function)) return null;
displayName = 'string' === typeof unresolveModule ? unresolveModule : displayName || module.displayName || module.name || module.constructor.displayName || module.constructor.name;
const patchId = `${displayName}:${functionName}`;
displayName = 'string' === typeof unresolvedModule ? unresolvedModule : displayName || module.displayName || module.name || module.constructor.displayName || module.constructor.name;
const patchId = `${displayName}:${functionName}:${caller}`;
const patch = this.patches[patchId] || this.pushPatch(patchId, module, functionName);
const patch = this.patches[patchId] || this.pushPatch(caller, patchId, module, functionName);
if (!patch.proxyFunction) this.rePatch(patch);
const id = patch.supers.length + 1;
const superPatch = {
id,
const child = {
caller,
type,
id: patch.counter,
callback,
unpactch: () => patch.slaves.splice(patch.slaves.findIndex(slave => slave.id === id), 1) // This doesn't actually work correctly not, fix in a moment
unpatch: () => {
patch.children.splice(patch.children.findIndex(cpatch => cpatch.id === child.id && cpatch.type === type), 1);
if (patch.children.length <= 0) delete this.patches[patchId];
}
};
patch.supers.push(superPatch);
return superPatch;
patch.children.push(child);
patch.counter++;
return child.unpatch;
}
static get after() { return this.slavepatch; }
static slavepatch(unresolveModule, functionName, callback, displayName) {
const module = this.resolveModule(unresolveModule);
if (!module || !module[functionName] || !(module[functionName] instanceof Function)) return null;
displayName = 'string' === typeof unresolveModule ? unresolveModule : displayName || module.displayName || module.name || module.constructor.displayName || module.constructor.name;
const patchId = `${displayName}:${functionName}`;
const patch = this.patches[patchId] || this.pushPatch(patchId, module, functionName);
if (!patch.proxyFunction) this.rePatch(patch);
const id = patch.slaves.length + 1;
const slavePatch = {
id,
callback,
unpactch: () => patch.slaves.splice(patch.slaves.findIndex(slave => slave.id === id), 1) // This doesn't actually work correctly not, fix in a moment
};
patch.slaves.push(slavePatch);
return slavePatch;
}
}
export const MonkeyPatch = (caller, module, displayName) => ({
before: (functionName, callBack) => Patcher.before(caller, module, functionName, callBack, displayName),
after: (functionName, callBack) => Patcher.after(caller, module, functionName, callBack, displayName),
instead: (functionName, callBack) => Patcher.instead(caller, module, functionName, callBack, displayName)
});

View File

@ -20,6 +20,7 @@ import { SettingsSet, SettingsCategory, Setting, SettingsScheme } from 'structs'
import { BdMenuItems, Modals, DOM, Reflection } from 'ui';
import DiscordApi from './discordapi';
import { ReactComponents } from './reactcomponents';
import { MonkeyPatch } from './patcher';
export default class PluginApi {
@ -39,6 +40,9 @@ export default class PluginApi {
get Reflection() {
return Reflection;
}
get MonkeyPatch() {
return module => MonkeyPatch(this.pluginInfo.id, module);
}
get plugin() {
return PluginManager.getPluginById(this.pluginInfo.id || this.pluginInfo.name.toLowerCase().replace(/[^a-zA-Z0-9-]/g, '-').replace(/--/g, '-'));
}

View File

@ -9,7 +9,7 @@
* LICENSE file in the root directory of this source tree.
*/
import Patcher from './patcher';
import { MonkeyPatch, Patcher } from './patcher';
import { WebpackModules, Filters } from './webpackmodules';
import DiscordApi from './discordapi';
import { EmoteModule } from 'builtin';
@ -42,7 +42,7 @@ class Helpers {
return this.recursiveArray(parent, key, count);
}
static get recursiveChildren() {
return function*(parent, key, index = 0, count = 1) {
return function* (parent, key, index = 0, count = 1) {
const item = parent[key];
yield { item, parent, key, index, count };
if (item && item.props && item.props.children) {
@ -132,6 +132,18 @@ class Helpers {
}
return null;
}
static findProp(obj, what) {
if (obj.hasOwnProperty(what)) return obj[what];
if (obj.props && !obj.children) return this.findProp(obj.props, what);
if (!obj.children) return null;
if (!(obj.children instanceof Array)) return this.findProp(obj.children, what);
for (const child of obj.children) {
if (!child) continue;
const findInChild = this.findProp(child, what);
if (findInChild) return findInChild;
}
return null;
}
static get ReactDOM() {
return WebpackModules.getModuleByName('ReactDOM');
}
@ -142,206 +154,23 @@ class ReactComponent {
this._id = id;
this._component = component;
this._retVal = retVal;
const self = this;
Patcher.slavepatch(this.component.prototype, 'componentWillMount', function(args, parv) {
self.eventCallback('componentWillMount', {
component: this,
retVal: parv.retVal
});
});
Patcher.slavepatch(this.component.prototype, 'render', function (args, parv) {
self.eventCallback('render', {
component: this,
retVal: parv.retVal
});
});
Patcher.slavepatch(this.component.prototype, 'componentDidMount', function (args, parv) {
self.eventCallback('componentDidMount', {
component: this,
props: this.props,
state: this.state,
element: Helpers.ReactDOM.findDOMNode(this),
retVal: parv.retVal
});
});
Patcher.slavepatch(this.component.prototype, 'componentWillReceiveProps', function (args, parv) {
const [nextProps] = args;
self.eventCallback('componentWillReceiveProps', {
component: this,
nextProps,
retVal: parv.retVal
});
});
Patcher.slavepatch(this.component.prototype, 'shouldComponentUpdate', function (args, parv) {
const [nextProps, nextState] = args;
self.eventCallback('shouldComponentUpdate', {
component: this,
nextProps,
nextState,
retVal: parv.retVal
});
});
Patcher.slavepatch(this.component.prototype, 'componentWillUpdate', function (args, parv) {
const [nextProps, nextState] = args;
self.eventCallback('componentWillUpdate', {
component: this,
nextProps,
nextState,
retVal: parv.retVal
});
});
Patcher.slavepatch(this.component.prototype, 'componentDidUpdate', function(args, parv) {
const [prevProps, prevState] = args;
self.eventCallback('componentDidUpdate', {
component: this,
prevProps,
prevState,
props: this.props,
state: this.state,
element: Helpers.ReactDOM.findDOMNode(this),
retVal: parv.retVal
});
});
Patcher.slavepatch(this.component.prototype, 'componentWillUnmount', function (args, parv) {
self.eventCallback('componentWillUnmount', {
component: this,
retVal: parv.retVal
});
});
Patcher.slavepatch(this.component.prototype, 'componentDidCatch', function (args, parv) {
const [error, info] = args;
self.eventCallback('componentDidCatch', {
component: this,
error,
info,
retVal: parv.retVal
});
});
}
eventCallback(event, eventData) {
for (const listener of this.events.find(e => e.id === event).listeners) {
listener(eventData);
}
}
get events() {
return this._events || (this._events = [
{ id: 'componentWillMount', listeners: [] },
{ id: 'render', listeners: [] },
{ id: 'componentDidMount', listeners: [] },
{ id: 'componentWillReceiveProps', listeners: [] },
{ id: 'shouldComponentUpdate', listeners: [] },
{ id: 'componentWillUpdate', listeners: [] },
{ id: 'componentDidUpdate', listeners: [] },
{ id: 'componentWillUnmount', listeners: [] },
{ id: 'componentDidCatch', listeners: [] }
]);
}
on(event, callback) {
const have = this.events.find(e => e.id === event);
if (!have) return;
have.listeners.push(callback);
}
get id() {
return this._id;
}
get component() {
return this._component;
}
get retVal() {
return this._retVal;
}
forceUpdateOthers() {
}
}
export class ReactAutoPatcher {
static async autoPatch() {
await this.ensureReact();
Patcher.superpatch('React', 'createElement', (component, retVal) => ReactComponents.push(component, retVal));
this.patchem();
return 1;
}
static async ensureReact() {
while (!window.webpackJsonp || !WebpackModules.getModuleByName('React')) await new Promise(resolve => setTimeout(resolve, 10));
return 1;
}
static patchem() {
this.patchMessage();
this.patchMessageGroup();
this.patchChannelMember();
}
static async patchMessage() {
this.Message.component = await ReactComponents.getComponent('Message', true, { selector: '.message' });
this.Message.component.on('render', ({ component, retVal, p }) => {
const { message } = component.props;
const { id, colorString, bot, author, attachments, embeds } = message;
retVal.props['data-message-id'] = id;
retVal.props['data-colourstring'] = colorString;
if (author && author.id) retVal.props['data-user-id'] = author.id;
if (bot || (author && author.bot)) retVal.props.className += ' bd-isBot';
if (attachments && attachments.length) retVal.props.className += ' bd-hasAttachments';
if (embeds && embeds.length) retVal.props.className += ' bd-hasEmbeds';
if (author && author.id === DiscordApi.currentUser.id) retVal.props.className += ' bd-isCurrentUser';
try {
const markup = Helpers.findByProp(retVal, 'className', 'markup').children; // First child has all the actual text content, second is the edited timestamp
markup[0] = EmoteModule.processMarkup(markup[0]);
} catch (err) {
console.error('MARKUP PARSER ERROR', err);
}
});
}
static async patchMessageGroup() {
ReactComponents.setName('MessageGroup', this.MessageGroup.filter);
this.MessageGroup.component = await ReactComponents.getComponent('MessageGroup', true, { selector: '.message-group' });
this.MessageGroup.component.on('render', ({ component, retVal, p }) => {
const authorid = component.props.messages[0].author.id;
retVal.props['data-author-id'] = authorid;
if (authorid === DiscordApi.currentUser.id) retVal.props.className += ' bd-isCurrentUser';
});
}
static async patchChannelMember() {
this.ChannelMember.component = await ReactComponents.getComponent('ChannelMember');
this.ChannelMember.component.on('render', ({ component, retVal, p }) => {
const { user, isOwner } = component.props;
retVal.props.children.props['data-member-id'] = user.id;
if (user.id === DiscordApi.currentUser.id) retVal.props.children.props.className += ' bd-isCurrentUser';
if (isOwner) retVal.props.children.props.className += ' bd-isOwner';
});
}
static get MessageGroup() {
return this._messageGroup || (
this._messageGroup = {
filter: Filters.byCode(/"message-group"[\s\S]*"has-divider"[\s\S]*"hide-overflow"[\s\S]*"is-local-bot-message"/, c => c.prototype && c.prototype.render)
});
}
static get Message() {
return this._message || (this._message = {});
}
static get ChannelMember() {
return this._channelMember || (
this._channelMember = {});
}
}
export class ReactComponents {
static get components() { return this._components || (this._components = []) }
static get unknownComponents() { return this._unknownComponents || (this._unknownComponents = [])}
static get unknownComponents() { return this._unknownComponents || (this._unknownComponents = []) }
static get listeners() { return this._listeners || (this._listeners = []) }
static get nameSetters() { return this._nameSetters || (this._nameSetters =[])}
static get nameSetters() { return this._nameSetters || (this._nameSetters = []) }
static push(component, retVal) {
if (!(component instanceof Function)) return null;
@ -362,7 +191,7 @@ export class ReactComponents {
return c;
}
static async getComponent(name, important, importantArgs) {
static async getComponent(name, important) {
const have = this.components.find(c => c.id === name);
if (have) return have;
if (important) {
@ -372,11 +201,11 @@ export class ReactComponents {
clearInterval(importantInterval);
return;
}
const select = document.querySelector(importantArgs.selector);
const select = document.querySelector(important.selector);
if (!select) return;
const reflect = Reflection(select);
if (!reflect.component) {
clearInterval(important);
clearInterval(importantInterval);
console.error(`FAILED TO GET IMPORTANT COMPONENT ${name} WITH REFLECTION FROM`, select);
return;
}
@ -413,7 +242,8 @@ export class ReactComponents {
static processUnknown(component, retVal) {
const have = this.unknownComponents.find(c => c.component === component);
for (const [fi, filter] of this.nameSetters.entries()) {
if (filter.filter(component)) {
if (filter.filter.filter(component)) {
console.log('filter match!');
component.displayName = filter.name;
this.nameSetters.splice(fi, 1);
return this.push(component, retVal);
@ -424,3 +254,61 @@ export class ReactComponents {
return component;
}
}
export class ReactAutoPatcher {
static async autoPatch() {
await this.ensureReact();
this.React = {};
this.React.unpatchCreateElement = MonkeyPatch('BD:ReactComponents:createElement', 'React').before('createElement', (component, args) => {
ReactComponents.push(args[0]);
});
this.patchComponents();
return 1;
}
static async ensureReact() {
while (!window.webpackJsonp || !WebpackModules.getModuleByName('React')) await new Promise(resolve => setTimeout(resolve, 10));
return 1;
}
static async patchComponents() {
this.patchMessage();
this.patchMessageGroup();
this.patchChannelMember();
}
static async patchMessage() {
this.Message = await ReactComponents.getComponent('Message', { selector: '.message' });
this.unpatchMessageRender = MonkeyPatch('BD:ReactComponents', this.Message.component.prototype).after('render', (component, args, retVal) => {
const { message } = component.props;
const { id, colorString, bot, author, attachments, embeds } = message;
retVal.props['data-message-id'] = id;
retVal.props['data-colourstring'] = colorString;
if (author && author.id) retVal.props['data-user-id'] = author.id;
if (bot || (author && author.bot)) retVal.props.className += ' bd-isBot';
if (attachments && attachments.length) retVal.props.className += ' bd-hasAttachments';
if (embeds && embeds.length) retVal.props.className += ' bd-hasEmbeds';
if (author && author.id === DiscordApi.currentUser.id) retVal.props.className += ' bd-isCurrentUser';
});
}
static async patchMessageGroup() {
this.MessageGroup = await ReactComponents.getComponent('MessageGroup', { selector: '.message-group' });
this.unpatchMessageGroupRender = MonkeyPatch('BD:ReactComponents', this.MessageGroup.component.prototype).after('render', (component, args, retVal) => {
const { author, type } = component.props.messages[0];
retVal.props['data-author-id'] = author.id;
if (author.id === DiscordApi.currentUser.id) retVal.props.className += ' bd-isCurrentUser';
if (type !== 0) retVal.props.className += ' bd-isSystemMessage';
});
}
static async patchChannelMember() {
this.ChannelMember = await ReactComponents.getComponent('ChannelMember', { selector: '.member.member-status' });
this.unpatchChannelMemberRender = MonkeyPatch('BD:ReactComponents', this.ChannelMember.component.prototype).after('render', (component, args, retVal) => {
if (!retVal.props || !retVal.props.children || !retVal.props.children.length) return;
const user = Helpers.findProp(component, 'user');
if (!user) return;
retVal.props['data-user-id'] = user.id;
});
}
}

View File

@ -89,7 +89,20 @@ class Reflection {
}
}
static getComponent(node) {
static getComponent(node, first = true) {
try {
return this.reactInternalInstance(node).return.type;
} catch (err) {
return null;
}
if (!node) return null;
if (first) node = this.reactInternalInstance(node);
if (node.hasOwnProperty('return')) {
if (node.return.hasOwnProperty('return') && !node.return.type) return node.type;
return this.getComponent(node.return, false);
}
if (node.hasOwnProperty('type')) return node.type;
return null;
// IMPORTANT TODO Currently only checks the first found component. For example channel-member will not return the correct component
try {
return this.reactInternalInstance(node).return.type;

View File

@ -18,6 +18,9 @@ import { Vendor } from 'modules';
import filetype from 'file-type';
export class Utils {
static isArrowFunction(fn) {
return !fn.toString().startsWith('function');
}
static overload(fn, cb) {
const orig = fn;
return function (...args) {

View File

@ -0,0 +1,12 @@
{
"info": {
"id": "patcher-test",
"name": "Patcher Test",
"authors": [ "Jiiks" ],
"version": 1.0,
"description": "Patcher Test Description"
},
"main": "index.js",
"type": "plugin",
"defaultConfig": []
}

View File

@ -0,0 +1,26 @@
module.exports = (Plugin, Api, Vendor) => {
const { ReactComponents } = Api;
return class extends Plugin {
test() {
}
onStart() {
this.patchMessage();
return true;
}
async patchMessage() {
const Message = await ReactComponents.getComponent('Message');
this.unpatchTest = Api.MonkeyPatch(Message.component.prototype).after('render', () => {
console.log('MESSAGE RENDER!');
});
}
onStop() {
this.unpatchTest(); // The automatic unpatcher is not there yet
return true;
}
}
}