BetterDiscordApp-rauenzi/renderer/src/modules/updater.js

220 lines
7.2 KiB
JavaScript

import request from "request";
import fileSystem from "fs";
import path from "path";
import Logger from "@common/logger";
import Config from "@data/config";
import {comparator as semverComparator, regex as semverRegex} from "@structs/semver";
import Events from "./emitter";
import IPC from "./ipc";
import Strings from "./strings";
import DataStore from "./datastore";
import React from "./react";
import Settings from "./settingsmanager";
import PluginManager from "./pluginmanager";
import ThemeManager from "./thememanager";
import WebpackModules from "./webpackmodules";
import Toasts from "@ui/toasts";
import Notices from "@ui/notices";
import Modals from "@ui/modals";
import UpdaterPanel from "@ui/updater";
const UserSettingsWindow = WebpackModules.getByProps("updateAccount");
const base = "https://api.betterdiscord.app/v2/store/";
const route = r => `${base}${r}s`;
const redirect = addonId => `https://betterdiscord.app/gh-redirect?id=${addonId}`;
const getJSON = url => {
return new Promise(resolve => {
request(url, (error, _, body) => {
if (error) return resolve([]);
resolve(JSON.parse(body));
});
});
};
const reducer = (acc, addon) => {
if (addon.version === "Unknown") return acc;
acc[addon.file_name] = {name: addon.name, version: addon.version, id: addon.id};
return acc;
};
export default class Updater {
static initialize() {
Settings.registerPanel("updates", Strings.Panels.updates, {
order: 1,
element: () => {
return React.createElement(UpdaterPanel, {
coreUpdater: CoreUpdater,
pluginUpdater: PluginUpdater,
themeUpdater: ThemeUpdater
});
}
});
CoreUpdater.initialize();
PluginUpdater.initialize();
ThemeUpdater.initialize();
}
}
export class CoreUpdater {
static hasUpdate = false;
static apiData = {};
static remoteVersion = "";
static async initialize() {
this.checkForUpdate();
}
static async checkForUpdate(showNotice = true) {
const resp = await fetch(`https://api.github.com/repos/BetterDiscord/BetterDiscord/releases/latest`,{
method: "GET",
headers: {
"Accept": "application/json",
"Content-Type": "application/json",
"User-Agent": "BetterDiscord Updater"
}
});
const data = await resp.json();
this.apiData = data;
const remoteVersion = data.tag_name.startsWith("v") ? data.tag_name.slice(1) : data.tag_name;
this.hasUpdate = remoteVersion > Config.version;
this.remoteVersion = remoteVersion;
if (!this.hasUpdate || !showNotice) return;
const close = Notices.info(Strings.Updater.updateAvailable.format({version: remoteVersion}), {
buttons: [{
label: Strings.Notices.moreInfo,
onClick: () => {
close();
UserSettingsWindow?.open?.("updates");
}
}]
});
}
static async update() {
try {
const asar = this.apiData.assets.find(a => a.name === "betterdiscord.asar");
const buff = await new Promise((resolve, reject) =>
request(asar.url, {headers: {"Content-Type": "application/octet-stream", "User-Agent": "BetterDiscord Updater", "Accept": "application/octet-stream"}}, (err, resp, body) => {
if (err || resp.statusCode != 200) return reject(err || `${resp.statusCode} ${resp.statusMessage}`);
return resolve(body);
}));
const asarPath = path.join(DataStore.baseFolder, "betterdiscord.asar");
const fs = require("original-fs");
fs.writeFileSync(asarPath, buff);
this.hasUpdate = false;
Config.version = this.remoteVersion;
Modals.showConfirmationModal(Strings.Updater.updateSuccessful, Strings.Modals.restartPrompt, {
confirmText: Strings.Modals.restartNow,
cancelText: Strings.Modals.restartLater,
danger: true,
onConfirm: () => IPC.relaunch()
});
}
catch (err) {
Logger.stacktrace("Updater", "Failed to update", err);
Modals.showConfirmationModal(Strings.Updater.updateFailed, Strings.Updater.updateFailedMessage, {
cancelText: null
});
}
}
}
class AddonUpdater {
constructor(type) {
this.manager = type === "plugin" ? PluginManager : ThemeManager;
this.type = type;
this.cache = {};
this.pending = [];
}
async initialize() {
await this.updateCache();
this.checkAll();
Events.on(`${this.type}-loaded`, addon => {
this.checkForUpdate(addon.filename, addon.version);
});
Events.on(`${this.type}-unloaded`, addon => {
const index = this.pending.indexOf(addon.filename);
if (index >= 0) this.pending.splice(index, 1);
});
}
async updateCache() {
this.cache = {};
const addonData = await getJSON(route(this.type));
addonData.reduce(reducer, this.cache);
}
clearPending() {
this.pending.splice(0, this.pending.length);
}
checkAll(showNotice = true) {
for (const addon of this.manager.addonList) this.checkForUpdate(addon.filename, addon.version);
if (showNotice) this.showUpdateNotice();
}
checkForUpdate(filename, currentVersion) {
if (this.pending.includes(filename)) return;
const info = this.cache[path.basename(filename)];
if (!info) return;
let hasUpdate = info.version > currentVersion;
if (semverRegex.test(info.version) && semverRegex.test(currentVersion)) {
hasUpdate = semverComparator(currentVersion, info.version) > 0;
}
if (!hasUpdate) return;
this.pending.push(filename);
}
async updateAddon(filename) {
const info = this.cache[filename];
request(redirect(info.id), (error, _, body) => {
if (error) {
Logger.stacktrace("AddonUpdater", `Failed to download body for ${info.id}:`, error);
return;
}
const file = path.join(path.resolve(this.manager.addonFolder), filename);
fileSystem.writeFile(file, body.toString(), () => {
Toasts.success(Strings.Updater.addonUpdated.format({name: info.name, version: info.version}));
this.pending.splice(this.pending.indexOf(filename), 1);
});
});
}
showUpdateNotice() {
if (!this.pending.length) return;
const close = Notices.info(Strings.Updater.addonUpdatesAvailable.format({count: this.pending.length, type: this.type}), {
buttons: [{
label: Strings.Notices.moreInfo,
onClick: () => {
close();
UserSettingsWindow?.open?.("updates");
}
}]
});
}
}
export const PluginUpdater = new AddonUpdater("plugin");
export const ThemeUpdater = new AddonUpdater("theme");