BetterDiscordApp-v2/client/src/modules/contentmanager.js

327 lines
11 KiB
JavaScript
Raw Normal View History

2018-01-30 16:59:27 +01:00
/**
* BetterDiscord Content Manager Module
* 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 Globals from './globals';
import { FileUtils, ClientLogger as Logger } from 'common';
import path from 'path';
import { Events } from 'modules';
2018-02-13 23:28:58 +01:00
import { ErrorEvent } from 'structs';
2018-02-13 17:44:07 +01:00
import { Modals } from 'ui';
2018-01-30 16:59:27 +01:00
2018-02-14 13:55:06 +01:00
/**
* Base class for external content managing
*/
2018-01-30 16:59:27 +01:00
export default class {
2018-02-14 13:55:06 +01:00
/**
* Any errors that happened
* returns {Array}
*/
2018-02-07 17:02:27 +01:00
static get errors() {
return this._errors || (this._errors = []);
}
2018-02-14 13:55:06 +01:00
/**
* Locallly stored content
* returns {Array}
*/
2018-01-30 16:59:27 +01:00
static get localContent() {
return this._localContent ? this._localContent : (this._localContent = []);
}
2018-02-14 13:55:06 +01:00
/**
* Local path for content
* returns {String}
*/
2018-01-30 16:59:27 +01:00
static get contentPath() {
return this._contentPath ? this._contentPath : (this._contentPath = Globals.getObject('paths').find(path => path.id === this.pathId).path);
}
2018-02-14 13:55:06 +01:00
/**
* Load all locally stored content
* @param {bool} suppressErrors Suppress any errors that occur during loading of content
*/
static async loadAllContent(suppressErrors = false) {
2018-01-30 16:59:27 +01:00
try {
2018-01-30 23:21:06 +01:00
await FileUtils.ensureDirectory(this.contentPath);
const directories = await FileUtils.listDirectory(this.contentPath);
for (let dir of directories) {
try {
await this.preloadContent(dir);
} catch (err) {
2018-02-13 23:28:58 +01:00
this.errors.push(new ErrorEvent({
2018-02-07 17:02:27 +01:00
module: this.moduleName,
message: `Failed to load ${dir}`,
err
}));
2018-01-30 23:21:06 +01:00
Logger.err(this.moduleName, err);
}
}
if (this.errors.length && !suppressErrors) {
2018-02-13 17:44:07 +01:00
Modals.error({
header: `${this.moduleName} - ${this.errors.length} ${this.contentType}${this.errors.length !== 1 ? 's' : ''} failed to load`,
2018-02-07 17:02:27 +01:00
module: this.moduleName,
type: 'err',
content: this.errors
});
2018-02-13 17:57:05 +01:00
this._errors = [];
2018-02-07 17:02:27 +01:00
}
2018-01-30 23:21:06 +01:00
return this.localContent;
} catch (err) {
throw err;
}
}
2018-01-30 16:59:27 +01:00
2018-02-14 13:55:06 +01:00
/**
* Refresh locally stored content
* @param {bool} suppressErrors Suppress any errors that occur during loading of content
2018-02-14 13:55:06 +01:00
*/
static async refreshContent(suppressErrors = false) {
2018-01-30 23:21:06 +01:00
if (!this.localContent.length) return this.loadAllContent();
try {
2018-01-30 16:59:27 +01:00
await FileUtils.ensureDirectory(this.contentPath);
const directories = await FileUtils.listDirectory(this.contentPath);
for (let dir of directories) {
2018-01-30 23:21:06 +01:00
// If content is already loaded this should resolve.
if (this.getContentByDirName(dir)) continue;
2018-01-30 16:59:27 +01:00
try {
2018-01-30 23:21:06 +01:00
// Load if not
2018-01-30 16:59:27 +01:00
await this.preloadContent(dir);
} catch (err) {
// We don't want every plugin/theme to fail loading when one does
this.errors.push(new ErrorEvent({
module: this.moduleName,
message: `Failed to load ${dir}`,
err
}));
2018-01-30 16:59:27 +01:00
Logger.err(this.moduleName, err);
}
}
2018-01-30 23:21:06 +01:00
for (let content of this.localContent) {
if (directories.includes(content.dirName)) continue;
try {
// Plugin/theme was deleted manually, stop it and remove any reference
await this.unloadContent(content);
} catch (err) {
this.errors.push(new ErrorEvent({
module: this.moduleName,
message: `Failed to unload ${dir}`,
err
}));
Logger.err(this.moduleName, err);
}
2018-01-30 23:21:06 +01:00
}
if (this.errors.length && !suppressErrors) {
Modals.error({
header: `${this.moduleName} - ${this.errors.length} ${this.contentType}${this.errors.length !== 1 ? 's' : ''} failed to load`,
module: this.moduleName,
type: 'err',
content: this.errors
});
this._errors = [];
}
2018-01-30 23:21:06 +01:00
return this.localContent;
2018-01-30 16:59:27 +01:00
} catch (err) {
throw err;
}
}
2018-02-14 13:55:06 +01:00
/**
* Common loading procedure for loading content before passing it to the actual loader
* @param {any} dirName Base directory for content
* @param {any} reload Is content being reloaded
* @param {any} index Index of content in {localContent}
*/
2018-01-30 16:59:27 +01:00
static async preloadContent(dirName, reload = false, index) {
try {
const contentPath = path.join(this.contentPath, dirName);
await FileUtils.directoryExists(contentPath);
if (!reload) {
const loaded = this.localContent.find(content => content.contentPath === contentPath);
if (loaded) {
throw { 'message': `Attempted to load already loaded user content: ${path}` };
}
}
const readConfig = await this.readConfig(contentPath);
const mainPath = path.join(contentPath, readConfig.main);
2018-02-12 23:49:44 +01:00
readConfig.defaultConfig = readConfig.defaultConfig || [];
2018-01-30 16:59:27 +01:00
const userConfig = {
enabled: false,
config: readConfig.defaultConfig
};
try {
const readUserConfig = await this.readUserConfig(contentPath);
userConfig.enabled = readUserConfig.enabled || false;
2018-02-21 16:58:45 +01:00
userConfig.config = readConfig.defaultConfig.map(category => {
let newCategory = readUserConfig.config.find(c => c.category === category.category);
// return userSet || config;
2018-02-21 16:58:45 +01:00
if (!newCategory) newCategory = {settings: []};
2018-02-21 16:58:45 +01:00
category.settings = category.settings.map(setting => {
if (setting.type === 'array' || setting.type === 'custom') setting.path = contentPath;
const newSetting = newCategory.settings.find(s => s.id === setting.id);
if (!newSetting) return setting;
2018-02-21 16:58:45 +01:00
setting.value = newSetting.value;
return setting;
});
2018-02-21 16:58:45 +01:00
return category;
2018-01-30 16:59:27 +01:00
});
userConfig.css = readUserConfig.css || null;
// userConfig.config = readUserConfig.config;
} catch (err) { /*We don't care if this fails it either means that user config doesn't exist or there's something wrong with it so we revert to default config*/
}
2018-01-30 16:59:27 +01:00
const configs = {
defaultConfig: readConfig.defaultConfig,
2018-02-15 18:09:06 +01:00
schemes: readConfig.configSchemes,
2018-01-30 16:59:27 +01:00
userConfig
2018-02-21 16:58:45 +01:00
};
2018-01-30 16:59:27 +01:00
const paths = {
contentPath,
dirName,
mainPath
2018-02-21 16:58:45 +01:00
};
2018-01-30 16:59:27 +01:00
const content = await this.loadContent(paths, configs, readConfig.info, readConfig.main, readConfig.dependencies);
2018-02-21 18:46:27 +01:00
if (!reload && this.getContentById(content.id))
throw {message: `A ${this.contentType} with the ID ${content.id} already exists.`};
2018-01-31 09:17:15 +01:00
if (reload) this.localContent[index] = content;
else this.localContent.push(content);
2018-01-30 16:59:27 +01:00
return content;
} catch (err) {
throw err;
}
}
2018-02-21 18:46:27 +01:00
/**
* Unload content
* @param {any} content Content to unload
* @param {bool} reload Whether to reload the content after
*/
static async unloadContent(content, reload) {
content = this.findContent(content);
if (!content) throw {message: `Could not find a ${this.contentType} from ${content}.`};
try {
if (content.enabled && content.disable) content.disable(false);
if (content.enabled && content.stop) content.stop(false);
if (content.onunload) content.onunload(reload);
if (content.onUnload) content.onUnload(reload);
const index = this.getContentIndex(content);
delete window.require.cache[window.require.resolve(content.paths.mainPath)];
if (reload) {
const newcontent = await this.preloadContent(content.dirName, true, index);
if (newcontent.enabled && newcontent.start) newcontent.start(false);
return newcontent;
} else this.localContent.splice(index, 1);
} catch (err) {
Logger.err(this.moduleName, err);
throw err;
}
}
/**
* Reload content
* @param {any} content Content to reload
*/
static async reloadContent(content) {
return this.unloadContent(content, true);
}
2018-02-14 13:55:06 +01:00
/**
* Read content config file
* @param {any} configPath Config file path
*/
2018-01-30 16:59:27 +01:00
static async readConfig(configPath) {
configPath = path.resolve(configPath, 'config.json');
return FileUtils.readJsonFromFile(configPath);
}
2018-02-14 13:55:06 +01:00
/**
* Read content user config file
* @param {any} configPath User config file path
*/
2018-01-30 16:59:27 +01:00
static async readUserConfig(configPath) {
configPath = path.resolve(configPath, 'user.config.json');
return FileUtils.readJsonFromFile(configPath);
}
2018-02-21 18:46:27 +01:00
/**
* Checks if the passed object is an instance of this content type.
* @param {any} content Object to check
*/
static isThisContent(content) {
return false;
}
2018-02-14 13:55:06 +01:00
/**
* Wildcard content finder
* @param {any} wild Content name | id | path | dirname
2018-02-21 18:46:27 +01:00
* @param {bool} nonunique Allow searching attributes that may not be unique
2018-02-14 13:55:06 +01:00
*/
2018-02-21 18:46:27 +01:00
static findContent(wild, nonunique) {
if (this.isThisContent(wild)) return wild;
let content;
if (content = this.getContentById(wild)) return content;
if (content = this.getContentByDirName(wild)) return content;
if (content = this.getContentByPath(wild)) return content;
if (content = nonunique && this.getContentByName(wild)) return content;
2018-01-30 23:21:06 +01:00
}
static getContentIndex(content) { return this.localContent.findIndex(c => c === content) }
static getContentByName(name) { return this.localContent.find(c => c.name === name) }
static getContentById(id) { return this.localContent.find(c => c.id === id) }
static getContentByPath(path) { return this.localContent.find(c => c.contentPath === path) }
static getContentByDirName(dirName) { return this.localContent.find(c => c.dirName === dirName) }
2018-02-14 13:55:06 +01:00
/**
* Wait for content to load
* @param {any} content_id
*/
2018-02-12 23:49:44 +01:00
static waitForContent(content_id) {
return new Promise((resolve, reject) => {
const check = () => {
const content = this.getContentById(content_id);
if (content) return resolve(content);
setTimeout(check, 100);
};
check();
});
}
}