/** * BetterDiscord Updater Module * Copyright (c) 2015-present JsSucks - https://github.com/JsSucks * All rights reserved. * https://github.com/JsSucks - 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 Module from './modulebase'; import { FileUtils } from './utils'; import semver from 'semver'; import Axi from './axi'; import zlib from 'zlib'; import tarfs from 'tar-fs'; const TEST_UPDATE = [ { 'id': 'core', 'version': '2.0.0-beta.5' }, { 'id': 'client', 'version': '2.0.0-beta.5' }, { 'id': 'editor', 'version': '0.4.1' } ]; class ReleaseInfo { constructor(versions) { this.versions = versions; } get core() { const f = this.files.find(f => f.id === 'core'); f.upToDate = semver.satisfies(this.versions.core, `>=${f.version}`, { includePrerelease: true }); f.currentVersion = this.versions.core; return f; } get client() { const f = this.files.find(f => f.id === 'client'); f.upToDate = semver.satisfies(this.versions.client, `>=${f.version}`, { includePrerelease: true }); f.currentVersion = this.versions.client; return f; } get editor() { const f = this.files.find(f => f.id === 'editor'); f.upToDate = semver.satisfies(this.versions.editor, `>=${f.version}`, { includePrerelease: true }); f.currentVersion = this.versions.editor; return f; } test() { this.files = TEST_UPDATE; } } export default class Updater extends Module { constructor(bd) { super(); this.bd = bd; } bindings() { this.checkForUpdates = this.checkForUpdates.bind(this); this.checkForBdUpdates = this.checkForBdUpdates.bind(this); this.updateAll = this.updateAll.bind(this); this.updateFinished = this.updateFinished.bind(this); this.start = this.start.bind(this); } events(ipc) { ipc.on('updater-startUpdate', (_, updates) => { clearInterval(this.updaterThread); this.updateAll(updates); }); ipc.on('debug-updater-forceUpdate', () => { this.checkForUpdates(true); }); } async updateBd(update) { try { console.log('[BetterDiscord:Updater] Updating', update.id); await this.downloadTarGz(`https://github.com/JsSucks/BetterDiscordApp${update.remote}`, this.bd.config.getPath('base')); this.updateFinished(update); // Cleanup await FileUtils.rm(`${this.bd.config.getPath(update.id)}_old`); } catch (err) { console.log('[BetterDiscord:Updater] Failed to update', update.id); console.log(err); update.error = err; this.bd.sendToDiscord('updater-updateError', update); } } async updateAll(updates) { const bd = updates.bd || []; const plugins = updates.plugins || []; const themes = updates.themes || []; const modules = updates.modules || []; this.restartRequired = this.reloadRequired = false; this.finishedUpdates = 0; this.totalUpdates = bd.length + plugins.length + themes.length + modules.length; const renamed = []; // TODO cleaner if (bd.length) { for (const update of bd) { try { await FileUtils.rm(`${this.bd.config.getPath(update.id)}_old`); // Try to rename dirs first await FileUtils.rn(this.bd.config.getPath(update.id), `${this.bd.config.getPath(update.id)}_old`); renamed.push({ 'old': this.bd.config.getPath(update.id), 'new': `${this.bd.config.getPath(update.id)}_old`}); } catch (err) { if (renamed.length) { // Restore dirs for (const r of renamed) { await FileUtils.rn(r.new, r.old); } } throw err; } } for (const update of bd) { this.updateBd(update); } } } updateFinished(update) { if (update.id === 'core') this.restartRequired = true; if (update.id === 'client') this.reloadRequired = true; console.log('[BetterDiscord:Updater] Finished updating', update.id); this.bd.sendToDiscord('updater-updateFinished', update); this.finishedUpdates++; if (this.finishedUpdates >= this.totalUpdates) { this.bd.sendToDiscord('updater-updated', { restartRequired: this.restartRequired, reloadRequired: this.reloadRequired }); } } start(interval = 30) { this.updaterThread = setInterval(this.checkForUpdates, interval * 60 * 1000); } validate(releaseInfo) { return releaseInfo && typeof releaseInfo === 'object' && releaseInfo.files && Array.isArray(releaseInfo.files) && releaseInfo.files.length >= 4; } async latestRelease() { try { const release = await Axi.github.api.get('repos/JsSucks/BetterDiscordApp/releases/latest'); // TODO replace with config const releaseInfoAsset = release.data.assets.find(asset => asset.name === 'releaseinfo.json'); const releaseInfo = await Axi.get(releaseInfoAsset['browser_download_url']); if (this.validate(releaseInfo.data)) return releaseInfo.data; return this.latestReleaseFallback(); } catch (err) { console.log(err); return this.latestReleaseFallback(); } } async latestReleaseFallback() { console.log('fallback'); } async checkForBdUpdates(forced = false) { try { const { coreVersion, clientVersion, editorVersion } = this.bd.config; const releaseInfo = new ReleaseInfo({ core: coreVersion, client: clientVersion, editor: editorVersion }); const latestRelease = await this.latestRelease(); if (forced) { latestRelease.files = latestRelease.files.map(file => { file.version = '10.0.0'; return file; }); } releaseInfo.files = latestRelease.files; const updates = []; const { core, client, editor } = releaseInfo; if (!core.upToDate) updates.push(core); if (!client.upToDate) updates.push(client); if (!editor.upToDate) updates.push(editor); return updates; } catch (err) { console.log('[BetterDiscord:Updater]', err); return []; } } async checkForUpdates(forced = false) { console.log('[BetterDiscord:Updater] Checking for updates'); this.bd.sendToDiscord('updater-checkForUpdates', ''); try { const bd = await this.checkForBdUpdates(forced); const updates = { bd, haveUpdates: false }; if (bd.length) updates.haveUpdates = true; if (!updates.haveUpdates) { this.bd.sendToDiscord('updater-noUpdates', ''); return true; } this.bd.sendToDiscord('updater-updatesAvailable', updates); return true; } catch (err) { console.log('[BetterDiscord:Updater]', err); this.bd.sendToDiscord('updater-error', err); return 'err'; } } async downloadTarGz(url, dest, responseType = 'stream', headers = null) { try { const stream = await Axi.axios({ url, type: 'GET', responseType, headers: headers || { 'Content-Type': 'application/octet-stream', 'Accept': 'application/octet-stream' } }); return new Promise((resolve, reject) => { stream.data.pipe(zlib.createGunzip()).pipe(tarfs.extract(dest)).on('finish', resolve).on('error', reject); }); } catch (err) { throw err; } } debug(releaseInfo) { const { core, client, editor } = releaseInfo; if (!core.upToDate) { console.log(`[BetterDiscord:Updater] Core update available: ${core.currentVersion} > ${core.version}`); } else { console.log(`[BetterDiscord:Updater] Core up to date: ${core.currentVersion} = ${core.version}`); } if (!client.upToDate) { console.log(`[BetterDiscord:Updater] Client update available: ${client.currentVersion} > ${client.version}`); } else { console.log(`[BetterDiscord:Updater] Client up to date: ${client.currentVersion} = ${client.version}`); } if (!editor.upToDate) { console.log(`[BetterDiscord:Updater] Editor update available: ${editor.currentVersion} > ${editor.version}`); } else { console.log(`[BetterDiscord:Updater] Editor up to date: ${editor.currentVersion} = ${editor.version}`); } } }