add injector-updater

This commit is contained in:
Zack Rauen 2020-04-11 23:26:48 -04:00
parent 8110fffdd5
commit abcbfda554
9 changed files with 224 additions and 68 deletions

File diff suppressed because one or more lines are too long

2
js/main.min.js vendored

File diff suppressed because one or more lines are too long

View File

@ -2,24 +2,23 @@
export const minimumDiscordVersion = "0.0.306";
export const currentDiscordVersion = (window.DiscordNative && window.DiscordNative.remoteApp && window.DiscordNative.remoteApp.getVersion && window.DiscordNative.remoteApp.getVersion()) || "0.0.306";
export const minSupportedVersion = "0.3.0";
export const bbdVersion = "0.3.2";
export const bbdVersion = "0.3.3";
export const bbdChangelog = {
description: "More big things.",
description: "Big things are coming.",
changes: [
{
title: "What's New?",
items: [
"**jQuery** is no longer used internally in BBD. This should speed things up and hopefully close some memory leaks.",
"**VoiceMode** was redone to act more like it used to."
"**In-App Updater** for the injection module now exists to try and decrease the number of issues with updates to the injector.",
"**Window Transparency** changes were made to more compatible with external window managers and addons like Glasscord.",
"Initialization sequence has once again been changed slightly to hopefully improve loading times."
]
},
{
title: "Improvements",
type: "improved",
title: "Bug Fixes",
type: "fixed",
items: [
"**Copy Selector** option was revamped to be more consistent and functional.",
"**Emote Menu** has gone through some serious changes to be more efficient and less buggy.",
"Some speed improvements when entering the plugins and themes tabs."
"Some fixes related to showing modals in the `BdApi`."
]
}
]

View File

@ -54,6 +54,7 @@ deprecateGlobal("ClassNormalizer", ClassNormalizer);
window.BdApi = BdApi;
import Core from "./modules/core";
deprecateGlobal("mainCore", Core);
export default class CoreWrapper {
constructor(bdConfig) {
Core.setConfig(bdConfig);

View File

@ -93,7 +93,7 @@ BdApi.alert = function (title, content) {
/**
* Shows a generic but very customizable confirmation modal with optional confirm and cancel callbacks.
* @param {string} title - title of the modal
* @param {(string|ReactElement|Array<string|ReactElement>)} children - a single or mixed array of react elements and strings. Everything is wrapped in Discord's `TextElement` component so strings will show and render properly.
* @param {(string|ReactElement|Array<string|ReactElement>)} children - a single or mixed array of react elements and strings. Every string is wrapped in Discord's `Markdown` component so strings will show and render properly.
* @param {object} [options] - options to modify the modal
* @param {boolean} [options.danger=false] - whether the main button should be red or not
* @param {string} [options.confirmText=Okay] - text for the confirmation/submit button
@ -102,28 +102,7 @@ BdApi.alert = function (title, content) {
* @param {callable} [options.onCancel=NOOP] - callback to occur when clicking the cancel button
*/
BdApi.showConfirmationModal = function (title, content, options = {}) {
const ModalStack = BdApi.findModuleByProps("push", "update", "pop", "popWithKey");
const TextElement = BdApi.findModuleByProps("Sizes", "Weights");
const ConfirmationModal = BdApi.findModule(m => m.defaultProps && m.key && m.key() == "confirm-modal");
if (!ModalStack || !ConfirmationModal || !TextElement) return mainCore.alert(title, content);
const {onConfirm, onCancel, confirmText, cancelText, danger = false} = options;
if (typeof(content) == "string") content = TextElement({color: TextElement.Colors.PRIMARY, children: [content]});
else if (Array.isArray(content)) content = TextElement({color: TextElement.Colors.PRIMARY, children: content});
content = [content];
const emptyFunction = () => {};
ModalStack.push(function(props) {
return BdApi.React.createElement(ConfirmationModal, Object.assign({
header: title,
children: content,
red: danger,
confirmText: confirmText ? confirmText : "Okay",
cancelText: cancelText ? cancelText : "Cancel",
onConfirm: onConfirm ? onConfirm : emptyFunction,
onCancel: onCancel ? onCancel : emptyFunction
}, props));
});
Utils.showConfirmationModal(title, content, options);
};
//Show toast alert

View File

@ -14,7 +14,10 @@ import DOM from "./domtools";
import BDLogo from "../ui/bdLogo";
import TooltipWrap from "../ui/tooltipWrap";
function Core() {}
function Core() {
// Object.assign(bdConfig, __non_webpack_require__(DataStore.configFile));
// this.init();
}
Core.prototype.setConfig = function(config) {
Object.assign(bdConfig, config);
@ -43,10 +46,23 @@ Core.prototype.init = async function() {
const latestLocalVersion = bdConfig.updater ? bdConfig.updater.LatestVersion : bdConfig.latestVersion;
if (latestLocalVersion > bdConfig.version) {
Utils.alert("Update Available", `
An update for BandagedBD is available (${latestLocalVersion})! Please Reinstall!<br /><br />
<a href='https://github.com/rauenzi/BetterDiscordApp/releases/latest' target='_blank'>Download Installer</a>
`);
Utils.showConfirmationModal("Update Available", [`There is an update available for BandagedBD's Injector (${latestLocalVersion}).`, "You can either update and restart now, or later."], {
confirmText: "Update Now",
cancelText: "Maybe Later",
onConfirm: async () => {
const onUpdateFailed = () => {Utils.alert("Could Not Update", `Unable to update automatically, please download the installer and reinstall normally.<br /><br /><a href='https://github.com/rauenzi/BetterDiscordApp/releases/latest' target='_blank'>Download Installer</a>`);};
try {
const didUpdate = await this.updateInjector();
if (!didUpdate) return onUpdateFailed();
const app = require("electron").remote.app;
app.relaunch();
app.exit();
}
catch (err) {
onUpdateFailed();
}
}
});
}
Utils.log("Startup", "Initializing Settings");
@ -281,4 +297,90 @@ Core.prototype.patchGuildSeparator = function() {
}});
};
Core.prototype.updateInjector = async function() {
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
Utils.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));
Utils.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();
Utils.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
Utils.log("InjectorUpdate", `Renaming ${path.basename(extractedFolder)} to ${folderName}`);
success = await new Promise(resolve => fs.rename(extractedFolder, injectionPath, err => resolve(!err)));
if (!success) {
Utils.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) {
Utils.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));
Utils.log("InjectorUpdate", "Injector Updated!");
return success;
};
export default new Core();

View File

@ -8,6 +8,7 @@ const releaseChannel = DiscordNative.globals.releaseChannel;
export default new class DataStore {
constructor() {
this.config =
this.data = {settings: {stable: {}, canary: {}, ptb: {}}};
this.pluginData = {};
}
@ -30,6 +31,19 @@ export default new class DataStore {
}
}
get injectionPath() {
if (this._injectionPath) return this._injectionPath;
const electron = require("electron").remote.app;
const base = electron.getAppPath();
const roamingBase = electron.getPath("userData");
const roamingLocation = path.resolve(roamingBase, electron.getVersion(), "modules", "discord_desktop_core", "injector");
const location = path.resolve(base, "..", "app");
const realLocation = fs.existsSync(location) ? location : fs.existsSync(roamingLocation) ? roamingLocation : null;
if (!realLocation) return this._injectionPath = null;
return this._injectionPath = realLocation;
}
get configFile() {return this._configFile || (this._configFile = path.resolve(this.injectionPath, "betterdiscord", "config.json"));}
get BDFile() {return this._BDFile || (this._BDFile = path.resolve(bdConfig.dataPath, "bdstorage.json"));}
get settingsFile() {return this._settingsFile || (this._settingsFile = path.resolve(bdConfig.dataPath, "bdsettings.json"));}
getPluginFile(pluginName) {return path.resolve(ContentManager.pluginsFolder, pluginName + ".config.json");}

View File

@ -1,4 +1,4 @@
import {bbdVersion} from "../0globals";
import {bbdVersion, settingsCookie} from "../0globals";
import WebpackModules from "./webpackModules";
import BDV2 from "./v2";
import DOM from "./domtools";
@ -10,36 +10,26 @@ export default class Utils {
static get screenHeight() { return Math.max(document.documentElement.clientHeight, window.innerHeight || 0); }
static get WindowConfigFile() {
if (this._windowConfigFile) return this._windowConfigFile;
const electron = require("electron").remote.app;
const path = require("path");
const base = electron.getAppPath();
const roamingBase = electron.getPath("userData");
const roamingLocation = path.resolve(roamingBase, electron.getVersion(), "modules", "discord_desktop_core", "injector", "config.json");
const location = path.resolve(base, "..", "app", "config.json");
const fs = require("fs");
const realLocation = fs.existsSync(location) ? location : fs.existsSync(roamingLocation) ? roamingLocation : null;
if (!realLocation) return this._windowConfigFile = null;
return this._windowConfigFile = realLocation;
return this._windowConfigFile = null;
}
static getAllWindowPreferences() {
if (!this.WindowConfigFile) return {}; // Tempfix until new injection on other platforms
return __non_webpack_require__(this.WindowConfigFile);
return {
transparent: settingsCookie["fork-wp-1"] || settingsCookie.transparency,
frame: settingsCookie.frame
};
}
static getWindowPreference(key) {
if (!this.WindowConfigFile) return undefined; // Tempfix until new injection on other platforms
return this.getAllWindowPreferences()[key];
if (key === "transparent") return settingsCookie["fork-wp-1"] || settingsCookie.transparency;
if (key === "frame") return settingsCookie.frame;
return null;
}
static setWindowPreference(key, value) {
if (!this.WindowConfigFile) return; // Tempfix until new injection on other platforms
const fs = require("fs");
const prefs = this.getAllWindowPreferences();
prefs[key] = value;
delete __non_webpack_require__.cache[this.WindowConfigFile];
fs.writeFileSync(this.WindowConfigFile, JSON.stringify(prefs, null, 4));
if (key === "transparent") return settingsCookie["fork-wp-1"] = settingsCookie.transparency = value;
if (key === "frame") return settingsCookie.frame = value;
return null;
}
static stripBOM(content) {
@ -382,6 +372,41 @@ export default class Utils {
}, props));
});
}
/**
* Shows a generic but very customizable confirmation modal with optional confirm and cancel callbacks.
* @param {string} title - title of the modal
* @param {(string|ReactElement|Array<string|ReactElement>)} children - a single or mixed array of react elements and strings. Every string is wrapped in Discord's `Markdown` component so strings will show and render properly.
* @param {object} [options] - options to modify the modal
* @param {boolean} [options.danger=false] - whether the main button should be red or not
* @param {string} [options.confirmText=Okay] - text for the confirmation/submit button
* @param {string} [options.cancelText=Cancel] - text for the cancel button
* @param {callable} [options.onConfirm=NOOP] - callback to occur when clicking the submit button
* @param {callable} [options.onCancel=NOOP] - callback to occur when clicking the cancel button
*/
static showConfirmationModal(title, content, options = {}) {
const ModalStack = WebpackModules.findByProps("push", "update", "pop", "popWithKey");
const Markdown = WebpackModules.findByDisplayName("Markdown");
const ConfirmationModal = WebpackModules.find(m => m.defaultProps && m.key && m.key() == "confirm-modal");
if (!ModalStack || !ConfirmationModal || !Markdown) return Utils.alert(title, content);
const emptyFunction = () => {};
const {onConfirm = emptyFunction, onCancel = emptyFunction, confirmText = "Okay", cancelText = "Cancel", danger = false} = options;
if (!Array.isArray(content)) content = [content];
content = content.map(c => typeof(c) === "string" ? BDV2.React.createElement(Markdown, null, c) : c);
ModalStack.push(function(props) {
return BDV2.React.createElement(ConfirmationModal, Object.assign({
header: title,
children: content,
red: danger,
confirmText: confirmText,
cancelText: cancelText,
onConfirm: onConfirm,
onCancel: onCancel
}, props));
});
}
}
Utils.showToast = Utils.suppressErrors(Utils.showToast, "Could not show toast.");

View File

@ -1,6 +1,6 @@
const path = require("path");
const CircularDependencyPlugin = require("circular-dependency-plugin");
const TerserPlugin = require('terser-webpack-plugin');
const TerserPlugin = require("terser-webpack-plugin");
module.exports = {
mode: "development",
@ -19,7 +19,10 @@ module.exports = {
fs: `require("fs")`,
path: `require("path")`,
request: `require("request")`,
events: `require("events")`
events: `require("events")`,
rimraf: `require("rimraf")`,
yauzl: `require("yauzl")`,
mkdirp: `require("mkdirp")`
},
resolve: {
extensions: [".js", ".jsx"],