import LocaleManager from "./localemanager"; import Logger from "./logger"; import {Config, Changelog} from "data"; // import EmoteModule from "./emotes"; // import QuickEmoteMenu from "../builtins/emotemenu"; import DOMManager from "./dommanager"; import PluginManager from "./pluginmanager"; import ThemeManager from "./thememanager"; import Settings from "./settingsmanager"; import * as Builtins from "builtins"; import Modals from "../ui/modals"; import ReactComponents from "./reactcomponents"; import DataStore from "./datastore"; import DiscordModules from "./discordmodules"; import ComponentPatcher from "./componentpatcher"; import Strings from "./strings"; import LoadingIcon from "../loadingicon"; import Utilities from "./utilities"; const {ipcRenderer} = require("electron"); const GuildClasses = DiscordModules.GuildClasses; export default class Core { constructor() { ipcRenderer.invoke("bd-config", "get").then(injectorConfig => { if (this.hasStarted) return; Object.assign(Config, injectorConfig); this.init(); }); } get dependencies() { return [ { name: "jquery", type: "script", url: "//ajax.googleapis.com/ajax/libs/jquery/2.0.0/jquery.min.js", backup: "//cdn.jsdelivr.net/gh/jquery/jquery@2.0.0/jquery.min.js", local: null }, { name: "bd-stylesheet", type: "style", url: "//cdn.staticaly.com/gh/{{repo}}/BetterDiscordApp/{{hash}}/dist/style.min.css", backup: "//rauenzi.github.io/BetterDiscordApp/dist/style.min.css", local: "{{localServer}}/BetterDiscordApp/dist/style.min.css" } ]; } setConfig(config) { if (this.hasStarted) return; Object.assign(Config, config); } async init() { if (this.hasStarted) return; this.hasStarted = true; // Load dependencies asynchronously if they don't exist let dependencyPromise = new Promise(r => r()); if (!window.$ || !window.jQuery) dependencyPromise = this.loadDependencies(); DataStore.initialize(); await LocaleManager.initialize(); if (Config.version < Config.minSupportedVersion) return Modals.alert(Strings.Startup.notSupported, Strings.Startup.versionMismatch.format({injector: Config.version, remote: Config.bbdVersion})); if (window.ED) return Modals.alert(Strings.Startup.notSupported, Strings.Startup.incompatibleApp.format({app: "EnhancedDiscord"})); if (window.WebSocket && window.WebSocket.name && window.WebSocket.name.includes("Patched")) return Modals.alert(Strings.Startup.notSupported, Strings.Startup.incompatibleApp.format({app: "Powercord"})); const latestLocalVersion = Config.updater ? Config.updater.LatestVersion : Config.latestVersion; if (latestLocalVersion > Config.version) { Modals.showConfirmationModal(Strings.Startup.updateAvailable, Strings.Startup.updateInfo.format({version: latestLocalVersion}), { confirmText: Strings.Startup.updateNow, cancelText: Strings.Startup.maybeLater, onConfirm: async () => { const onUpdateFailed = () => { Modals.alert(Strings.Startup.updateFailed, Strings.Startup.manualUpdate); }; try { const didUpdate = await this.updateInjector(); if (!didUpdate) return onUpdateFailed(); const app = require("electron").remote.app; app.relaunch(); app.exit(); } catch (err) { onUpdateFailed(); } } }); } Logger.log("Startup", "Initializing Settings"); Settings.initialize(); // DOMManager.initialize(); await this.waitForGuilds(); ReactComponents.initialize(); ComponentPatcher.initialize(); for (const module in Builtins) Builtins[module].initialize(); await dependencyPromise; Logger.log("Startup", "Loading Plugins"); const pluginErrors = PluginManager.initialize(); Logger.log("Startup", "Loading Themes"); const themeErrors = ThemeManager.initialize(); Logger.log("Startup", "Removing Loading Icon"); LoadingIcon.hide(); // Show loading errors Logger.log("Startup", "Collecting Startup Errors"); Modals.showAddonErrors({plugins: pluginErrors, themes: themeErrors}); const previousVersion = DataStore.getBDData("version"); if (Config.bbdVersion > previousVersion) { Modals.showChangelogModal(Changelog); DataStore.setBDData("version", Config.bbdVersion); } } waitForGuilds() { let timesChecked = 0; return new Promise(resolve => { const checkForGuilds = function () { timesChecked++; if (document.readyState != "complete") setTimeout(checkForGuilds, 100); const wrapper = GuildClasses.wrapper.split(" ")[0]; const guild = GuildClasses.listItem.split(" ")[0]; const blob = GuildClasses.blobContainer.split(" ")[0]; if (document.querySelectorAll(`.${wrapper} .${guild} .${blob}`).length > 0) return resolve(Config.deferLoaded = true); else if (timesChecked >= 50) return resolve(Config.deferLoaded = true); setTimeout(checkForGuilds, 100); }; checkForGuilds(); }); } async loadDependencies() { for (const data of this.dependencies) { const url = Utilities.formatString((Config.local && data.local != null) ? data.local : data.url, {repo: Config.repo, hash: Config.hash, localServer: Config.localServer}); Logger.log(`Startup`, `Loading Resource (${url})`); const injector = (data.type == "script" ? DOMManager.injectScript : DOMManager.linkStyle).bind(DOMManager); try { await injector(data.name, url); } catch (err) { const backup = Utilities.formatString(data.backup, {minified: Config.minified ? ".min" : ""}); Logger.stacktrace(`Startup`, `Could not load ${url}. Using backup ${backup}`, err); try { await injector(data.name, backup); } catch (e) { Logger.stacktrace(`Startup`, `Could not load ${url}. Using backup ${backup}`, err); if (data.name === "jquery") Modals.alert(Strings.Startup.jqueryFailed, Strings.Startup.jqueryFailedDetails); } } } } async updateInjector() { const injectionPath = DataStore.injectionPath; if (!injectionPath) return false; const fs = require("fs"); const path = require("path"); const rmrf = require("rimraf"); const yauzl = require("yauzl"); const mkdirp = require("mkdirp"); const request = require("request"); const parentPath = path.resolve(injectionPath, ".."); const folderName = path.basename(injectionPath); const zipLink = "https://github.com/rauenzi/BetterDiscordApp/archive/injector.zip"; const savedZip = path.resolve(parentPath, "injector.zip"); const extractedFolder = path.resolve(parentPath, "BetterDiscordApp-injector"); // Download the injector zip file Logger.log("InjectorUpdate", "Downloading " + zipLink); let success = await new Promise(resolve => { request.get({url: zipLink, encoding: null}, async (error, response, body) => { if (error || response.statusCode !== 200) return resolve(false); // Save a backup in case someone has their own copy const alreadyExists = await new Promise(res => fs.exists(savedZip, res)); if (alreadyExists) await new Promise(res => fs.rename(savedZip, `${savedZip}.bak${Math.round(performance.now())}`, res)); Logger.log("InjectorUpdate", "Writing " + savedZip); fs.writeFile(savedZip, body, err => resolve(!err)); }); }); if (!success) return success; // Check and delete rename extraction const alreadyExists = await new Promise(res => fs.exists(extractedFolder, res)); if (alreadyExists) await new Promise(res => fs.rename(extractedFolder, `${extractedFolder}.bak${Math.round(performance.now())}`, res)); // Unzip the downloaded zip file const zipfile = await new Promise(r => yauzl.open(savedZip, {lazyEntries: true}, (err, zip) => r(zip))); zipfile.on("entry", function (entry) { // Skip directories, they are handled with mkdirp if (entry.fileName.endsWith("/")) return zipfile.readEntry(); Logger.log("InjectorUpdate", "Extracting " + entry.fileName); // Make any needed parent directories const fullPath = path.resolve(parentPath, entry.fileName); mkdirp.sync(path.dirname(fullPath)); zipfile.openReadStream(entry, function (err, readStream) { if (err) return success = false; readStream.on("end", function () { zipfile.readEntry(); }); // Go to next file after this readStream.pipe(fs.createWriteStream(fullPath)); }); }); zipfile.readEntry(); // Start reading // Wait for the final file to finish await new Promise(resolve => zipfile.once("end", resolve)); // Save a backup in case something goes wrong during final step const backupFolder = path.resolve(parentPath, `${folderName}.bak${Math.round(performance.now())}`); await new Promise(resolve => fs.rename(injectionPath, backupFolder, resolve)); // Rename the extracted folder to what it should be Logger.log("InjectorUpdate", `Renaming ${path.basename(extractedFolder)} to ${folderName}`); success = await new Promise(resolve => fs.rename(extractedFolder, injectionPath, err => resolve(!err))); if (!success) { Logger.err("InjectorUpdate", "Failed to rename the final directory"); return success; } // If rename had issues, delete what we tried to rename and restore backup if (!success) { Logger.err("InjectorUpdate", "Something went wrong... restoring backups."); await new Promise(resolve => rmrf(extractedFolder, resolve)); await new Promise(resolve => fs.rename(backupFolder, injectionPath, resolve)); return success; } // If we've gotten to this point, everything should have gone smoothly. // Cleanup the backup folder then remove the zip await new Promise(resolve => rmrf(backupFolder, resolve)); await new Promise(resolve => fs.unlink(savedZip, resolve)); Logger.log("InjectorUpdate", "Injector Updated!"); return success; } }