/** * BetterDiscord Plugin API * 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 { EmoteModule } from 'builtin'; import { SettingsSet, SettingsCategory, Setting, SettingsScheme } from 'structs'; import { BdMenu, Modals, DOM, DOMObserver, VueInjector, Toasts, Notifications, BdContextMenu, DiscordContextMenu } from 'ui'; import * as CommonComponents from 'commoncomponents'; import { default as Components } from '../ui/components/generic'; import { Utils, Filters, ClientLogger as Logger, ClientIPC, AsyncEventEmitter } from 'common'; import Settings from './settings'; import ExtModuleManager from './extmodulemanager'; import PluginManager from './pluginmanager'; import ThemeManager from './thememanager'; import Events from './events'; import EventsWrapper from './eventswrapper'; import Reflection from './reflection/index'; 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'; export default class PluginApi { constructor(pluginInfo, pluginPath) { this.pluginInfo = pluginInfo; this.pluginPath = pluginPath; this.Events = new EventsWrapper(Events); Utils.defineSoftGetter(this.Events, 'bind', () => this.plugin); this._menuItems = undefined; this._injectedStyles = undefined; this._modalStack = undefined; } get plugin() { return PluginManager.getPluginByPath(this.pluginPath); } async bridge(plugin_id) { const plugin = await PluginManager.waitForPlugin(plugin_id); return plugin.bridge; } get require() { return this.import } import(m) { const module = ExtModuleManager.findModule(m); if (module && module.__require) return module.__require; return null; } get Api() { return this } get AsyncEventEmitter() { return AsyncEventEmitter } get EventsWrapper() { return EventsWrapper } get CommonComponents() { return CommonComponents } get Components() { return Components } get Filters() { return Filters } get Discord() { return DiscordApi } get DiscordApi() { return DiscordApi } get ReactComponents() { return ReactComponents } get ReactHelpers() { return ReactHelpers } get Reflection() { return Reflection } get DOM() { return DOM } get VueInjector() { return VueInjector } get observer() { return this._observer || (this._observer = new DOMObserver()); } /** * Logger */ get Logger() { return { log: (...message) => Logger.log(this.plugin.name, message), error: (...message) => Logger.err(this.plugin.name, message), err: (...message) => Logger.err(this.plugin.name, message), warn: (...message) => Logger.warn(this.plugin.name, message), info: (...message) => Logger.info(this.plugin.name, message), debug: (...message) => Logger.dbg(this.plugin.name, message), dbg: (...message) => Logger.dbg(this.plugin.name, message) }; } /** * Utils */ get Utils() { return { overload: (...args) => Utils.overload.apply(Utils, args), tryParseJson: (...args) => Utils.tryParseJson.apply(Utils, args), toCamelCase: (...args) => Utils.toCamelCase.apply(Utils, args), compare: (...args) => Utils.compare.apply(Utils, args), deepclone: (...args) => Utils.deepclone.apply(Utils, args), deepfreeze: (...args) => Utils.deepfreeze.apply(Utils, args), removeFromArray: (...args) => Utils.removeFromArray.apply(Utils, args), defineSoftGetter: (...args) => Utils.defineSoftGetter.apply(Utils, args), wait: (...args) => Utils.wait.apply(Utils, args), until: (...args) => Utils.until.apply(Utils, args), findInTree: (...args) => Utils.findInTree.apply(Utils, args), findInReactTree: (...args) => Utils.findInReactTree.apply(Utils, args) }; } /** * Settings */ 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.createSettingsSet.bind(this), createCategory: this.createSettingsCategory.bind(this), createSetting: this.createSetting.bind(this), createScheme: this.createSettingsScheme.bind(this) }; } /** * InternalSettings */ getInternalSetting(set, category, setting) { return Settings.get(set, category, setting); } get InternalSettings() { return { get: this.getInternalSetting.bind(this) }; } /** * BdMenu */ get BdMenu() { return { open: BdMenu.open.bind(BdMenu), close: BdMenu.close.bind(BdMenu), items: this.BdMenuItems, BdMenuItems: this.BdMenuItems }; } /** * BdMenuItems */ get menuItems() { return this._menuItems || (this._menuItems = []); } addMenuItem(item) { return BdMenu.items.add(item); } addMenuSettingsSet(category, set, text) { const item = BdMenu.items.addSettingsSet(category, set, text); return this.menuItems.push(item); } addMenuVueComponent(category, text, component) { const item = BdMenu.items.addVueComponent(category, text, component); return this.menuItems.push(item); } removeMenuItem(item) { BdMenu.items.remove(item); Utils.removeFromArray(this.menuItems, item); } removeAllMenuItems() { for (const item of this.menuItems) BdMenu.items.remove(item); } get BdMenuItems() { return Object.defineProperty({ add: this.addMenuItem.bind(this), addSettingsSet: this.addMenuSettingsSet.bind(this), addVueComponent: this.addMenuVueComponent.bind(this), remove: this.removeMenuItem.bind(this), removeAll: this.removeAllMenuItems.bind(this) }, 'items', { get: () => this.menuItems }); } /** * BdContextMenu */ showContextMenu(event, groups) { BdContextMenu.show(event, groups); this.activeMenu.menu = BdContextMenu.activeMenu.menu; } get activeMenu() { return this._activeMenu || (this._activeMenu = { menu: null }); } get BdContextMenu() { return Object.defineProperty({ show: this.showContextMenu.bind(this) }, 'activeMenu', { get: () => this.activeMenu }); } /** * CssUtils */ get injectedStyles() { return this._injectedStyles || (this._injectedStyles = []); } compileSass(scss, options) { return ClientIPC.send('bd-compileSass', Object.assign({ data: scss }, options)); } getConfigAsSCSS(settingsset) { return ThemeManager.getConfigAsSCSS(settingsset ? settingsset : this.plugin.settings); } getConfigAsSCSSMap(settingsset) { return ThemeManager.getConfigAsSCSSMap(settingsset ? settingsset : this.plugin.settings); } injectStyle(id, css) { if (id && !css) css = id, id = undefined; this.deleteStyle(id); const styleid = `plugin-${this.plugin.id}-${id}`; this.injectedStyles.push(id); DOM.injectStyle(css, styleid); } async injectSass(id, scss, options) { // In most cases a plugin's styles should be precompiled instead of using this if (id && !scss && !options) scss = id, id = undefined; const css = (await this.compileSass(scss, options)).css.toString(); this.injectStyle(id, css, options); } deleteStyle(id) { const styleid = `plugin-${this.plugin.id}-${id}`; this.injectedStyles.splice(this.injectedStyles.indexOf(styleid), 1); DOM.deleteStyle(styleid); } deleteAllStyles(id) { for (const id of this.injectedStyles) { this.deleteStyle(id); } } get CssUtils() { return { compileSass: this.compileSass.bind(this), getConfigAsSCSS: this.getConfigAsSCSS.bind(this), getConfigAsSCSSMap: this.getConfigAsSCSSMap.bind(this), injectStyle: this.injectStyle.bind(this), injectSass: this.injectSass.bind(this), deleteStyle: this.deleteStyle.bind(this), deleteAllStyles: this.deleteAllStyles.bind(this) }; } /** * Modals */ get modalStack() { return this._modalStack || (this._modalStack = []); } addModal(_modal, component) { const modal = Modals.add(_modal, component); modal.on('close', () => Utils.removeFromArray(this.modalStack, modal)); this.modalStack.push(modal); return modal; } closeModal(modal, force) { return Modals.close(modal, force); } closeAllModals(force) { const promises = []; for (const modal of this.modalStack) promises.push(modal.close(force)); return Promise.all(promises); } closeLastModal(force) { if (!this.modalStack.length) return; return this.modalStack[this.modalStack.length - 1].close(force); } basicModal(title, text) { return this.addModal(Modals.createBasicModal(title, text)); } settingsModal(settingsset, headertext, options) { return this.addModal(Modals.createSettingsModal(settingsset, headertext, options)); } get Modals() { return Object.defineProperties({ add: this.addModal.bind(this), close: this.closeModal.bind(this), closeAll: this.closeAllModals.bind(this), closeLast: this.closeLastModal.bind(this), basic: this.basicModal.bind(this), settings: this.settingsModal.bind(this) }, { stack: { get: () => this.modalStack }, baseComponent: { get: () => Modals.baseComponent } }); } /** * Toasts */ showToast(message, options = {}) { return Toasts.push(message, options); } showSuccessToast(message, options = {}) { return Toasts.success(message, options); } showInfoToast(message, options = {}) { return Toasts.info(message, options); } showErrorToast(message, options = {}) { return Toasts.error(message, options); } showWarningToast(message, options = {}) { return Toasts.warning(message, options); } get Toasts() { return { 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 } }; } /** * Notifications */ get notificationStack() { return this._notificationStack || (this._notificationStack = []); } addNotification(title, text, buttons = []) { if (arguments.length <= 1) text = title, title = undefined; if (arguments[1] instanceof Array) [text, buttons] = arguments, title = undefined; const notification = Notifications.add(title, text, buttons, () => Utils.removeFromArray(this.notificationStack, notification)); this.notificationStack.push(notification); return notification; } dismissNotification(index) { index = Notifications.stack.indexOf(this.notificationStack[index]); if (index) Notifications.dismiss(index); } dismissAllNotifications() { for (const index in this.notificationStack) { this.dismissNotification(index); } } get Notifications() { return Object.defineProperty({ add: this.addNotification.bind(this), dismiss: this.dismissNotification.bind(this), dismissAll: this.dismissAllNotifications.bind(this) }, 'stack', { get: () => this.notificationStack }); } /** * Autocomplete */ get autocompleteSets() { return this._autocompleteSets || (this._autocompleteSets = new Map()); } addAutocompleteController(prefix, controller) { if (!controller) controller = this.plugin; if (GlobalAc.validPrefix(prefix)) return; GlobalAc.add(prefix, controller); this.autocompleteSets.set(prefix, controller); } removeAutocompleteController(prefix) { if (this.autocompleteSets.get(prefix) !== GlobalAc.sets.get(prefix)) return; GlobalAc.remove(prefix); this.autocompleteSets.delete(prefix); } removeAllAutocompleteControllers() { for (const [prefix] of this.autocompleteSets) { this.removeAutocompleteController(prefix); } } validAutocompletePrefix(prefix) { return GlobalAc.validPrefix(prefix); } toggleAutocompleteMode(prefix, sterm) { return GlobalAc.toggle(prefix, sterm); } searchAutocomplete(prefix, sterm) { return GlobalAc.items(prefix, sterm); } get Autocomplete() { return Object.defineProperty({ add: this.addAutocompleteController.bind(this), remove: this.removeAutocompleteController.bind(this), removeAll: this.removeAllAutocompleteControllers.bind(this), validPrefix: this.validAutocompletePrefix.bind(this), toggle: this.toggleAutocompleteMode.bind(this), search: this.searchAutocomplete.bind(this) }, 'sets', { get: () => this.autocompleteSets }); } /** * Emotes */ get emotes() { return EmoteModule.database; } get favouriteEmotes() { return EmoteModule.favourites; } get mostUsedEmotes() { return EmoteModule.mostUsed; } setFavouriteEmote(emote, favourite) { return EmoteModule[favourite ? 'removeFavourite' : 'addFavourite'](emote); } addFavouriteEmote(emote) { return EmoteModule.addFavourite(emote); } removeFavouriteEmote(emote) { return EmoteModule.removeFavourite(emote); } isFavouriteEmote(emote) { return EmoteModule.isFavourite(emote); } getEmote(emote) { return EmoteModule.findByName(emote, true); } getEmoteUseCount(emote) { const mostUsed = EmoteModule.mostUsed.find(mu => mu.key === emote.name); return mostUsed ? mostUsed.useCount : 0; } incrementEmoteUseCount(emote) { return EmoteModule.addToMostUsed(emote); } searchEmotes(regex, limit) { return EmoteModule.search(regex, limit); } get Emotes() { return Object.defineProperties({ setFavourite: this.setFavouriteEmote.bind(this), addFavourite: this.addFavouriteEmote.bind(this), removeFavourite: this.removeFavouriteEmote.bind(this), isFavourite: this.isFavouriteEmote.bind(this), getEmote: this.getEmote.bind(this), getUseCount: this.getEmoteUseCount.bind(this), incrementUseCount: this.incrementEmoteUseCount.bind(this), search: this.searchEmotes.bind(this) }, { emotes: { get: () => this.emotes }, favourites: { get: () => this.favouriteEmotes }, mostused: { get: () => this.mostUsedEmotes } }); } /** * Plugins */ async getPlugin(plugin_id) { // This should require extra permissions return PluginManager.waitForPlugin(plugin_id); } listPlugins() { return PluginManager.localContent.map(plugin => plugin.id); } get Plugins() { return { getPlugin: this.getPlugin.bind(this), listPlugins: this.listPlugins.bind(this) }; } /** * Themes */ async getTheme(theme_id) { // This should require extra permissions return ThemeManager.waitForContent(theme_id); } listThemes() { return ThemeManager.localContent.map(theme => theme.id); } get Themes() { return { getTheme: this.getTheme.bind(this), listThemes: this.listThemes.bind(this) }; } /** * ExtModules */ async getModule(module_id) { // This should require extra permissions return ExtModuleManager.waitForContent(module_id); } listModules() { return ExtModuleManager.localContent.map(module => module.id); } get ExtModules() { return { getModule: this.getModule.bind(this), listModules: this.listModules.bind(this) }; } /** * Patcher */ get patches() { return Patcher.getPatchesByCaller(this.plugin.id); } patchBefore(...args) { return this.pushChildPatch(...args, 'before') } patchAfter(...args) { return this.pushChildPatch(...args, 'after') } patchInstead(...args) { return this.pushChildPatch(...args, 'instead') } pushChildPatch(...args) { return Patcher.pushChildPatch(this.plugin.id, ...args); } unpatchAll(patches) { return Patcher.unpatchAll(patches || this.plugin.id); } get Patcher() { return Object.defineProperty({ before: this.patchBefore.bind(this), after: this.patchAfter.bind(this), instead: this.patchInstead.bind(this), pushChildPatch: this.pushChildPatch.bind(this), unpatchAll: this.unpatchAll.bind(this), monkeyPatch: this.monkeyPatch.bind(this) }, 'patches', { get: () => this.patches }); } get monkeyPatch() { return m => MonkeyPatch(this.plugin.id, m); } /** * DiscordContextMenu */ get discordContextMenus() { return this._discordContextMenus || (this._discordContextMenus = []); } addDiscordContextMenu(items, filter) { const menu = DiscordContextMenu.add(items, filter); this.discordContextMenus.push(menu); return menu; } removeDiscordContextMenu(menu) { DiscordContextMenu.remove(menu); Utils.removeFromArray(this.discordContextMenus, menu); } removeAllDiscordContextMenus() { for (const menu of this.discordContextMenus) { this.removeDiscordContextMenu(menu); } } get DiscordContextMenu() { return Object.defineProperty({ add: this.addDiscordContextMenu.bind(this), remove: this.removeDiscordContextMenu.bind(this), removeAll: this.removeAllDiscordContextMenus.bind(this) }, 'menus', { get: () => this.discordContextMenus }); } Vuewrap(id, component, props) { return VueInjector.createReactElement(Vue.component(id, component), props); } } // Stop plugins from modifying the plugin API for all plugins // Plugins can still modify their own plugin API object Object.freeze(PluginApi); Object.freeze(PluginApi.prototype);