This commit is contained in:
Samuel Elliott 2019-03-27 14:20:24 +00:00 committed by GitHub
commit b02800c34d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 698 additions and 404 deletions

View File

@ -21,6 +21,7 @@ import { SettingsSet, ErrorEvent } from 'structs';
import { Modals } from 'ui';
import Combokeys from 'combokeys';
import Settings from './settings';
import semver from 'semver';
/**
* Base class for managing external content
@ -148,7 +149,7 @@ export default class {
* @param {bool} suppressErrors Suppress any errors that occur during loading of content
*/
static async refreshContent(suppressErrors = false) {
if (!this.localContent.length) return this.loadAllContent();
if (!this.localContent.length) return this.loadAllContent(suppressErrors);
try {
await FileUtils.ensureDirectory(this.contentPath);
@ -251,7 +252,7 @@ export default class {
throw 'Blocked unsafe content';
}
const contentPath = packed ? dirName.contentPath : path.join(this.contentPath, dirName);
const contentPath = packed ? dirName.contentPath : await FileUtils.realpath(path.join(this.contentPath, dirName));
await FileUtils.directoryExists(contentPath);
@ -262,6 +263,8 @@ export default class {
const readConfig = packed ? dirName.config : await FileUtils.readJsonFromFile(configPath);
const mainPath = path.join(contentPath, readConfig.main || 'index.js');
readConfig.info.version = semver.coerce(`${readConfig.info.version || .1}`).version;
const defaultConfig = new SettingsSet({
settings: readConfig.defaultConfig,
schemes: readConfig.configSchemes
@ -289,16 +292,6 @@ export default class {
userConfig.config = defaultConfig.clone({ settings: userConfig.config });
userConfig.config.setSaved();
for (const setting of userConfig.config.findSettings(() => true)) {
// This will load custom settings
// Setting the content's path on only the live config (and not the default config) ensures that custom settings will not be loaded on the default settings
setting.setContentPath(contentPath);
}
for (const scheme of userConfig.config.schemes) {
scheme.setContentPath(contentPath);
}
Utils.deepfreeze(defaultConfig, object => object instanceof Combokeys);
const configs = {
@ -313,11 +306,21 @@ export default class {
mainPath
};
const content = await this.loadContent(paths, configs, readConfig.info, readConfig.main, readConfig.dependencies, readConfig.permissions, readConfig.mainExport, packed ? dirName : false);
const content = await this.loadContent(paths, configs, readConfig.info, readConfig.main, readConfig.alternateVersions, readConfig.dependencies, readConfig.permissions, readConfig.mainExport, packed ? dirName : false);
if (!content) return undefined;
if (!reload && this.getContentById(content.id))
throw { message: `A ${this.contentType} with the ID ${content.id} already exists.` };
for (const setting of userConfig.config.findSettings(() => true)) {
// This will load custom settings
// Setting the content's path on only the live config (and not the default config) ensures that custom settings will not be loaded on the default settings
setting.setContentPath(contentPath);
}
for (const scheme of userConfig.config.schemes) {
scheme.setContentPath(contentPath);
}
if (reload) this.localContent.splice(index, 1, content);
else this.localContent.push(content);
return content;

View File

@ -10,6 +10,8 @@
import Globals from './globals';
import Content from './content';
import path from 'path';
import semver from 'semver';
export default class ExtModule extends Content {
@ -19,5 +21,19 @@ export default class ExtModule extends Content {
}
get type() { return 'module' }
get alternateVersions() { return this.__internals.alternateVersions }
getVersion(value) {
if (this.alternateVersions) for (const version of Object.keys(this.alternateVersions)) {
if (semver.satisfies(version, value)) {
return Globals.require(path.join(this.contentPath, this.alternateVersions[version]));
}
}
if (semver.satisfies(this.version, value)) {
return this.__require;
}
throw new Error(`Module cannot satisfy version ${value}.`);
}
}

View File

@ -36,9 +36,9 @@ export default class extends ContentManager {
static get refreshModules() { return this.refreshContent }
static get loadContent() { return this.loadModule }
static async loadModule(paths, configs, info, main) {
static async loadModule(paths, configs, info, main, alternateVersions) {
return new ExtModule({
configs, info, main,
configs, info, main, alternateVersions,
paths: {
contentPath: paths.contentPath,
dirName: paths.dirName,

View File

@ -25,9 +25,7 @@ import DiscordApi from './discordapi';
import { ReactComponents, ReactHelpers } from './reactcomponents';
import { Patcher, MonkeyPatch } from './patcher';
import GlobalAc from '../ui/autocomplete';
import Vue from 'vue';
import path from 'path';
import Globals from './globals';
import semver from 'semver';
export default class PluginApi {
@ -47,8 +45,18 @@ export default class PluginApi {
return PluginManager.getPluginByPath(this.pluginPath);
}
async bridge(plugin_id) {
async bridge(plugin_id, request_version) {
const plugin = await PluginManager.waitForPlugin(plugin_id);
if (!request_version) return plugin.bridge;
if (plugin.bridges) for (const version of Object.keys(plugin.bridges)) {
if (semver.satisfies(version, request_version)) return plugin.bridges[version];
}
if (!semver.satisfies(plugin.version, request_version)) {
throw new Error(`Requested version ${request_version} not satisfied by plugin.`);
}
return plugin.bridge;
}
@ -84,7 +92,7 @@ export default class PluginApi {
*/
get Logger() {
return {
return Object.defineProperty(this, 'Logger', {value: {
log: (...message) => Logger.log(this.plugin.name, message),
error: (...message) => Logger.err(this.plugin.name, message),
err: (...message) => Logger.err(this.plugin.name, message),
@ -92,7 +100,7 @@ export default class PluginApi {
info: (...message) => Logger.info(this.plugin.name, message),
debug: (...message) => Logger.dbg(this.plugin.name, message),
dbg: (...message) => Logger.dbg(this.plugin.name, message)
};
}}).Logger;
}
/**
@ -100,7 +108,7 @@ export default class PluginApi {
*/
get Utils() {
return {
return Object.defineProperty(this, 'Utils', {value: {
overload: (...args) => Utils.overload.apply(Utils, args),
tryParseJson: (...args) => Utils.tryParseJson.apply(Utils, args),
toCamelCase: (...args) => Utils.toCamelCase.apply(Utils, args),
@ -113,7 +121,7 @@ export default class PluginApi {
until: (...args) => Utils.until.apply(Utils, args),
findInTree: (...args) => Utils.findInTree.apply(Utils, args),
findInReactTree: (...args) => Utils.findInReactTree.apply(Utils, args)
};
}}).Utils;
}
/**
@ -133,12 +141,12 @@ export default class PluginApi {
return new SettingsScheme(args);
}
get Settings() {
return {
return Object.defineProperty(this, 'Settings', {value: {
createSet: this.createSettingsSet.bind(this),
createCategory: this.createSettingsCategory.bind(this),
createSetting: this.createSetting.bind(this),
createScheme: this.createSettingsScheme.bind(this)
};
}}).Settings;
}
/**
@ -149,9 +157,9 @@ export default class PluginApi {
return Settings.get(set, category, setting);
}
get InternalSettings() {
return {
return Object.defineProperty(this, 'InternalSettings', {value: {
get: this.getInternalSetting.bind(this)
};
}}).InternalSettings;
}
/**
@ -159,12 +167,12 @@ export default class PluginApi {
*/
get BdMenu() {
return {
return Object.defineProperty(this, 'BdMenu', {value: {
open: BdMenu.open.bind(BdMenu),
close: BdMenu.close.bind(BdMenu),
items: this.BdMenuItems,
BdMenuItems: this.BdMenuItems
};
}}).BdMenu;
}
/**
@ -194,7 +202,7 @@ export default class PluginApi {
BdMenu.items.remove(item);
}
get BdMenuItems() {
return Object.defineProperty({
return Object.defineProperty(this, 'BdMenuItems', {value: Object.defineProperty({
add: this.addMenuItem.bind(this),
addSettingsSet: this.addMenuSettingsSet.bind(this),
addVueComponent: this.addMenuVueComponent.bind(this),
@ -202,7 +210,7 @@ export default class PluginApi {
removeAll: this.removeAllMenuItems.bind(this)
}, 'items', {
get: () => this.menuItems
});
})}).BdMenuItems;
}
/**
@ -217,11 +225,11 @@ export default class PluginApi {
return this._activeMenu || (this._activeMenu = { menu: null });
}
get BdContextMenu() {
return Object.defineProperty({
return Object.defineProperty(this, 'BdContextMenu', {value: Object.defineProperty({
show: this.showContextMenu.bind(this)
}, 'activeMenu', {
get: () => this.activeMenu
});
})}).BdContextMenu;
}
/**
@ -264,7 +272,7 @@ export default class PluginApi {
}
}
get CssUtils() {
return {
return Object.defineProperty(this, 'CssUtils', {value: {
compileSass: this.compileSass.bind(this),
getConfigAsSCSS: this.getConfigAsSCSS.bind(this),
getConfigAsSCSSMap: this.getConfigAsSCSSMap.bind(this),
@ -272,7 +280,7 @@ export default class PluginApi {
injectSass: this.injectSass.bind(this),
deleteStyle: this.deleteStyle.bind(this),
deleteAllStyles: this.deleteAllStyles.bind(this)
};
}}).CssUtils;
}
/**
@ -308,7 +316,7 @@ export default class PluginApi {
return this.addModal(Modals.createSettingsModal(settingsset, headertext, options));
}
get Modals() {
return Object.defineProperties({
return Object.defineProperty(this, 'Modals', {value: Object.defineProperties({
add: this.addModal.bind(this),
close: this.closeModal.bind(this),
closeAll: this.closeAllModals.bind(this),
@ -322,7 +330,7 @@ export default class PluginApi {
baseComponent: {
get: () => Modals.baseComponent
}
});
})}).Modals;
}
/**
@ -345,14 +353,14 @@ export default class PluginApi {
return Toasts.warning(message, options);
}
get Toasts() {
return {
return Object.defineProperty(this, 'Toasts', {value: {
push: this.showToast.bind(this),
success: this.showSuccessToast.bind(this),
error: this.showErrorToast.bind(this),
info: this.showInfoToast.bind(this),
warning: this.showWarningToast.bind(this),
get enabled() { return Toasts.enabled }
};
}}).Toasts;
}
/**
@ -380,13 +388,13 @@ export default class PluginApi {
}
}
get Notifications() {
return Object.defineProperty({
return Object.defineProperty(this, 'Notifications', {value: Object.defineProperty({
add: this.addNotification.bind(this),
dismiss: this.dismissNotification.bind(this),
dismissAll: this.dismissAllNotifications.bind(this)
}, 'stack', {
get: () => this.notificationStack
});
})}).Notifications;
}
/**
@ -422,7 +430,7 @@ export default class PluginApi {
return GlobalAc.items(prefix, sterm);
}
get Autocomplete() {
return Object.defineProperty({
return Object.defineProperty(this, 'Autocomplete', {value: Object.defineProperty({
add: this.addAutocompleteController.bind(this),
remove: this.removeAutocompleteController.bind(this),
removeAll: this.removeAllAutocompleteControllers.bind(this),
@ -431,7 +439,7 @@ export default class PluginApi {
search: this.searchAutocomplete.bind(this)
}, 'sets', {
get: () => this.autocompleteSets
});
})}).Autocomplete;
}
/**
@ -473,7 +481,7 @@ export default class PluginApi {
return EmoteModule.search(regex, limit);
}
get Emotes() {
return Object.defineProperties({
return Object.defineProperty(this, 'Emotes', {value: Object.defineProperties({
setFavourite: this.setFavouriteEmote.bind(this),
addFavourite: this.addFavouriteEmote.bind(this),
removeFavourite: this.removeFavouriteEmote.bind(this),
@ -492,7 +500,7 @@ export default class PluginApi {
mostused: {
get: () => this.mostUsedEmotes
}
});
})}).Emotes;
}
/**
@ -507,10 +515,10 @@ export default class PluginApi {
return PluginManager.localContent.map(plugin => plugin.id);
}
get Plugins() {
return {
return Object.defineProperty(this, 'Plugins', {value: {
getPlugin: this.getPlugin.bind(this),
listPlugins: this.listPlugins.bind(this)
};
}}).Plugins;
}
/**
@ -525,10 +533,10 @@ export default class PluginApi {
return ThemeManager.localContent.map(theme => theme.id);
}
get Themes() {
return {
return Object.defineProperty(this, 'Themes', {value: {
getTheme: this.getTheme.bind(this),
listThemes: this.listThemes.bind(this)
};
}}).Themes;
}
/**
@ -543,10 +551,10 @@ export default class PluginApi {
return ExtModuleManager.localContent.map(module => module.id);
}
get ExtModules() {
return {
return Object.defineProperty(this, 'ExtModules', {value: {
getModule: this.getModule.bind(this),
listModules: this.listModules.bind(this)
};
}}).ExtModules;
}
/**
@ -566,7 +574,7 @@ export default class PluginApi {
return Patcher.unpatchAll(patches || this.plugin.id);
}
get Patcher() {
return Object.defineProperty({
return Object.defineProperty(this, 'Patcher', {value: Object.defineProperty({
before: this.patchBefore.bind(this),
after: this.patchAfter.bind(this),
instead: this.patchInstead.bind(this),
@ -575,10 +583,10 @@ export default class PluginApi {
monkeyPatch: this.monkeyPatch.bind(this)
}, 'patches', {
get: () => this.patches
});
})}).Patcher;
}
get monkeyPatch() {
return m => MonkeyPatch(this.plugin.id, m);
return Object.defineProperty(this, 'monkeyPatch', {value: m => MonkeyPatch(this.plugin.id, m)}).monkeyPatch;
}
/**
@ -603,17 +611,32 @@ export default class PluginApi {
}
}
get DiscordContextMenu() {
return Object.defineProperty({
return Object.defineProperty(this, 'DiscordContextMenu', {value: Object.defineProperty({
add: this.addDiscordContextMenu.bind(this),
remove: this.removeDiscordContextMenu.bind(this),
removeAll: this.removeAllDiscordContextMenus.bind(this)
}, 'menus', {
get: () => this.discordContextMenus
});
})}).DiscordContextMenu;
}
Vuewrap(id, component, props) {
return VueInjector.createReactElement(Vue.component(id, component), props);
get Vuewrap() {
return Object.defineProperty(this, 'Vuewrap', {value: (id, component, props) => {
if (!component.name) component.name = id;
return VueInjector.createReactElement(component, props);
}}).Vuewrap;
}
unloadAll(closeModals = true) {
this.Events.unsubscribeAll();
this.observer.unsubscribeAll();
this.BdMenuItems.removeAll();
this.CssUtils.deleteAllStyles();
if (closeModals) this.Modals.closeAll();
if (closeModals) this.Notifications.dismissAll();
this.Autocomplete.removeAll();
this.Patcher.unpatchAll();
this.DiscordContextMenu.removeAll();
}
}

View File

@ -10,7 +10,7 @@
import { Permissions } from 'modules';
import { Modals } from 'ui';
import { ErrorEvent } from 'structs';
import { ErrorEvent, CustomSetting } from 'structs';
import { ClientLogger as Logger } from 'common';
import Globals from './globals';
import ContentManager from './contentmanager';
@ -18,8 +18,10 @@ import ExtModuleManager from './extmodulemanager';
import Plugin from './plugin';
import PluginApi from './pluginapi';
import Vendor from './vendor';
import path from 'path';
import semver from 'semver';
export default class extends ContentManager {
export default class PluginManager extends ContentManager {
static get localPlugins() {
return this.localContent;
@ -37,9 +39,21 @@ export default class extends ContentManager {
return 'plugins';
}
static async loadAllPlugins(suppressErrors) {
static get pluginApiInstances() {
return this._pluginApiInstances || (this._pluginApiInstances = {});
}
static get pluginDependencies() {
return this._pluginDependencies || (this._pluginDependencies = {});
}
static get pluginInstanceModules() {
return this._pluginInstanceModules || (this._pluginInstanceModules = {});
}
static async loadAllContent(suppressErrors) {
this.loaded = false;
const loadAll = await this.loadAllContent(true);
const loadAll = await super.loadAllContent(true);
this.loaded = true;
for (const plugin of this.localPlugins) {
if (!plugin.enabled) continue;
@ -71,10 +85,11 @@ export default class extends ContentManager {
return loadAll;
}
static get loadAllPlugins() { return this.loadAllContent }
static get refreshPlugins() { return this.refreshContent }
static get loadContent() { return this.loadPlugin }
static async loadPlugin(paths, configs, info, main, dependencies, permissions, mainExport, packed = false) {
static async loadPlugin(paths, configs, info, main, alternateVersions, dependencies, permissions, mainExport, packed = false) {
if (permissions && permissions.length > 0) {
for (const perm of permissions) {
Logger.log(this.moduleName, `Permission: ${Permissions.permissionText(perm).HEADER} - ${Permissions.permissionText(perm).BODY}`);
@ -86,27 +101,49 @@ export default class extends ContentManager {
}
}
const deps = {};
const pluginapi = this.pluginApiInstances[paths.contentPath] = new PluginApi(info, paths.contentPath);
const deps = this.pluginDependencies[paths.contentPath] = {};
if (dependencies) {
let refreshedModules = false;
for (const [key, value] of Object.entries(dependencies)) {
const extModule = ExtModuleManager.findModule(key);
if (!extModule) {
throw {message: `Dependency ${key}:${value} is not loaded.`};
if (key === 'betterdiscord') {
if (semver.satisfies(Globals.version, value)) continue;
throw {message: 'This plugin requires a different version of BetterDiscord.'};
}
deps[key] = extModule.__require;
let extModule = ExtModuleManager.findModule(key);
if (!extModule) {
if (!refreshedModules) {
await ExtModuleManager.refreshContent(true);
refreshedModules = true;
}
extModule = ExtModuleManager.findModule(key);
if (!extModule) throw {message: `Dependency ${key} is not loaded.`};
}
deps[key] = deps[extModule.id] = extModule.getVersion(value);
}
}
this.pluginInstanceModules[paths.contentPath] = Object.freeze(Object.defineProperty({
__esModule: true
}, 'default', {
get: () => instance
}));
const pluginExports = Globals.require(paths.mainPath);
const pluginFunction = mainExport ? pluginExports[mainExport]
let plugin = mainExport ? pluginExports[mainExport]
: pluginExports.__esModule ? pluginExports.default : pluginExports;
if (typeof pluginFunction !== 'function')
throw {message: `Plugin ${info.name} did not export a function.`};
if (typeof plugin === 'function' && !(plugin.prototype instanceof Plugin))
plugin = plugin.call(pluginExports, Plugin, pluginapi, Vendor, deps);
const plugin = pluginFunction.call(pluginExports, Plugin, new PluginApi(info, paths.contentPath), Vendor, deps);
if (!plugin || !(plugin.prototype instanceof Plugin))
throw {message: `Plugin ${info.name} did not return a class that extends Plugin.`};
throw {message: `Plugin ${info.name} did not export a class that extends Plugin or a function that returns a class that extends Plugin.`};
const instance = new plugin({
configs, info, main, paths
@ -124,6 +161,13 @@ export default class extends ContentManager {
static get reloadPlugin() { return this.reloadContent }
static unloadContentHook(content, reload) {
const pluginapi = this.pluginApiInstances[content.contentPath];
pluginapi.unloadAll();
delete this.pluginApiInstances[content.contentPath];
delete this.pluginDependencies[content.contentPath];
delete this.pluginInstanceModules[content.contentPath];
delete Globals.require.cache[Globals.require.resolve(content.paths.mainPath)];
const uncache = [];
for (const required in Globals.require.cache) {
@ -167,4 +211,111 @@ export default class extends ContentManager {
static get waitForPlugin() { return this.waitForContent }
static patchModuleLoad() {
const Module = Globals.require('module');
const load = Module._load;
const resolveFilename = Module._resolveFilename;
Module._load = function (request, parent, isMain) {
if (request === 'betterdiscord' || request.startsWith('betterdiscord/')) {
const plugin = PluginManager.getPluginByModule(parent);
const contentPath = plugin ? plugin.contentPath : PluginManager.getPluginPathByModule(parent);
if (contentPath) {
const module = PluginManager.requireApi(request, plugin, contentPath, parent);
if (module) return module;
}
}
return load.apply(this, arguments);
};
Module._resolveFilename = function (request, parent, isMain) {
if (request === 'betterdiscord' || request.startsWith('betterdiscord/')) {
const contentPath = PluginManager.getPluginPathByModule(parent);
if (contentPath) return request;
}
return resolveFilename.apply(this, arguments);
};
}
static getPluginByModule(module) {
return this.localContent.find(plugin => module.filename === plugin.contentPath || module.filename.startsWith(plugin.contentPath + path.sep));
}
static getPluginPathByModule(module) {
return Object.keys(this.pluginApiInstances).find(contentPath => module.filename === contentPath || module.filename.startsWith(contentPath + path.sep));
}
static requireApi(request, plugin, contentPath, parent) {
if (request === 'betterdiscord/plugin') return Plugin;
if (request === 'betterdiscord/plugin-api') return this.pluginApiInstances[contentPath];
if (request === 'betterdiscord/vendor') return Vendor;
if (request === 'betterdiscord/dependencies') return this.pluginDependencies[contentPath];
if (request.startsWith('betterdiscord/vendor/')) {
return Vendor[request.substr(21)];
}
if (request.startsWith('betterdiscord/dependencies/')) {
return this.pluginDependencies[contentPath][request.substr(27)];
}
if (request === 'betterdiscord/plugin-instance') return this.pluginInstanceModules[contentPath];
if (request.startsWith('betterdiscord/bridge/')) {
const plugin = this.getPluginById(request.substr(21));
return plugin.bridge;
}
if (request.startsWith('betterdiscord/extmodule/')) {
const module = ExtModuleManager.findModule(request.substr(24));
return module && module.__require ? module.__require : null;
}
if (request.startsWith('betterdiscord/plugin-api/')) {
const api = this.pluginApiInstances[contentPath];
const apirequest = request.substr(25);
if (apirequest === 'async-eventemitter') return api.AsyncEventEmitter;
if (apirequest === 'eventswrapper') return api.EventsWrapper;
if (apirequest === 'commoncomponents') return api.CommonComponents;
if (apirequest === 'components') return api.Components;
if (apirequest === 'filters') return api.Filters;
if (apirequest === 'discord-api') return api.DiscordApi;
if (apirequest === 'react-components') return api.ReactComponents;
if (apirequest === 'react-helpers') return api.ReactHelpers;
if (apirequest === 'reflection') return api.Reflection;
if (apirequest === 'dom') return api.DOM;
if (apirequest === 'vueinjector') return api.VueInjector;
if (apirequest === 'reflection/modules') return api.Reflection.modules;
if (apirequest === 'observer') return api.observer;
if (apirequest === 'logger') return api.Logger;
if (apirequest === 'utils') return api.Utils;
if (apirequest === 'settings') return api.Settings;
if (apirequest === 'internalsettings') return api.InternalSettings;
if (apirequest === 'bdmenu') return api.BdMenu;
if (apirequest === 'bdmenuitems') return api.BdMenuItems;
if (apirequest === 'bdcontextmenu') return api.BdContextMenu;
if (apirequest === 'cssutils') return api.CssUtils;
if (apirequest === 'modals') return api.Modals;
if (apirequest === 'toasts') return api.Toasts;
if (apirequest === 'notifications') return api.Notifications;
if (apirequest === 'autocomplete') return api.Autocomplete;
if (apirequest === 'emotes') return api.Emotes;
if (apirequest === 'patcher') return api.Patcher;
if (apirequest === 'discordcontextmenu') return api.DiscordContextMenu;
if (apirequest === 'vuewrap') return api.Vuewrap.bind(api);
if (apirequest === 'settings/custom') return CustomSetting;
}
}
}
PluginManager.patchModuleLoad();

View File

@ -28,6 +28,7 @@ export default class {
* jQuery
*/
static get jQuery() { return jQuery }
static get jquery() { return jQuery }
static get $() { return this.jQuery }
/**
@ -40,6 +41,7 @@ export default class {
* Vue
*/
static get Vue() { return Vue }
static get vue() { return Vue }
static get axios() { return Axi.axios }
@ -49,5 +51,8 @@ export default class {
static get filetype() { return filetype }
static get filewatcher() { return filewatcher }
static get VTooltip() { return VTooltip }
static get combokeys() { return Combokeys }
static get 'file-type'() { return filetype }
static get 'v-tooltip'() { return VTooltip }
}

View File

@ -250,6 +250,10 @@ export default class ArraySetting extends Setting {
setting.setContentPath(contentPath);
}
}
for (const scheme of this.schemes) {
scheme.setContentPath(contentPath);
}
}
/**

View File

@ -67,11 +67,14 @@ export default class CustomSetting extends Setting {
* @param {String} classExport The name of a property of the file's exports that will be used (optional)
*/
setClass(class_file, class_export) {
const component = Globals.require(path.join(this.path, class_file));
const setting_class = class_export ? component[class_export](CustomSetting) : component.default ? component.default(CustomSetting) : component(CustomSetting);
const class_exports = Globals.require(path.resolve(this.path, class_file));
let setting_class = class_export ? class_exports[class_export] : class_exports.default ? class_exports.default : class_exports;
if (typeof setting_class === 'function' && !(setting_class.prototype instanceof CustomSetting))
setting_class = setting_class.call(class_exports, CustomSetting);
if (!(setting_class.prototype instanceof CustomSetting))
throw {message: 'Custom setting class function returned a class that doesn\'t extend from CustomSetting.'};
throw {message: 'Custom setting class doesn\'t extend from CustomSetting.'};
this.__proto__ = setting_class.prototype;
}

View File

@ -310,6 +310,24 @@ export class FileUtils {
});
}
/**
* Gets a files real path.
* @param {String} path The file's path
* @return {Promise}
*/
static async realpath(path) {
return new Promise((resolve, reject) => {
fs.realpath(path, (err, realpath) => {
if (err) return reject({
message: `No such file or directory: ${err.path}`,
err
});
resolve(realpath);
});
});
}
/**
* Checks if a file exists and is a file.
* @param {String} path The file's path

View File

@ -38,21 +38,21 @@ class ReleaseInfo {
get core() {
const f = this.files.find(f => f.id === 'core');
f.upToDate = semver.satisfies(this.versions.core, `>=${f.version}`, { includePrerelease: true });
f.upToDate = semver.gte(this.versions.core, f.version, { includePrerelease: true });
f.currentVersion = this.versions.core;
return f;
}
get client() {
const f = this.files.find(f => f.id === 'client');
f.upToDate = semver.satisfies(this.versions.client, `>=${f.version}`, { includePrerelease: true });
f.upToDate = semver.gte(this.versions.client, f.version, { includePrerelease: true });
f.currentVersion = this.versions.client;
return f;
}
get editor() {
const f = this.files.find(f => f.id === 'editor');
f.upToDate = semver.satisfies(this.versions.editor, `>=${f.version}`, { includePrerelease: true });
f.upToDate = semver.gte(this.versions.editor, f.version, { includePrerelease: true });
f.currentVersion = this.versions.editor;
return f;
}

View File

@ -2,9 +2,13 @@
"info": {
"name": "Example Module",
"authors": [ "Jiiks" ],
"version": "1.0",
"version": "2.0.0",
"description": "Module Example"
},
"main": "index.js",
"type": "module"
"type": "module",
"alternateVersions": {
"3.0.0": "index-v3.js",
"1.0.0": "index-v1.js"
}
}

View File

@ -0,0 +1,15 @@
module.exports = class {
constructor() {
console.warn('[Example Module] Using deprecated synchronous API');
}
get foo() {
return 'Bar';
}
add(i1, i2) {
return i1 + i2;
}
}

View File

@ -0,0 +1,15 @@
module.exports = class {
get foo() {
return 'Bar';
}
async add(i1, i2) {
return i1 + i2;
}
async multiply(i1, i2) {
return i1 * i2;
}
}

View File

@ -1,13 +1,10 @@
module.exports = class {
constructor() {
}
get foo() {
return 'Bar';
}
add(i1, i2) {
async add(i1, i2) {
return i1 + i2;
}

View File

@ -1,27 +1,26 @@
module.exports = (Plugin, Api, Vendor) => {
const Plugin = require('betterdiscord/plugin');
return class extends Plugin {
onStart() {
document.addEventListener('dblclick', this.handler);
return true;
}
module.exports = class extends Plugin {
onStart() {
document.addEventListener('dblclick', this.handler);
return true;
}
onStop() {
document.removeEventListener('dblclick', this.handler);
return true;
}
onStop() {
document.removeEventListener('dblclick', this.handler);
return true;
}
handler(e) {
const message = e.target.closest('[class^=messageCozy]') || e.target.closest('[class^=messageCompact]');
if (!message) return;
const btn = message.querySelector('[class^=buttonContainer] [class^=button-]');
if (!btn) return;
btn.click();
const popup = document.querySelector('[class^=container][role=menu]');
if (!popup) return;
const rii = popup[Object.keys(popup).find(k => k.startsWith('__reactInternal'))];
if (!rii || !rii.memoizedProps || !rii.memoizedProps.children || !rii.memoizedProps.children[1] || !rii.memoizedProps.children[1].props || !rii.memoizedProps.children[1].props.onClick) return;
rii.memoizedProps.children[1].props.onClick();
}
handler(e) {
const message = e.target.closest('[class^=messageCozy]') || e.target.closest('[class^=messageCompact]');
if (!message) return;
const btn = message.querySelector('[class^=buttonContainer] [class^=button-]');
if (!btn) return;
btn.click();
const popup = document.querySelector('[class^=container][role=menu]');
if (!popup) return;
const rii = popup[Object.keys(popup).find(k => k.startsWith('__reactInternal'))];
if (!rii || !rii.memoizedProps || !rii.memoizedProps.children || !rii.memoizedProps.children[1] || !rii.memoizedProps.children[1].props || !rii.memoizedProps.children[1].props.onClick) return;
rii.memoizedProps.children[1].props.onClick();
}
}

View File

@ -5,12 +5,13 @@ module.exports = (Plugin, Api, Vendor) => {
return class extends Plugin {
async onstart() {
const example_plugin = await Api.bridge('example-plugin');
// Use version 1 of Example Plugin's bridge
const example_plugin = await Api.bridge('example-plugin', '^1');
console.log('Example plugin exports:', example_plugin.test1());
}
async onstop() {
const example_plugin = await Api.bridge('example-plugin');
const example_plugin = await Api.bridge('example-plugin', '^1');
console.log('Example plugin exports:', example_plugin.test2());
}
}

View File

@ -1,4 +1,7 @@
exports.main = (Plugin, { Logger, Settings, Modals, BdMenu: { BdMenuItems }, CommonComponents, DiscordContextMenu, Autocomplete, Notifications, Api }) => class extends Plugin {
const Plugin = require('betterdiscord/plugin');
const { Logger, Settings, Modals, BdMenuItems, CommonComponents, DiscordContextMenu, Autocomplete, Notifications, Api } = require('betterdiscord/plugin-api');
exports.main = class extends Plugin {
async onstart() {
this.keybindEvent = this.keybindEvent.bind(this);
@ -141,4 +144,4 @@ exports.main = (Plugin, { Logger, Settings, Modals, BdMenu: { BdMenuItems }, Com
get api() {
return Api;
}
};
}

View File

@ -1,21 +1,28 @@
module.exports.default = {
template: "<div style=\"margin-bottom: 15px; background-color: rgba(0, 0, 0, 0.2); border: 1px dashed rgba(255, 255, 255, 0.2); padding: 10px; color: #f6f6f7; font-weight: 500; font-size: 15px;\">Test custom setting {{ setting.id }}. This is from component.js in the plugin/theme's directory. (It can use functions.)</div>",
exports.default = {
template: `<div style="margin-bottom: 15px; background-color: rgba(0, 0, 0, 0.2); border: 1px dashed rgba(255, 255, 255, 0.2); padding: 10px; color: #f6f6f7; font-weight: 500; font-size: 15px;">
Test custom setting {{ setting.id }}. This is from component.js in the plugin/theme's directory. (It can use functions.)
</div>`,
props: ['setting', 'change']
};
const CustomSetting = require('betterdiscord/plugin-api/settings/custom');
const component = {
template: "<div style=\"margin-bottom: 15px; background-color: rgba(0, 0, 0, 0.2); border: 1px dashed rgba(255, 255, 255, 0.2); padding: 10px; color: #f6f6f7; font-weight: 500; font-size: 15px;\">Test custom setting {{ setting.id }}. Also in component.js. It extends the CustomSetting class. <button class=\"bd-button bd-buttonPrimary\" style=\"display: inline-block; margin-left: 10px;\" @click=\"change(1)\">Set value to 1</button> <button class=\"bd-button bd-buttonPrimary\" style=\"display: inline-block; margin-left: 10px;\" @click=\"change(2)\">Set value to 2</button></div>",
template: `<div style="margin-bottom: 15px; background-color: rgba(0, 0, 0, 0.2); border: 1px dashed rgba(255, 255, 255, 0.2); padding: 10px; color: #f6f6f7; font-weight: 500; font-size: 15px;">
Test custom setting {{ setting.id }}. Also in component.js. It extends the CustomSetting class.
<button class="bd-button bd-buttonPrimary" style="display: inline-block; margin-left: 10px;" @click="change(1)">Set value to 1</button>
<button class="bd-button bd-buttonPrimary" style="display: inline-block; margin-left: 10px;" @click="change(2)">Set value to 2</button>
</div>`,
props: ['setting', 'change']
};
module.exports.CustomSetting = function (CustomSetting) {
return class extends CustomSetting {
get component() {
return component;
}
get debug() {
return true;
}
exports.CustomSetting = class extends CustomSetting {
get component() {
return component;
}
};
get debug() {
return true;
}
}

View File

@ -22,14 +22,14 @@
},
"Just a string"
],
"version": "1.0",
"version": "2.0",
"description": "Example Plugin Description.\n\nDescriptions are preformatted (you can use newlines).",
"icon": "icon.svg",
"icon_type": "image/svg+xml"
},
"main": "index.js",
"dependencies": {
"Example Module": "1.0"
"Example Module": "^1.0"
},
"defaultConfig": [
{

View File

@ -1,144 +1,175 @@
module.exports = (Plugin, Api, Vendor, Dependencies) => {
const Plugin = require('betterdiscord/plugin');
const PluginApi = require('betterdiscord/plugin-api');
const Vendor = require('betterdiscord/vendor');
const Dependencies = require('betterdiscord/dependencies');
const { $, _ } = Vendor;
const { Events, Logger, InternalSettings, CssUtils } = Api;
const { Events, Logger, Utils, InternalSettings, CssUtils } = PluginApi;
const { $, _ } = Vendor;
return class extends Plugin {
get api() {
return Api;
}
// This will use version 1 of Example Module as that's listed in the config.json
const ExampleModule = require('betterdiscord/dependencies/example-module');
async onStart() {
await this.injectStyles();
Events.subscribe('TEST_EVENT', this.eventTest);
Logger.log('onStart');
Logger.log(`Plugin setting "default-0" value: ${this.settings.get('default-0')}`);
this.on('setting-updated', event => {
console.log('Received plugin setting update:', event);
});
this.on('settings-updated', event => {
console.log('Received plugin settings update:', event);
});
this.settings.on('setting-updated', event => {
Logger.log(`Setting ${event.category.id}/${event.setting.id} changed from ${event.old_value} to ${event.value}:`, event);
});
// this.settings.categories.find(c => c.id === 'default').settings.find(s => s.id === 'default-5')
this.settings.getSetting('default', 'default-0').on('setting-updated', async event => {
Logger.log(`Some feature ${event.value ? 'enabled' : 'disabled'}`);
});
this.settings.on('settings-updated', async event => {
await this.injectStyles();
Logger.log('Settings updated:', event, 'Waiting before saving complete...');
await new Promise(resolve => setTimeout(resolve, 5000));
Logger.log('Done');
});
Logger.log(`Internal setting "core/default/test-setting" value: ${InternalSettings.get('core', 'default', 'test-setting')}`);
Events.subscribe('setting-updated', event => {
console.log('Received internal setting update:', event);
});
const exampleModule = new Dependencies['Example Module'];
Logger.log(`2+4=${exampleModule.add(2, 4)}`);
return true;
}
async injectStyles() {
const scss = await CssUtils.getConfigAsSCSS() + `.layer-kosS71 .guilds-wrapper + * {
&::before {
content: 'Example plugin stuff (test radio setting #{$default-5} selected)';
display: block;
padding: 10px 40px;
color: #eee;
background-color: #202225;
text-align: center;
font-size: 14px;
}
}`;
Logger.log('Plugin SCSS:', scss);
await CssUtils.injectSass(scss);
}
onStop() {
CssUtils.deleteAllStyles();
Events.unsubscribeAll();
Logger.log('onStop');
console.log(this.showSettingsModal());
return true;
}
onUnload(reload) {
Logger.log('Unloading plugin');
delete require.cache[require.resolve('./component')];
}
eventTest(e) {
Logger.log(e);
}
get bridge() {
return {
test1: this.test1.bind(this),
test2: this.test2.bind(this)
};
}
test1() { return 'It works!'; }
test2() { return 'This works too!'; }
settingChanged(event) {
if (!this.enabled) return;
Logger.log(`${event.category_id}/${event.setting_id} changed to ${event.value}`);
}
settingsChanged(event) {
if (!this.enabled) return;
Logger.log([ 'Settings updated', event.updatedSettings ]);
}
get settingscomponent() {
const plugin = this;
return this._settingscomponent ? this._settingscomponent : this._settingscomponent = {
template: "<div style=\"margin-bottom: 15px; background-color: rgba(0, 0, 0, 0.2); border: 1px dashed rgba(255, 255, 255, 0.2); padding: 10px; color: #f6f6f7; font-weight: 500; font-size: 15px;\">Test custom setting {{ setting.id }}. This is from Plugin.settingscomponent.<br />Plugin ID: {{ plugin.id }}</div>",
props: ['setting', 'change'],
data() { return { plugin }; }
};
}
getSettingsComponent(setting, change) {
return this._settingscomponent2 ? this._settingscomponent2 : this.settingscomponent2 = {
template: "<div style=\"margin-bottom: 15px; background-color: rgba(0, 0, 0, 0.2); border: 1px dashed rgba(255, 255, 255, 0.2); padding: 10px; color: #f6f6f7; font-weight: 500; font-size: 15px;\">Test custom setting {{ setting.id }}. This is from Plugin.getSettingsComponent().</div>",
props: ['setting', 'change']
};
}
getSettingsComponentHTMLElement(setting, change) {
const el = document.createElement('div');
el.setAttribute('style', 'margin-bottom: 15px; background-color: rgba(0, 0, 0, 0.2); border: 1px dashed rgba(255, 255, 255, 0.2); padding: 10px; color: #f6f6f7; font-weight: 500; font-size: 15px;');
el.textContent = `Test custom setting ${setting.id}. This is from Plugin.getSettingsComponentHTMLElement(). Current value: ${setting.value}.`;
const button1 = document.createElement('button');
button1.setAttribute('class', 'bd-button bd-buttonPrimary');
button1.setAttribute('style', 'display: inline-block; margin-left: 10px;');
button1.addEventListener('click', () => change(1));
button1.textContent = 'Set value to 1';
el.appendChild(button1);
const button2 = document.createElement('button');
button2.setAttribute('class', 'bd-button bd-buttonPrimary');
button2.setAttribute('style', 'display: inline-block; margin-left: 10px;');
button2.addEventListener('click', () => change(2));
button2.textContent = 'Set value to 2';
el.appendChild(button2);
return el;
}
module.exports = class extends Plugin {
get require() {
return require;
}
get api() {
return PluginApi;
}
async onStart() {
await this.injectStyles();
Events.subscribe('TEST_EVENT', this.eventTest);
Logger.log('onStart');
Logger.log(`Plugin setting "default-0" value: ${this.settings.get('default-0')}`);
this.on('setting-updated', event => {
console.log('Received plugin setting update:', event);
});
this.on('settings-updated', event => {
console.log('Received plugin settings update:', event);
});
this.settings.on('setting-updated', event => {
Logger.log(`Setting ${event.category.id}/${event.setting.id} changed from ${event.old_value} to ${event.value}:`, event);
});
// this.settings.categories.find(c => c.id === 'default').settings.find(s => s.id === 'default-5')
this.settings.getSetting('default', 'default-0').on('setting-updated', async event => {
Logger.log(`Some feature ${event.value ? 'enabled' : 'disabled'}`);
});
this.settings.on('settings-updated', async event => {
await this.injectStyles();
Logger.log('Settings updated:', event, 'Waiting before saving complete...');
await Utils.wait(5000);
Logger.log('Done');
});
Logger.log(`Internal setting "core/default/test-setting" value: ${InternalSettings.get('core', 'default', 'test-setting')}`);
Events.subscribe('setting-updated', event => {
Logger.log('Received internal setting update:', event);
});
const exampleModule = new ExampleModule();
Logger.log(`2+4=${exampleModule.add(2, 4)}`);
}
async injectStyles() {
const scss = await CssUtils.getConfigAsSCSS() + `.layer-kosS71 .guilds-wrapper + * {
&::before {
content: 'Example plugin stuff (test radio setting #{$default-5} selected)';
display: block;
padding: 10px 40px;
color: #eee;
background-color: #202225;
text-align: center;
font-size: 14px;
}
}`;
Logger.log('Plugin SCSS:', scss);
await CssUtils.injectSass(scss);
}
onStop() {
PluginApi.unloadAll();
Logger.log('onStop');
console.log(this.showSettingsModal());
}
onUnload(reload) {
Logger.log('Unloading plugin');
}
eventTest(e) {
Logger.log(e);
}
/**
* Allows plugins to support plugins using older versions of their bridge.
* The bridge property will be used when plugins don't ask for a version.
* This uses getters to avoid creating every available versions.
*/
get bridges() {
return Object.defineProperty(this, 'bridges', {value: Object.defineProperties({}, {
'2.0.0': {get: () => this.bridge},
'1.0.0': {get: () => this.v1bridge}
})}).bridges;
}
get bridge() {
return Object.defineProperty(this, 'bridge', {value: {
test1: this.test1.bind(this),
test2: this.test2.bind(this)
}}).bridge;
}
async test1() { return 'It works!'; }
async test2() { return 'This works too!'; }
get v1bridge() {
return Object.defineProperty(this, 'v1bridge', {value: {
test1: this.test1Sync.bind(this),
test2: this.test2Sync.bind(this)
}}).v1bridge;
}
test1Sync() { return 'It works!'; }
test2Sync() { return 'This works too!'; }
settingChanged(event) {
if (!this.enabled) return;
Logger.log(`${event.category_id}/${event.setting_id} changed to ${event.value}`);
}
settingsChanged(event) {
if (!this.enabled) return;
Logger.log('Settings updated', event.updatedSettings);
}
get settingscomponent() {
const plugin = this;
return this._settingscomponent ? this._settingscomponent : this._settingscomponent = {
template: `<div style="margin-bottom: 15px; background-color: rgba(0, 0, 0, 0.2); border: 1px dashed rgba(255, 255, 255, 0.2); padding: 10px; color: #f6f6f7; font-weight: 500; font-size: 15px;">
Test custom setting {{ setting.id }}. This is from Plugin.settingscomponent.<br />
Plugin ID: {{ plugin.id }}
</div>`,
props: ['setting', 'change'],
data() { return { plugin }; }
};
}
getSettingsComponent(setting, change) {
return this._settingscomponent2 ? this._settingscomponent2 : this.settingscomponent2 = {
template: `<div style="margin-bottom: 15px; background-color: rgba(0, 0, 0, 0.2); border: 1px dashed rgba(255, 255, 255, 0.2); padding: 10px; color: #f6f6f7; font-weight: 500; font-size: 15px;">
Test custom setting {{ setting.id }}. This is from Plugin.getSettingsComponent().
</div>`,
props: ['setting', 'change']
};
}
getSettingsComponentHTMLElement(setting, change) {
const el = document.createElement('div');
el.setAttribute('style', 'margin-bottom: 15px; background-color: rgba(0, 0, 0, 0.2); border: 1px dashed rgba(255, 255, 255, 0.2); padding: 10px; color: #f6f6f7; font-weight: 500; font-size: 15px;');
el.textContent = `Test custom setting ${setting.id}. This is from Plugin.getSettingsComponentHTMLElement(). Current value: ${setting.value}.`;
const button1 = document.createElement('button');
button1.setAttribute('class', 'bd-button bd-buttonPrimary');
button1.setAttribute('style', 'display: inline-block; margin-left: 10px;');
button1.addEventListener('click', () => change(1));
button1.textContent = 'Set value to 1';
el.appendChild(button1);
const button2 = document.createElement('button');
button2.setAttribute('class', 'bd-button bd-buttonPrimary');
button2.setAttribute('style', 'display: inline-block; margin-left: 10px;');
button2.addEventListener('click', () => change(2));
button2.textContent = 'Set value to 2';
el.appendChild(button2);
return el;
}
}

View File

@ -1,25 +1,21 @@
module.exports = (Plugin, Api, Vendor) => {
const { Logger, ReactComponents, Patcher, monkeyPatch } = Api;
const Plugin = require('betterdiscord/plugin');
const { Logger, ReactComponents, Patcher, monkeyPatch } = require('betterdiscord/plugin-api');
return class extends Plugin {
onStart() {
this.patchMessage();
return true;
}
async patchMessage() {
const Message = await ReactComponents.getComponent('Message');
monkeyPatch(Message.component.prototype).after('render', e => {
Logger.log('MESSAGE RENDER!', e);
});
}
onStop() {
// The automatic unpatcher is not there yet
Patcher.unpatchAll();
return true;
}
module.exports = class extends Plugin {
onStart() {
this.patchMessage();
}
};
async patchMessage() {
const Message = await ReactComponents.getComponent('Message');
monkeyPatch(Message.component.prototype).after('render', e => {
Logger.log('MESSAGE RENDER!', e);
});
}
onStop() {
// The automatic unpatcher is not there yet
Patcher.unpatchAll();
}
}

View File

@ -1,4 +1,6 @@
module.exports = (React, props) => {
const { React } = require('betterdiscord/plugin-api/reflection/modules');
module.exports = props => {
return React.createElement(
'button',
{ className: 'exampleCustomElement', onClick: props.onClick },

View File

@ -1,5 +1,7 @@
module.exports = (VueWrap, props) => {
return VueWrap('somecomponent', {
const { Vuewrap } = require('betterdiscord/plugin-api');
module.exports = props => {
return Vuewrap('somecomponent', {
render: function (createElement) {
return createElement('button', {
class: 'exampleCustomElement',

View File

@ -2,127 +2,126 @@
* This is an example of how you should add custom elements instead of manipulating the DOM directly
*/
const Plugin = require('betterdiscord/plugin');
const PluginApi = require('betterdiscord/plugin-api');
const Vendor = require('betterdiscord/vendor');
// Destructure some apis
const { Logger, ReactComponents, Patcher, monkeyPatch, Components, Utils, CssUtils } = PluginApi;
// Be careful with this - some modules won't have been loaded yet
const { React } = require('betterdiscord/plugin-api/reflection/modules');
// Import custom components
const customVueComponent = require('./components/vuecomponent');
const customReactComponent = require('./components/reactcomponent');
module.exports = (Plugin, Api, Vendor) => {
module.exports = class extends Plugin {
// Destructure some apis
const { Logger, ReactComponents, Patcher, monkeyPatch, Reflection, Utils, CssUtils, VueInjector, Vuewrap, requireUncached } = Api;
const { Vue } = Vendor;
const { React } = Reflection.modules; // This should be in vendor
return class extends Plugin {
async onStart() {
this.injectStyle();
this.patchGuildTextChannel();
this.patchMessages();
return true;
}
async onStop() {
// The automatic unpatcher is not there yet
Patcher.unpatchAll();
CssUtils.deleteAllStyles();
// Force update elements to remove our changes
const GuildTextChannel = await ReactComponents.getComponent('GuildTextChannel');
GuildTextChannel.forceUpdateAll();
const MessageContent = await ReactComponents.getComponent('MessageContent', { selector: Reflection.resolve('container', 'containerCozy', 'containerCompact', 'edited').selector }, m => m.defaultProps && m.defaultProps.hasOwnProperty('disableButtons'));
MessageContent.forceUpdateAll();
return true;
}
/* Inject some style for our custom element */
async injectStyle() {
const css = `
.exampleCustomElement {
background: #7a7d82;
color: #FFF;
border-radius: 5px;
font-size: 12px;
font-weight: 600;
opacity: .5;
&:hover {
opacity: 1;
}
}
.exampleBtnGroup {
.bd-button {
font-size: 14px;
padding: 5px;
}
}
`;
await CssUtils.injectSass(css);
}
async patchGuildTextChannel() {
// Get the GuildTextChannel component and patch it's render function
const GuildTextChannel = await ReactComponents.getComponent('GuildTextChannel');
monkeyPatch(GuildTextChannel.component.prototype).after('render', this.injectCustomElements.bind(this));
// Force update to see our changes immediatly
GuildTextChannel.forceUpdateAll();
}
async patchMessages() {
// Get Message component and patch it's render function
const MessageContent = await ReactComponents.getComponent('MessageContent', { selector: Reflection.resolve('container', 'containerCozy', 'containerCompact', 'edited').selector });
monkeyPatch(MessageContent.component.prototype).after('render', this.injectGenericComponents.bind(this));
// Force update to see our changes immediatly
MessageContent.forceUpdateAll();
}
/*
* Injecting a custom React element using React.createElement
* https://reactjs.org/docs/react-api.html#createelement
* Injecting a custom Vue element using Vue.component
* https://vuejs.org/v2/guide/render-function.html
**/
injectCustomElements(that, args, returnValue) {
// Get the child we want using a treewalker since we know the child we want has a channel property and children.
const child = Utils.findInReactTree(returnValue, filter => filter.hasOwnProperty('channel') && filter.children);
if (!child) return;
// If children is not an array make it into one
if (!child.children instanceof Array) child.children = [child.children];
// Add our custom components to children
child.children.push(customReactComponent(React, { onClick: e => this.handleClick(e, child.channel) }));
child.children.push(customVueComponent(Vuewrap, { onClick: e => this.handleClick(e, child.channel) }));
}
/**
* Inject generic components provided by BD
*/
injectGenericComponents(that, args, returnValue) {
// If children is not an array make it into one
if (!returnValue.props.children instanceof Array) returnValue.props.children = [returnValue.props.children];
// Add a generic Button component provided by BD
returnValue.props.children.push(Api.Components.ButtonGroup({
classes: [ 'exampleBtnGroup' ], // Additional classes for button group
buttons: [
{
classes: ['exampleBtn'], // Additional classes for button
text: 'Hello World!', // Text for button
onClick: e => Logger.log('Hello World!') // Button click handler
},
{
classes: ['exampleBtn'],
text: 'Button',
onClick: e => Logger.log('Button!')
}
]
}).render()); // Render will return the wrapped component that can then be displayed
}
/**
* Will log the channel object
*/
handleClick(e, channel) {
Logger.log('Clicked!', channel);
}
async onStart() {
this.injectStyle();
this.patchGuildTextChannel();
this.patchMessages();
}
async onStop() {
// The automatic unpatcher is not there yet
Patcher.unpatchAll();
CssUtils.deleteAllStyles();
// Force update elements to remove our changes
const GuildTextChannel = await ReactComponents.getComponent('GuildTextChannel');
GuildTextChannel.forceUpdateAll();
const MessageContent = await ReactComponents.getComponent('MessageContent');
MessageContent.forceUpdateAll();
}
/* Inject some style for our custom element */
async injectStyle() {
const css = `
.exampleCustomElement {
background: #7a7d82;
color: #FFF;
border-radius: 5px;
font-size: 12px;
font-weight: 600;
opacity: .5;
&:hover {
opacity: 1;
}
}
.exampleBtnGroup {
.bd-button {
font-size: 14px;
padding: 5px;
}
}
`;
await CssUtils.injectSass(css);
}
async patchGuildTextChannel() {
// Get the GuildTextChannel component and patch it's render function
const GuildTextChannel = await ReactComponents.getComponent('GuildTextChannel');
monkeyPatch(GuildTextChannel.component.prototype).after('render', this.injectCustomElements.bind(this));
// Force update to see our changes immediatly
GuildTextChannel.forceUpdateAll();
}
async patchMessages() {
// Get Message component and patch it's render function
const MessageContent = await ReactComponents.getComponent('MessageContent');
monkeyPatch(MessageContent.component.prototype).after('render', this.injectGenericComponents.bind(this));
// Force update to see our changes immediatly
MessageContent.forceUpdateAll();
}
/*
* Injecting a custom React element using React.createElement
* https://reactjs.org/docs/react-api.html#createelement
* Injecting a custom Vue element using Vue.component
* https://vuejs.org/v2/guide/render-function.html
**/
injectCustomElements(that, args, returnValue) {
// Get the child we want using a treewalker since we know the child we want has a channel property and children.
const child = Utils.findInReactTree(returnValue, filter => filter.hasOwnProperty('channel') && filter.children);
if (!child) return;
// If children is not an array make it into one
if (!child.children instanceof Array) child.children = [child.children];
// Add our custom components to children
child.children.push(customReactComponent({ onClick: e => this.handleClick(e, child.channel) }));
child.children.push(customVueComponent({ onClick: e => this.handleClick(e, child.channel) }));
}
/**
* Inject generic components provided by BD
*/
injectGenericComponents(that, args, returnValue) {
// If children is not an array make it into one
if (!returnValue.props.children instanceof Array) returnValue.props.children = [returnValue.props.children];
// Add a generic Button component provided by BD
returnValue.props.children.push(Components.ButtonGroup({
classes: [ 'exampleBtnGroup' ], // Additional classes for button group
buttons: [
{
classes: ['exampleBtn'], // Additional classes for button
text: 'Hello World!', // Text for button
onClick: e => Logger.log('Hello World!') // Button click handler
},
{
classes: ['exampleBtn'],
text: 'Button',
onClick: e => Logger.log('Button!')
}
]
}).render()); // Render will return the wrapped component that can then be displayed
}
/**
* Will log the channel object
*/
handleClick(e, channel) {
Logger.log('Clicked!', channel);
}
};