/** * * @module Vizality * @namespace Vizality */ import { log as _log, warn as _warn, error as _error, deprecate as _deprecate } from '@vizality/util/logger'; import { initialize as initializeWebpackModules, getModule, FluxDispatcher, getAllModules } from '@vizality/webpack'; import { Directories, Events, Protocols } from '@vizality/constants'; import { resolveCompiler } from '@vizality/compilers'; import { createElement } from '@vizality/util/dom'; import { toPlural } from '@vizality/util/string'; import { isArray } from '@vizality/util/array'; import { Updatable } from '@vizality/entities'; import { debounce } from 'lodash'; import { promisify } from 'util'; import cp from 'child_process'; import { promises } from 'fs'; import { join } from 'path'; const { readFile, writeFile } = promises; const exec = promisify(cp.exec); /** * @extends Updatable * @extends Events */ export default class Vizality extends Updatable { constructor () { super(Directories.ROOT, '', 'vizality'); /** * This is a leftover prop from Updatable, not needed for Vizality's core. */ delete this.addonId; /** * @note Copy over VizalityNative to vizality.native and then delete * VizalityNative, so that we are staying consistent and only have * one top level global variable. */ this.native = window.VizalityNative; delete window.VizalityNative; /** * Set up the git info, defaulting to ??? */ this.git = { upstream: '???', branch: '???', revision: '???' }; /** * Labels used for console logging purposes. */ this._labels = [ 'Vizality', 'Core' ]; this.initialize(); } async handleConnectionOpen () { return new Promise(resolve => { if (getAllModules()?.length > 7000) { return resolve(); } FluxDispatcher.subscribe('CONNECTION_OPEN', () => resolve()); }); } async ensureWebpackModules () { try { /** * Initialize the webpack modules. */ await initializeWebpackModules(); await this.handleConnectionOpen(); } catch (err) { this.error(`Something went wrong while initializing webpack modules: ${err}`); } } /** * Initialize Vizality. */ async initialize () { try { await this.ensureWebpackModules(); /** * Set up a connectStoresAsync Flux method. * @note This has to be after webpack modules have been initialized. */ const Flux = await getModule('Store', 'PersistedStore', true); Flux.connectStoresAsync = (stores, fn) => Component => import('./components').AsyncComponent.from((async () => { const awaitedStores = await Promise.all(stores); return Flux.connectStores(awaitedStores, props => fn(awaitedStores, props))(Component); })()); /** * Get rid of Discord's "Hold Up" dev tools warning. */ DiscordNative?.window?.setDevtoolsCallbacks(null, null); /** * Instantiate the managers. * @note We're doing this down here so that we can utilize webpack modules and * components inside of the managers. */ this.manager = {}; const managers = [ 'API', 'Builtin', 'Plugin', 'Theme', 'Community' ]; for (const manager of managers) { /** * Make the manager names on the global object plural, except for Community. */ const formatted = manager === 'Community' ? manager : toPlural(manager); this.manager[formatted.toLowerCase()] = new (await import(`./managers/${manager}`))(); } await this.start(); /** * Get and assign the newly updated git info. */ this.git = await this.manager.builtins.get('updater')?.getGitInfo(); /** * Token manipulation stuff. Helps prevent unwanted logouts. */ if (this.settings.get('hideToken', true)) { const tokenModule = getModule('hideToken'); tokenModule.hideToken = () => void 0; setImmediate(() => tokenModule.showToken()); } /** * Enables/disables Discord Experiments. */ if (this.settings.get('discordExperiments', false)) { const experimentsModule = getModule(user => user.isDeveloper !== void 0); Object.defineProperty(experimentsModule, 'isDeveloper', { get: () => true, configurable: true }); } /** * Trigger an event indicating that Vizality has been initialized. */ this.emit(Events.VIZALITY_READY); } catch (err) { return this.error(err); } } /** * Starts up the core functionality of Vizality, including APIs, builtins, plugins, and themes. */ async start () { /** * Clean up console by clearing it first, then log our startup banner. */ console.clear(); console.log('%c ', `background: url('${Protocols.ASSETS}/images/console-banner.gif') no-repeat center / contain; padding: 110px 350px; font-size: 1px; margin: 10px 0;`); /** * Set up the modules for the global vizality object. */ this.modules = {}; const modules = await import('./modules'); Object.assign(this.modules, modules); /** * Set up a shorthand for Vizality's Discord module. * Make sure it doesn't exist already, just in case Discord ever uses the same global namespace itself. */ if (!window.$discord) { const discord = await import('./modules/discord'); window.$discord = Object.assign({}, discord); window.$discord.constants = Object.assign({}, { ...window.$discord.constants, ...window.$discord.constants.default }); } /** * Perform some cleanup, removing unnecessary properties. */ delete this.modules.discord; delete window.$discord.constants.default; delete window.$discord.default; this.deprecate('The global namespace object "discord" will be removed on May 24, 2022. Please use "$discord" instead.'); this.deprecate('The global namespace object "vizality" will be removed on May 24, 2022. Please use "$vz" instead. "$vz" has a slightly different data structure, but provides the same information.'); /** * Initialize the APIs. */ this.api = {}; await this.manager.apis.initialize(); /** * Set up a shorthand vizality global object with the namespace $vz. */ window.$vz = Object.assign({}, this.manager, this.modules); window.$vz.api = this.api; /** * Set up and initialize Vizality's core settings. */ this.settings = this.api.settings._buildCategoryObject('settings'); window.$vz.settings = this.settings; this.settings.set('developer', false); /** * Check if the current user is a Vizality Developer. * @note This is going before the settings ready event below, because we check this in * the Icon component after settings ready event has triggered. */ /* * if (Developers.some(developer => developer.id === $discord.users.getCurrentUser()?.id)) { * console.log('yes'); * this.settings.set('developer', true); * } else { * this.settings.set('developer', false); * } */ /** * Trigger an event indicating that Vizality's settings are ready. */ this.emit(Events.VIZALITY_SETTINGS_READY); /** * Inject core Vizality styles. */ this._injectCoreStyles(); /** * Patch Discord's stylized console logs. * @note This has to be after settings have been initialized. */ this._patchDiscordLogs(); /** * Initialize builtins, plugins, and themes. */ await this.manager.builtins.initialize(); this.manager.themes.initialize(); this.manager.plugins.initialize(); this.manager.community.initialize(); } /** * Shuts down Vizality's APIs, builtins, plugins, and themes. */ async stop () { /** * Most importantly here is to stop entities in the order of plugins -> builtins -> apis * to ensure there aren't any problems shutting down something that relies on * something else. */ this.manager.themes.stop(); this.manager.plugins.stop(); await this.manager.builtins.stop(); await this.manager.apis.stop(); } /** * */ async _patchDiscordLogs () { try { const { setLogFn } = await getModule('setLogFn', true); if (!this.settings.get('showDiscordConsoleLogs', false)) { /** * Removes Discord's logs entirely... except for the logs that don't use the * setLogFn function (i.e. normal console methods). */ setLogFn(() => void 0); } else { /** * Patch Discord's logs to adhere to Vizality's log styles. */ setLogFn((submodule, type, ...message) => { switch (type) { case 'info': case 'log': return _log({ badge: `${Protocols.ASSETS}/images/discord.png`, labels: [ 'DiscordNative', submodule ], message }); case 'error': case 'trace': return _error({ badge: `${Protocols.ASSETS}/images/discord.png`, labels: [ 'DiscordNative', submodule ], message }); case 'warn': return _warn({ badge: `${Protocols.ASSETS}/images/discord.png`, labels: [ 'DiscordNative', submodule ], message }); default: return _log({ badge: `${Protocols.ASSETS}/images/discord.png`, labels: [ 'DiscordNative', submodule ], message }); } }); } } catch (err) { return this.error(err); } } /** * */ _patchWebSocket () { const _this = this; window.WebSocket = class PatchedWebSocket extends window.WebSocket { constructor (url) { super(url); this.addEventListener('message', data => { _this.emit(`webSocketMessage:${data.origin.slice(6)}`, data); }); } }; } /** * * @param {boolean} [force=false] Whether to * @returns {Promise} */ async _update (force = false) { try { const success = await super._update(force); /** * */ if (success) { try { await exec('npm install --only=prod --legacy-peer-deps', { cwd: this.dir }); } catch (err) { return this.error(`An error occurred while updating Vizality's dependencies!`, err); } /** * */ if (!document.querySelector(`#vz-updater-update-complete, [vz-route='updater']`)) { this.api.notifications.sendToast({ id: 'VIZALITY_UPDATER_UPDATE_COMPLETE', header: 'Update complete!', content: `Please click 'Reload' to complete the final stages of this Vizality update.`, icon: 'CloudDone', buttons: [ { text: 'Reload', color: 'green', look: 'ghost', onClick: () => DiscordNative?.app?.relaunch() }, { text: 'Postpone', color: 'grey', look: 'outlined', onClick: () => this.api.notifications.closeToast('VIZALITY_UPDATER_UPDATE_COMPLETE') } ] }); } this.manager.builtins.get('updater').settings.set('awaitingReload', true); } return success; } catch (err) { return this.error(`An error occurred while updating Vizality!`, err); } } /** * Injects a style element containing Vizality's core styles. * @note Includes a file watcher and CSS file generator only for Vizality developers. * @returns {Promise} */ async _injectCoreStyles () { try { const id = 'vizality-core-styles'; /** * Check if the user is a Vizality developer. */ if (this.settings.get('developer', false)) { const path = join(Directories.STYLES, 'main.scss'); const compiler = resolveCompiler(path); const style = createElement('style', { id, 'vz-style': '' }); document.head.appendChild(style); /** * Compiles the Sass and then writes it into a main.css file. * @param {boolean} [showLogs=true] Whether to show log messages * @returns {Promise} */ const compile = debounce(async (showLogs = true) => { try { const before = performance.now(); const compiled = await compiler.compile(); style.innerHTML = compiled; if (showLogs) { const after = performance.now(); const time = parseFloat((after - before).toFixed()).toString().replace(/^0+/, '') || 0; /** * Let's format the milliseconds to seconds. */ let formattedTime = Math.round((time / 1000 + Number.EPSILON) * 100) / 100; /** * If it ends up being so fast that it rounds to 0, let's show formatting * to 3 decimal places, otherwise show 2 decimal places. */ if (formattedTime === 0) { formattedTime = Math.round((time / 1000 + Number.EPSILON) * 1000) / 1000; } /** * If it is still 0, let's just say it's fast. */ if (formattedTime === 0) { this.log(`Core styles compiled. Compilation was nearly instant!`); } else { this.log(`Core styles compiled. Compilation took ${formattedTime} seconds!`); } } await writeFile(join(Directories.STYLES, 'main.css'), compiled); } catch (err) { return this.error(err); } }, 300); /** * Set up the watcher for the compiler. */ compiler.enableWatcher(); compiler.on('src-update', compile); this[`__compileStylesheet_${id}`] = compile; this[`__compiler_${id}`] = compiler; return compile(false); } /** * Create a style element and inject the CSS into it. */ const path = join(__dirname, 'styles', 'main.css'); const style = createElement('style', { id, 'vz-style': '' }); document.head.appendChild(style); const css = await readFile(path, 'utf8'); style.innerHTML = css; } catch (err) { return this.error(err); } } /** * * @param {any} message Message to log * @returns {void} */ log (...message) { // In case the addon wants to provide their own labels if (isArray(message[0])) { const _message = message.slice(1); return _log({ labels: message[0], message: _message }); } return _log({ labels: this._labels, message }); } /** * * @param {any} message Message to log * @returns {void} */ warn (...message) { // In case the addon wants to provide their own labels if (isArray(message[0])) { const _message = message.slice(1); return _warn({ labels: message[0], message: _message }); } return _warn({ labels: this._labels, message }); } /** * * @param {any} message Message to log * @returns {void} */ error (...message) { // In case the addon wants to provide their own labels if (isArray(message[0])) { const _message = message.slice(1); return _error({ labels: message[0], message: _message }); } return _error({ labels: this._labels, message }); } /** * * @param {any} message Message to log * @returns {void} */ deprecate (...message) { // In case the addon wants to provide their own labels if (isArray(message[0])) { const _message = message.slice(1); return _deprecate({ labels: message[0], message: _message }); } return _deprecate({ labels: this._labels, message }); } }