BetterDiscordApp-v2/client/src/modules/packageinstaller.js

184 lines
7.0 KiB
JavaScript
Raw Normal View History

2018-08-29 05:19:18 +02:00
import EventListener from './eventlistener';
import asar from 'asar';
import fs from 'fs';
import path from 'path';
import rimraf from 'rimraf';
2018-08-29 05:19:18 +02:00
import { request } from 'vendor';
2018-08-29 05:19:18 +02:00
import { Modals } from 'ui';
import { Utils } from 'common';
2018-08-29 22:02:41 +02:00
import PluginManager from './pluginmanager';
import Globals from './globals';
2018-08-30 06:05:14 +02:00
import Security from './security';
import Reflection from './reflection';
import DiscordApi from './discordapi';
import ThemeManager from './thememanager';
2019-03-10 21:29:55 +01:00
import { MonkeyPatch } from './patcher';
import { DOM } from 'ui';
2018-08-29 05:19:18 +02:00
export default class PackageInstaller {
2018-08-29 05:19:18 +02:00
/**
* Handler for drag and drop package install
* @param {String} filePath Path to local file
* @param {Boolean} canUpload If the user can upload files in current window
* @returns {Number} returns action code from modal
*/
static async dragAndDropHandler(filePath, canUpload) {
2018-08-29 05:19:18 +02:00
try {
const config = JSON.parse(asar.extractFile(filePath, 'config.json').toString());
2018-08-29 05:19:18 +02:00
const { info, main } = config;
let icon = null;
if (info.icon && info.icon_type) {
const extractIcon = asar.extractFile(filePath, info.icon);
2018-08-29 05:19:18 +02:00
icon = `data:${info.icon_type};base64,${Utils.arrayBufferToBase64(extractIcon)}`;
}
2018-08-30 08:08:52 +02:00
const isPlugin = info.type && info.type === 'plugin' || main.endsWith('.js');
2018-08-29 05:19:18 +02:00
// Show install modal
const modalResult = await Modals.installModal(isPlugin ? 'plugin' : 'theme', config, filePath, icon, canUpload).promise;
return modalResult;
} catch (err) {
console.log(err);
}
2018-08-29 05:19:18 +02:00
}
2018-08-30 06:05:14 +02:00
/**
* Hash and verify a package
* @param {Byte[]|String} bytesOrPath byte array of binary or path to local file
2018-08-30 08:08:52 +02:00
* @param {String} id Package id
2018-08-30 06:05:14 +02:00
*/
2018-08-30 08:08:52 +02:00
static async verifyPackage(bytesOrPath, id) {
2018-08-30 06:05:14 +02:00
const bytes = typeof bytesOrPath === 'string' ? fs.readFileSync(bytesOrPath) : bytesOrPath;
// Temporary hash to simulate response from server
2018-08-30 08:08:52 +02:00
const tempVerified = ['2e3532ee366816adc37b0f478bfef35e03f96e7aeee9b115f5918ef6a4e94de8', '06a2eb4e37b926354ab80cd83207db67e544c932e9beddce545967a21f8db5aa'];
2018-08-30 06:05:14 +02:00
const hashBytes = Security.hash('sha256', bytes, 'hex');
2018-08-30 08:08:52 +02:00
return tempVerified.includes(hashBytes);
2018-08-30 06:05:14 +02:00
}
// TODO lots of stuff
/**
* Installs or updates defined package
* @param {Byte[]|String} bytesOrPath byte array of binary or path to local file
* @param {String} nameOrId Package name
* @param {Boolean} update Does an older version already exist
*/
static async installPackage(bytesOrPath, nameOrId, contentType, update = false) {
2018-08-30 23:42:05 +02:00
let outputPath = null;
2018-08-30 17:30:49 +02:00
try {
2018-08-30 17:30:49 +02:00
const bytes = typeof bytesOrPath === 'string' ? fs.readFileSync(bytesOrPath) : bytesOrPath;
const outputName = `${nameOrId}.bd`;
2018-08-30 06:05:14 +02:00
2018-12-02 03:13:57 +01:00
outputPath = path.join(Globals.getPath(`${contentType}s`), outputName);
2018-08-30 17:30:49 +02:00
fs.writeFileSync(outputPath, bytes);
const manager = contentType === 'plugin' ? PluginManager : ThemeManager;
2018-08-31 07:33:23 +02:00
if (!update) return manager.preloadPackedContent(outputName);
const oldContent = manager.findContent(nameOrId);
await oldContent.unload(true);
2018-08-30 17:30:49 +02:00
if (oldContent.packed && oldContent.packed.packageName !== nameOrId) {
rimraf(oldContent.packed.packagePath, err => {
if (err) throw err;
});
} else {
rimraf(oldContent.contentPath, err => {
if (err) throw err;
});
}
return manager.preloadPackedContent(outputName);
2018-08-30 17:30:49 +02:00
} catch (err) {
throw err;
}
}
/**
2018-12-06 09:02:07 +01:00
* Install package from remote location. Only github/bdapi is supported.
* @param {String} remoteLocation Remote resource location
*/
static async installRemotePackage(remoteLocation) {
try {
const { hostname } = Object.assign(document.createElement('a'), { href: remoteLocation });
if (hostname !== 'api.github.com' && hostname !== 'secretbdapi') throw 'Invalid host!';
const options = {
uri: remoteLocation,
2018-12-06 09:02:07 +01:00
encoding: null,
headers: {
'User-Agent': 'BetterDiscordClient',
'Accept': 'application/octet-stream'
}
};
const response = await request.get(options);
const outputPath = path.join(Globals.getPath('tmp'), Security.hash('sha256', response, 'hex'));
fs.writeFileSync(outputPath, response);
2018-12-06 09:02:07 +01:00
console.log('response', response);
console.log('output', outputPath);
await this.dragAndDropHandler(outputPath);
rimraf(outputPath, err => {
if (err) console.log(err);
});
} catch (err) {
throw err;
}
}
2019-03-10 21:29:55 +01:00
static async handleDrop(stateNode, e, original) {
if (!e.dataTransfer.files.length || !e.dataTransfer.files[0].name.endsWith('.bd')) return original && original.call(stateNode, e);
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
if (stateNode) stateNode.clearDragging();
const currentChannel = DiscordApi.currentChannel;
const canUpload = currentChannel ?
currentChannel.checkPermissions(Reflection.modules.DiscordConstants.Permissions.SEND_MESSAGES) &&
currentChannel.checkPermissions(Reflection.modules.DiscordConstants.Permissions.ATTACH_FILES) : false;
const files = Array.from(e.dataTransfer.files).slice(0);
const actionCode = await this.dragAndDropHandler(e.dataTransfer.files[0].path, canUpload);
if (actionCode === 0 && stateNode) stateNode.promptToUpload(files, currentChannel.id, true, !e.shiftKey);
}
/**
* Patches Discord upload area for .bd files
*/
static async uploadAreaPatch(UploadArea) {
// Add a listener to root for when not in a channel
const root = DOM.getElement('#app-mount');
2019-03-10 21:29:55 +01:00
const rootHandleDrop = this.handleDrop.bind(this, undefined);
root.addEventListener('drop', rootHandleDrop);
2019-03-10 21:29:55 +01:00
const unpatchUploadAreaHandleDrop = MonkeyPatch('BD:ReactComponents', UploadArea.component.prototype).instead('handleDrop', (component, [e], original) => this.handleDrop(component, e, original));
2019-03-10 21:29:55 +01:00
this.unpatchUploadArea = () => {
unpatchUploadAreaHandleDrop();
root.removeEventListener('drop', rootHandleDrop);
this.unpatchUploadArea = undefined;
};
2019-03-10 21:29:55 +01:00
for (const element of document.querySelectorAll(UploadArea.important.selector)) {
const stateNode = Reflection.DOM(element).getComponentStateNode(UploadArea);
element.removeEventListener('drop', stateNode.handleDrop);
stateNode.handleDrop = UploadArea.component.prototype.handleDrop.bind(stateNode);
element.addEventListener('drop', stateNode.handleDrop);
stateNode.forceUpdate();
}
}
2018-08-29 05:19:18 +02:00
}