commit
b9f3c3f4e2
|
@ -8,34 +8,187 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import { Settings } from 'modules';
|
||||
import BuiltinModule from './BuiltinModule';
|
||||
import { WebpackModules, ReactComponents, MonkeyPatch, Patcher } from 'modules';
|
||||
import { WebpackModules, ReactComponents, MonkeyPatch, Patcher, DiscordApi } from 'modules';
|
||||
import { VueInjector, Reflection } from 'ui';
|
||||
import { ClientLogger as Logger } from 'common';
|
||||
import E2EEComponent from './E2EEComponent.vue';
|
||||
import E2EEMessageButton from './E2EEMessageButton.vue';
|
||||
import aes256 from 'aes256';
|
||||
import crypto from 'crypto';
|
||||
|
||||
const userMentionPattern = new RegExp(`<@!?([0-9]{10,})>`, "g");
|
||||
const roleMentionPattern = new RegExp(`<@&([0-9]{10,})>`, "g");
|
||||
const everyoneMentionPattern = new RegExp(`(?:\\s+|^)@everyone(?:\\s+|$)`);
|
||||
|
||||
let seed = Math.random().toString(36).replace(/[^a-z]+/g, '');
|
||||
|
||||
export default new class E2EE extends BuiltinModule {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.master = this.encrypt(seed, 'temporarymasterkey');
|
||||
this.encryptNewMessages = true;
|
||||
}
|
||||
|
||||
setMaster(key) {
|
||||
seed = Math.random().toString(36).replace(/[^a-z]+/g, '');
|
||||
const newMaster = this.encrypt(seed, key);
|
||||
// TODO re-encrypt everything with new master
|
||||
return (this.master = newMaster);
|
||||
}
|
||||
|
||||
get settingPath() {
|
||||
return ['security', 'default', 'e2ee'];
|
||||
}
|
||||
|
||||
async enabled(e) {
|
||||
const ctaComponent = await ReactComponents.getComponent('ChannelTextArea');
|
||||
MonkeyPatch('BD:E2EE', ctaComponent.component.prototype).after('render', this.render);
|
||||
MonkeyPatch('BD:E2EE', ctaComponent.component.prototype).before('handleSubmit', this.handleSubmit);
|
||||
|
||||
get database() {
|
||||
return Settings.getSetting('security', 'e2eedb', 'e2ekvps').value;
|
||||
}
|
||||
|
||||
render(component, args, retVal) {
|
||||
encrypt(key, content, prefix = '') {
|
||||
return prefix + aes256.encrypt(key, content);
|
||||
}
|
||||
|
||||
decrypt(key, content, prefix = '') {
|
||||
return aes256.decrypt(key, content.replace(prefix, ''));
|
||||
}
|
||||
|
||||
getKey(channelId) {
|
||||
const haveKey = this.database.find(kvp => kvp.value.key === channelId);
|
||||
if (!haveKey) return null;
|
||||
return haveKey.value.value;
|
||||
}
|
||||
|
||||
setKey(channelId, key) {
|
||||
const items = Settings.getSetting('security', 'e2eedb', 'e2ekvps').items;
|
||||
const index = items.findIndex(kvp => kvp.value.key === channelId);
|
||||
if (index > -1) {
|
||||
items[index].value = {key: channelId, value: key};
|
||||
return;
|
||||
}
|
||||
Settings.getSetting('security', 'e2eedb', 'e2ekvps').addItem({ value: { key: channelId, value: key } });
|
||||
}
|
||||
|
||||
async enabled(e) {
|
||||
this.patchDispatcher();
|
||||
this.patchMessageContent();
|
||||
const selector = '.' + WebpackModules.getClassName('channelTextArea', 'emojiButton');
|
||||
const cta = await ReactComponents.getComponent('ChannelTextArea', { selector });
|
||||
this.patchChannelTextArea(cta);
|
||||
this.patchChannelTextAreaSubmit(cta);
|
||||
cta.forceUpdateAll();
|
||||
}
|
||||
|
||||
patchDispatcher() {
|
||||
const Dispatcher = WebpackModules.getModuleByName('Dispatcher');
|
||||
MonkeyPatch('BD:E2EE', Dispatcher).before('dispatch', (_, [event]) => {
|
||||
if (event.type !== "MESSAGE_CREATE") return;
|
||||
|
||||
const key = this.getKey(event.message.channel_id);
|
||||
if (!key) return; // We don't have a key for this channel
|
||||
|
||||
if (typeof event.message.content !== 'string') return; // Ignore any non string content
|
||||
if (!event.message.content.startsWith('$:')) return; // Not an encrypted string
|
||||
let decrypt;
|
||||
try {
|
||||
decrypt = this.decrypt(this.decrypt(this.decrypt(seed, this.master), key), event.message.content);
|
||||
} catch (err) { return } // Ignore errors such as non empty
|
||||
|
||||
const MessageParser = WebpackModules.getModuleByName('MessageParser');
|
||||
const Permissions = WebpackModules.getModuleByName('GuildPermissions');
|
||||
const DiscordConstants = WebpackModules.getModuleByName('DiscordConstants');
|
||||
const currentChannel = DiscordApi.Channel.fromId(event.message.channel_id).discordObject;
|
||||
|
||||
// Create a generic message object to parse mentions with
|
||||
const parsed = MessageParser.parse(currentChannel, decrypt).content;
|
||||
|
||||
if (userMentionPattern.test(parsed))
|
||||
event.message.mentions = parsed.match(userMentionPattern).map(m => {return {id: m.replace(/[^0-9]/g, '')}});
|
||||
if (roleMentionPattern.test(parsed))
|
||||
event.message.mention_roles = parsed.match(roleMentionPattern).map(m => m.replace(/[^0-9]/g, ''));
|
||||
if (everyoneMentionPattern.test(parsed))
|
||||
event.message.mention_everyone = Permissions.can(DiscordConstants.Permissions.MENTION_EVERYONE, currentChannel);
|
||||
});
|
||||
}
|
||||
|
||||
async patchMessageContent() {
|
||||
const selector = '.' + WebpackModules.getClassName('container', 'containerCozy', 'containerCompact', 'edited');
|
||||
const MessageContent = await ReactComponents.getComponent('MessageContent', { selector });
|
||||
MonkeyPatch('BD:E2EE', MessageContent.component.prototype).before('render', this.beforeRenderMessageContent.bind(this));
|
||||
MonkeyPatch('BD:E2EE', MessageContent.component.prototype).after('render', this.renderMessageContent.bind(this));
|
||||
}
|
||||
|
||||
beforeRenderMessageContent(component) {
|
||||
if (!component.props || !component.props.message) return;
|
||||
|
||||
const key = this.getKey(component.props.message.channel_id);
|
||||
if (!key) return; // We don't have a key for this channel
|
||||
|
||||
const Message = WebpackModules.getModuleByPrototypes(['isMentioned']);
|
||||
const MessageParser = WebpackModules.getModuleByName('MessageParser');
|
||||
const Permissions = WebpackModules.getModuleByName('GuildPermissions');
|
||||
const DiscordConstants = WebpackModules.getModuleByName('DiscordConstants');
|
||||
const currentChannel = DiscordApi.Channel.fromId(component.props.message.channel_id).discordObject;
|
||||
|
||||
if (typeof component.props.message.content !== 'string') return; // Ignore any non string content
|
||||
if (!component.props.message.content.startsWith('$:')) return; // Not an encrypted string
|
||||
let decrypt;
|
||||
try {
|
||||
decrypt = this.decrypt(this.decrypt(this.decrypt(seed, this.master), key), component.props.message.content);
|
||||
} catch (err) { return } // Ignore errors such as non empty
|
||||
|
||||
component.props.message.bd_encrypted = true; // signal as encrypted
|
||||
|
||||
// Create a generic message object to parse mentions with
|
||||
const message = MessageParser.createMessage(currentChannel.id, MessageParser.parse(currentChannel, decrypt).content);
|
||||
|
||||
if (userMentionPattern.test(message.content))
|
||||
message.mentions = message.content.match(userMentionPattern).map(m => {return {id: m.replace(/[^0-9]/g, '')}});
|
||||
if (roleMentionPattern.test(message.content))
|
||||
message.mention_roles = message.content.match(roleMentionPattern).map(m => m.replace(/[^0-9]/g, ''));
|
||||
if (everyoneMentionPattern.test(message.content))
|
||||
message.mention_everyone = Permissions.can(DiscordConstants.Permissions.MENTION_EVERYONE, currentChannel);
|
||||
|
||||
// Create a new message to parse it properly
|
||||
const create = Message.create(message);
|
||||
if (!create.content || !create.contentParsed) return;
|
||||
|
||||
component.props.message.mentions = create.mentions;
|
||||
component.props.message.mentionRoles = create.mentionRoles;
|
||||
component.props.message.mentionEveryone = create.mentionEveryone;
|
||||
component.props.message.mentioned = create.mentioned;
|
||||
component.props.message.content = create.content;
|
||||
component.props.message.contentParsed = create.contentParsed;
|
||||
}
|
||||
|
||||
renderMessageContent(component, args, retVal) {
|
||||
if (!component.props.message.bd_encrypted) return;
|
||||
|
||||
retVal.props.children[0].props.children.props.children.props.children.unshift(VueInjector.createReactElement(E2EEMessageButton, {
|
||||
message: component.props.message
|
||||
}));
|
||||
}
|
||||
|
||||
patchChannelTextArea(cta) {
|
||||
MonkeyPatch('BD:E2EE', cta.component.prototype).after('render', this.renderChannelTextArea);
|
||||
}
|
||||
|
||||
renderChannelTextArea(component, args, retVal) {
|
||||
if (!(retVal.props.children instanceof Array)) retVal.props.children = [retVal.props.children];
|
||||
const inner = retVal.props.children.find(child => child.props.className && child.props.className.includes('inner'));
|
||||
|
||||
inner.props.children.splice(0, 0, VueInjector.createReactElement(E2EEComponent, {}, true));
|
||||
inner.props.children.splice(0, 0, VueInjector.createReactElement(E2EEComponent));
|
||||
}
|
||||
|
||||
handleSubmit(component, args, retVal) {
|
||||
component.props.value = aes256.encrypt('randomkey', component.props.value);
|
||||
patchChannelTextAreaSubmit(cta) {
|
||||
MonkeyPatch('BD:E2EE', cta.component.prototype).before('handleSubmit', this.handleChannelTextAreaSubmit.bind(this));
|
||||
}
|
||||
|
||||
get ecdh() {
|
||||
if (!this._ecdh) this._ecdh = {};
|
||||
return this._ecdh;
|
||||
}
|
||||
|
||||
get ecdh() {
|
||||
|
@ -61,8 +214,37 @@ export default new class E2EE extends BuiltinModule {
|
|||
return hash.digest('base64');
|
||||
}
|
||||
|
||||
disabled(e) {
|
||||
createKeyExchange(userID) {
|
||||
this.ecdh[userID] = crypto.createECDH('secp521r1');
|
||||
return this.ecdh[userID].generateKeys('base64');
|
||||
}
|
||||
|
||||
publicKeyFor(userID) {
|
||||
return this.ecdh[userID].getPublicKey('base64');
|
||||
}
|
||||
|
||||
computeSecret(userID, otherKey) {
|
||||
try {
|
||||
const secret = this.ecdh[userID].computeSecret(otherKey, 'base64', 'base64');
|
||||
delete this.ecdh[userID];
|
||||
const hash = crypto.createHash('sha256');
|
||||
hash.update(secret);
|
||||
return hash.digest('base64');
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
handleChannelTextAreaSubmit(component, args, retVal) {
|
||||
const key = this.getKey(DiscordApi.currentChannel.id);
|
||||
if (!this.encryptNewMessages || !key) return;
|
||||
component.props.value = this.encrypt(this.decrypt(this.decrypt(seed, this.master), key), component.props.value, '$:');
|
||||
}
|
||||
|
||||
async disabled(e) {
|
||||
for (const patch of Patcher.getPatchesByCaller('BD:E2EE')) patch.unpatch();
|
||||
const ctaComponent = await ReactComponents.getComponent('ChannelTextArea');
|
||||
ctaComponent.forceUpdateAll();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -9,21 +9,85 @@
|
|||
*/
|
||||
|
||||
<template>
|
||||
<div class="bd-e2eeTaContainer">
|
||||
<div class="bd-e2eeTaBtn bd-e2eeLock">
|
||||
<MiLock v-tooltip="'E2EE'" />
|
||||
<div class="bd-e2eeTaContainer" @contextmenu.prevent="channelType === 'DM' && $refs.ee2eLockContextMenu.open()">
|
||||
<div v-if="error" class="bd-e2eeTaBtn bd-e2eeLock bd-error">
|
||||
<MiLock v-tooltip="error" />
|
||||
</div>
|
||||
<div v-else-if="state === 'loading'" class="bd-e2eeTaBtn bd-e2eeLock bd-loading bd-warn">
|
||||
<MiLock v-tooltip="'Loading'" />
|
||||
</div>
|
||||
<div v-else-if="!E2EE.encryptNewMessages" class="bd-e2eeTaBtn bd-e2eeLock bd-warn" @click="E2EE.encryptNewMessages = true">
|
||||
<MiLock v-tooltip="'New messages will not be encrypted.'" />
|
||||
</div>
|
||||
<div v-else class="bd-e2eeTaBtn bd-e2eeLock bd-ok" @click="E2EE.encryptNewMessages = false">
|
||||
<MiLock v-tooltip="'Ready!'" />
|
||||
</div>
|
||||
|
||||
<div class="bd-taDivider"></div>
|
||||
<context-menu id="bd-e2eeLockContextMenu" class="bd-e2eeLockContextMenu" ref="ee2eLockContextMenu" v-if="channelType === 'DM'">
|
||||
<li class="bd-e2eeLockContextMenuOption" @click="generatePublicKey()">Generate Public Key</li>
|
||||
<li class="bd-e2eeLockContextMenuOption" @click="computeSharedSecret()">Receive Public Key</li>
|
||||
</context-menu>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { E2EE } from 'builtin';
|
||||
import { DiscordApi } from 'modules';
|
||||
import { MiLock } from '../ui/components/common/MaterialIcon';
|
||||
import contextMenu from 'vue-context-menu';
|
||||
import { clipboard } from 'electron';
|
||||
import { Toasts } from 'ui';
|
||||
|
||||
function generatePublicKey() {
|
||||
const dmChannelID = location.pathname.split("/")[3];
|
||||
const publicKeyMessage = `My public key is: \`${E2EE.createKeyExchange(dmChannelID)}\`. Please give me your public key if you haven't done so and add my public key by pasting it in the chat textbox, right clicking the lock icon, and selecting \`Receive Public Key\`.`;
|
||||
const chatInput = document.getElementsByClassName('da-textArea')[0];
|
||||
chatInput.value = publicKeyMessage;
|
||||
const evt = { currentTarget: chatInput };
|
||||
chatInput[Object.keys(chatInput).find(k => k.startsWith('__reactEventHandlers'))].onChange.call(chatInput, evt);
|
||||
}
|
||||
|
||||
function computeSharedSecret() {
|
||||
try {
|
||||
const dmChannelID = location.pathname.split("/")[3];
|
||||
const chatInput = document.getElementsByClassName('da-textArea')[0];
|
||||
const otherPublicKey = chatInput.value;
|
||||
const secret = E2EE.computeSecret(dmChannelID, otherPublicKey);
|
||||
E2EE.setKey(dmChannelID, secret);
|
||||
chatInput.value = "";
|
||||
const evt = { currentTarget: chatInput };
|
||||
chatInput[Object.keys(chatInput).find(k => k.startsWith('__reactEventHandlers'))].onChange.call(chatInput, evt);
|
||||
Toasts.success("Encryption key has been set for this DM channel.");
|
||||
} catch (e) {
|
||||
Toasts.error("Invalid public key. Please set up a new key exchange.");
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
components: { MiLock },
|
||||
components: { MiLock, contextMenu },
|
||||
data() {
|
||||
return {};
|
||||
return {
|
||||
E2EE,
|
||||
state: 'loading',
|
||||
error: null,
|
||||
channelType: DiscordApi.currentChannel.type
|
||||
};
|
||||
},
|
||||
methods: {}
|
||||
methods: { generatePublicKey, computeSharedSecret },
|
||||
mounted() {
|
||||
if (!E2EE.master) {
|
||||
this.error = 'No master key set!';
|
||||
return;
|
||||
}
|
||||
const haveKey = E2EE.getKey(DiscordApi.currentChannel.id);
|
||||
if (!haveKey) {
|
||||
this.error = 'No key for channel!';
|
||||
return;
|
||||
}
|
||||
this.state = 'OK';
|
||||
this.error = null;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
/**
|
||||
* BetterDiscord E2EE Component
|
||||
* 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.
|
||||
*/
|
||||
|
||||
<template>
|
||||
<div class="bd-e2eeMessageButtonWrap">
|
||||
<div class="bd-e2eeMessageButton">
|
||||
<MiLock v-tooltip="'Encrypted'" :size="16" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { MiLock } from '../ui/components/common/MaterialIcon';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MiLock
|
||||
},
|
||||
props: ['message']
|
||||
}
|
||||
</script>
|
|
@ -3,3 +3,4 @@ export { default as ReactDevtoolsModule } from './ReactDevtoolsModule';
|
|||
export { default as VueDevtoolsModule } from './VueDevToolsModule';
|
||||
export { default as TrackingProtection } from './TrackingProtection';
|
||||
export { default as BuiltinManager } from './Manager';
|
||||
export { default as E2EE } from './E2EE';
|
||||
|
|
|
@ -12,7 +12,7 @@ import { DOM, BdUI, BdMenu, Modals, Reflection, Toasts } from 'ui';
|
|||
import BdCss from './styles/index.scss';
|
||||
import { Events, CssEditor, Globals, Settings, Database, Updater, ModuleManager, PluginManager, ThemeManager, ExtModuleManager, Vendor, WebpackModules, Patcher, MonkeyPatch, ReactComponents, ReactHelpers, ReactAutoPatcher, DiscordApi, BdWebApi, Connectivity } from 'modules';
|
||||
import { ClientLogger as Logger, ClientIPC, Utils } from 'common';
|
||||
import { BuiltinManager, EmoteModule, ReactDevtoolsModule, VueDevtoolsModule, TrackingProtection } from 'builtin';
|
||||
import { BuiltinManager, EmoteModule, ReactDevtoolsModule, VueDevtoolsModule, TrackingProtection, E2EE } from 'builtin';
|
||||
import electron from 'electron';
|
||||
import path from 'path';
|
||||
|
||||
|
|
|
@ -24,3 +24,4 @@ export { default as EventHook } from './eventhook';
|
|||
export { default as DiscordApi, Modules as DiscordApiModules } from './discordapi';
|
||||
export { default as BdWebApi } from './bdwebapi';
|
||||
export { default as Connectivity } from './connectivity';
|
||||
export { default as Security } from './security';
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
/**
|
||||
* BetterDiscord Security Module
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import aes256 from 'aes256';
|
||||
|
||||
export default class Security {
|
||||
|
||||
static encrypt(key, content, prefix = '') {
|
||||
if (key instanceof Array) return this.deepEncrypt(key, content, prefix);
|
||||
return `${prefix}${aes256.encrypt(key, content)}`;
|
||||
}
|
||||
|
||||
static decrypt(key, content, prefix = '') {
|
||||
if (key instanceof Array) return this.deepDecrypt(key, content, prefix);
|
||||
return aes256.decrypt(key, content.replace(prefix, ''));
|
||||
}
|
||||
|
||||
static deepEncrypt(keys, content, prefix = '') {
|
||||
let encrypt = null;
|
||||
for (const key of keys) {
|
||||
if (encrypt === null) encrypt = this.encrypt(key, content, prefix);
|
||||
else encrypt = this.encrypt(key, encrypt, prefix);
|
||||
}
|
||||
return encrypt;
|
||||
}
|
||||
|
||||
static deepDecrypt(keys, content, prefix = '') {
|
||||
let decrypt = null;
|
||||
for (const key of keys.reverse()) {
|
||||
if (decrypt === null) decrypt = this.decrypt(key, content, prefix);
|
||||
else decrypt = this.decrypt(key, decrypt, prefix);
|
||||
}
|
||||
return decrypt;
|
||||
}
|
||||
|
||||
}
|
|
@ -129,3 +129,17 @@
|
|||
transform: translate3d(4px, -4px, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bd-pulse {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: .7;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,18 +7,102 @@
|
|||
flex: 1 1 auto;
|
||||
flex-direction: row;
|
||||
cursor: pointer;
|
||||
opacity: .8;
|
||||
transition: opacity .2s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.bd-e2eeLock {
|
||||
fill: #cc3e3e;
|
||||
&.bd-error {
|
||||
fill: #cc3e3e;
|
||||
}
|
||||
|
||||
&.bd-ok {
|
||||
fill: $colbdgreen;
|
||||
}
|
||||
|
||||
&.bd-warn {
|
||||
fill: #ccab3e;
|
||||
animation: bd-pulse 2s ease-in-out infinite;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bd-taDivider {
|
||||
background-color: hsla(0,0%,100%,.1);
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
box-sizing: border-box;
|
||||
height: 80%;
|
||||
position: relative;
|
||||
top: 10%;
|
||||
width: 1px;
|
||||
height: 37px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.bd-e2eeLockContextMenu {
|
||||
|
||||
border: none;
|
||||
|
||||
.ctx-menu {
|
||||
background: #23272A;
|
||||
border-radius: 4px;
|
||||
box-shadow: none;
|
||||
padding: 0;
|
||||
|
||||
.bd-e2eeLockContextMenuOption {
|
||||
background: #23272A;
|
||||
color: #99AAB5;
|
||||
padding: 8px 8px;
|
||||
font-weight: 500;
|
||||
cursor: default;
|
||||
|
||||
&:hover {
|
||||
background: #000000;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bd-e2eeMdContainer {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
top: 4px;
|
||||
|
||||
.bd-e2eeMdBtn {
|
||||
cursor: pointer;
|
||||
|
||||
&.bd-e2eeLock {
|
||||
&.bd-error {
|
||||
fill: #cc3e3e;
|
||||
}
|
||||
|
||||
&.bd-ok {
|
||||
fill: $colbdgreen;
|
||||
}
|
||||
|
||||
&.bd-warn {
|
||||
fill: #ccab3e;
|
||||
}
|
||||
|
||||
&.bd-loading {
|
||||
animation: bd-pulse 2s ease-in-out infinite;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bd-e2eeMessageButtonWrap {
|
||||
.bd-e2eeMessageButton {
|
||||
fill: #99aab5;
|
||||
margin-left: 6px;
|
||||
margin-right: -2px;
|
||||
opacity: .4;
|
||||
transition: opacity .2s ease;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,7 +39,8 @@ module.exports = {
|
|||
net: 'require("net")',
|
||||
crypto: 'require("crypto")',
|
||||
request: 'require(require("path").join(require("electron").remote.app.getAppPath(), "node_modules", "request"))',
|
||||
sparkplug: 'require("../../core/dist/sparkplug")'
|
||||
sparkplug: 'require("../../core/dist/sparkplug")',
|
||||
crypto: 'require("crypto")'
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
|
|
|
@ -40,7 +40,8 @@ module.exports = {
|
|||
net: 'require("net")',
|
||||
crypto: 'require("crypto")',
|
||||
request: 'require(require("path").join(require("electron").remote.app.getAppPath(), "node_modules", "request"))',
|
||||
sparkplug: 'require("./sparkplug")'
|
||||
sparkplug: 'require("./sparkplug")',
|
||||
crypto: 'require("crypto")'
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -61,6 +61,7 @@
|
|||
"vue": "^2.5.17",
|
||||
"vue-codemirror": "^4.0.5",
|
||||
"vue-color": "^2.4.6",
|
||||
"vue-context-menu": "^2.0.6",
|
||||
"vue-loader": "^13.7.2",
|
||||
"vue-material-design-icons": "^1.6.0",
|
||||
"vue-template-compiler": "^2.5.17",
|
||||
|
|
Loading…
Reference in New Issue