diff --git a/client/src/modules/csseditor.js b/client/src/modules/csseditor.js index a430bcbc..ebc17c16 100644 --- a/client/src/modules/csseditor.js +++ b/client/src/modules/csseditor.js @@ -35,7 +35,7 @@ export default new class { * Init css editor */ init() { - ClientIPC.on('bd-get-scss', () => this.sendToEditor('set-scss', { scss: this.scss })); + ClientIPC.on('bd-get-scss', () => this.scss, true); ClientIPC.on('bd-update-scss', (e, scss) => this.updateScss(scss)); ClientIPC.on('bd-save-csseditor-bounds', (e, bounds) => this.saveEditorBounds(bounds)); @@ -49,7 +49,7 @@ export default new class { this.sendToEditor('set-liveupdate', event.value); }); - ClientIPC.on('bd-get-liveupdate', () => this.sendToEditor('set-liveupdate', this.liveupdate.value)); + ClientIPC.on('bd-get-liveupdate', () => this.liveupdate.value, true); ClientIPC.on('bd-set-liveupdate', (e, value) => this.liveupdate.value = value); this.watchfilessetting = Settings.getSetting('css', 'default', 'watch-files'); @@ -73,7 +73,7 @@ export default new class { */ async updateScss(scss, sendSource) { if (sendSource) - this.sendToEditor('set-scss', { scss }); + this.sendToEditor('set-scss', scss); if (!scss) { this._scss = this.css = ''; @@ -137,7 +137,7 @@ export default new class { * @param {any} data */ async sendToEditor(channel, data) { - return await ClientIPC.send('sendToCssEditor', { channel, data }); + return ClientIPC.sendToCssEditor(channel, data); } /** @@ -168,7 +168,7 @@ export default new class { */ setState(scss, css, files, err) { this._scss = scss; - this.sendToEditor('set-scss', { scss }); + this.sendToEditor('set-scss', scss); this.css = css; this.files = files; this.error = err; diff --git a/client/src/modules/database.js b/client/src/modules/database.js index 1405b4aa..d926bb9b 100644 --- a/client/src/modules/database.js +++ b/client/src/modules/database.js @@ -8,7 +8,7 @@ * LICENSE file in the root directory of this source tree. */ -import { ClientIPC } from 'bdipc'; +import { ClientIPC } from 'common'; export default class { @@ -42,4 +42,5 @@ export default class { throw err; } } + } diff --git a/client/src/modules/globals.js b/client/src/modules/globals.js index 4971ec2a..bb4792b4 100644 --- a/client/src/modules/globals.js +++ b/client/src/modules/globals.js @@ -9,7 +9,7 @@ */ import sparkplug from 'sparkplug'; -import { ClientIPC } from 'bdipc'; +import { ClientIPC } from 'common'; import Module from './module'; import Events from './events'; diff --git a/client/src/modules/pluginapi.js b/client/src/modules/pluginapi.js index 38284465..dbdc74ce 100644 --- a/client/src/modules/pluginapi.js +++ b/client/src/modules/pluginapi.js @@ -20,7 +20,7 @@ import { SettingsSet, SettingsCategory, Setting, SettingsScheme } from 'structs' import { BdMenuItems, Modals, DOM, Reflection } from 'ui'; import DiscordApi from './discordapi'; import { ReactComponents } from './reactcomponents'; -import { MonkeyPatch } from './patcher'; +import { Patcher, MonkeyPatch } from './patcher'; export default class PluginApi { diff --git a/common/modules/bdipc.js b/common/modules/bdipc.js index 97d598b4..02ab2caf 100644 --- a/common/modules/bdipc.js +++ b/common/modules/bdipc.js @@ -8,51 +8,153 @@ * LICENSE file in the root directory of this source tree. */ -const { ipcRenderer, ipcMain } = require('electron'); +import { ipcRenderer } from 'electron'; -export class ClientIPC { - static on(channel, cb) { - ipcRenderer.on(channel, (event, message) => cb(event, message)); +const callbacks = new WeakMap(); + +export default new class ClientIPC { + + constructor() { + this.on('ping', () => 'pong', true); } - static async send(channel, message) { + /** + * Adds an IPC event listener. + * @param {String} channel The channel to listen on + * @param {Function} callback A function that will be called when a message is received + * @param {Boolean} reply Whether to automatically reply to the message with the callback's return value + * @return {Promise} + */ + on(channel, callback, reply) { channel = channel.startsWith('bd-') ? channel : `bd-${channel}`; - const __eid = Date.now().toString(); - ipcRenderer.send(channel, Object.assign(message ? message : {}, { __eid })); + + const boundCallback = async (event, args) => { + const ipcevent = new BDIpcEvent(event, args); + try { + const r = callback(ipcevent, ipcevent.message); + if (reply) ipcevent.reply(await r); + } catch (err) { + console.error('Error in IPC callback:', err); + if (reply) ipcevent.reject(err); + } + }; + + callbacks.set(callback, boundCallback); + ipcRenderer.on(channel, boundCallback); + } + + off(channel, callback) { + ipcRenderer.removeListener(channel, callbacks.get(callback)); + } + + /** + * Sends a message to the main process and returns a promise that is resolved when the main process replies. + * @param {String} channel The channel to send a message to + * @param {Any} message Data to send to the main process + * @param {Boolean} error Whether to mark the message as an error + * @return {Promise} + */ + async send(channel, message, error) { + channel = channel.startsWith('bd-') ? channel : `bd-${channel}`; + + const eid = 'bd-' + Date.now().toString(); + ipcRenderer.send(channel, { eid, message, error }); + return new Promise((resolve, reject) => { - ipcRenderer.once(__eid, (event, arg) => { - if (arg.err) { - return reject(arg.err); - } - resolve(arg); + ipcRenderer.once(eid, (event, arg) => { + if (arg.error) reject(arg.message); + else resolve(arg.message); }); }); } + + /** + * Sends a message to the Discord window and returns a promise that is resolved when it replies. + * @param {String} channel The channel to send a message to + * @param {Any} message Data to send to the renderer process + * @return {Promise} + */ + sendToDiscord(channel, message) { + return this.send('bd-sendToDiscord', { + channel, message + }); + } + + /** + * Sends a message to the CSS editor window and returns a promise that is resolved when it replies. + * @param {String} channel The channel to send a message to + * @param {Any} message Data to send to the CSS editor window + * @return {Promise} + */ + sendToCssEditor(channel, message) { + return this.send('bd-sendToCssEditor', { + channel, message + }); + } + + ping() { + return this.send('ping'); + } + + getConfig() { + return this.send('getConfig'); + } + + showOpenDialog(options) { + return this.send('native-open', options); + } + + compileSass(options) { + return this.send('compileSass', options); + } + + dba(command) { + return this.send('dba', command); + } + } +/** + * An IPC event. + */ class BDIpcEvent { constructor(event, args) { this.args = args; this.ipcEvent = event; + this.replied = false; } bindings() { - this.send = this.send.bind(this); this.reply = this.reply.bind(this); } - send(message) { - this.ipcEvent.sender.send(this.args.__eid, message); + /** + * Sends a message back to the message's sender. + * @param {Any} message Data to send to this message's sender + */ + get send() { return this.reply } + reply(message, error) { + if (this.replied) + throw {message: 'This message has already been replied to.'}; + + this.replied = true; + return ClientIPC.send(this.eid, message, error); } - reply(message) { - this.send(message); + reject(err) { + return this.reply(err, true); } -} -export class CoreIPC { - static on(channel, cb) { - ipcMain.on(channel, (event, args) => cb(new BDIpcEvent(event, args))); + get message() { + return this.args.message; + } + + get error() { + return this.args.error; + } + + get eid() { + return this.args.eid; } } diff --git a/common/modules/common.js b/common/modules/common.js index 0305c577..41865a34 100644 --- a/common/modules/common.js +++ b/common/modules/common.js @@ -1,4 +1,4 @@ -export { ClientIPC } from './bdipc'; +export { default as ClientIPC } from './bdipc'; export * from './utils'; export { ClientLogger } from './logger'; export { default as AsyncEventEmitter } from './async-eventemitter'; diff --git a/common/modules/logger.js b/common/modules/logger.js index 78f4cee0..6f21848d 100644 --- a/common/modules/logger.js +++ b/common/modules/logger.js @@ -21,7 +21,7 @@ export const logLevels = { 'info': 'info' }; -export class Logger { +export default class Logger { constructor(file) { this.logs = []; diff --git a/core/src/main.js b/core/src/main.js index 3f58fd1c..baf0f5f3 100644 --- a/core/src/main.js +++ b/core/src/main.js @@ -60,48 +60,48 @@ class Comms { } initListeners() { - BDIpc.on('bd-getConfig', o => o.reply(this.bd.config.config)); + BDIpc.on('ping', () => 'pong', true); - BDIpc.on('bd-sendToDiscord', event => this.bd.windowUtils.send(event.args.channel, event.args.message)); + BDIpc.on('bd-getConfig', () => this.bd.config.config, true); - BDIpc.on('bd-openCssEditor', o => this.bd.csseditor.openEditor(o)); - // BDIpc.on('bd-setScss', o => this.bd.csseditor.setSCSS(o.args.scss)); - BDIpc.on('bd-sendToCssEditor', o => this.bd.csseditor.send(o.args.channel, o.args.data)); + BDIpc.on('bd-sendToDiscord', (event, m) => this.sendToDiscord(m.channel, m.message), true); - BDIpc.on('bd-native-open', o => { - dialog.showOpenDialog(BrowserWindow.fromWebContents(o.ipcEvent.sender), o.args, filenames => { - o.reply(filenames); + BDIpc.on('bd-openCssEditor', (event, options) => this.bd.csseditor.openEditor(options), true); + BDIpc.on('bd-sendToCssEditor', (event, m) => this.sendToCssEditor(m.channel, m.message), true); + + BDIpc.on('bd-native-open', (event, options) => { + dialog.showOpenDialog(BrowserWindow.fromWebContents(event.ipcEvent.sender), options, filenames => { + event.reply(filenames); }); }); - BDIpc.on('bd-compileSass', o => { - if (!o.args.path && !o.args.data) return o.reply(''); - if (typeof o.args.path === 'string' && typeof o.args.data === 'string') { - o.args.data = `${o.args.data} @import '${o.args.path.replace(/\\/g, '\\\\').replace(/'/g, '\\\'')}';`; - o.args.path = undefined; + BDIpc.on('bd-compileSass', (event, options) => { + if (typeof options.path === 'string' && typeof options.data === 'string') { + options.data = `${options.data} @import '${options.path.replace(/\\/g, '\\\\').replace(/'/g, '\\\'')}';`; + options.path = undefined; } - sass.render(o.args, (err, result) => { - if (err) return o.reply({ err }); - else o.reply(result); + sass.render(options, (err, result) => { + if (err) event.reject(err); + else event.reply(result); }); }); - BDIpc.on('bd-dba', o => { - (async () => { - try { - o.reply(await this.bd.dbInstance.exec(o.args)); - } catch (err) { - o.reply({ err }); - } - })(); - }); + BDIpc.on('bd-dba', (event, options) => this.bd.dbInstance.exec(options), true); } async send(channel, message) { BDIpc.send(channel, message); } + async sendToDiscord(channel, message) { + return this.bd.windowUtils.send(channel, message); + } + + async sendToCssEditor(channel, message) { + return this.bd.csseditor.send(channel, message); + } + } class BetterDiscord { @@ -156,7 +156,7 @@ class BetterDiscord { const windows = BrowserWindow.getAllWindows(); for (let window of windows) { - BetterDiscord.ignite(window); + if (window) BetterDiscord.ignite(window); } if (windows.length === 1 && windows[0].webContents.getURL().includes('discordapp.com')) { diff --git a/core/src/modules/bdipc.js b/core/src/modules/bdipc.js index 7cfcdb31..de9a9599 100644 --- a/core/src/modules/bdipc.js +++ b/core/src/modules/bdipc.js @@ -8,36 +8,114 @@ * LICENSE file in the root directory of this source tree. */ +const { ipcMain } = require('electron'); + const { Module } = require('./modulebase'); -const { ipcMain } = require('electron'); +const callbacks = new WeakMap(); + +/** + * The IPC module used in the main process. + */ +class BDIpc { + + /** + * Adds an IPC event listener. + * @param {String} channel The channel to listen on + * @param {Function} callback A function that will be called when a message is received + * @param {Boolean} reply Whether to automatically reply to the message with the callback's return value + * @return {Promise} + */ + static on(channel, callback, reply) { + channel = channel.startsWith('bd-') ? channel : `bd-${channel}`; + + const boundCallback = async (event, args) => { + const ipcevent = new BDIpcEvent(event, args); + try { + const r = callback(ipcevent, ipcevent.message); + if (reply) ipcevent.reply(await r); + } catch (err) { + console.error('Error in IPC callback:', err); + if (reply) ipcevent.reject(err); + } + }; + + callbacks.set(callback, boundCallback); + ipcMain.on(channel, boundCallback); + } + + static off(channel, callback) { + ipcMain.removeListener(channel, callbacks.get(callback)); + } + + /** + * Sends a message to the main process and returns a promise that is resolved when the main process replies. + * @param {BrowserWindow} window The window to send a message to + * @param {String} channel The channel to send a message to + * @param {Any} message Data to send to the main process + * @param {Boolean} error Whether to mark the message as an error + * @return {Promise} + */ + static send(window, channel, message, error) { + channel = channel.startsWith('bd-') ? channel : `bd-${channel}`; + + const eid = 'bd-' + Date.now().toString(); + window.send(channel, { eid, message, error }); + + return new Promise((resolve, reject) => { + ipcMain.once(eid, (event, arg) => { + if (arg.error) reject(arg.message); + else resolve(arg.message); + }); + }); + } + + static ping(window) { + return this.send(window, 'ping'); + } + +} class BDIpcEvent extends Module { constructor(event, args) { super(args); this.ipcEvent = event; + this.replied = false; } bindings() { - this.send = this.send.bind(this); this.reply = this.reply.bind(this); } - send(message) { - this.ipcEvent.sender.send(this.args.__eid, message); + /** + * Sends a message back to the message's sender. + * @param {Any} message Data to send to this message's sender + */ + reply(message, error) { + if (this.replied) + throw {message: 'This message has already been replied to.'}; + + this.replied = true; + return BDIpc.send(this.ipcEvent.sender, this.eid, message, error); } - reply(message) { - this.send(message); + reject(err) { + return this.reply(err, true); } -} - -class BDIpc { - static on(channel, cb) { - ipcMain.on(channel, (event, args) => cb(new BDIpcEvent(event, args))); + get message() { + return this.args.message; } + + get error() { + return this.args.error; + } + + get eid() { + return this.args.eid; + } + } module.exports = { BDIpc }; diff --git a/core/src/modules/csseditor.js b/core/src/modules/csseditor.js index 9e50d053..b4920405 100644 --- a/core/src/modules/csseditor.js +++ b/core/src/modules/csseditor.js @@ -13,6 +13,7 @@ const { BrowserWindow } = require('electron'); const { Module } = require('./modulebase'); const { WindowUtils } = require('./utils'); +const { BDIpc } = require('./bdipc'); class CSSEditor extends Module { @@ -23,46 +24,40 @@ class CSSEditor extends Module { } /** - * Opens an editor and replies to an IPC event. - * @param {BDIpcEvent} event + * Opens an editor. + * @return {Promise} */ - openEditor(o) { - if (this.editor) { - if (this.editor.isFocused()) return; + openEditor(options) { + return new Promise((resolve, reject) => { + if (this.editor) { + if (this.editor.isFocused()) return; - this.editor.focus(); - this.editor.flashFrame(true); - o.reply(true); - return; - } + this.editor.focus(); + this.editor.flashFrame(true); + return resolve(true); + } - const options = Object.assign({}, this.options, o.message); + options = Object.assign({}, this.options, options); - this.editor = new BrowserWindow(options); - this.editor.loadURL('about:blank'); - this.editor.setSheetOffset(33); - this.editorUtils = new WindowUtils({ window: this.editor }); + this.editor = new BrowserWindow(options); + this.editor.loadURL('about:blank'); + this.editor.setSheetOffset(33); + this.editorUtils = new WindowUtils({ window: this.editor }); - this.editor.on('close', () => { - this.bd.windowUtils.send('bd-save-csseditor-bounds', this.editor.getBounds()); - this.editor = null; - }); + this.editor.on('close', () => { + this.bd.windowUtils.send('bd-save-csseditor-bounds', this.editor.getBounds()); + this.editor = null; + }); - this.editor.once('ready-to-show', () => { - this.editor.show(); - }); + this.editor.once('ready-to-show', () => { + this.editor.show(); + }); - this.editor.webContents.on('did-finish-load', () => { - this.editorUtils.injectScript(path.join(this.editorPath, 'csseditor.js')); - o.reply(true); - }); - } - - /** - * Sets the SCSS in the editor. - */ - setSCSS(scss) { - this.send('set-scss', scss); + this.editor.webContents.on('did-finish-load', () => { + this.editorUtils.injectScript(path.join(this.editorPath, 'csseditor.js')); + resolve(true); + }); + }) } /** @@ -71,8 +66,8 @@ class CSSEditor extends Module { * @param {Any} data */ send(channel, data) { - if (!this.editor) return; - this.editor.webContents.send(channel, data); + if (!this.editor) throw {message: 'The CSS editor is not open.'}; + return BDIpc.send(this.editor, channel, data); } /** diff --git a/core/src/modules/utils.js b/core/src/modules/utils.js index 12c205f1..c41c2400 100644 --- a/core/src/modules/utils.js +++ b/core/src/modules/utils.js @@ -14,6 +14,7 @@ const path = require('path'); const fs = require('fs'); const { Module } = require('./modulebase'); +const { BDIpc } = require('./bdipc'); class Utils { static async tryParseJson(jsonString) { @@ -177,7 +178,7 @@ class WindowUtils extends Module { } executeJavascript(script) { - this.webContents.executeJavaScript(script); + return this.webContents.executeJavaScript(script); } injectScript(fpath, variable) { @@ -185,13 +186,15 @@ class WindowUtils extends Module { } static injectScript(window, fpath, variable) { - // console.log(`Injecting: ${fpath}`); + window = window.webContents || window; + if (!window) return; + // console.log(`Injecting: ${fpath} to`, window); const escaped_path = fpath.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); const escaped_variable = variable ? variable.replace(/\\/g, '\\\\').replace(/"/g, '\\"') : null; - if (variable) window.executeJavaScript(`window["${escaped_variable}"] = require("${escaped_path}");`); - else window.executeJavaScript(`require("${escaped_path}");`); + if (variable) return window.executeJavaScript(`window["${escaped_variable}"] = require("${escaped_path}");`); + else return window.executeJavaScript(`require("${escaped_path}");`); } on(event, callback) { @@ -199,8 +202,7 @@ class WindowUtils extends Module { } send(channel, message) { - channel = channel.startsWith('bd-') ? channel : `bd-${channel}`; - this.webContents.send(channel, message); + return BDIpc.send(this.window, channel, message); } } diff --git a/csseditor/src/Editor.vue b/csseditor/src/Editor.vue index 2eb8de54..66d68f5f 100644 --- a/csseditor/src/Editor.vue +++ b/csseditor/src/Editor.vue @@ -33,7 +33,7 @@