Fix waitForModule and meta handling

This commit is contained in:
Zack Rauen 2022-08-09 13:28:50 -04:00
parent 9e5c090c6e
commit 81474344f0
8 changed files with 89 additions and 123 deletions

View File

@ -2,6 +2,19 @@
This changelog starts with the restructured 1.0.0 release that happened after context isolation changes. The changelogs here should more-or-less mirror the ones that get shown in the client but probably with less formatting and pizzazz.
## 1.6.1
### Added
### Removed
### Changed
### Fixed
- Fixed an issue where `waitForModule` would not return the found module.
- Fixed an issue where broken addon METAs could prevent BD from fully loading.
- Fixed an issue where developer badges stopped rendering.
## 1.6.0
### Added

View File

@ -1,6 +1,6 @@
{
"name": "betterdiscord",
"version": "1.6.0",
"version": "1.6.1",
"description": "Enhances Discord by adding functionality and themes.",
"main": "src/index.js",
"scripts": {

View File

@ -1,26 +1,14 @@
// fixed, improved, added, progress
export default {
description: "Big things are coming soon, this is just the tip of the iceberg!",
description: "Big things are coming soon, please be patient!",
changes: [
{
title: "What's New?",
type: "added",
title: "Fixes",
type: "fixed",
items: [
"Better handling and fallback when the editor fails to load. (Thanks Qb)",
"Now able to sort addons by whether they're enabled. (Thanks TheGreenPig)",
"New `Webpack` API added for plugin developers to take advantage of. Please see the docs for details!",
"New developer docs (still work-in-progress) available at https://docs.betterdiscord.app"
]
},
{
title: "Improvements",
type: "improved",
items: [
"Addons should now load faster, use less memory, and be a bit more consistent!",
"Addon error modal should work a little better. (Thanks Qb)",
"Plugin and startup errors should make more sense now.",
"The crash dialog has more information and more buttons.",
"Minor speed and memory improvements."
"Fixed an issue where `waitForModule` would not return the found module.",
"Fixed an issue where broken addon METAs could prevent BD from fully loading.",
"Fixed an issue where developer badges stopped rendering."
]
}
]

View File

@ -4,7 +4,6 @@ import Settings from "./settingsmanager";
import Events from "./emitter";
import DataStore from "./datastore";
import AddonError from "../structs/addonerror";
import MetaError from "../structs/metaerror";
import Toasts from "../ui/toasts";
import DiscordModules from "./discordmodules";
import Strings from "./strings";
@ -121,21 +120,21 @@ export default class AddonManager {
Logger.log(this.name, `No longer watching ${this.prefix} addons.`);
}
extractMeta(fileContent) {
extractMeta(fileContent, filename) {
const firstLine = fileContent.split("\n")[0];
const hasOldMeta = firstLine.includes("//META");
if (hasOldMeta) return this.parseOldMeta(fileContent);
const hasOldMeta = firstLine.includes("//META") && firstLine.includes("*//");
if (hasOldMeta) return this.parseOldMeta(fileContent, filename);
const hasNewMeta = firstLine.includes("/**");
if (hasNewMeta) return this.parseNewMeta(fileContent);
throw new MetaError(Strings.Addons.metaNotFound);
throw new AddonError(filename, filename, Strings.Addons.metaNotFound, {message: "", stack: fileContent}, this.prefix);
}
parseOldMeta(fileContent) {
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 MetaError(Strings.Addons.metaError);
if (!parsed.name) throw new MetaError(Strings.Addons.missingNameData);
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);
parsed.format = "json";
return parsed;
}
@ -168,12 +167,12 @@ export default class AddonManager {
let fileContent = fs.readFileSync(filename, "utf8");
fileContent = stripBOM(fileContent);
const stats = fs.statSync(filename);
const addon = this.extractMeta(fileContent);
const addon = this.extractMeta(fileContent, path.basename(filename));
if (!addon.author) addon.author = Strings.Addons.unknownAuthor;
if (!addon.version) addon.version = "???";
if (!addon.description) addon.description = Strings.Addons.noDescription;
// if (!addon.name || !addon.author || !addon.description || !addon.version) return new AddonError(addon.name || path.basename(filename), filename, "Addon is missing name, author, description, or version", {message: "Addon must provide name, author, description, and version.", stack: ""}, this.prefix);
addon.id = addon.name;
addon.id = addon.name || path.basename(filename);
addon.slug = path.basename(filename).replace(this.extension, "").replace(/ /g, "-");
addon.filename = path.basename(filename);
addon.added = stats.atimeMs;
@ -186,9 +185,13 @@ export default class AddonManager {
// Subclasses should use the return (if not AddonError) and push to this.addonList
loadAddon(filename, shouldToast = false) {
if (typeof(filename) === "undefined") return;
const addon = this.requireAddon(path.resolve(this.addonFolder, filename));
if (addon instanceof AddonError) return addon;
let addon;
try {
addon = this.requireAddon(path.resolve(this.addonFolder, filename));
}
catch (e) {
return e;
}
if (this.addonList.find(c => c.id == addon.id)) return new AddonError(addon.name, filename, Strings.Addons.alreadyExists.format({type: this.prefix, name: addon.name}), this.prefix);
const error = this.initializeAddon(addon);

View File

@ -148,29 +148,45 @@ export default new class ComponentPatcher {
patchMessageHeader() {
if (this.messageHeaderPatch) return;
const MessageTimestamp = WebpackModules.getModule(m => m?.default?.toString().indexOf("showTimestampOnHover") > -1);
this.messageHeaderPatch = Patcher.after("ComponentPatcher", MessageTimestamp, "default", (_, [{message}], returnValue) => {
const userId = Utilities.getNestedProp(message, "author.id");
if (Developers.indexOf(userId) < 0) return;
const children = Utilities.getNestedProp(returnValue, "props.children.1.props.children");
if (!Array.isArray(children)) return;
// const MessageTimestamp = WebpackModules.getModule(m => m?.default?.toString().indexOf("showTimestampOnHover") > -1);
// this.messageHeaderPatch = Patcher.after("ComponentPatcher", MessageTimestamp, "default", (_, [{message}], returnValue) => {
// const userId = Utilities.getNestedProp(message, "author.id");
// if (Developers.indexOf(userId) < 0) return;
// if (!returnValue?.type) return;
// const orig = returnValue.type;
// returnValue.type = function() {
// const retVal = Reflect.apply(orig, this, arguments);
children.splice(2, 0,
React.createElement(DeveloperBadge, {
type: "chat"
})
);
});
// const children = Utilities.getNestedProp(retVal, "props.children.1.props.children");
// if (!Array.isArray(children)) return;
// children.splice(3, 0,
// React.createElement(DeveloperBadge, {
// type: "chat"
// })
// );
// return retVal;
// };
// });
}
patchMemberList() {
async patchMemberList() {
if (this.memberListPatch) return;
const MemberListItem = WebpackModules.findByDisplayName("MemberListItem");
const memo = WebpackModules.find(m => m?.type?.toString().includes("useGlobalHasAvatarDecorations"));
if (!memo?.type) return;
const MemberListItem = await new Promise(resolve => {
const cancelFindListItem = Patcher.after("ComponentPatcher", memo, "type", (_, props, returnValue) => {
cancelFindListItem();
resolve(returnValue?.type);
});
});
if (!MemberListItem?.prototype?.renderDecorators) return;
this.memberListPatch = Patcher.after("ComponentPatcher", MemberListItem.prototype, "renderDecorators", (thisObject, args, returnValue) => {
const user = Utilities.getNestedProp(thisObject, "props.user");
const children = Utilities.getNestedProp(returnValue, "props.children");
if (!children || Developers.indexOf(user.id) < 0) return;
if (!children || Developers.indexOf(user?.id) < 0) return;
if (!Array.isArray(children)) return;
children.push(
React.createElement(DeveloperBadge, {
@ -182,19 +198,21 @@ export default new class ComponentPatcher {
patchProfile() {
if (this.profilePatch) return;
const UserProfileBadgeList = WebpackModules.getModule(m => m?.default?.displayName === "UserProfileBadgeList");
this.profilePatch = Patcher.after("ComponentPatcher", UserProfileBadgeList, "default", (_, [{user}], res) => {
if (Developers.indexOf(user?.id) < 0) return;
const children = Utilities.getNestedProp(res, "props.children");
if (!Array.isArray(children)) return;
const UserProfileBadgeLists = WebpackModules.getModule(m => m?.default?.displayName === "UserProfileBadgeList", {first: false});
for (const UserProfileBadgeList of UserProfileBadgeLists) {
this.profilePatch = Patcher.after("ComponentPatcher", UserProfileBadgeList, "default", (_, [{user}], res) => {
if (Developers.indexOf(user?.id) < 0) return;
const children = Utilities.getNestedProp(res, "props.children");
if (!Array.isArray(children)) return;
children.unshift(
React.createElement(DeveloperBadge, {
type: "profile",
size: 18
})
);
});
children.unshift(
React.createElement(DeveloperBadge, {
type: "profile",
size: 18
})
);
});
}
}
};

View File

@ -1,6 +1,5 @@
import {Config} from "data";
import Logger from "common/logger";
import DOM from "./domtools";
export default class Utilities {
@ -8,36 +7,6 @@ export default class Utilities {
return `https://cdn.staticaly.com/gh/BetterDiscord/BetterDiscord/${Config.hash}/${path}`;
}
/**
* Parses a string of HTML and returns the results. If the second parameter is true,
* the parsed HTML will be returned as a document fragment {@see https://developer.mozilla.org/en-US/docs/Web/API/DocumentFragment}.
* This is extremely useful if you have a list of elements at the top level, they can then be appended all at once to another node.
*
* If the second parameter is false, then the return value will be the list of parsed
* nodes and there were multiple top level nodes, otherwise the single node is returned.
* @param {string} html - HTML to be parsed
* @param {boolean} [fragment=false] - Whether or not the return should be the raw `DocumentFragment`
* @returns {(DocumentFragment|NodeList|HTMLElement)} - The result of HTML parsing
*/
static parseHTML(html, fragment = false) {
const template = document.createElement("template");
template.innerHTML = html;
const node = template.content.cloneNode(true);
if (fragment) return node;
return node.childNodes.length > 1 ? node.childNodes : node.childNodes[0];
}
static getTextArea() {
return DOM.query(".channelTextArea-1LDbYG textarea");
}
static insertText(textarea, text) {
textarea.focus();
textarea.selectionStart = 0;
textarea.selectionEnd = textarea.value.length;
document.execCommand("insertText", false, text);
}
static escape(s) {
return s.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&");
}
@ -148,25 +117,6 @@ export default class Utilities {
return proxy;
}
/**
* Protects prototypes from external assignment.
*
* Needs some work before full usage
* @param {Class} Component - component with prototype to protect
*/
static protectPrototype(Component) {
const descriptors = Object.getOwnPropertyDescriptors(Component.prototype);
for (const name in descriptors) {
const descriptor = descriptors[name];
descriptor.configurable = false;
descriptor.enumerable = false;
if (Object.prototype.hasOwnProperty.call(descriptor, "get")) descriptor.set = () => Logger.warn("protectPrototype", "Addon policy for plugins #5 https://github.com/BetterDiscord/BetterDiscord/wiki/Addon-Policies#plugins");
if (Object.prototype.hasOwnProperty.call(descriptor, "value") && typeof(descriptor.value) === "function") descriptor.value.bind(Component.prototype);
if (Object.prototype.hasOwnProperty.call(descriptor, "writable")) descriptor.writable = false;
}
Object.defineProperties(Component.prototype, descriptors);
}
/**
* Deep extends an object with a set of other objects. Objects later in the list
* of `extenders` have priority, that is to say if one sets a key to be a primitive,

View File

@ -142,8 +142,8 @@ export default class WebpackModules {
/**
* Finds a module using a filter function.
* @param {Function} filter A function to use to filter modules
* @param {object} [options] Whether to return only the first matching module
* @param {function} filter A function to use to filter modules
* @param {object} [options] Set of options to customize the search
* @param {Boolean} [options.first=true] Whether to return only the first matching module
* @param {Boolean} [options.defaultExport=true] Whether to return default export when matching the default export
* @return {Any}
@ -312,7 +312,7 @@ export default class WebpackModules {
/**
* Finds a module that lazily loaded.
* @param {(m) => boolean} filter A function to use to filter modules.
* @param {object} [options] Whether to return only the first matching module
* @param {object} [options] Set of options to customize the search
* @param {AbortSignal} [options.signal] AbortSignal of an AbortController to cancel the promise
* @param {Boolean} [options.defaultExport=true] Whether to return default export when matching the default export
* @returns {Promise<any>}
@ -320,25 +320,25 @@ export default class WebpackModules {
static getLazy(filter, options = {}) {
/** @type {AbortSignal} */
const abortSignal = options.signal;
const defaultExport = options.defaultExport;
const defaultExport = options.defaultExport ?? true;
const fromCache = this.getModule(filter);
if (fromCache) return Promise.resolve(fromCache);
return new Promise((resolve) => {
const cancel = () => {this.removeListener(listener);};
const listener = function (m) {
const directMatch = filter(m);
const listener = function (mod) {
const directMatch = filter(mod);
if (directMatch) {
cancel();
return resolve(directMatch);
}
const defaultMatch = filter(m.default);
const defaultMatch = filter(mod.default);
if (!defaultMatch) return;
cancel();
resolve(defaultExport ? m.default : defaultExport);
resolve(defaultExport ? mod.default : mod);
};
this.addListener(listener);

View File

@ -1,6 +0,0 @@
export default class MetaError extends Error {
constructor(message) {
super(message);
this.name = "MetaError";
}
}