diff --git a/package.json b/package.json
index cd8e8dad..81b3f853 100644
--- a/package.json
+++ b/package.json
@@ -14,7 +14,7 @@
"lint": "eslint --ext .js common/ && pnpm --filter injector lint && pnpm --filter preload lint && pnpm --filter renderer lint-js",
"test": "mocha --require @babel/register --recursive \"./tests/renderer/*.js\"",
"dist": "pnpm run build-prod && node scripts/pack.js",
- "api": "jsdoc -X renderer/src/modules/pluginapi.js > jsdoc-ast.json"
+ "api": "jsdoc -X -r renderer/src/modules/api/ > jsdoc-ast.json"
},
"devDependencies": {
"asar": "^3.2.0",
diff --git a/renderer/src/builtins/customcss.js b/renderer/src/builtins/customcss.js
index f2e4a8da..6f830f1c 100644
--- a/renderer/src/builtins/customcss.js
+++ b/renderer/src/builtins/customcss.js
@@ -39,7 +39,7 @@ export default new class CustomCSS extends Builtin {
if (this.isDetached) return;
if (this.nativeOpen) return this.openNative();
else if (this.startDetached) return this.openDetached(this.savedCss);
- const settingsView = Utilities.findInRenderTree(thisObject._reactInternals, m => m && m.onSetSection, {walkable: ["child", "memoizedProps", "props", "children"]});
+ const settingsView = Utilities.findInTree(thisObject._reactInternals, m => m && m.onSetSection, {walkable: ["child", "memoizedProps", "props", "children"]});
if (settingsView && settingsView.onSetSection) settingsView.onSetSection(this.id);
}
});
diff --git a/renderer/src/builtins/developer/debuglogs.js b/renderer/src/builtins/developer/debuglogs.js
index 03404328..d10a58bb 100644
--- a/renderer/src/builtins/developer/debuglogs.js
+++ b/renderer/src/builtins/developer/debuglogs.js
@@ -2,7 +2,6 @@ const fs = require("fs");
const path = require("path");
import Builtin from "../../structs/builtin";
import DataStore from "../../modules/datastore";
-import Utilities from "../../modules/utilities";
const timestamp = () => new Date().toISOString().replace("T", " ").replace("Z", "");
@@ -18,6 +17,11 @@ const getCircularReplacer = () => {
};
};
+const occurrences = (source, substring) => {
+ const regex = new RegExp(substring, "g");
+ return (source.match(regex) || []).length;
+};
+
export default new class DebugLogs extends Builtin {
get name() {return "DebugLogs";}
get category() {return "developer";}
@@ -45,7 +49,7 @@ export default new class DebugLogs extends Builtin {
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (typeof(arg) === "string") {
- const styleCount = Utilities.occurrences(arg, "%c");
+ const styleCount = occurrences(arg, "%c");
sanitized.push(arg.replace(/%c/g, ""));
if (styleCount > 0) i += styleCount;
}
diff --git a/renderer/src/builtins/emotes/emotemenu.js b/renderer/src/builtins/emotes/emotemenu.js
index 0ff37072..f1c37bfd 100644
--- a/renderer/src/builtins/emotes/emotemenu.js
+++ b/renderer/src/builtins/emotes/emotemenu.js
@@ -20,7 +20,7 @@ export default new class EmoteMenu extends Builtin {
enabled() {
this.after(EmojiPicker, "type", (_, __, returnValue) => {
- const originalChildren = Utilities.getNestedProp(returnValue, "props.children.props.children");
+ const originalChildren = returnValue?.props?.children?.props?.children;
if (!originalChildren || originalChildren.__patched) return;
const activePicker = useExpressionPickerStore((state) => state.activeView);
@@ -30,8 +30,8 @@ export default new class EmoteMenu extends Builtin {
// Attach a try {} catch {} because this might crash the user.
try {
- const head = Utilities.findInReactTree(childrenReturn, (e) => e?.role === "tablist")?.children;
- const body = Utilities.findInReactTree(childrenReturn, (e) => e?.[0]?.type === "nav");
+ const head = Utilities.findInTree(childrenReturn, (e) => e?.role === "tablist", {walkable: ["props", "children", "return", "stateNode"]})?.children;
+ const body = Utilities.findInTree(childrenReturn, (e) => e?.[0]?.type === "nav", {walkable: ["props", "children", "return", "stateNode"]});
if (!head || !body) return childrenReturn;
const isActive = activePicker == "bd-emotes";
diff --git a/renderer/src/builtins/emotes/emotes.js b/renderer/src/builtins/emotes/emotes.js
index a8e0bad7..1afef9f1 100644
--- a/renderer/src/builtins/emotes/emotes.js
+++ b/renderer/src/builtins/emotes/emotes.js
@@ -1,7 +1,7 @@
import Builtin from "../../structs/builtin";
import {EmoteConfig, Config} from "data";
-import {Utilities, WebpackModules, DataStore, DiscordModules, Events, Settings, Strings} from "modules";
+import {WebpackModules, DataStore, DiscordModules, Events, Settings, Strings} from "modules";
import BDEmote from "../../ui/emote";
import Modals from "../../ui/modals";
import Toasts from "../../ui/toasts";
@@ -24,6 +24,10 @@ const Emotes = {
FrankerFaceZ: {}
};
+const escape = (s) => {
+ return s.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&");
+};
+
const blocklist = [];
const overrides = ["twitch", "subscriber", "bttv", "ffz"];
const modifiers = ["flip", "spin", "pulse", "spin2", "spin3", "1spin", "2spin", "3spin", "tr", "bl", "br", "shake", "shake2", "shake3", "flap"];
@@ -53,7 +57,7 @@ const modifiers = ["flip", "spin", "pulse", "spin2", "spin3", "1spin", "2spin",
getUrl(category, name) {return EmoteURLs[category].format({id: Emotes[category][name]});}
getCategory(category) {return Emotes[category];}
- getRemoteFile(category) {return Utilities.repoUrl(`assets/emotes/${category.toLowerCase()}.json`);}
+ getRemoteFile(category) {return `https://cdn.staticaly.com/gh/BetterDiscord/BetterDiscord/${Config.hash}/assets/emotes/${category.toLowerCase()}.json`;}
initialize() {
super.initialize();
@@ -154,7 +158,7 @@ const modifiers = ["flip", "spin", "pulse", "spin2", "spin3", "1spin", "2spin",
}
if (!Emotes[current][emoteName]) continue;
- const results = nodes[n].match(new RegExp(`([\\s]|^)${Utilities.escape(emoteModifier ? emoteName + ":" + emoteModifier : emoteName)}([\\s]|$)`));
+ const results = nodes[n].match(new RegExp(`([\\s]|^)${escape(emoteModifier ? emoteName + ":" + emoteModifier : emoteName)}([\\s]|$)`));
if (!results) continue;
const pre = nodes[n].substring(0, results.index + results[1].length);
const post = nodes[n].substring(results.index + results[0].length - results[2].length);
diff --git a/renderer/src/builtins/general/publicservers.js b/renderer/src/builtins/general/publicservers.js
index 395acb4b..b5b350fb 100644
--- a/renderer/src/builtins/general/publicservers.js
+++ b/renderer/src/builtins/general/publicservers.js
@@ -1,5 +1,5 @@
import Builtin from "../../structs/builtin";
-import {DiscordModules, WebpackModules, Strings, DOM, React} from "modules";
+import {DiscordModules, WebpackModules, Strings, DOMManager, React} from "modules";
import PublicServersMenu from "../../ui/publicservers/menu";
import Globe from "../../ui/icons/globe";
@@ -80,13 +80,13 @@ export default new class PublicServers extends Builtin {
async _appendButton() {
await new Promise(r => setTimeout(r, 1000));
- const existing = DOM.query("#bd-pub-li");
+ const existing = document.querySelector("#bd-pub-li");
if (existing) return;
- const guilds = DOM.query(`.${DiscordModules.GuildClasses.guilds} .${DiscordModules.GuildClasses.listItem}`);
+ const guilds = document.querySelector(`.${DiscordModules.GuildClasses.guilds} .${DiscordModules.GuildClasses.listItem}`);
if (!guilds) return;
- DOM.after(guilds, this.button);
+ guilds.parentNode.insertBefore(this.button, guilds.nextSibling);
}
openPublicServers() {
@@ -94,8 +94,8 @@ export default new class PublicServers extends Builtin {
}
get button() {
- const btn = DOM.createElement(`
`);
- const label = DOM.createElement(`
${Strings.PublicServers.button}
`);
+ const btn = DOMManager.parseHTML(`
`);
+ const label = DOMManager.parseHTML(`
${Strings.PublicServers.button}
`);
label.addEventListener("click", () => {this.openPublicServers();});
btn.append(label);
return btn;
diff --git a/renderer/src/index.js b/renderer/src/index.js
index 21208310..65173ca5 100644
--- a/renderer/src/index.js
+++ b/renderer/src/index.js
@@ -2,7 +2,7 @@ import require from "./polyfill"; // eslint-disable-line no-unused-vars
import secure from "./secure";
import LoadingIcon from "./loadingicon";
import BetterDiscord from "./modules/core";
-import BdApi from "./modules/pluginapi";
+import BdApi from "./modules/api/index";
// Perform some setup
secure();
diff --git a/renderer/src/modules/addonmanager.js b/renderer/src/modules/addonmanager.js
index 7eed3965..a58d5e19 100644
--- a/renderer/src/modules/addonmanager.js
+++ b/renderer/src/modules/addonmanager.js
@@ -1,4 +1,3 @@
-import Utilities from "./utilities";
import Logger from "common/logger";
import Settings from "./settingsmanager";
import Events from "./emitter";
@@ -138,9 +137,14 @@ export default class AddonManager {
parseOldMeta(fileContent, filename) {
const meta = fileContent.split("\n")[0];
const metaData = meta.substring(meta.lastIndexOf("//META") + 6, meta.lastIndexOf("*//"));
- const parsed = Utilities.testJSON(metaData);
- if (!parsed) throw new AddonError(filename, filename, Strings.Addons.metaError, {message: "", stack: meta}, this.prefix);
- if (!parsed.name) throw new AddonError(filename, filename, Strings.Addons.missingNameData, {message: "", stack: meta}, this.prefix);
+ let parsed = null;
+ try {
+ parsed = JSON.parse(metaData);
+ }
+ catch (err) {
+ throw new AddonError(filename, filename, Strings.Addons.metaError, err, this.prefix);
+ }
+ if (!parsed || !parsed.name) throw new AddonError(filename, filename, Strings.Addons.missingNameData, {message: "", stack: meta}, this.prefix);
parsed.format = "json";
return parsed;
}
diff --git a/renderer/src/modules/api/addonapi.js b/renderer/src/modules/api/addonapi.js
new file mode 100644
index 00000000..295551ab
--- /dev/null
+++ b/renderer/src/modules/api/addonapi.js
@@ -0,0 +1,64 @@
+/**
+ * `AddonAPI` is a utility class for working with plugins and themes. Instances are accessible through the {@link BdApi}.
+ * @name AddonAPI
+ */
+ class AddonAPI {
+ #manager;
+
+ constructor(manager) {this.#manager = manager;}
+
+ /**
+ * The path to the addon folder.
+ * @type string
+ */
+ get folder() {return this.#manager.addonFolder;}
+
+ /**
+ * Determines if a particular adon is enabled.
+ * @param {string} idOrFile Addon id or filename.
+ * @returns {boolean}
+ */
+ isEnabled(idOrFile) {return this.#manager.isEnabled(idOrFile);}
+
+ /**
+ * Enables the given addon.
+ * @param {string} idOrFile Addon id or filename.
+ */
+ enable(idOrAddon) {return this.#manager.enableAddon(idOrAddon);}
+
+ /**
+ * Disables the given addon.
+ * @param {string} idOrFile Addon id or filename.
+ */
+ disable(idOrAddon) {return this.#manager.disableAddon(idOrAddon);}
+
+ /**
+ * Toggles if a particular addon is enabled.
+ * @param {string} idOrFile Addon id or filename.
+ */
+ toggle(idOrAddon) {return this.#manager.toggleAddon(idOrAddon);}
+
+ /**
+ * Reloads if a particular addon is enabled.
+ * @param {string} idOrFile Addon id or filename.
+ */
+ reload(idOrFileOrAddon) {return this.#manager.reloadAddon(idOrFileOrAddon);}
+
+ /**
+ * Gets a particular addon.
+ * @param {string} idOrFile Addon id or filename.
+ * @returns {object} Addon instance
+ */
+ get(idOrFile) {return this.#manager.getAddon(idOrFile);}
+
+ /**
+ * Gets all addons of this type.
+ * @returns {Array