Merge pull request #253 from JsSucks/package-installer

Package installer
This commit is contained in:
Alexei Stukov 2018-08-31 08:36:45 +03:00 committed by GitHub
commit 990453cdad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 1057 additions and 513 deletions

View File

@ -36,5 +36,5 @@ export default new class TwentyFourHour extends BuiltinModule {
if (matched[3] === 'AM') return returnValue.replace(matched[0], `${matched[1] === '12' ? '00' : matched[1].padStart(2, '0')}:${matched[2]}`)
return returnValue.replace(matched[0], `${parseInt(matched[1]) + 12}:${matched[2]}`)
}
}

View File

@ -222,6 +222,13 @@
{
"id": "default",
"settings": [
{
"id": "unsafe-content",
"type": "bool",
"text": "Allow unverified content",
"hint": "Allow loading unverified plugins/themes",
"value": "false"
},
{
"id": "tracking-protection",
"type": "bool",

View File

@ -8,14 +8,18 @@
* LICENSE file in the root directory of this source tree.
*/
import asar from 'asar';
import path, { dirname } from 'path';
import rimraf from 'rimraf';
import Content from './content';
import Globals from './globals';
import Database from './database';
import { Utils, FileUtils, ClientLogger as Logger } from 'common';
import { SettingsSet, ErrorEvent } from 'structs';
import { Modals } from 'ui';
import path from 'path';
import Combokeys from 'combokeys';
import Settings from './settings';
/**
* Base class for managing external content
@ -77,12 +81,20 @@ export default class {
const directories = await FileUtils.listDirectory(this.contentPath);
for (const dir of directories) {
try {
await FileUtils.directoryExists(path.join(this.contentPath, dir));
} catch (err) { continue; }
const packed = dir.endsWith('.bd');
if (!packed) {
try {
await FileUtils.directoryExists(path.join(this.contentPath, dir));
} catch (err) { continue; }
}
try {
await this.preloadContent(dir);
if (packed) {
await this.preloadPackedContent(dir);
} else {
await this.preloadContent(dir);
}
} catch (err) {
this.errors.push(new ErrorEvent({
module: this.moduleName,
@ -120,6 +132,8 @@ export default class {
const directories = await FileUtils.listDirectory(this.contentPath);
for (const dir of directories) {
const packed = dir.endsWith('.bd');
// If content is already loaded this should resolve
if (this.getContentByDirName(dir)) continue;
@ -173,6 +187,30 @@ export default class {
}
}
static async preloadPackedContent(pkg, reload = false, index) {
try {
const packagePath = path.join(this.contentPath, pkg);
const packageName = pkg.replace('.bd', '');
await FileUtils.fileExists(packagePath);
const config = JSON.parse(asar.extractFile(packagePath, 'config.json').toString());
const unpackedPath = path.join(Globals.getPath('tmp'), packageName);
asar.extractAll(packagePath, unpackedPath);
return this.preloadContent({
config,
contentPath: unpackedPath,
packagePath: packagePath,
pkg,
packageName,
packed: true
}, reload, index);
} catch (err) {
throw err;
}
}
/**
* Common loading procedure for loading content before passing it to the actual loader
* @param {any} dirName Base directory for content
@ -181,7 +219,15 @@ export default class {
*/
static async preloadContent(dirName, reload = false, index) {
try {
const contentPath = path.join(this.contentPath, dirName);
const unsafeAllowed = Settings.getSetting('security', 'default', 'unsafe-content').value;
const packed = typeof dirName === 'object' && dirName.packed;
// Block any unpacked content as they can't be verified
if (!packed && !unsafeAllowed) {
throw 'Blocked unsafe content';
}
const contentPath = packed ? dirName.contentPath : path.join(this.contentPath, dirName);
await FileUtils.directoryExists(contentPath);
@ -189,7 +235,7 @@ export default class {
throw { 'message': `Attempted to load already loaded user content: ${path}` };
const configPath = path.resolve(contentPath, 'config.json');
const readConfig = await FileUtils.readJsonFromFile(configPath);
const readConfig = packed ? dirName.config : await FileUtils.readJsonFromFile(configPath);
const mainPath = path.join(contentPath, readConfig.main || 'index.js');
const defaultConfig = new SettingsSet({
@ -213,7 +259,7 @@ export default class {
}
} catch (err) {
// We don't care if this fails it either means that user config doesn't exist or there's something wrong with it so we revert to default config
Logger.warn(this.moduleName, [`Failed reading config for ${this.contentType} ${readConfig.info.name} in ${dirName}`, err]);
Logger.warn(this.moduleName, [`Failed reading config for ${this.contentType} ${readConfig.info.name} in ${packed ? dirName.pkg : dirName}`, err]);
}
userConfig.config = defaultConfig.clone({ settings: userConfig.config });
@ -243,16 +289,22 @@ export default class {
mainPath
};
const content = await this.loadContent(paths, configs, readConfig.info, readConfig.main, readConfig.dependencies, readConfig.permissions, readConfig.mainExport);
const content = await this.loadContent(paths, configs, readConfig.info, readConfig.main, readConfig.dependencies, readConfig.permissions, readConfig.mainExport, packed ? dirName : false);
if (!content) return undefined;
if (!reload && this.getContentById(content.id))
throw {message: `A ${this.contentType} with the ID ${content.id} already exists.`};
throw { message: `A ${this.contentType} with the ID ${content.id} already exists.` };
if (reload) this.localContent.splice(index, 1, content);
else this.localContent.push(content);
return content;
} catch (err) {
throw err;
} finally {
if (typeof dirName === 'object' && dirName.packed) {
rimraf(dirName.contentPath, err => {
if (err) Logger.err(err);
});
}
}
}
@ -309,14 +361,7 @@ export default class {
if (this.unloadContentHook) this.unloadContentHook(content);
if (reload) {
const newcontent = await this.preloadContent(content.dirName, true, index);
if (newcontent.enabled) {
newcontent.userConfig.enabled = false;
newcontent.start(false);
}
return newcontent;
}
if (reload) return content.packed ? this.preloadPackedContent(content.packed.pkg, true, index) : this.preloadContent(content.dirName, true, index);
this.localContent.splice(index, 1);
} catch (err) {

View File

@ -8,6 +8,7 @@
* LICENSE file in the root directory of this source tree.
*/
import path from 'path';
import sparkplug from 'sparkplug';
import { ClientIPC } from 'common';
import Module from './module';
@ -35,6 +36,10 @@ export default new class extends Module {
async first() {
const config = await ClientIPC.send('getConfig');
config.paths.push({
id: 'tmp',
path: path.join(config.paths.find(p => p.id === 'base').path, 'tmp')
});
this.setState({ config });
// This is for Discord to stop error reporting :3

View File

@ -27,3 +27,4 @@ export { default as Connectivity } from './connectivity';
export { default as Security } from './security';
export { default as Cache } from './cache';
export { default as Reflection } from './reflection/index';
export { default as PackageInstaller } from './packageinstaller';

View File

@ -0,0 +1,172 @@
import EventListener from './eventlistener';
import asar from 'asar';
import fs from 'fs';
import path from 'path';
import rimraf from 'rimraf';
import { request } from 'vendor';
import { Modals } from 'ui';
import { Utils } from 'common';
import PluginManager from './pluginmanager';
import Globals from './globals';
import Security from './security';
import { ReactComponents } from './reactcomponents';
import Reflection from './reflection';
import DiscordApi from './discordapi';
export default class PackageInstaller {
/**
* Handler for drag and drop package install
* @param {String} filePath Path to local file
* @param {String} channelId Current channel id
*/
static async dragAndDropHandler(filePath, channelId) {
try {
const config = JSON.parse(asar.extractFile(filePath, 'config.json').toString());
const { info, main } = config;
let icon = null;
if (info.icon && info.icon_type) {
const extractIcon = asar.extractFile(filePath, info.icon);
icon = `data:${info.icon_type};base64,${Utils.arrayBufferToBase64(extractIcon)}`;
}
const isPlugin = info.type && info.type === 'plugin' || main.endsWith('.js');
// Show install modal
const modalResult = await Modals.installModal(isPlugin ? 'plugin' : 'theme', config, filePath, icon).promise;
if (modalResult === 0) {
// Upload it instead
}
} catch (err) {
console.log(err);
}
}
/**
* Hash and verify a package
* @param {Byte[]|String} bytesOrPath byte array of binary or path to local file
* @param {String} id Package id
*/
static async verifyPackage(bytesOrPath, id) {
const bytes = typeof bytesOrPath === 'string' ? fs.readFileSync(bytesOrPath) : bytesOrPath;
// Temporary hash to simulate response from server
const tempVerified = ['2e3532ee366816adc37b0f478bfef35e03f96e7aeee9b115f5918ef6a4e94de8', '06a2eb4e37b926354ab80cd83207db67e544c932e9beddce545967a21f8db5aa'];
const hashBytes = Security.hash('sha256', bytes, 'hex');
return tempVerified.includes(hashBytes);
}
// TODO lots of stuff
/**
* Installs or updates defined package
* @param {Byte[]|String} bytesOrPath byte array of binary or path to local file
* @param {String} name Package name
* @param {Boolean} update Does an older version already exist
*/
static async installPackage(bytesOrPath, id, update = false) {
let outputPath = null;
try {
const bytes = typeof bytesOrPath === 'string' ? fs.readFileSync(bytesOrPath) : bytesOrPath;
const outputName = `${id}.bd`;
outputPath = path.join(Globals.getPath('plugins'), outputName);
fs.writeFileSync(outputPath, bytes);
if (!update) return PluginManager.preloadPackedContent(outputName);
const oldContent = PluginManager.getPluginById(id);
if (update && oldContent.packed && oldContent.packed.packageName !== id) {
await oldContent.unload(true);
rimraf(oldContent.packed.packagePath, err => {
if(err) console.log(err);
});
return PluginManager.preloadPackedContent(outputName);
}
if (update && !oldContent.packed) {
await oldContent.unload(true);
rimraf(oldContent.contentPath, err => {
if (err) console.log(err);
});
return PluginManager.preloadPackedContent(outputName);
}
return PluginManager.reloadContent(oldContent);
} catch (err) {
if (outputPath) {
rimraf(outputPath, err => {
if (err) console.log(err);
});
}
throw err;
}
}
/**
* Install package from remote location. Only github/bdapi is supoorted.
* @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,
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);
await this.dragAndDropHandler(outputPath);
rimraf(outputPath, err => {
if (err) console.log(err);
});
} catch (err) {
throw err;
}
}
/**
* Patches Discord upload area for .bd files
*/
static async uploadAreaPatch() {
const { selector } = Reflection.resolve('uploadArea');
this.UploadArea = await ReactComponents.getComponent('UploadArea', { selector });
const reflect = Reflection.DOM(selector);
const stateNode = reflect.getComponentStateNode(this.UploadArea);
const callback = function (e) {
if (!e.dataTransfer.files.length || !e.dataTransfer.files[0].name.endsWith('.bd')) return;
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
stateNode.clearDragging();
PackageInstaller.dragAndDropHandler(e.dataTransfer.files[0].path, DiscordApi.currentChannel.id);
};
// Remove their handler, add ours, then read theirs to give ours priority to stop theirs when we get a .bd file.
reflect.element.removeEventListener('drop', stateNode.handleDrop);
reflect.element.addEventListener('drop', callback);
reflect.element.addEventListener('drop', stateNode.handleDrop);
this.unpatchUploadArea = function () {
reflect.element.removeEventListener('drop', callback);
};
}
}

View File

@ -74,7 +74,7 @@ export default class extends ContentManager {
static get refreshPlugins() { return this.refreshContent }
static get loadContent() { return this.loadPlugin }
static async loadPlugin(paths, configs, info, main, dependencies, permissions, mainExport) {
static async loadPlugin(paths, configs, info, main, dependencies, permissions, mainExport, packed = false) {
if (permissions && permissions.length > 0) {
for (const perm of permissions) {
Logger.log(this.moduleName, `Permission: ${Permissions.permissionText(perm).HEADER} - ${Permissions.permissionText(perm).BODY}`);
@ -112,11 +112,18 @@ export default class extends ContentManager {
configs, info, main,
paths: {
contentPath: paths.contentPath,
dirName: paths.dirName,
dirName: packed ? packed.packageName : paths.dirName,
mainPath: paths.mainPath
}
});
if (packed) instance.packed = {
pkg: packed.pkg,
packageName: packed.packageName,
packagePath: packed.packagePath,
packed: true
}; else instance.packed = false;
if (instance.enabled && this.loaded) {
instance.userConfig.enabled = false;
instance.start(false);

View File

@ -15,6 +15,7 @@ import { Utils, Filters, ClientLogger as Logger } from 'common';
import { MonkeyPatch } from './patcher';
import Reflection from './reflection/index';
import DiscordApi from './discordapi';
import PackageInstaller from './packageinstaller';
class Helpers {
static get plannedActions() {
@ -501,28 +502,6 @@ export class ReactAutoPatcher {
}
static async patchUploadArea() {
const { selector } = Reflection.resolve('uploadArea');
this.UploadArea = await ReactComponents.getComponent('UploadArea', {selector});
const reflect = Reflection.DOM(selector);
const stateNode = reflect.getComponentStateNode(this.UploadArea);
const callback = function(e) {
if (!e.dataTransfer.files.length || !e.dataTransfer.files[0].name.endsWith('.bd')) return;
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
stateNode.clearDragging();
Modals.confirm('Function not ready', `You tried to install "${e.dataTransfer.files[0].path}", but installing .bd files isn't ready yet.`)
// Possibly something like Events.emit('install-file', e.dataTransfer.files[0]);
};
// Remove their handler, add ours, then readd theirs to give ours priority to stop theirs when we get a .bd file.
reflect.element.removeEventListener('drop', stateNode.handleDrop);
reflect.element.addEventListener('drop', callback);
reflect.element.addEventListener('drop', stateNode.handleDrop);
this.unpatchUploadArea = function() {
reflect.element.removeEventListener('drop', callback);
};
PackageInstaller.uploadAreaPatch();
}
}

View File

@ -8,3 +8,4 @@
@import './settings-modal';
@import './permission-modal';
@import './input-modal';
@import './install-modal';

View File

@ -0,0 +1,231 @@
.bd-installModal {
.bd-modalInner {
min-width: 520px;
min-height: 200px;
@at-root .bd-err#{&} {
border: 2px solid $colerr;
}
.bd-modalBody {
flex-grow: 1;
padding: 0;
.bd-installModalBody {
display: flex;
flex-direction: column;
flex-grow: 1;
.bd-installModalTop {
padding: 0 15px;
display: flex;
height: 100px;
span {
font-weight: 700;
}
.bd-installModalIcon {
background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgMTAwIDEzMCIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgMTAwIDEzMDsiIHhtbDpzcGFjZT0icHJlc2VydmUiPjxzdHlsZSB0eXBlPSJ0ZXh0L2NzcyI+LnN0MHtmaWxsOiNGNEY2RkM7fS5zdDF7ZmlsbDpub25lO3N0cm9rZTojMzYzOTNFO3N0cm9rZS13aWR0aDoyO30uc3Qye29wYWNpdHk6MC42O2ZpbGw6bm9uZTtzdHJva2U6IzM2MzkzRTtzdHJva2Utd2lkdGg6MjtlbmFibGUtYmFja2dyb3VuZDpuZXcgICAgO308L3N0eWxlPjxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik0xNSwxOWMwLTMuMywyLjctNiw2LTZoNDRjNC43LDAsMjAsMTUuMywyMCwyMHY3NGMwLDMuMy0yLjcsNi02LDZIMjFjLTMuMywwLTYtMi43LTYtNlYxOXoiLz48cGF0aCBjbGFzcz0ic3QxIiBkPSJNMTUsMTljMC0zLjMsMi43LTYsNi02aDQ0YzQuNywwLDIwLDE1LjMsMjAsMjB2NzRjMCwzLjMtMi43LDYtNiw2SDIxYy0zLjMsMC02LTIuNy02LTZWMTl6Ii8+PHBhdGggY2xhc3M9InN0MCIgZD0iTTY2LDE2YzAtMy4zLDEuOS00LjEsNC4yLTEuOGwxMy41LDEzLjVjMi4zLDIuMywxLjYsNC4yLTEuOCw0LjJINjljLTEuNywwLTMtMS4zLTMtM1YxNkw2NiwxNnoiLz48cGF0aCBjbGFzcz0ic3QxIiBkPSJNNjYsMTZjMC0zLjMsMS45LTQuMSw0LjItMS44bDEzLjUsMTMuNWMyLjMsMi4zLDEuNiw0LjItMS44LDQuMkg2OWMtMS43LDAtMy0xLjMtMy0zVjE2TDY2LDE2eiIvPjxwYXRoIGNsYXNzPSJzdDIiIGQ9Ik02MSwxN0gyM2MtMi44LDAtNSwyLjItNSw1djciLz48cGF0aCBjbGFzcz0ic3QyIiBkPSJNNjksMTl2Ny41YzAsMS40LDEuMSwyLjUsMi41LDIuNUg3NCIvPjwvc3ZnPg==');
background-size: 100% 100%;
flex-shrink: 0;
width: 100px;
height: 130px;
position: fixed;
border-radius: 3px;
margin-top: -30px;
display: flex;
align-content: center;
justify-content: center;
align-items: center;
justify-items: center;
@at-root .bd-err#{&} {
background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgMTAwIDEzMCIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgMTAwIDEzMDsiIHhtbDpzcGFjZT0icHJlc2VydmUiPjxzdHlsZSB0eXBlPSJ0ZXh0L2NzcyI+LnN0MHtmaWxsOiNGNEY2RkM7fS5zdDF7ZmlsbDpub25lO3N0cm9rZTojRDg0MDQwO3N0cm9rZS13aWR0aDoyO30uc3Qye29wYWNpdHk6MC42O2ZpbGw6bm9uZTtzdHJva2U6I0Q4NDA0MDtzdHJva2Utd2lkdGg6MjtlbmFibGUtYmFja2dyb3VuZDpuZXcgICAgO308L3N0eWxlPjxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik0xNSwxOWMwLTMuMywyLjctNiw2LTZoNDRjNC43LDAsMjAsMTUuMywyMCwyMHY3NGMwLDMuMy0yLjcsNi02LDZIMjFjLTMuMywwLTYtMi43LTYtNlYxOXoiLz48cGF0aCBjbGFzcz0ic3QxIiBkPSJNMTUsMTljMC0zLjMsMi43LTYsNi02aDQ0YzQuNywwLDIwLDE1LjMsMjAsMjB2NzRjMCwzLjMtMi43LDYtNiw2SDIxYy0zLjMsMC02LTIuNy02LTZWMTl6Ii8+PHBhdGggY2xhc3M9InN0MCIgZD0iTTY2LDE2YzAtMy4zLDEuOS00LjEsNC4yLTEuOGwxMy41LDEzLjVjMi4zLDIuMywxLjYsNC4yLTEuOCw0LjJINjljLTEuNywwLTMtMS4zLTMtM1YxNkw2NiwxNnoiLz48cGF0aCBjbGFzcz0ic3QxIiBkPSJNNjYsMTZjMC0zLjMsMS45LTQuMSw0LjItMS44bDEzLjUsMTMuNWMyLjMsMi4zLDEuNiw0LjItMS44LDQuMkg2OWMtMS43LDAtMy0xLjMtMy0zVjE2TDY2LDE2eiIvPjxwYXRoIGNsYXNzPSJzdDIiIGQ9Ik02MSwxN0gyM2MtMi44LDAtNSwyLjItNSw1djciLz48cGF0aCBjbGFzcz0ic3QyIiBkPSJNNjksMTl2Ny41YzAsMS40LDEuMSwyLjUsMi41LDIuNUg3NCIvPjwvc3ZnPg==');
}
.bd-installModalCi {
width: 40px;
height: 40px;
}
svg {
fill: #36393e;
width: 40px;
height: 40px;
}
}
.bd-installModalInfo {
display: flex;
flex-grow: 1;
flex-direction: column;
padding: 15px 10px;
margin-left: 100px;
}
.bd-installModalDesc {
margin-top: 5px;
}
}
.bd-installModalBottom {
padding: 0 15px;
flex-grow: 1;
.bd-permScope {
&:last-child {
.bd-permInner {
border: 0;
}
}
}
}
}
}
.bd-installModalFooter {
display: flex;
justify-content: flex-end;
padding: 10px;
background: rgba(0, 0, 0, .1);
.bd-installModalStatus {
height: 36px;
line-height: 36px;
flex-grow: 1;
padding: 0 25px;
font-weight: 600;
color: #fff;
&.bd-ok {
color: $colok;
}
&.bd-err {
color: $colerr;
}
}
.bd-button {
font-size: 16px;
padding: 10px;
border-radius: 3px;
min-width: 100px;
transition: opacity .2s ease-in-out;
&.bd-ok {
background: #3ecc9c;
}
&.bd-err {
color: #fff;
}
&:hover {
opacity: .8;
}
&.bd-installModalUpload {
background: transparent;
transition: none;
&:hover {
opacity: 1;
text-decoration: underline;
}
}
}
}
}
&.bd-installModalDone {
.bd-modalInner {
min-width: 400px;
max-width: 400px;
height: 250px;
border: 2px solid $colok;
.bd-installModalBody {
display: flex;
flex-grow: 1;
align-content: center;
justify-content: center;
align-items: center;
h3 {
color: $colok;
font-size: 1.2em;
font-weight: 700;
text-align: center;
padding: 20px;
}
}
.bd-materialDesignIcon {
svg {
fill: $colok;
width: 100px;
height: 100px;
}
}
.bd-installModalFooter {
.bd-button {
margin-left: 5px;
}
}
}
}
&.bd-installModalFail {
.bd-modalInner {
min-width: 400px;
max-width: 400px;
height: 250px;
border: 2px solid $colerr;
.bd-installModalBody {
display: flex;
flex-grow: 1;
align-content: center;
justify-content: center;
align-items: center;
h3 {
color: $colerr;
font-size: 1.2em;
font-weight: 700;
text-align: center;
padding: 20px;
}
}
.bd-materialDesignIcon {
svg {
fill: $colerr;
width: 100px;
height: 100px;
}
}
.bd-installModalFooter {
&.bd-installModalErrMsg {
justify-content: flex-start;
padding: 10px;
background: rgba(0, 0, 0, .2);
font-weight: 700;
span {
color: #fff;
flex-grow: 1;
text-align: right;
}
}
}
}
}
}

View File

@ -12,6 +12,7 @@
<Card :item="plugin">
<SettingSwitch v-if="plugin.type === 'plugin'" slot="toggle" :value="plugin.enabled" @input="$emit('toggle-plugin')" />
<ButtonGroup slot="controls">
<Button v-if="devmode && !plugin.packed" v-tooltip="'Package Plugin'" @click="package"><MiBoxDownload size="18"/></Button>
<Button v-tooltip="'Settings (shift + click to open settings without cloning the set)'" v-if="plugin.hasSettings" @click="$emit('show-settings', $event.shiftKey)"><MiSettings size="18" /></Button>
<Button v-tooltip="'Reload'" @click="$emit('reload-plugin')"><MiRefresh size="18" /></Button>
<Button v-tooltip="'Edit'" @click="editPlugin"><MiPencil size="18" /></Button>
@ -22,18 +23,47 @@
<script>
// Imports
import asar from 'asar';
import electron from 'electron';
import fs from 'fs';
import { Toasts } from 'ui';
import { Settings } from 'modules';
import { ClientLogger as Logger } from 'common';
import { shell } from 'electron';
import Card from './Card.vue';
import { Button, ButtonGroup, SettingSwitch, MiSettings, MiRefresh, MiPencil, MiDelete, MiExtension } from '../common';
import { Button, ButtonGroup, SettingSwitch, MiSettings, MiRefresh, MiPencil, MiDelete, MiExtension, MiBoxDownload } from '../common';
export default {
data() {
return {
devmode: Settings.getSetting('core', 'advanced', 'developer-mode').value
}
},
props: ['plugin'],
components: {
Card, Button, ButtonGroup, SettingSwitch,
MiSettings, MiRefresh, MiPencil, MiDelete, MiExtension
MiSettings, MiRefresh, MiPencil, MiDelete, MiExtension, MiBoxDownload
},
methods: {
package() {
electron.remote.dialog.showSaveDialog({
title: 'Save Plugin Package',
defaultPath: this.plugin.name,
filters: [
{
name: 'BetterDiscord Package',
extensions: ['bd']
}
]
}, filepath => {
if (!filepath) return;
asar.uncache(filepath);
asar.createPackage(this.plugin.contentPath, filepath, () => {
Toasts.success('Plugin Packaged!');
});
});
},
editPlugin() {
try {
shell.openItem(this.plugin.contentPath);

View File

@ -0,0 +1,124 @@
<template>
<Modal class="bd-installModal" :headertext="modal.title" :closing="modal.closing" @close="modal.close" :noheader="true" :class="{'bd-err': !verifying && !verified, 'bd-installModalDone': installed, 'bd-installModalFail': err}">
<template v-if="!installed && !err">
<div slot="body" class="bd-installModalBody">
<div class="bd-installModalTop">
<div class="bd-installModalIcon">
<div v-if="modal.icon" class="bd-installModalCi" :style="{backgroundImage: `url(${modal.icon})`}" />
<MiExtension v-else />
</div>
<div class="bd-installModalInfo">
<span>{{modal.config.info.name}} v{{modal.config.info.version}} by {{modal.config.info.authors.map(a => a.name).join(', ')}}</span>
<div class="bd-installModalDesc">
{{modal.config.info.description}}
</div>
</div>
</div>
<div class="bd-installModalBottom">
<div v-for="(perm, i) in modal.config.permissions" :key="`perm-${i}`" class="bd-permScope">
<div class="bd-permAllow">
<div class="bd-permCheck">
<div class="bd-permCheckInner"></div>
</div>
<div class="bd-permInner">
<div class="bd-permName">{{perm.HEADER}}</div>
<div class="bd-permDesc">{{perm.BODY}}</div>
</div>
</div>
</div>
</div>
</div>
<div v-if="verifying" slot="footer" class="bd-installModalFooter">
<span class="bd-installModalStatus">Verifying {{modal.contentType}}</span>
</div>
<div v-else-if="!verified" slot="footer" class="bd-installModalFooter">
<span class="bd-installModalStatus bd-err">Not verified!</span>
<div class="bd-button bd-installModalUpload" @click="modal.confirm(0); modal.close();">Upload</div>
<div class="bd-button bd-err" @click="install" v-if="allowUnsafe">{{ !alreadyInstalled ? 'Install' : 'Update' }}</div>
</div>
<div v-else-if="alreadyInstalled && upToDate" slot="footer" class="bd-installModalFooter">
<span class="bd-installModalStatus">Up to date version already installed!</span>
<div class="bd-button bd-installModalUpload" @click="modal.confirm(0); modal.close();">Upload</div>
</div>
<div v-else slot="footer" class="bd-installModalFooter">
<span class="bd-installModalStatus bd-ok">Verified!</span>
<div class="bd-button bd-installModalUpload" @click="modal.confirm(0); modal.close();">Upload</div>
<div class="bd-button bd-ok" @click="install">{{ !alreadyInstalled ? 'Install' : 'Update' }}</div>
</div>
</template>
<template v-else-if="err">
<div slot="body" class="bd-installModalBody">
<h3>Something went wrong :(</h3>
<MiError />
</div>
<div slot="footer" class="bd-installModalFooter bd-installModalErrMsg">
{{err.message}}
<span>Ctrl+Shift+I</span>
</div>
</template>
<template v-else>
<div slot="body" class="bd-installModalBody">
<h3>{{alreadyInstalled ? 'Succesfully Updated!' : 'Successfully Installed!'}}</h3>
<MiSuccessCircle/>
</div>
<div slot="footer" class="bd-installModalFooter">
<div class="bd-button bd-ok" v-if="installed.hasSettings" @click="showSettingsModal">Settings</div>
<div class="bd-button bd-ok" @click="modal.confirm(); modal.close();">OK</div>
</div>
</template>
</Modal>
</template>
<script>
// Imports
import { Modal, MiExtension, MiSuccessCircle, MiError } from '../../common';
import { PluginManager, ThemeManager, PackageInstaller, Settings } from 'modules';
export default {
data() {
return {
installing: false,
verifying: true,
alreadyInstalled: false,
upToDate: true,
allowUnsafe: Settings.getSetting('security', 'default', 'unsafe-content').value,
installed: false,
err: null
}
},
props: ['modal'],
components: {
Modal, MiExtension, MiSuccessCircle, MiError
},
mounted() {
const { contentType, config } = this.modal;
const alreadyInstalled = contentType === 'plugin' ? PluginManager.getPluginById(config.info.id) : ThemeManager.getContentById(config.info.id);
if (alreadyInstalled) {
this.alreadyInstalled = true;
if (config.info.version > alreadyInstalled.version) {
this.upToDate = false;
}
}
this.verify();
},
methods: {
async verify() {
const verified = await PackageInstaller.verifyPackage(this.modal.filePath);
this.verified = verified;
this.verifying = false;
},
async install() {
try {
const installed = await PackageInstaller.installPackage(this.modal.filePath, this.modal.config.info.id, this.alreadyInstalled);
this.installed = installed;
} catch (err) {
console.log(err);
this.err = err;
}
},
showSettingsModal() {
this.installed.showSettingsModal();
}
}
}
</script>

View File

@ -22,3 +22,5 @@ export { default as MiLock } from './materialicons/Lock.vue';
export { default as MiImagePlus } from './materialicons/ImagePlus.vue';
export { default as MiIcVpnKey } from './materialicons/IcVpnKey.vue';
export { default as MiArrowLeft } from './materialicons/ArrowLeft.vue';
export { default as MiBoxDownload } from './materialicons/BoxDownload.vue';
export { default as MiSuccessCircle } from './materialicons/SuccessCircle.vue';

View File

@ -11,7 +11,7 @@
<template>
<div :class="['bd-modal', {'bd-modalOut': closing, 'bd-modalScrolled': scrolled}]">
<div class="bd-modalInner">
<div class="bd-modalHeader">
<div class="bd-modalHeader" v-if="!noheader">
<slot name="header">
<div v-if="$slots.icon" class="bd-modalIcon">
<slot name="icon" />
@ -41,7 +41,7 @@
import { MiClose } from './MaterialIcon';
export default {
props: ['headertext', 'closing'],
props: ['headertext', 'closing', 'noheader'],
components: {
MiClose
},

View File

@ -0,0 +1,27 @@
/**
* BetterDiscord Material Design Icon
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* 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.
*
* Material Design Icons
* Copyright (c) 2014 Google
* Apache 2.0 LICENSE
* https://www.apache.org/licenses/LICENSE-2.0.txt
*/
<template>
<span class="bd-materialDesignIcon">
<svg :width="size || 24" :height="size || 24" viewBox="0 0 24 24">
<path d="M 4.9956,3L 19.0076,3L 20.7359,5.99339L 20.7304,5.99653C 20.9018,6.29149 21,6.63428 21,7L 21,19C 21,20.1046 20.1046,21 19,21L 5,21C 3.89543,21 3,20.1046 3,19L 3,7C 3,6.638 3.09618,6.29846 3.26437,6.00554L 3.26135,6.0038L 4.9956,3 Z M 5.57294,4.00001L 4.99559,5.00001L 5,5.00001L 19.0076,5.00002L 18.4303,4.00001L 5.57294,4.00001 Z M 7,12L 12,17L 17,12L 14,12L 14,10L 10,10L 10,12L 7,12 Z "/>
</svg>
</span>
</template>
<script>
export default {
props: ['size']
}
</script>

View File

@ -0,0 +1,27 @@
/**
* BetterDiscord Material Design Icon
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* 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.
*
* Material Design Icons
* Copyright (c) 2014 Google
* Apache 2.0 LICENSE
* https://www.apache.org/licenses/LICENSE-2.0.txt
*/
<template>
<span class="bd-materialDesignIcon">
<svg :width="size || 24" :height="size || 24" viewBox="0 0 24 24">
<path d="M 19.9994,11.9981C 19.9994,16.4161 16.4174,19.9981 11.9994,19.9981C 7.58139,19.9981 3.99939,16.4161 3.99939,11.9981C 3.99939,7.58007 7.58139,3.99807 11.9994,3.99807C 12.7634,3.99807 13.5004,4.11207 14.2004,4.31207L 15.7724,2.74007C 14.6074,2.26467 13.3354,1.99807 11.9994,1.99807C 6.47638,1.99807 1.99939,6.47507 1.99939,11.9981C 1.99939,17.5211 6.47638,21.9981 11.9994,21.9981C 17.5224,21.9981 21.9994,17.5211 21.9994,11.9981M 7.91339,10.0841L 6.49939,11.4981L 10.9994,15.9981L 20.9994,5.99807L 19.5854,4.58407L 10.9994,13.1701L 7.91339,10.0841 Z " />
</svg>
</span>
</template>
<script>
export default {
props: ['size']
}
</script>

View File

@ -17,6 +17,7 @@ import ErrorModal from './components/bd/modals/ErrorModal.vue';
import SettingsModal from './components/bd/modals/SettingsModal.vue';
import PermissionModal from './components/bd/modals/PermissionModal.vue';
import InputModal from './components/bd/modals/InputModal.vue';
import InstallModal from './components/bd/modals/InstallModal.vue';
let modals = 0;
@ -190,6 +191,19 @@ export default class Modals {
return new Modal(modal, InputModal);
}
static installModal(contentType, config, filePath, icon) {
return this.add(this.createInstallModal(contentType, config, filePath, icon));
}
static createInstallModal(contentType, config, filePath, icon) {
const modal = { contentType, config, filePath, icon };
modal.promise = new Promise((resolve, reject) => {
modal.confirm = value => resolve(value);
modal.beforeClose = () => reject();
});
return new Modal(modal, InstallModal);
}
/**
* Creates a new permissions modal and adds it to the open stack.
* The modal will have a promise property that will be set to a Promise object that is resolved or rejected if the user accepts the permissions or closes the modal.

View File

@ -32,6 +32,7 @@ module.exports = {
},
externals: {
electron: 'require("electron")',
asar: 'require("asar")',
fs: 'require("fs")',
path: 'require("path")',
util: 'require("util")',

775
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -21,11 +21,11 @@
"fs-extra": "^7.0.0",
"keytar": "4.2.1",
"nedb": "^1.8.0",
"node-sass": "^4.9.2"
"node-sass": "^4.9.2",
"asar": "^0.14.3"
},
"devDependencies": {
"aes256": "^1.0.4",
"archiver": "^2.1.1",
"babel-core": "^6.26.3",
"babel-loader": "^7.1.5",
"babel-preset-env": "^1.7.0",

View File

@ -2,7 +2,7 @@
"info": {
"name": "Example Module",
"authors": [ "Jiiks" ],
"version": 1.0,
"version": "1.0",
"description": "Module Example"
},
"main": "index.js",

View File

@ -3,7 +3,7 @@
"id": "depend-error",
"name": "Depend Error",
"authors": [ "Jiiks" ],
"version": 1.0,
"version": "1.0",
"description": "Depend Error Plugin Description",
"icon": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgdmVyc2lvbj0iMS4xIiBpZD0iQ2FscXVlXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgMjAwMCAyMDAwIiBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDAgMCAyMDAwIDIwMDAiIHhtbDpzcGFjZT0icHJlc2VydmUiPjxnPjxwYXRoIGZpbGw9IiMzRTgyRTUiIGQ9Ik0xNDAyLjIsNjMxLjdjLTkuNy0zNTMuNC0yODYuMi00OTYtNjQyLjYtNDk2SDY4LjR2NzE0LjFsNDQyLDM5OFY0OTAuN2gyNTdjMjc0LjUsMCwyNzQuNSwzNDQuOSwwLDM0NC45SDU5Ny42djMyOS41aDE2OS44YzI3NC41LDAsMjc0LjUsMzQ0LjgsMCwzNDQuOGgtNjk5djM1NC45aDY5MS4yYzM1Ni4zLDAsNjMyLjgtMTQyLjYsNjQyLjYtNDk2YzAtMTYyLjYtNDQuNS0yODQuMS0xMjIuOS0zNjguNkMxMzU3LjcsOTE1LjgsMTQwMi4yLDc5NC4zLDE0MDIuMiw2MzEuN3oiLz48cGF0aCBmaWxsPSIjRkZGRkZGIiBkPSJNMTI2Mi41LDEzNS4yTDEyNjIuNSwxMzUuMmwtNzYuOCwwYzI2LjYsMTMuMyw1MS43LDI4LjEsNzUsNDQuM2M3MC43LDQ5LjEsMTI2LjEsMTExLjUsMTY0LjYsMTg1LjNjMzkuOSw3Ni42LDYxLjUsMTY1LjYsNjQuMywyNjQuNmwwLDEuMnYxLjJjMCwxNDEuMSwwLDU5Ni4xLDAsNzM3LjF2MS4ybDAsMS4yYy0yLjcsOTktMjQuMywxODgtNjQuMywyNjQuNmMtMzguNSw3My44LTkzLjgsMTM2LjItMTY0LjYsMTg1LjNjLTIyLjYsMTUuNy00Ni45LDMwLjEtNzIuNiw0My4xaDcyLjVjMzQ2LjIsMS45LDY3MS0xNzEuMiw2NzEtNTY3LjlWNzE2LjdDMTkzMy41LDMxMi4yLDE2MDguNywxMzUuMiwxMjYyLjUsMTM1LjJ6Ii8+PC9nPjwvc3ZnPg=="
},

View File

@ -10,7 +10,7 @@
"twitter_username": "Jiiksi"
}
],
"version": 1.0,
"version": "1.0,"
"description": "Edit messages by double clicking them. This is a v1 fix"
},
"main": "index.js"

View File

@ -4,7 +4,7 @@
"authors": [
"Jiiks"
],
"version": 1.0,
"version": "1.0",
"description": "Example Plugin 2 Description"
}
}

View File

@ -17,7 +17,7 @@
"twitter_username": "Jiiksi"
}
],
"version": 1.0,
"version": "1.0",
"description": "A plugin for testing BetterDiscord plugin bridge."
},
"main": "index.js"

View File

@ -11,7 +11,7 @@
"twitter_username": "_samuelelliott"
}
],
"version": 1.0,
"version": "1.0",
"description": "Plugin for testing array setting events as the first example plugin has a lot of stuff in it now."
},
"main": "index.js",

View File

@ -22,7 +22,7 @@
},
"Just a string"
],
"version": 1.0,
"version": "1.0",
"description": "Example Plugin Description.\n\nDescriptions are preformatted (you can use newlines).",
"icon": "icon.svg",
"icon_type": "image/svg+xml"

Binary file not shown.

View File

@ -10,7 +10,7 @@
"twitter_username": "Jiiksi"
}
],
"version": 1.0,
"version": "1.0",
"description": "Patcher Test Description"
},
"main": "index.js"

View File

@ -10,7 +10,7 @@
"twitter_username": "Jiiksi"
}
],
"version": 1.0,
"version": "1.0",
"description": "Permission Test Description"
},
"main": "index.js",

View File

@ -10,7 +10,7 @@
"twitter_username": "Jiiksi"
}
],
"version": 1.0,
"version": "1.0",
"description": "Example for rendering stuff"
},
"main": "index.js"

View File

@ -2,7 +2,7 @@
"info": {
"name": "Example Theme 1",
"authors": [ "Jiiks" ],
"version": 1.0,
"version": "1.0",
"description": "Example Theme 1 Description",
"icon": "icon.svg",
"icon_type": "image/svg+xml",