├── _config.yml ├── .gitattributes ├── resources ├── icons │ ├── logo.icns │ ├── logo.ico │ ├── logo.png │ ├── minimizeWindow.svg │ ├── snapWindowSize.svg │ ├── PFPMask.svg │ ├── closeWindow.svg │ ├── GuildNewsChannel.svg │ ├── GuildVoiceChannel.svg │ ├── checkmark.svg │ ├── GuildVoiceChannelBlocked.svg │ ├── GuildTextChannel.svg │ ├── pullOut.svg │ ├── categoryArrow.svg │ ├── GuildTextChannelBlocked.svg │ ├── logo.svg │ ├── embedIcon.svg │ └── logoLarge.svg ├── images │ ├── Barry.png │ └── default.png └── fonts │ ├── whitney.ttf │ ├── UniSans-Heavy.ttf │ ├── whitney-light.otf │ ├── UniSans-Regular.otf │ ├── UniSans-SemiBold.otf │ └── whitney-medium.otf ├── .gitignore ├── themes └── template.css ├── css ├── fonts.css ├── titlebar.css ├── dmList.css ├── rcMenu.css ├── memberList.css ├── embeds.css ├── memberMenu.css ├── miscs.css ├── messageBar.css ├── channelList.css ├── guildList.css ├── textFormat.css ├── master.css └── splashScreen.css ├── js ├── embedMenu.js ├── titlebar.js ├── guildSelect.js ├── handleThemes.js ├── splashScreen.js ├── customSettingsFuncs.js ├── command.js ├── buildMemberMenu.js ├── rcMenuFuncs.js ├── handleScripts.js ├── parseFunctions.js ├── app.js ├── addMemberList.js ├── setToken.js ├── typingStatus.js ├── buildTeamMemberCards.js ├── messageGeneration.js ├── channelSelect.js ├── errorHandler.js ├── parseMessage.js ├── channelList.js ├── guildList.js └── dmList.js ├── package.json ├── .github └── ISSUE_TEMPLATE │ └── bug_report.md ├── scripts ├── template.js └── how_to_write_livebot_scripts.md ├── start.js └── README.md /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.exe filter=lfs diff=lfs merge=lfs -text 2 | -------------------------------------------------------------------------------- /resources/icons/logo.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SebOuellette/LiveBot/HEAD/resources/icons/logo.icns -------------------------------------------------------------------------------- /resources/icons/logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SebOuellette/LiveBot/HEAD/resources/icons/logo.ico -------------------------------------------------------------------------------- /resources/icons/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SebOuellette/LiveBot/HEAD/resources/icons/logo.png -------------------------------------------------------------------------------- /resources/images/Barry.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SebOuellette/LiveBot/HEAD/resources/images/Barry.png -------------------------------------------------------------------------------- /resources/fonts/whitney.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SebOuellette/LiveBot/HEAD/resources/fonts/whitney.ttf -------------------------------------------------------------------------------- /resources/images/default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SebOuellette/LiveBot/HEAD/resources/images/default.png -------------------------------------------------------------------------------- /resources/fonts/UniSans-Heavy.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SebOuellette/LiveBot/HEAD/resources/fonts/UniSans-Heavy.ttf -------------------------------------------------------------------------------- /resources/fonts/whitney-light.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SebOuellette/LiveBot/HEAD/resources/fonts/whitney-light.otf -------------------------------------------------------------------------------- /resources/fonts/UniSans-Regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SebOuellette/LiveBot/HEAD/resources/fonts/UniSans-Regular.otf -------------------------------------------------------------------------------- /resources/fonts/UniSans-SemiBold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SebOuellette/LiveBot/HEAD/resources/fonts/UniSans-SemiBold.otf -------------------------------------------------------------------------------- /resources/fonts/whitney-medium.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SebOuellette/LiveBot/HEAD/resources/fonts/whitney-medium.otf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Folders 2 | node_modules 3 | resources/app 4 | 5 | # File types to emit 6 | *.log 7 | *.bat 8 | *.db 9 | *.exe 10 | 11 | # Some other files that don't need to be uploaded to github 12 | package-lock.json 13 | logins.json 14 | scripts/* 15 | themes/* 16 | !themes/template.css 17 | !scripts/template.js -------------------------------------------------------------------------------- /themes/template.css: -------------------------------------------------------------------------------- 1 | /* It's as easy as just finding the class or the ID and changing the properties 2 | You can also use github or some other site so people don't have to download the theme every time 3 | Don't forget if you do that to give the user some way to customize it with global css variables*/ 4 | #memberBar { 5 | background-color: darkred; 6 | } 7 | -------------------------------------------------------------------------------- /css/fonts.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: UniSans-regular; 3 | src: url("../resources/fonts/UniSans-Regular.otf"); 4 | } 5 | 6 | @font-face { 7 | font-family: UniSans-bold; 8 | src: url("../resources/fonts/UniSans-SemiBold.otf"); 9 | } 10 | 11 | @font-face { 12 | font-family: Whitney; 13 | src: url("../resources/fonts/whitney-medium.otf"); 14 | font-weight: 100; 15 | } 16 | 17 | @font-face { 18 | font-family: Whitney-light; 19 | src: url("../resources/fonts/whitney-light.otf"); 20 | font-weight: 100; 21 | } 22 | 23 | @font-face { 24 | font-family: UniSans; 25 | src: url(../resources/fonts/UniSans-Heavy.ttf); 26 | } 27 | -------------------------------------------------------------------------------- /js/embedMenu.js: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Sebastian Ouellette 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | 'use strict'; 16 | 17 | function buildEmbedMenu() {} 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "livebot", 3 | "version": "v1.3.2-alpha", 4 | "description": "Discord for bots!", 5 | "main": "start.js", 6 | "scripts": { 7 | "test": "electron .", 8 | "start": "electron ." 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/SebOuellette/LiveBot.git" 13 | }, 14 | "keywords": [ 15 | "discord", 16 | "livebot", 17 | "discord-livebot" 18 | ], 19 | "author": "SebOuellette", 20 | "license": "Apache-2.0", 21 | "bugs": { 22 | "url": "https://github.com/SebOuellette/LiveBot/issues" 23 | }, 24 | "homepage": "https://github.com/SebOuellette/LiveBot#readme", 25 | "dependencies": { 26 | "discord.js": "^14.18.0", 27 | "electron": "^35.0.0" 28 | }, 29 | "devDependencies": { 30 | "@electron/packager": "^18.3.6" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: A template for bug reports 4 | title: Issue name goes here 5 | labels: Bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | #### What branch/release are you using? 11 | 12 | - Branch/Release: 13 | 14 | #### Errors 15 | 16 | 17 | #### Steps required to reproduce the bug 18 | 19 | - Steps: 20 | 21 | 22 | #### Screenshot (In most cases required) 23 | 25 | - Screenshot: 26 | 27 | #### Description 28 | 29 | -------------------------------------------------------------------------------- /css/titlebar.css: -------------------------------------------------------------------------------- 1 | #titlebar { 2 | height: 20px !important; 3 | width: 100vw; 4 | background-color: #202225; 5 | margin: -8px 0 0 -8px; 6 | -webkit-user-select: none; 7 | user-select: none; 8 | -webkit-app-region: drag; 9 | } 10 | 11 | #titlebar span { 12 | font-family: UniSans-bold, "Helvetica Neue", Helvetica, Arial, sans-serif; 13 | color: #aaa; 14 | font-weight: lighter; 15 | letter-spacing: 2px; 16 | font-size: 13px; 17 | margin-left: 10px; 18 | user-select: none; 19 | display: inline-block; 20 | } 21 | 22 | /* Titlebar buttons */ 23 | 24 | .titleButton { 25 | -webkit-app-region: no-drag; 26 | height: 100%; 27 | width: 25px; 28 | right: 2px; 29 | float: right; 30 | cursor: pointer; 31 | } 32 | 33 | .titleButton:hover { 34 | background-color: #282b2e; 35 | } 36 | 37 | #exit:hover { 38 | background-color: #ed4245; 39 | } 40 | 41 | .titleButton img { 42 | margin-left: 50%; 43 | transform: translate(calc(-50% - 1px)); 44 | } 45 | -------------------------------------------------------------------------------- /js/titlebar.js: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Sebastian Ouellette 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | 'use strict'; 16 | 17 | const { ipcRenderer } = require('electron'); 18 | 19 | document.getElementById('minimize').addEventListener('click', function (e) { 20 | ipcRenderer.send('titlebar', 'minimize'); 21 | }); 22 | 23 | document.getElementById('screenSnap').addEventListener('click', function (e) { 24 | ipcRenderer.send('titlebar', 'screenSnap'); 25 | }); 26 | 27 | document.getElementById('exit').addEventListener('click', function (e) { 28 | ipcRenderer.send('titlebar', 'exit'); 29 | }); 30 | -------------------------------------------------------------------------------- /css/dmList.css: -------------------------------------------------------------------------------- 1 | /* Copyright 2017 Sebastian Ouellette 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. */ 14 | 15 | .dmChannel { 16 | user-select: none; 17 | margin: 2px 0 0 -5px; 18 | padding: 5px 20px; 19 | cursor: pointer; 20 | border-radius: 3px; 21 | height: 25px; 22 | } 23 | 24 | .dmChannelImage { 25 | height: 25; 26 | width: 25; 27 | border-radius: 50%; 28 | user-select: none; 29 | position: relative; 30 | margin-bottom: 6px; 31 | } 32 | 33 | #dmChannel-elements h5 { 34 | transform: translateY(-5px); 35 | display: inline-block; 36 | margin: 0 !important; 37 | padding: 7px 0 7px 10px; 38 | width: 170px; 39 | } 40 | 41 | .dmChannel:hover { 42 | background-color: #34373c; 43 | color: #b4b8bc; 44 | } 45 | 46 | /* Channel ... formatting */ 47 | .dmChannel h5 { 48 | max-width: calc(100% - 40px); 49 | overflow: hidden; 50 | text-overflow: ellipsis; 51 | white-space: nowrap; 52 | user-select: none; 53 | } 54 | 55 | .selectedChan { 56 | background-color: #393c43 !important; 57 | color: white !important; 58 | } 59 | 60 | .dmChannel.newMsg { 61 | color: white !important; 62 | } 63 | -------------------------------------------------------------------------------- /resources/icons/minimizeWindow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 36 | 38 | 42 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /resources/icons/snapWindowSize.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 36 | 38 | 42 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /scripts/template.js: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Sebastian Ouellette 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | module.exports.info = { 16 | author: 'Your discord username and tag (ex: SharkFin#1504)', 17 | title: 'Name of your script', 18 | description: 'Description of your script', 19 | version: '1.0.0', 20 | }; 21 | 22 | module.exports.start = () => { 23 | /* 24 | Global Variables Available: 25 | bot - the Discord.client() that is signed in 26 | selectedChan - the current channel you're in (Object, not the DOM) 27 | selectedGuild - the current guild you're in 28 | selectedVoice - the last selected voice channel 29 | This isn't really supported, but it's here for future use. If you need better accessibility with this variable, 30 | ask for it in the discord (which you can find on the readme) and we'll see what we can do 31 | */ 32 | 33 | // Your code goes here 34 | 35 | myFunction(); 36 | }; 37 | 38 | module.exports.stop = () => { 39 | // This code will call when the script should be unloaded 40 | }; 41 | 42 | function myFunction() { 43 | // You can make normal functions and call them from start() 44 | } 45 | -------------------------------------------------------------------------------- /resources/icons/PFPMask.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 36 | 38 | 42 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /resources/icons/closeWindow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 36 | 38 | 42 | 50 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /resources/icons/GuildNewsChannel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 7 | 8 | -------------------------------------------------------------------------------- /css/rcMenu.css: -------------------------------------------------------------------------------- 1 | /* Copyright 2017 Sebastian Ouellette 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. */ 14 | 15 | #rcMenu { 16 | position: fixed; 17 | width: 160px; 18 | background-color: #18191c; 19 | border-radius: 5px; 20 | z-index: 100; 21 | display: none; 22 | padding: 5px 0; 23 | } 24 | 25 | #rcMenu.open { 26 | display: block; 27 | } 28 | 29 | /* Options */ 30 | .rcOption { 31 | padding: 8px 10px; 32 | margin: 0 8px; 33 | color: #b9bbbe; 34 | border-radius: 3px; 35 | cursor: pointer; 36 | font-size: 15px; 37 | } 38 | 39 | .rcBreak { 40 | border: none; 41 | padding: 2px 0 0 0; 42 | background-color: #393c43; 43 | margin: 0px 15px 0px 15px; 44 | } 45 | 46 | .rcOption:hover { 47 | color: white !important; 48 | background-color: #5c6fb1; 49 | } 50 | 51 | .rcOption.red { 52 | color: #f04747; 53 | } 54 | 55 | .rcOption:hover.red { 56 | background-color: #f04747; 57 | } 58 | 59 | .rcOption.greyed { 60 | color: #b9bbbe !important; 61 | cursor: default; 62 | background-color: transparent !important; 63 | opacity: 0.4; 64 | } 65 | 66 | /* Edit message textfield */ 67 | .editTextarea::-webkit-scrollbar { 68 | width: 0; 69 | } 70 | 71 | .editTextarea { 72 | width: calc(100% - 50px); 73 | display: block; 74 | background-color: #484b51; 75 | border: none; 76 | padding: 10px; 77 | border-radius: 5px; 78 | outline: none; 79 | color: white; 80 | resize: none; 81 | position: relative; 82 | margin: 5px 0; 83 | } 84 | 85 | .firstmsg .editTextarea { 86 | margin-top: -26px; 87 | } 88 | -------------------------------------------------------------------------------- /js/guildSelect.js: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Sebastian Ouellette 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | 'use strict'; 16 | 17 | // Selecting new guild 18 | let guildSelect = (g, img) => { 19 | // Update the selected guild 20 | selectedGuild = g; 21 | 22 | //document.getElementById('guildIndicator').style.display = 'block'; 23 | if (oldimg) oldimg.classList.remove('selectedGuild'); 24 | img.classList.add('selectedGuild'); 25 | 26 | // this should be done another way 27 | document.getElementById('guildIndicator').style.marginTop = `${ 28 | img.offsetTop - 64 29 | }px`; 30 | document.getElementById('guildIndicator').style.display = 'block'; 31 | 32 | oldimg = img; 33 | 34 | // Make the text display 'block' (default) incase switching from DMs 35 | document.getElementById('members-text').style.display = 'block'; 36 | document.getElementById('guildName').classList.remove('directMsg'); 37 | // Set the count to begin 38 | document.getElementById('members-count').innerText = g.memberCount; 39 | 40 | // Update guild profile name 41 | let name = g.name; 42 | document.getElementById('guildName').innerText = name; 43 | 44 | // Update guild profile image 45 | let icon = g.iconURL(); 46 | if (!icon) { 47 | icon = 'resources/images/default.png'; 48 | } 49 | document.getElementById('guildImg').src = icon; 50 | 51 | // Clear the message list 52 | let messages = document.getElementById('message-list'); 53 | while (messages.firstChild) { 54 | messages.removeChild(messages.firstChild); 55 | } 56 | 57 | // Create the channels 58 | createChannels(g); 59 | }; 60 | -------------------------------------------------------------------------------- /js/handleThemes.js: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Sebastian Ouellette 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | 'use strict'; 16 | 17 | let loadedThemes = []; 18 | 19 | function loadThemes() { 20 | // Set the directory of the themes 21 | let dir = './themes/'; 22 | 23 | // Get all the files in the themes folder 24 | let files = fs.readdirSync(dir); 25 | 26 | // Remove the template file out of the equation if there is one 27 | if (files.includes('template.css')) 28 | files.splice(files.indexOf('template.css'), 1); 29 | 30 | // Don't bother loading if there is nothing 31 | if (!files.length) return; 32 | 33 | console.log('Loading themes...'); 34 | 35 | files.forEach((file) => { 36 | if (!file.endsWith('.css')) return; 37 | 38 | let script = document.createElement('link'); 39 | script.id = file; 40 | script.rel = 'stylesheet'; 41 | script.href = dir + file; 42 | 43 | loadedThemes.push([script, file]); 44 | 45 | document.head.appendChild(script); 46 | console.log('\n%c ' + file + ' loaded', 'color:Lime'); 47 | }); 48 | } 49 | 50 | function unloadThemes() { 51 | // If there's nothing there don't bother 52 | if (!loadedThemes.length) return; 53 | 54 | // Display that the themes are being unloaded 55 | console.log('Unloading themes...'); 56 | 57 | // Go through each of the elements that are saved in the memory 58 | for (element of loadedThemes) { 59 | document.head.removeChild(element[0]); 60 | console.log('%c ' + element[1] + ' unloaded', 'color:Red'); 61 | } 62 | // Delete all the elements that were once loaded 63 | loadedThemes = []; 64 | } 65 | -------------------------------------------------------------------------------- /js/splashScreen.js: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Sebastian Ouellette 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | 'use strict'; 16 | 17 | function hideSplashScreen() { 18 | if (!settings.options.splash) return; 19 | 20 | settings.options.splash = false; 21 | document.getElementById('splashLoading').style.opacity = '0'; 22 | setTimeout( 23 | () => (document.getElementById('percentageText').style.opacity = '0'), 24 | 2000 25 | ); 26 | setTimeout( 27 | () => (document.getElementById('loadingBar').style.opacity = '0'), 28 | 2000 29 | ); 30 | setTimeout( 31 | () => (document.getElementById('splashScreen').style.opacity = '0'), 32 | 3000 33 | ); 34 | setTimeout(() => { 35 | document.getElementById('splashScreen').style.visibility = 'hidden'; 36 | 37 | // Reset opacity of inner elements 38 | setTimeout(() => { 39 | document.getElementById('percentageText').style.opacity = '1'; 40 | document.getElementById('loadingBar').style.opacity = '1'; 41 | document.getElementById('splashScreen').style.opacity = '1'; 42 | setLoadingPerc(0); 43 | }, 1500); 44 | }, 3500); 45 | } 46 | 47 | async function showSplashScreen(token = undefined, save = false) { 48 | if (settings.options.splash) return; 49 | 50 | if (!token) return; 51 | let error; 52 | if (save) error = await saveToken(token); 53 | else error = await setToken(token); 54 | 55 | if (!error[0]) { 56 | settings.options.splash = true; 57 | document.getElementById('splashScreen').style.visibility = 'visible'; 58 | } else { 59 | errorHandler(error[1]); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /js/customSettingsFuncs.js: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Sebastian Ouellette 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | 'use strict'; 16 | 17 | // Set the activity 18 | function setActivity(dropdowns, activityName, streamurl) { 19 | let status = dropdowns[0]; 20 | let activity = dropdowns[1]; 21 | 22 | if (status.includes('Not')) { 23 | status = 'dnd'; 24 | } 25 | 26 | if (activity == 'None') 27 | bot.user.setPresence({ 28 | activities: [], 29 | status: status.toLowerCase(), 30 | }); 31 | else 32 | bot.user.setPresence({ 33 | activities: [ 34 | { 35 | name: activityName, 36 | type: Discord.ActivityType[activity], 37 | url: streamurl || undefined, 38 | }, 39 | ], 40 | status: status.toLowerCase(), 41 | }); 42 | } 43 | 44 | // Set the new username 45 | async function setUsername(name) { 46 | try { 47 | if (!name.replace(/ |#/, '').length) { 48 | errorHandler('EMPTY-NAME'); 49 | throw 'EMPTY-NAME'; 50 | } 51 | await bot.user.setUsername(name).catch((err) => { 52 | errorHandler(err); 53 | throw err; 54 | }); 55 | document.getElementById('userCardName').innerText = bot.user.username; 56 | } catch (err) { 57 | document 58 | .getElementsByClassName('newNameInput')[0] 59 | .animate(animations.flashRed, { duration: 500 }); 60 | } 61 | } 62 | 63 | // Generate the invite code 64 | function generateInvite(items) { 65 | let sum; 66 | if (items.length) { 67 | sum = items.reduce((a, b) => a + b); 68 | } else { 69 | sum = 0; 70 | } 71 | let invite = `https://discordapp.com/oauth2/authorize?client_id=${bot.user.id}&scope=bot&permissions=${sum}`; 72 | console.log(`Copied to Clipboard: ${invite}`); 73 | clipboard.writeText(invite); 74 | } 75 | -------------------------------------------------------------------------------- /resources/icons/GuildVoiceChannel.svg: -------------------------------------------------------------------------------- 1 | 2 | 19 | 21 | 22 | 24 | image/svg+xml 25 | 27 | 28 | 29 | 30 | 31 | 33 | 53 | 59 | -------------------------------------------------------------------------------- /js/command.js: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Sebastian Ouellette 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | 'use strict'; 16 | 17 | let command = (text, del = NaN) => { 18 | let div = document.createElement('div'); 19 | div.id = 'messageCont'; 20 | div.classList.add('barryCommand'); 21 | div.style.backgroundColor = 'rgba(50,50,50,0.4)'; 22 | document.getElementById('message-list').appendChild(div); 23 | 24 | let img = document.createElement('img'); 25 | img.classList.add('messageImg'); 26 | img.classList.add('barryImg'); 27 | img.src = './resources/images/Barry.png'; 28 | div.appendChild(img); 29 | 30 | let inlineContainer = document.createElement('div'); 31 | inlineContainer.classList.add('inlineMsgCont'); 32 | div.appendChild(inlineContainer); 33 | 34 | let name = document.createElement('p'); 35 | let username; 36 | 37 | username = document.createTextNode('Barry'); 38 | name.appendChild(username); 39 | name.id = 'messageUsername'; 40 | name.style.color = `#999999`; 41 | inlineContainer.appendChild(name); 42 | 43 | let text2 = document.createElement('p'); 44 | 45 | console.log(text); 46 | if (text.split('\n').length > 1) { 47 | for (let i = 0; i < text.split('\n').length; i++) { 48 | text2.innerHTML += parseMessage(text.split('\n')[i]); 49 | text2.classList.add('messageText'); 50 | 51 | let contentBreak = document.createElement('br'); 52 | text2.appendChild(contentBreak); 53 | } 54 | } else { 55 | text2.innerHTML = parseMessage(text); 56 | text2.classList.add('messageText'); 57 | } 58 | inlineContainer.appendChild(text2); 59 | document.getElementById('message-list').scrollTop = 60 | document.getElementById('message-list').scrollHeight; 61 | document.getElementById('msgbox').value = ''; 62 | barry = true; 63 | if (del && del > 1) 64 | setTimeout(() => { 65 | document.getElementById('message-list').removeChild(div); 66 | }, del); 67 | }; 68 | -------------------------------------------------------------------------------- /css/memberList.css: -------------------------------------------------------------------------------- 1 | /* Copyright 2017 Sebastian Ouellette 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. */ 14 | 15 | #memberBar { 16 | height: calc(100vh - 40px); 17 | width: 201px; 18 | margin-top: calc(-100vh + 60px); 19 | margin-left: calc(100vw - 209px); 20 | background-color: #2f3136; 21 | overflow-y: auto; 22 | padding: 20px 10px 0 0px; 23 | overflow-y: scroll; 24 | } 25 | 26 | #memberBar::-webkit-scrollbar { 27 | background-color: transparent; 28 | width: 6px; 29 | } 30 | 31 | #memberBar::-webkit-scrollbar-track { 32 | background-color: transparent; 33 | } 34 | 35 | #memberBar::-webkit-scrollbar-thumb { 36 | background-color: #25272b; 37 | border-radius: 3px; 38 | } 39 | 40 | .roleContainer { 41 | margin-bottom: 20px; 42 | margin-left: 15px; 43 | } 44 | 45 | .roleTitle { 46 | font-family: Whitney, "Helvetica Neue", Helvetica, Arial, sans-serif; 47 | color: #8e9297; 48 | font-size: 12px; 49 | text-transform: uppercase; 50 | } 51 | 52 | .mLUserDiv { 53 | margin-left: -10px; 54 | padding: 7px 10px 4px 10px; 55 | user-select: none; 56 | border-radius: 3px; 57 | } 58 | 59 | .mLUserDivOffline { 60 | opacity: 0.3; 61 | } 62 | 63 | .mLUserDiv:hover { 64 | cursor: pointer; 65 | opacity: 1; 66 | background-color: #34373c; 67 | } 68 | 69 | .mLIcon { 70 | height: 30px; 71 | border-radius: 50%; 72 | display: inline-block; 73 | margin-right: 7px; 74 | -webkit-mask-image: url("../resources/icons/PFPMask.svg"); 75 | mask-image: url("../resources/icons/PFPMask.svg"); 76 | -webkit-mask-size: contain; 77 | mask-size: contain; 78 | } 79 | 80 | .mLUsername { 81 | font-family: Whitney, "Helvetica Neue", Helvetica, Arial, sans-serif; 82 | font-weight: 100; 83 | font-size: 16px; 84 | color: #8e9297; 85 | margin: 10px 0 0 0; 86 | padding: 0; 87 | display: inline-block; 88 | transform: translateY(-9px); 89 | text-overflow: ellipsis; 90 | max-width: calc(100% - 40px); 91 | overflow: hidden; 92 | white-space: nowrap; 93 | } 94 | 95 | #offlineUsersTransparent { 96 | opacity: 0.3; 97 | } 98 | -------------------------------------------------------------------------------- /css/embeds.css: -------------------------------------------------------------------------------- 1 | /* Copyright 2017 Sebastian Ouellette 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. */ 14 | 15 | .embed { 16 | margin: 0 0 7px 0; 17 | background-color: #2f3136; 18 | padding: 6px 15px 15px 15px; 19 | border-radius: 5px; 20 | border-left: 5px solid; 21 | color: white; 22 | min-height: 50px; 23 | max-width: 512px; 24 | } 25 | 26 | .messageTimestamp + .embed { 27 | margin-top: -20px; 28 | } 29 | 30 | .embedLargeIcon { 31 | height: 90px; 32 | border-radius: 5px; 33 | float: right; 34 | } 35 | 36 | .embedSmallIcon { 37 | height: 40px; 38 | border-radius: 5px; 39 | float: right; 40 | } 41 | 42 | .embedArticleLargeIcon { 43 | height: 212px; 44 | padding: 15px 0px 0px; 45 | border-radius: 5px; 46 | } 47 | 48 | /*Author*/ 49 | .embedAuthor { 50 | width: 100%; 51 | margin: 0 0 5px 0; 52 | } 53 | 54 | .embedAuthorImg { 55 | height: 20px; 56 | border-radius: 50%; 57 | display: inline-block; 58 | margin: 0; 59 | } 60 | 61 | .embedAuthorName { 62 | display: inline-block; 63 | margin: 0 0 0 5px; 64 | font-size: 14px; 65 | transform: translateY(-4px); 66 | color: #aaa; 67 | } 68 | 69 | /*Title section*/ 70 | .embedTitle { 71 | margin: 0; 72 | font-size: 17px; 73 | font-weight: bold; 74 | } 75 | 76 | .embedDescription { 77 | margin: 5px 0 0 0; 78 | } 79 | 80 | .embedContent { 81 | color: #aaa; 82 | font-size: 15px; 83 | } 84 | 85 | .embedProviderName { 86 | margin: 5px 0 5px 0; 87 | font-size: 13px; 88 | } 89 | 90 | /*Fields*/ 91 | .field { 92 | margin: 10px 20px 0 0; 93 | } 94 | 95 | .fieldName { 96 | color: #ddd; 97 | margin: 0; 98 | } 99 | 100 | .fieldText { 101 | margin: 0 0 0 5px; 102 | } 103 | 104 | /* Image */ 105 | .embedImage { 106 | max-width: 100%; 107 | margin-top: 10px; 108 | border-radius: 5px; 109 | } 110 | 111 | /*Footer*/ 112 | .footer { 113 | color: #888; 114 | margin: 10px 0 0 0; 115 | } 116 | 117 | .footerText { 118 | font-size: 12px; 119 | margin: 0; 120 | } 121 | -------------------------------------------------------------------------------- /resources/icons/checkmark.svg: -------------------------------------------------------------------------------- 1 | 2 | 16 | 18 | 38 | 40 | 41 | 43 | image/svg+xml 44 | 46 | 47 | 48 | 49 | 50 | 54 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /resources/icons/GuildVoiceChannelBlocked.svg: -------------------------------------------------------------------------------- 1 | 2 | 18 | 20 | 21 | 23 | image/svg+xml 24 | 26 | 27 | 28 | 29 | 30 | 32 | 52 | 58 | 62 | -------------------------------------------------------------------------------- /resources/icons/GuildTextChannel.svg: -------------------------------------------------------------------------------- 1 | 2 | 18 | 20 | 21 | 23 | image/svg+xml 24 | 26 | 27 | 28 | 29 | 30 | 32 | 52 | 58 | -------------------------------------------------------------------------------- /resources/icons/pullOut.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 41 | 46 | 47 | 49 | 50 | 52 | image/svg+xml 53 | 55 | 56 | 57 | 58 | 59 | 63 | 73 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /resources/icons/categoryArrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 41 | 46 | 47 | 49 | 50 | 52 | image/svg+xml 53 | 55 | 56 | 57 | 58 | 59 | 63 | 73 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /css/memberMenu.css: -------------------------------------------------------------------------------- 1 | /* Copyright 2017 Sebastian Ouellette 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. */ 14 | 15 | .memberMenu { 16 | position: absolute; 17 | background-color: #2f3136; 18 | height: 400px; 19 | width: 300px; 20 | margin-left: -330px; 21 | border-radius: 7px; 22 | cursor: default; 23 | overflow: hidden; 24 | text-align: center; 25 | z-index: 1; 26 | } 27 | .memberMenuUserDiv { 28 | width: 100%; 29 | background-color: #202225; 30 | padding: 20px 0; 31 | } 32 | 33 | .memberMenuRolesDiv { 34 | width: 100%; 35 | background-color: #2f3136; 36 | padding: 20px 0; 37 | } 38 | 39 | .memberIcon { 40 | height: 80px; 41 | width: 80px; 42 | border-radius: 50%; 43 | margin-left: auto; 44 | margin-right: auto; 45 | display: block; 46 | -webkit-mask-image: url("../resources/icons/PFPMask.svg"); 47 | mask-image: url("../resources/icons/PFPMask.svg"); 48 | -webkit-mask-size: contain; 49 | mask-size: contain; 50 | } 51 | 52 | .memberTopName { 53 | color: white; 54 | font-size: 16px; 55 | font-weight: 600; 56 | display: block; 57 | margin-top: 10px; 58 | } 59 | 60 | .memberTopDisc { 61 | color: #b9bbbe; 62 | } 63 | 64 | .memberBottomName { 65 | color: #b9bbbe; 66 | font-size: 14px; 67 | display: block; 68 | } 69 | 70 | .menuPresenceDiv { 71 | margin-top: 15px; 72 | width: 80%; 73 | margin-left: auto; 74 | margin-right: auto; 75 | display: block; 76 | } 77 | 78 | .memberCustomPresence { 79 | color: #b9bbbe; 80 | font-size: 15px; 81 | display: inline-block; 82 | word-wrap: break-word; 83 | white-space: pre-wrap; 84 | max-width: 100%; 85 | } 86 | 87 | .menuRolesDiv { 88 | width: 80%; 89 | margin-left: auto; 90 | margin-right: auto; 91 | display: block; 92 | } 93 | 94 | .memberRoles { 95 | color: #b9bbbe; 96 | font-size: 15px; 97 | display: inline-block; 98 | word-wrap: break-word; 99 | white-space: pre-wrap; 100 | max-width: 100%; 101 | } 102 | 103 | .memberRoles span { 104 | background-color: #202225; 105 | margin: 4px; 106 | padding: 4px 7px; 107 | border-radius: 5px; 108 | display: inline-block; 109 | } 110 | 111 | .memberCustomEmoji { 112 | display: inline-block; 113 | transform: translateY(4px); 114 | padding: 5px; 115 | } 116 | -------------------------------------------------------------------------------- /resources/icons/GuildTextChannelBlocked.svg: -------------------------------------------------------------------------------- 1 | 2 | 18 | 20 | 21 | 23 | image/svg+xml 24 | 26 | 27 | 28 | 29 | 30 | 32 | 52 | 56 | 60 | -------------------------------------------------------------------------------- /css/miscs.css: -------------------------------------------------------------------------------- 1 | /* Copyright 2017 Sebastian Ouellette 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. */ 14 | 15 | @charset "UTF-8"; 16 | /** 17 | * 18 | * three-dots.css v0.1.0 19 | * 20 | * https://nzbin.github.io/three-dots/ 21 | * 22 | * Copyright (c) 2018 nzbin 23 | * 24 | * Released under the MIT license 25 | * 26 | */ 27 | 28 | .dot-bricks { 29 | position: relative; 30 | top: 8px; 31 | left: -9999px; 32 | width: 10px; 33 | height: 10px; 34 | border-radius: 5px; 35 | background-color: #9880ff; 36 | color: #9880ff; 37 | box-shadow: 9991px -16px 0 0 #9880ff, 9991px 0 0 0 #9880ff, 38 | 10007px 0 0 0 #9880ff; 39 | animation: dotBricks 2s infinite ease; 40 | } 41 | 42 | @keyframes dotBricks { 43 | 0% { 44 | box-shadow: 9991px -16px 0 0 #9880ff, 9991px 0 0 0 #9880ff, 45 | 10007px 0 0 0 #9880ff; 46 | } 47 | 8.333% { 48 | box-shadow: 10007px -16px 0 0 #9880ff, 9991px 0 0 0 #9880ff, 49 | 10007px 0 0 0 #9880ff; 50 | } 51 | 16.667% { 52 | box-shadow: 10007px -16px 0 0 #9880ff, 9991px -16px 0 0 #9880ff, 53 | 10007px 0 0 0 #9880ff; 54 | } 55 | 25% { 56 | box-shadow: 10007px -16px 0 0 #9880ff, 9991px -16px 0 0 #9880ff, 57 | 9991px 0 0 0 #9880ff; 58 | } 59 | 33.333% { 60 | box-shadow: 10007px 0 0 0 #9880ff, 9991px -16px 0 0 #9880ff, 61 | 9991px 0 0 0 #9880ff; 62 | } 63 | 41.667% { 64 | box-shadow: 10007px 0 0 0 #9880ff, 10007px -16px 0 0 #9880ff, 65 | 9991px 0 0 0 #9880ff; 66 | } 67 | 50% { 68 | box-shadow: 10007px 0 0 0 #9880ff, 10007px -16px 0 0 #9880ff, 69 | 9991px -16px 0 0 #9880ff; 70 | } 71 | 58.333% { 72 | box-shadow: 9991px 0 0 0 #9880ff, 10007px -16px 0 0 #9880ff, 73 | 9991px -16px 0 0 #9880ff; 74 | } 75 | 66.666% { 76 | box-shadow: 9991px 0 0 0 #9880ff, 10007px 0 0 0 #9880ff, 77 | 9991px -16px 0 0 #9880ff; 78 | } 79 | 75% { 80 | box-shadow: 9991px 0 0 0 #9880ff, 10007px 0 0 0 #9880ff, 81 | 10007px -16px 0 0 #9880ff; 82 | } 83 | 83.333% { 84 | box-shadow: 9991px -16px 0 0 #9880ff, 10007px 0 0 0 #9880ff, 85 | 10007px -16px 0 0 #9880ff; 86 | } 87 | 91.667% { 88 | box-shadow: 9991px -16px 0 0 #9880ff, 9991px 0 0 0 #9880ff, 89 | 10007px -16px 0 0 #9880ff; 90 | } 91 | 100% { 92 | box-shadow: 9991px -16px 0 0 #9880ff, 9991px 0 0 0 #9880ff, 93 | 10007px 0 0 0 #9880ff; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /resources/icons/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 40 | 42 | 43 | 45 | image/svg+xml 46 | 48 | 49 | 50 | 51 | 52 | 56 | 62 | 68 | 74 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /start.js: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Sebastian Ouellette 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | 'use strict'; 16 | 17 | const electron = require('electron'); 18 | const { app, BrowserWindow, ipcMain } = electron; 19 | const path = require('path'); 20 | const url = require('url'); 21 | const pack = require('./package.json'); 22 | 23 | let win; 24 | 25 | function createWindow() { 26 | win = new BrowserWindow({ 27 | width: 1300, 28 | height: 750, 29 | frame: false, 30 | backgroundColor: '#FFF', 31 | webPreferences: { 32 | nodeIntegration: true, // Use node in the render js files 33 | contextIsolation: false, // Makes node in the render js files work in newer electron versions 34 | }, 35 | icon: __dirname + '/resources/icons/logo.png', 36 | }); 37 | 38 | // `remote` alternative 39 | ipcMain.on('titlebar', (event, message) => { 40 | switch (message) { 41 | case 'minimize': 42 | win.minimize(); 43 | break; 44 | case 'screenSnap': 45 | if (!win.isMaximized()) { 46 | win.maximize(); 47 | } else { 48 | win.unmaximize(); 49 | } 50 | break; 51 | case 'exit': 52 | win.close(); 53 | break; 54 | default: 55 | break; 56 | } 57 | }); 58 | 59 | win.loadURL( 60 | url.pathToFileURL(path.join(__dirname, 'dontOpenMe.html')).toString() 61 | ); 62 | 63 | // win.webContents.on('new-window', (e, url) => { 64 | // e.preventDefault(); 65 | // electron.shell.openExternal(url.replace(/\/$/, '')); 66 | // }); 67 | win.webContents.setWindowOpenHandler(({ url }) => { 68 | electron.shell.openExternal(url.replace(/\/$/, '')); 69 | return { action: 'allow' }; 70 | }); 71 | 72 | // win.webContents.on('did-create-window', child => { 73 | // // For example... 74 | // child.webContents('will-navigate', e => { 75 | // e.preventDefault() 76 | // }) 77 | // }) 78 | 79 | win.on('closed', () => { 80 | win = null; 81 | }); 82 | } 83 | 84 | app.on('ready', createWindow); 85 | 86 | app.on('window-all-closed', () => { 87 | if (process.platform !== 'darwin') { 88 | app.quit(); 89 | } 90 | }); 91 | 92 | app.on('activate', () => { 93 | if (win === null) { 94 | createWindow(); 95 | } 96 | }); 97 | 98 | app.on('web-contents-created', (e, contents) => { 99 | contents.on('new-window', (newEvent) => { 100 | console.log("Blocked by 'new-window'"); 101 | newEvent.preventDefault(); 102 | }); 103 | 104 | contents.on('will-navigate', (newEvent) => { 105 | console.log("Blocked by 'will-navigate'"); 106 | newEvent.preventDefault(); 107 | }); 108 | 109 | contents.setWindowOpenHandler(({ url }) => { 110 | setImmediate(() => { 111 | shell.openExternal(url); 112 | }); 113 | return { action: 'allow' }; 114 | }); 115 | }); 116 | -------------------------------------------------------------------------------- /resources/icons/embedIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 36 | 38 | 42 | 50 | 54 | 58 | 62 | 66 | 70 | 74 | 78 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /css/messageBar.css: -------------------------------------------------------------------------------- 1 | /* Copyright 2017 Sebastian Ouellette 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. */ 14 | 15 | /* Send message bar */ 16 | #sendmsg { 17 | position: absolute; 18 | margin-left: 310px; 19 | margin-top: -20px; 20 | width: calc(100vw - 518px); 21 | height: 40px; 22 | padding-bottom: 10px; 23 | z-index: 2; 24 | } 25 | 26 | #sendmsg::before { 27 | content: ""; 28 | width: 100%; 29 | background-color: #36393e; 30 | height: 4000px; 31 | position: absolute; 32 | top: 20px; 33 | z-index: -1; 34 | } 35 | 36 | #messageBar { 37 | white-space: nowrap; 38 | overflow: hidden; 39 | width: calc(100% - 40px); 40 | background-color: #484b51; 41 | height: 100%; 42 | margin: 0 0 0 15px; 43 | border-radius: 5px; 44 | padding: 0 10px 0 0; 45 | display: flex; 46 | 47 | /* display: table; */ 48 | } 49 | 50 | #msgbox::-webkit-scrollbar { 51 | width: 0; 52 | } 53 | 54 | #msgbox { 55 | display: block; 56 | background-color: transparent; 57 | border: none; 58 | margin: 0; 59 | outline: none; 60 | color: white; 61 | resize: none; 62 | display: inline-block; 63 | padding: 10px; 64 | width: 100%; 65 | } 66 | 67 | .messageBoxText { 68 | font-family: Whitney-light, "Helvetica Neue", Helvetica, Arial, sans-serif; 69 | font-size: 16px; 70 | font-weight: 400; 71 | padding: 2px 0 1px 0; 72 | margin: 0; 73 | color: #dcddde; 74 | /* width: calc(100% - 40px); */ 75 | word-spacing: 2px; 76 | position: relative; 77 | /* display: table-row; */ 78 | } 79 | 80 | /* Additional message bar icons */ 81 | #msgMisc { 82 | display: block; 83 | height: 100%; 84 | float: right; 85 | margin-top: 4px; 86 | display: grid; 87 | grid-auto-flow: column; 88 | grid-column-gap: 4px; 89 | } 90 | 91 | #msgMisc img { 92 | height: 25px; 93 | float: left; 94 | cursor: pointer; 95 | padding: 3px; 96 | opacity: 0.6; 97 | border-radius: 3px; 98 | } 99 | 100 | #msgMisc img:hover { 101 | opacity: 1; 102 | } 103 | 104 | /* Typing Indicator */ 105 | #typingIndicator { 106 | font-size: 14px; 107 | color: white; 108 | position: absolute; 109 | z-index: 1; 110 | user-select: none; 111 | margin: 2px 0 0 0; 112 | left: 50px; 113 | } 114 | 115 | #typingDots { 116 | position: absolute; 117 | display: none; 118 | margin-top: 20px; 119 | } 120 | 121 | #typingDots.enabled { 122 | display: block; 123 | } 124 | 125 | .bold { 126 | font-family: Whitney; 127 | font-weight: bold; 128 | } 129 | 130 | .typingDot { 131 | height: 8px; 132 | width: 8px; 133 | background-color: #686a6e; 134 | position: absolute; 135 | top: -13px; 136 | left: 15px; 137 | border-radius: 50%; 138 | animation: typing 2s infinite; 139 | } 140 | 141 | .dot1 { 142 | animation-delay: 0s; 143 | } 144 | 145 | .dot2 { 146 | left: 25px; 147 | animation-delay: 0.2s; 148 | } 149 | 150 | .dot3 { 151 | left: 35px; 152 | animation-delay: 0.4s; 153 | } 154 | 155 | @keyframes typing { 156 | 0%, 157 | 20% { 158 | background-color: #686a6e; 159 | transform: scale(0.5); 160 | } 161 | 50% { 162 | transform: scale(1); 163 | background-color: white; 164 | } 165 | 80%, 166 | 100% { 167 | background-color: #686a6e; 168 | transform: scale(0.5); 169 | } 170 | } 171 | 172 | /* */ 173 | -------------------------------------------------------------------------------- /css/channelList.css: -------------------------------------------------------------------------------- 1 | /* Copyright 2017 Sebastian Ouellette 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. */ 14 | 15 | #channel-list { 16 | font-family: Whitney, "Helvetica Neue", Helvetica, Arial, sans-serif; 17 | height: calc(100% - 90px); 18 | margin-left: 50px; 19 | margin-top: 40px; 20 | width: 250px; 21 | padding-left: 10px; 22 | padding-top: 10px; 23 | padding-bottom: -10px; 24 | background-color: #2f3136; 25 | color: #606266; 26 | overflow-y: auto; 27 | overflow-x: hidden; 28 | font-weight: 100; 29 | } 30 | 31 | #channel-list::-webkit-scrollbar { 32 | background-color: transparent; 33 | width: 6px; 34 | } 35 | 36 | #channel-list::-webkit-scrollbar-track { 37 | background-color: transparent; 38 | } 39 | 40 | #channel-list::-webkit-scrollbar-thumb { 41 | background-color: #25272b; 42 | border-radius: 3px; 43 | } 44 | 45 | .categoryNameContainer { 46 | display: block; 47 | } 48 | 49 | .categoryText { 50 | /* font-variant: small-caps; */ 51 | text-transform: uppercase; 52 | user-select: none; 53 | padding: 15px 0 0 0 !important; 54 | height: 20px; 55 | font-size: 12px; 56 | font-weight: bold; 57 | 58 | transform: translateY(5px) !important; 59 | } 60 | 61 | .categoryNameContainer { 62 | cursor: pointer; 63 | } 64 | 65 | .categoryNameContainer:hover .categoryText { 66 | color: #b4b8bc; 67 | } 68 | 69 | .categorySVG { 70 | height: 10px; 71 | position: relative; 72 | top: 5px; 73 | margin-right: 8px; 74 | transform: rotate(90deg); 75 | transition-duration: 0.1s; 76 | } 77 | 78 | .open .categorySVG { 79 | transform: rotate(180deg); 80 | } 81 | 82 | .GuildNews { 83 | height: 18px !important; 84 | width: 18px !important; 85 | padding: 1px; 86 | } 87 | 88 | .channelContainer { 89 | display: none; 90 | } 91 | 92 | .open .channelContainer { 93 | display: block; 94 | } 95 | 96 | .channel { 97 | user-select: none; 98 | margin: 2px 0 0 -5px; 99 | } 100 | 101 | .GuildVoice { 102 | user-select: none; 103 | margin-left: -5px; 104 | } 105 | 106 | h5 { 107 | font-weight: 100; 108 | font-size: 16px; 109 | } 110 | 111 | .channelSVG { 112 | height: 20px; 113 | width: 20px; 114 | display: inline-box; 115 | transform: translateY(-9px); 116 | } 117 | 118 | #channel-elements h5 { 119 | transform: translateY(-5px); 120 | display: inline-block; 121 | margin: 0 !important; 122 | padding: 7px 0 7px 10px; 123 | } 124 | 125 | .channel { 126 | /* margin: 0 0 0 20px; */ 127 | padding: 5px 10px; 128 | cursor: pointer; 129 | border-radius: 3px; 130 | height: 25px; 131 | } 132 | 133 | .channel:hover { 134 | background-color: #34373c; 135 | color: #b4b8bc; 136 | } 137 | 138 | /* Channel ... formatting */ 139 | .channel h5 { 140 | max-width: calc(100% - 30px); 141 | overflow: hidden; 142 | text-overflow: ellipsis; 143 | white-space: nowrap; 144 | user-select: none; 145 | } 146 | 147 | .selectedChan { 148 | background-color: #393c43 !important; 149 | color: white !important; 150 | } 151 | 152 | .channel.newMsg { 153 | color: white !important; 154 | } 155 | 156 | .blocked { 157 | cursor: not-allowed; 158 | /* color: #904d3c; */ 159 | color: #fc5c65; 160 | } 161 | 162 | .blocked h5 { 163 | opacity: 0.35; 164 | } 165 | 166 | .blocked:hover { 167 | color: #fc5c65; 168 | /* background-color: #4d4040; */ 169 | background-color: #3c3434; 170 | } 171 | 172 | .blocked:hover h5 { 173 | opacity: 1; 174 | } 175 | -------------------------------------------------------------------------------- /resources/icons/logoLarge.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 40 | 42 | 43 | 45 | image/svg+xml 46 | 48 | 49 | 50 | 51 | 52 | 56 | 62 | 68 | 74 | 83 | LiveBot 94 | 95 | 96 | -------------------------------------------------------------------------------- /js/buildMemberMenu.js: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Sebastian Ouellette 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | 'use strict'; 16 | 17 | function buildMemberMenu(parent) { 18 | let member = selectedGuild.members.cache.get(parent.firstChild.id); 19 | 20 | let div = document.createElement('div'); 21 | div.classList.add('memberMenu'); 22 | parent.appendChild(div); 23 | 24 | // The top section of the member menu 25 | let user = document.createElement('div'); 26 | user.classList.add('memberMenuUserDiv'); 27 | div.appendChild(user); 28 | 29 | // The top section of the member menu 30 | let roles = document.createElement('div'); 31 | roles.classList.add('memberMenuRolesDiv'); 32 | div.appendChild(roles); 33 | 34 | // The user icon 35 | let userIcon = document.createElement('img'); 36 | userIcon.classList.add('memberIcon'); 37 | userIcon.src = member.displayAvatarURL({ size: 512 }); 38 | 39 | user.appendChild(userIcon); 40 | 41 | // Username and nickname 42 | let topName = document.createElement('span'); 43 | topName.classList.add('memberTopName'); 44 | topName.innerText = member.displayName; 45 | user.appendChild(topName); 46 | 47 | if (member.nickname) { 48 | // Create the full tag below 49 | let bottomName = document.createElement('span'); 50 | bottomName.classList.add('memberBottomName'); 51 | bottomName.innerText = member.user.tag; 52 | user.appendChild(bottomName); 53 | } else { 54 | // Add the discriminator to the top name 55 | let topDisc = document.createElement('span'); 56 | topDisc.classList.add('memberTopDisc'); 57 | topDisc.innerText = `#${member.user.discriminator}`; 58 | topName.appendChild(topDisc); 59 | } 60 | 61 | // Custom presence div 62 | let presenceDiv = document.createElement('div'); 63 | presenceDiv.classList.add('menuPresenceDiv'); 64 | user.appendChild(presenceDiv); 65 | 66 | // Custom presence 67 | let custPresence = member.presence?.activities.find( 68 | (a) => a.type == Discord.ActivityType.Custom 69 | ); 70 | if (custPresence) { 71 | if (custPresence.emoji) { 72 | // Status emoji 73 | let custEmoji = document.createElement('div'); 74 | custEmoji.classList.add('memberCustomEmoji'); 75 | if (custPresence.emoji.id) { 76 | // Custom emoji 77 | } else { 78 | // Twemoji 79 | custEmoji.innerHTML = twemoji.parse(custPresence.emoji.name); 80 | } 81 | presenceDiv.appendChild(custEmoji); 82 | } 83 | 84 | // Status text 85 | let custPres = document.createElement('span'); 86 | custPres.classList.add('memberCustomPresence'); 87 | custPres.innerText = custPresence.state; 88 | presenceDiv.appendChild(custPres); 89 | } 90 | 91 | // roles :D 92 | let rolesDiv = document.createElement('div'); 93 | rolesDiv.classList.add('menuRolesDiv'); 94 | roles.appendChild(rolesDiv); 95 | 96 | let rolestext = document.createElement('div'); 97 | rolestext.classList.add('memberRoles'); 98 | 99 | member.roles.cache.each((role) => { 100 | const spans = document.createElement('span'); 101 | spans.innerText = role.name; 102 | rolestext.appendChild(spans); 103 | }); 104 | 105 | rolesDiv.appendChild(rolestext); 106 | 107 | // Set the final position of the menu 108 | let borderOffset = 40; 109 | let y = window.pageYOffset + parent.getBoundingClientRect().top - 20; 110 | if (y + div.clientHeight > window.innerHeight) 111 | y = window.innerHeight - div.clientHeight - borderOffset; 112 | 113 | div.style.top = `${y}px`; 114 | } 115 | -------------------------------------------------------------------------------- /css/guildList.css: -------------------------------------------------------------------------------- 1 | /* Copyright 2017 Sebastian Ouellette 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. */ 14 | 15 | .homeBreak { 16 | border: none; 17 | margin: 9px 8px 8px 7px; 18 | padding: 2px 0 0 0; 19 | background-color: #72767d; 20 | } 21 | 22 | .guild-icon { 23 | border-radius: 50%; 24 | margin-left: 10px; 25 | transition-duration: 0.3s; 26 | user-select: none; 27 | border-radius: 50%; 28 | margin-top: 5px; 29 | } 30 | 31 | .guild-icon.selectedGuild { 32 | border-radius: 25% !important; 33 | } 34 | 35 | .guild-icon:hover { 36 | cursor: pointer; 37 | border-radius: 25%; 38 | } 39 | 40 | #guildAbrev { 41 | position: absolute; 42 | overflow-x: hidden; 43 | color: White; 44 | font-family: Whitney-light, "Helvetica Neue", Helvetica, Arial, sans-serif; 45 | text-align: center; 46 | font-size: 13px; 47 | width: 40px; 48 | } 49 | 50 | #guild-list { 51 | width: 60px; 52 | height: 100%; 53 | margin: 0 -10px 10px -10px; 54 | padding-top: 2px; 55 | background-color: #202225; 56 | overflow-y: auto; 57 | position: absolute; 58 | padding-bottom: 2px; 59 | } 60 | 61 | #guild-list::-webkit-scrollbar { 62 | display: none; 63 | } 64 | 65 | #guildIndicator { 66 | background-color: white; 67 | height: 40px; 68 | width: 6px; 69 | margin-left: 0px; 70 | position: absolute; 71 | z-index: 100; 72 | border-radius: 3px; 73 | display: none; 74 | transition-duration: 0.2s; 75 | } 76 | 77 | .guildIconDiv { 78 | color: transparent; 79 | width: auto; 80 | cursor: default; 81 | display: flex; 82 | transition-duration: 0.3s !important; 83 | } 84 | 85 | .guildIconDiv .guild-icon { 86 | order: -1; 87 | } 88 | 89 | /* DEBUG TESTING */ 90 | .guildNameContainer::before { 91 | left: -10px; 92 | top: 8px; 93 | margin: 3px 0 0 0px; 94 | border: 6px solid transparent; 95 | content: " "; 96 | position: absolute; 97 | border-right-color: transparent; 98 | /* transition-duration: 0.1s !important; */ 99 | } 100 | 101 | .guildNameContainer { 102 | color: transparent; 103 | height: 35px; 104 | /* margin-top: 12px; */ 105 | margin-left: 70px; 106 | z-index: 1; 107 | visibility: hidden; 108 | position: fixed; 109 | border-radius: 5px; 110 | background-color: transparent; 111 | padding: 0 10px; 112 | font-weight: bold; 113 | /* transition-duration: 0.3s !important; */ 114 | } 115 | 116 | .guildName { 117 | color: inherit; 118 | margin: 3px 4px 0; 119 | display: inherit; 120 | position: relative; 121 | font-size: 13px; 122 | line-height: 27px; 123 | } 124 | 125 | .guild-icon:hover + .guildNameContainer::before { 126 | display: block; 127 | border-right-color: black; 128 | } 129 | 130 | .guild-icon:hover + .guildNameContainer { 131 | color: white; 132 | visibility: visible; 133 | background-color: black; 134 | } 135 | 136 | /* WORKING COPY - DO NOT CHANGE */ 137 | /* .guildNameContainer::before { 138 | left: -10px; 139 | top: 4px; 140 | margin: 3px 0 0 0px; 141 | border: 6px solid transparent; 142 | content: ' '; 143 | position: absolute; 144 | border-right-color: transparent; 145 | transition-duration: 0.1s !important; 146 | } 147 | 148 | .guildNameContainer { 149 | color: transparent; 150 | height: 26px; 151 | margin: 12px 0 0 70px; 152 | z-index: 1; 153 | visibility: hidden; 154 | position: fixed; 155 | border-radius: 5px; 156 | background-color: transparent; 157 | transition-duration: 0.3s !important; 158 | } 159 | 160 | .guildName { 161 | color: inherit; 162 | margin: 3px 4px 0; 163 | display: inherit; 164 | position: fixed; 165 | } 166 | 167 | .guild-icon:hover + .guildNameContainer::before { 168 | display: block; 169 | border-right-color: black; 170 | } 171 | 172 | .guild-icon:hover + .guildNameContainer{ 173 | color: white; 174 | visibility: visible; 175 | background-color: black; 176 | } */ 177 | -------------------------------------------------------------------------------- /js/rcMenuFuncs.js: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Sebastian Ouellette 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | 'use strict'; 16 | 17 | // -------- MESSAGE MENU -------- 18 | 19 | // Used for removing the edit message DOM and replacing it with the original 20 | let editDOM = (target, textarea, elementText) => { 21 | // Remove the text field, replace with normal message 22 | target.removeChild(textarea); 23 | 24 | let newMsgElement = document.createElement('p'); 25 | newMsgElement.classList.add('messageText'); 26 | newMsgElement.innerHTML = elementText; 27 | target.appendChild(newMsgElement); 28 | }; 29 | 30 | let checkEditDoms = () => { 31 | // Check if there's any edit message menu already open and close it 32 | let messageMenus = document.getElementsByClassName('editTextarea'); 33 | 34 | for (let messageMenu of messageMenus) { 35 | let id = messageMenu.classList[2]; 36 | let text = selectedChan.messages.cache.get(id).cleanContent; 37 | editDOM(messageMenu.parentElement, messageMenu, text); 38 | } 39 | }; 40 | 41 | // Edit a message 42 | function editMsg(target) { 43 | // Safe check so there is only one DOM to edit messages 44 | checkEditDoms(); 45 | 46 | // Find and delete the message text

element 47 | let textElement = target.querySelector('.messageText'); 48 | 49 | // Get the text in the message 50 | let elementText = textElement.innerHTML; 51 | let msg = selectedChan.messages.cache.get(target.id); 52 | let text = msg.cleanContent; 53 | 54 | target.removeChild(textElement); 55 | 56 | let textarea = document.createElement('textarea'); 57 | textarea.value = text; 58 | textarea.classList.add('editTextarea'); 59 | textarea.classList.add('messageBoxText'); 60 | textarea.classList.add(target.id); 61 | textarea.rows = '1'; 62 | setRows(textarea); 63 | target.appendChild(textarea); 64 | 65 | textarea.addEventListener('keydown', (e) => { 66 | if (e.key == 'Enter' && !e.shiftKey) { 67 | if (textarea.value == text) return editDOM(target, textarea, text); 68 | let newText = textarea.value; 69 | newText = newText.replace( 70 | /()/gm, 71 | (a, b, c, d) => { 72 | if (c != '!') { 73 | return `${b}!${d}`; 74 | } 75 | return a; 76 | } 77 | ); 78 | 79 | newText = parseSend(newText); 80 | 81 | msg.edit(newText).catch((e) => { 82 | command('Message failed to send\nError: ' + e.message); 83 | }); 84 | 85 | editDOM(target, textarea, elementText); 86 | } 87 | }); 88 | 89 | textarea.addEventListener('input', (e) => setRows(textarea)); 90 | } 91 | 92 | function setRows(textarea) { 93 | let rows = textarea.value.split('\n').length; 94 | if (rows > 6) rows = 6; 95 | if (rows == 0) rows++; 96 | textarea.rows = rows; 97 | } 98 | 99 | function pinMsg(id, pin) { 100 | try { 101 | pin 102 | ? selectedChan.messages.cache.get(id).pin() 103 | : selectedChan.messages.cache.get(id).unpin(); 104 | } catch (e) { 105 | console.log(e); 106 | } 107 | } 108 | 109 | function deleteMsg(id) { 110 | try { 111 | selectedChan.messages.cache.get(id).delete(); 112 | } catch (e) { 113 | console.log(e); 114 | } 115 | } 116 | 117 | function copyMessageLink(id, msg) { 118 | clipboard.writeText(msg.url); 119 | } 120 | 121 | function copyMessageID(id) { 122 | clipboard.writeText(id); 123 | } 124 | 125 | // -------- USER MENU -------- 126 | 127 | function dmUser(user) { 128 | dmChannelSelect(user); 129 | } 130 | 131 | function mentionUser(id) { 132 | let msgBox = document.getElementById('msgbox'); 133 | msgBox.value += `<@${id}>`; 134 | } 135 | 136 | function copyUserID(id) { 137 | clipboard.writeText(id); 138 | } 139 | 140 | function copyAvatarLink(member) { 141 | member = member.user || member; 142 | clipboard.writeText(member.displayAvatarURL()); 143 | } 144 | -------------------------------------------------------------------------------- /js/handleScripts.js: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Sebastian Ouellette 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | 'use strict'; 16 | 17 | let loadAllScripts = () => { 18 | let files = fs.readdirSync('./scripts'); 19 | 20 | // Remove the template file out of the equation if there is one 21 | if (files.includes('template.js')) 22 | files.splice(files.indexOf('template.js'), 1); 23 | 24 | // If there are no scripts don't even bother 25 | if (!files.length) return; 26 | 27 | // Preload all the scripts 28 | console.log('Loading scripts...'); 29 | files.forEach((file) => { 30 | if (file.endsWith('.js') && file != 'template.js') { 31 | delete require.cache[require.resolve(`./scripts/${file}`)]; 32 | 33 | require('./scripts/' + file); 34 | console.log(`%c ${file} loaded!`, 'color:Green'); 35 | } 36 | }); 37 | 38 | // Start all the loaded scripts 39 | console.log('Starting scripts...'); 40 | files.forEach((file) => { 41 | if (!file.endsWith('.js')) return; 42 | 43 | try { 44 | const script = require.cache[require.resolve(`./scripts/${file}`)].exports; 45 | 46 | // Modern API: script is a function 47 | if (typeof script === 'function') { 48 | script(global.bot); 49 | console.log(`%c ${file} started! (function)`, 'color:Green'); 50 | return; 51 | } 52 | 53 | // Legacy API: script.start exists 54 | if (typeof script.start === 'function') { 55 | script.start(global.bot); 56 | console.log(`%c ${file} started! (start method)`, 'color:Green'); 57 | return; 58 | } 59 | 60 | // No valid entry point 61 | console.log(`%c ${file} has no start function`, 'color:Orange'); 62 | 63 | } catch (err) { 64 | console.log( 65 | `%c ${file} was unable to start\n${err}`, 66 | 'color:Red' 67 | ); 68 | } 69 | }); 70 | }; 71 | 72 | let unloadAllScripts = () => { 73 | let files = fs.readdirSync('./scripts'); 74 | 75 | // Stop and unload all the scripts 76 | console.log('Stopping scripts...'); 77 | files.forEach((file) => { 78 | if ( 79 | file.endsWith('.js') && 80 | file != 'template.js' && 81 | require.cache[require.resolve(`./scripts/${file}`)] 82 | ) { 83 | try { 84 | if ( 85 | !require.cache[require.resolve(`./scripts/${file}`)].exports 86 | .stop 87 | ) 88 | throw { message: '.exports.stop is not a function' }; 89 | console.log(`%c ${file} stopped!`, 'color:Red'); 90 | require.cache[ 91 | require.resolve(`./scripts/${file}`) 92 | ].exports.stop(); 93 | } catch (err) { 94 | // If it's missing the start function then display a short error and say which plugin it is 95 | if (err.message.includes('.exports.stop is not a function')) 96 | return console.log( 97 | `%c ${file} was unable to stop`, 98 | 'color:Red' 99 | ); 100 | console.log( 101 | `%c ${file} was unable to stop\n${err}`, 102 | 'color:Red' 103 | ); 104 | } 105 | 106 | delete require.cache[require.resolve(`./scripts/${file}`)]; 107 | console.log(`%c ${file} unloaded!`, 'color:Red'); 108 | } 109 | }); 110 | }; 111 | 112 | let getData = () => { 113 | let object = {}; 114 | 115 | let files = fs.readdirSync('./scripts'); 116 | 117 | files.forEach((file) => { 118 | if (file.endsWith('.js') && file != 'template.js') { 119 | object[file] = 120 | require.cache[ 121 | require.resolve(`./scripts/${file}`) 122 | ].exports.info; 123 | object[file].file = file; 124 | } 125 | }); 126 | 127 | return object; 128 | }; 129 | -------------------------------------------------------------------------------- /js/parseFunctions.js: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Sebastian Ouellette 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | 'use strict'; 16 | 17 | function parseHTML(text) { 18 | text = text.replace(/<|>|&/gm, (s) => 19 | s == '<' ? '<' : s == '>' ? '>' : '&' 20 | ); 21 | return text; 22 | } 23 | 24 | function parsePings(msg, text, embeddedLink, ping, embed) { 25 | // Format pings in embeds 26 | if (ping || !embed) { 27 | let dms = selectedChan.type == Discord.ChannelType.DM; 28 | text = formatEmbedPings(msg, text, dms); 29 | text = formatPings(msg, text, dms); 30 | } 31 | // Format links in embeds 32 | if (embeddedLink) { 33 | text = text.replace( 34 | /(?:\[(?:<(?:[\w\W]+?>([\w\.!@#$%^&*\-\/"=\[\];]+?)<(?:[\w\W\/]+?)>)|([\w\.!@#$%^&*\-\/"=<>\]\[; ]+?))\]\((?: ]+?)\)))/gm, 35 | (a, b, c, d, e) => { 36 | return `${ 37 | b ? b : c 38 | }`; 39 | } 40 | ); 41 | } 42 | 43 | return text; 44 | } 45 | 46 | function parseLinks(text) { 47 | text = text.replace( 48 | /https?:\/\/((?:\w|.)+?)(?=\/|(?= )|[>)}\]:; ]|$)(?:[\w\.!@#$%^&*\-\/]+?)*(?:\?.*?(?=[>)}\]:; ]|$))?/gm, 49 | (a, b, c) => { 50 | // Add '/' to the end if it's not already there 51 | a.endsWith('/') ? undefined : (a += '/'); 52 | // Return the html for the link 53 | return `${a}`; 54 | } 55 | ); 56 | return text; 57 | } 58 | 59 | function parseStyling(text, embed) { 60 | let code = false; 61 | // Check for major codeblock 62 | text = text.replace( 63 | /(? { 65 | code = true; 66 | c = c.length ? c : b; 67 | // console.log(b) 68 | return `

${c}
`; 71 | } 72 | ); 73 | // Check for inline codeblock 74 | text = text.replace(/(? { 75 | if (code) return a; 76 | code = true; 77 | return `${b}`; 78 | }); 79 | 80 | if (code == false) { 81 | // General styling 82 | text = text.replace( 83 | /(?$1' 85 | ); 86 | text = text.replace( 87 | /(?$1' 89 | ); 90 | text = text.replace(/(?$1'); 91 | text = text.replace(/(?$1'); 92 | text = text.replace(/(?$1'); 93 | text = text.replace( 94 | /(?$1' 96 | ); 97 | text = text.replace(/(?$1'); 98 | } 99 | 100 | return text; 101 | } 102 | 103 | function parseUnicodeEmojis(text) { 104 | if ( 105 | !text.replace( 106 | /((\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])| |(<a?:!?.+?:\d{17,19}?>))/g, 107 | '' 108 | ).length 109 | ) { 110 | text = `${text}`; 111 | } 112 | return text; 113 | } 114 | 115 | function parseCustomEmojis(text) { 116 | text = text.replace( 117 | /<(a)?:!?(.+?):(\d{17,19}?)>/gm, 118 | (original, animated, name, id) => { 119 | if (id !== undefined) 120 | return `:${name}:`; 123 | return animated; 124 | } 125 | ); 126 | return text; 127 | } 128 | -------------------------------------------------------------------------------- /js/app.js: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Sebastian Ouellette 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | 'use strict'; 16 | 17 | const Discord = require('discord.js'); 18 | const { clipboard } = require('electron'); 19 | const fs = require('fs'); 20 | let jsonSettings = require('./json/settings.json'); 21 | 22 | let selectedGuild; 23 | let selectedChan; 24 | let selectedChanDiv; 25 | let selectedVoice; 26 | let selectedChatDiv; 27 | let oldimg; 28 | let generatingMessages = false; 29 | let barry = false; 30 | 31 | // Disable the security warning from electron that comes from using an uncompiled version 32 | process.env['ELECTRON_DISABLE_SECURITY_WARNINGS'] = true; 33 | // Set the permissions umask to 0, allowing writing to files using the exact specified permissions 34 | process.umask(0); 35 | // Allows LiveBot to work as an executable 36 | process.chdir(__dirname); 37 | // Display that LiveBot has started 38 | console.log('LiveBot started'); 39 | 40 | // Create the app and attach event listeners 41 | async function create() { 42 | document.getElementById('clearCache').onclick = (e) => { 43 | clearSettingsFile(); 44 | document.getElementById('clearCache').parentElement.innerHTML = 45 | "

Cache cleared! Now you can restart LiveBot

"; 46 | }; 47 | 48 | document.getElementById('msgbox').addEventListener('keydown', (event) => { 49 | if (event.key == 'Enter' && !event.shiftKey) { 50 | event.preventDefault(); 51 | 52 | // If the message was able to be sent, reset the message box size 53 | if (!sendmsg()) { 54 | // Reset the textbox height 55 | let msgBox = document.getElementById('sendmsg'); 56 | msgBox.style.height = '38px'; // Reset the height first 57 | msgBox.style.transform = ''; 58 | } 59 | } 60 | }); 61 | 62 | document.getElementById('msgbox').addEventListener('input', (event) => { 63 | let textElem = document.getElementById('msgbox'); 64 | let rows = textElem.value.split('\n').length; 65 | rows = rows == 0 ? 1 : rows; 66 | // document.getElementById("msgbox").rows = rows; 67 | 68 | let msgBox = document.getElementById('sendmsg'); 69 | 70 | if (textElem.scrollHeight < 38 * 5) { 71 | msgBox.style.height = '0px'; // Reset the height first 72 | msgBox.style.height = `${textElem.scrollHeight}px`; 73 | msgBox.style.transform = `translateY(-${ 74 | textElem.scrollHeight - 38 75 | }px)`; 76 | } 77 | }); 78 | 79 | // Call the settings menu builder 80 | buildSettingsMenu(jsonSettings); 81 | 82 | // Call the general click event listener script 83 | addDocListener(); 84 | 85 | // Load the bot with the token in storage or throw an error if there isn't any 86 | setLoadingPerc(0); 87 | if (settings.defaultToken) { 88 | var error = await load(settings.defaultToken); 89 | if (error[0]) { 90 | buildSplashToken(); 91 | } 92 | } else { 93 | buildSplashToken(); 94 | // errorHandler('NO-TOKEN'); 95 | } 96 | } 97 | 98 | // Alert that you are typing 99 | function typing() { 100 | if (!selectedChan || !document.getElementById('msgbox').value) return; 101 | if (!bot.user._typing?.has(selectedChan.id)) selectedChan.sendTyping(); 102 | } 103 | 104 | // Why has this code been in livebot this long???? 105 | // Options on the right pane 106 | function options(type, content) { 107 | switch (type) { 108 | case 'username': 109 | bot.user.setUsername(content); 110 | document.getElementById('userCardName').innerText = content; 111 | break; 112 | 113 | case 'invite': 114 | selectedChan 115 | .createInvite() 116 | .then((invite) => 117 | command( 118 | 'Created invite for ' + 119 | invite.guild.name + 120 | ' \nhttps://discord.gg/' + 121 | invite.code 122 | ) 123 | ); 124 | break; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /js/addMemberList.js: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Sebastian Ouellette 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | 'use strict'; 16 | 17 | async function addMemberList(guild) { 18 | const listDiv = document.getElementById('memberBar'); 19 | 20 | // Clear members list 21 | listDiv.innerHTML = ''; 22 | 23 | if (!selectedChan) return; 24 | 25 | let members = guild.members.cache; 26 | let roles = new Discord.Collection(); 27 | 28 | if (members.size !== guild.memberCount) 29 | members = await guild.members.fetch(); 30 | 31 | guild.roles.cache 32 | .sort((role1, role2) => role2.rawPosition - role1.rawPosition) 33 | .forEach((role) => 34 | roles.set(role.id, { id: role.id, name: role.name }) 35 | ); 36 | 37 | // add Online and Offline roles to display members without hoist role 38 | roles.set('online', { id: 'online', name: 'Online' }); 39 | roles.set('offline', { id: 'offline', name: 'Offline' }); 40 | 41 | members 42 | .sort((member1, member2) => { 43 | if (member1.displayName < member2.displayName) return -1; 44 | if (member1.displayName > member2.displayName) return 1; 45 | 46 | return 0; 47 | }) 48 | .forEach((member) => { 49 | if ( 50 | !member 51 | .permissionsIn(selectedChan) 52 | .has(Discord.PermissionFlagsBits.ViewChannel) 53 | ) 54 | return; 55 | 56 | let role; 57 | 58 | if (!member.presence || member.presence.status == 'offline') { 59 | if (members.size < 1000) role = roles.get('offline'); 60 | else return; 61 | } else if (member.roles.hoist) 62 | role = roles.get(member.roles.hoist.id); 63 | else if (member.presence.status != 'offline') 64 | role = roles.get('online'); 65 | 66 | if (!role.container) { 67 | role.container = document.createElement('div'); 68 | role.container.id = role.id; 69 | role.container.classList.add('roleContainer'); 70 | 71 | role.container.name = document.createElement('span'); 72 | role.container.name.classList.add('roleTitle'); 73 | role.container.appendChild(role.container.name); 74 | } 75 | 76 | // Create the outer div 77 | let outerDiv = document.createElement('div'); 78 | outerDiv.classList.add('mLOuterDiv'); 79 | role.container.appendChild(outerDiv); 80 | 81 | // Make the div for the user 82 | let userDiv = document.createElement('div'); 83 | userDiv.id = member.id; 84 | userDiv.classList.add('mLUserDiv'); 85 | outerDiv.appendChild(userDiv); 86 | 87 | // Add the user icon 88 | let icon = document.createElement('img'); 89 | icon.classList.add('mLIcon'); 90 | icon.src = member.displayAvatarURL({ size: 64, forceStatic: true }); 91 | userDiv.onmouseenter = (e) => { 92 | icon.src = member.displayAvatarURL({ size: 64 }); 93 | }; 94 | userDiv.onmouseleave = (e) => { 95 | icon.src = member.displayAvatarURL({ 96 | size: 64, 97 | forceStatic: true, 98 | }); 99 | }; 100 | userDiv.appendChild(icon); 101 | 102 | // Make the username text 103 | let username = document.createElement('p'); 104 | username.classList.add('mLUsername'); 105 | let name = member.displayName; 106 | username.innerText = name; 107 | username.style.color = member.roles.color?.hexColor || '#8E9297'; 108 | userDiv.appendChild(username); 109 | }); 110 | 111 | roles 112 | .filter((role) => role.container) 113 | .forEach((role) => { 114 | // Add the role name with member count 115 | role.container.name.innerText = `${role.name} — ${ 116 | role.container.childElementCount - 1 117 | }`; 118 | 119 | listDiv.appendChild(role.container); 120 | }); 121 | } 122 | -------------------------------------------------------------------------------- /js/setToken.js: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Sebastian Ouellette 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | 'use strict'; 16 | 17 | async function setToken(token) { 18 | let client = new Discord.Client({ 19 | partials: [ 20 | Discord.Partials.Channel, // get DM messages 21 | ], 22 | intents: [ 23 | Discord.GatewayIntentBits.Guilds, 24 | Discord.GatewayIntentBits.GuildMessages, 25 | Discord.GatewayIntentBits.GuildMembers, 26 | Discord.GatewayIntentBits.GuildVoiceStates, 27 | Discord.GatewayIntentBits.MessageContent, 28 | Discord.GatewayIntentBits.GuildMessageTyping, 29 | Discord.GatewayIntentBits.GuildPresences, 30 | Discord.GatewayIntentBits.DirectMessageTyping, 31 | Discord.GatewayIntentBits.DirectMessages, 32 | ], 33 | }); 34 | 35 | let error = [false, 'none']; 36 | if (global.bot && bot.token == token) { 37 | return [true, 'SAME-TOKEN']; 38 | } 39 | try { 40 | setLoadingPerc(0.05); 41 | error = await validateToken(token); 42 | if (error[0]) { 43 | throw error[1]; 44 | } 45 | await client.login(token).catch((err) => { 46 | throw err; 47 | }); 48 | client.destroy(); 49 | 50 | setLoadingPerc(0.1); // Refreshing the everything 51 | // Clear the list of channels 52 | let channels = document.getElementById('channel-elements'); 53 | while (channels.firstChild) { 54 | channels.removeChild(channels.firstChild); 55 | } 56 | 57 | // Delete the list of the guilds 58 | let guildContainer = document.getElementById('guildContainer'); 59 | if (guildContainer && guildContainer.parentElement) { 60 | guildContainer.parentElement.removeChild(guildContainer); 61 | } 62 | 63 | // Clear the message list 64 | let messages = document.getElementById('message-list'); 65 | while (messages.firstChild) { 66 | messages.removeChild(messages.firstChild); 67 | } 68 | 69 | // Clear the member list 70 | let memberList = document.getElementById('memberBar'); 71 | memberList.innerHTML = ''; 72 | 73 | // Stop the current bot, unload scripts, and then load into the new token 74 | if (global.bot !== undefined) { 75 | bot.destroy(); 76 | } 77 | await unloadAllScripts(); 78 | await unloadThemes(); 79 | load(token); 80 | cachedGuilds = []; 81 | } catch (err) { 82 | // Set the error to true so it doesn't save the token 83 | error[0] = true; 84 | error[1] = err; 85 | } 86 | // Return if there's been an error or not 87 | 88 | return error; 89 | } 90 | 91 | async function saveToken(token) { 92 | let error = [false, 'none']; 93 | if (global.bot === undefined) error = await load(token); 94 | else error = await setToken(token); 95 | 96 | if (!error[0]) { 97 | // Set the default token 98 | settings.defaultToken = token; 99 | return error; 100 | } else { 101 | console.warn(`The token won't be saved since there was an error`); 102 | return error; 103 | } 104 | } 105 | 106 | function validateToken(token = '') { 107 | if (token.length == 0) return [true, 'EMPTY-TOKEN']; 108 | 109 | if (token.replace('.', '').length < 50) return [true, 'TOKEN-SHORT']; 110 | 111 | let invalidChars = [' ', '\t', '\r', '\n']; 112 | if (token.split('').filter((c) => invalidChars.includes(c)).length > 0) 113 | return [true, 'TOKEN-WHITESPACE']; 114 | 115 | if (token.replace(/\w|-|_|\./gm, '').length > 0) 116 | return [true, 'INVALID-TOKEN-CHARACTERS']; 117 | 118 | let tA = token.split('.'); 119 | if (tA.length != 3) return [true, 'INVALID-TOKEN-FORMAT']; 120 | // tA[0] = tA[0].length; 121 | // tA[1] = tA[1].length; 122 | // tA[2] = tA[2].length; 123 | // if ( 124 | // tA[0] < 24 || 125 | // tA[0] > 24 || 126 | // tA[1] < 6 || 127 | // tA[1] > 6 || 128 | // tA[2] < 27 || 129 | // tA[2] > 27 130 | // ) 131 | // return [true, 'INVALID-TOKEN-FORMAT']; 132 | 133 | return [false, 'none']; 134 | } 135 | -------------------------------------------------------------------------------- /css/textFormat.css: -------------------------------------------------------------------------------- 1 | /* Copyright 2017 Sebastian Ouellette 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. */ 14 | 15 | /* Message Container */ 16 | .inlineMsgCont { 17 | margin: -60px 0 20px 90px; 18 | } 19 | 20 | /* Emojis */ 21 | .emoji { 22 | height: 22px; 23 | margin: -2px 0 0 0; 24 | transform: translateY(2px); 25 | } 26 | 27 | .embed .emoji { 28 | height: 18px !important; 29 | } 30 | 31 | .bigEmoji .emoji { 32 | height: 50px !important; 33 | } 34 | 35 | .spoilerBlock .emoji { 36 | opacity: 0; 37 | } 38 | 39 | .discovered .emoji { 40 | opacity: 1; 41 | } 42 | 43 | /* Links */ 44 | a { 45 | color: #00b0f4; 46 | text-decoration: none; 47 | } 48 | 49 | a:hover { 50 | text-decoration: underline; 51 | } 52 | 53 | /* Image Previews */ 54 | .previewImage { 55 | margin-bottom: 10px; 56 | height: auto; 57 | } 58 | 59 | /* Message Text */ 60 | .messageText { 61 | font-family: Whitney-light, "Helvetica Neue", Helvetica, Arial, sans-serif; 62 | font-size: 16px; 63 | font-weight: 400; 64 | padding: 3px 0 3px 0; 65 | margin: 0; 66 | color: #dcddde; 67 | width: calc(100% - 40px); 68 | word-spacing: 2px; 69 | position: relative; 70 | overflow: hidden; 71 | } 72 | 73 | .messageCont { 74 | width: 100%; 75 | /* border-top: 1px solid #484B51; */ 76 | padding: 5px 0; 77 | min-height: 57px; 78 | overflow-y: hidden; 79 | overflow-x: auto; 80 | padding-bottom: -30px; 81 | } 82 | 83 | .messageCont::-webkit-scrollbar { 84 | background-color: transparent; 85 | height: 6px; 86 | } 87 | 88 | .messageCont::-webkit-scrollbar-track { 89 | background-color: transparent; 90 | } 91 | 92 | .messageCont::-webkit-scrollbar-thumb { 93 | background-color: #25272b; 94 | border-radius: 3px; 95 | } 96 | 97 | .messageTimestamp + .messageText { 98 | margin-top: -26px; 99 | } 100 | 101 | .messageBlock { 102 | display: block; 103 | position: relative; 104 | padding-left: 90px; 105 | border-radius: 3px; 106 | } 107 | 108 | .messageBlock.firstmsg { 109 | padding-top: 5px; 110 | padding-bottom: 2px; 111 | } 112 | 113 | .messageBlock:hover { 114 | background-color: #32353b; 115 | z-index: 0; 116 | } 117 | 118 | .messageUsername { 119 | -webkit-user-drag: none; 120 | cursor: default; 121 | display: inline-block; 122 | margin: 3px 0; 123 | left: -40px; 124 | top: -26px; 125 | position: relative; 126 | } 127 | 128 | .messageUsername:hover { 129 | text-decoration: underline; 130 | } 131 | 132 | .messageTimestamp { 133 | color: #72767d; 134 | font-size: 12px; 135 | display: inline-block; 136 | margin: 3px 0; 137 | top: -26px; 138 | left: -35px; 139 | position: relative; 140 | } 141 | 142 | .messageImg { 143 | height: 40px; 144 | left: -60px; 145 | border-radius: 50%; 146 | user-select: none; 147 | z-index: 1; 148 | position: relative; 149 | } 150 | 151 | .barryImg { 152 | left: 30px; 153 | top: 5px; 154 | user-select: none; 155 | } 156 | 157 | /* Code Block */ 158 | .codeBlock { 159 | width: 100%; 160 | background-color: #2f3136; 161 | border: 1px solid #202225; 162 | border-radius: 3px; 163 | padding: 5px; 164 | font-family: monospace !important; 165 | font-size: 14px; 166 | color: #b9bbbe; 167 | } 168 | 169 | .codeBlockEmbed { 170 | margin-top: 10px; 171 | background-color: #202225; 172 | } 173 | 174 | /* Inline Code Block */ 175 | .inlineCodeBlock { 176 | background-color: #2f3136; 177 | padding: 2px; 178 | font-family: monospace; 179 | font-size: 14px; 180 | } 181 | 182 | /* Spoiler Tag */ 183 | .spoilerBlock { 184 | background-color: #202225; 185 | color: transparent; 186 | cursor: pointer; 187 | border-radius: 3px; 188 | user-select: none; 189 | min-width: 20px; 190 | height: 22px; 191 | display: inline-block; 192 | } 193 | 194 | .spoilerBlock:hover { 195 | background-color: #24262a; 196 | } 197 | 198 | .spoilerBlock.discovered { 199 | color: #dcddde; 200 | background-color: #474a4f; 201 | } 202 | 203 | .spoilerBlock.discovered:hover { 204 | background-color: #474a4f; 205 | } 206 | 207 | /* Mention */ 208 | .ping { 209 | background-color: #3d414f; 210 | padding: 0 2px 2px 2px; 211 | color: #7289da; 212 | border-radius: 3px; 213 | } 214 | 215 | .ping:hover { 216 | background-color: #7289da; 217 | cursor: pointer; 218 | color: #dcddde; 219 | } 220 | 221 | .emoji { 222 | height: 22px; 223 | } 224 | 225 | .emoji.big { 226 | height: 50px; 227 | } 228 | 229 | .edited { 230 | font-size: 10px; 231 | color: #72767d; 232 | user-select: none; 233 | } 234 | -------------------------------------------------------------------------------- /js/typingStatus.js: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Sebastian Ouellette 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | 'use strict'; 16 | 17 | let typingTimer = { 18 | indicator: { 19 | timer: null, // Current timer 20 | func: function () { 21 | // Function 22 | typingStatus(false); 23 | }, 24 | check: function (time, size) { 25 | if (this.timer == null && size) { 26 | this.timer = setInterval(this.func, time); 27 | } else if (!size && this.timer != null) { 28 | clearInterval(this.timer); 29 | this.timer = null; 30 | } 31 | }, 32 | }, 33 | typingTimeout: { 34 | timers: {}, // Current timers 35 | check: function (id) { 36 | if (id in this.timers) { 37 | this.timers[id]['timeout'] = bot.user._typing.get(id).count; 38 | return; 39 | } 40 | 41 | let timeout = bot.user._typing.get(id).count; 42 | let channel = bot.channels.cache.find((e) => e.id == id); 43 | 44 | if (!channel) return; 45 | 46 | let interval = setInterval(() => { 47 | this.decrease(id, channel); 48 | }, 1000); 49 | this.timers[id] = { timeout, interval }; 50 | }, 51 | decrease: function (id, channel) { 52 | this.timers[id]['timeout']--; 53 | if (this.timers[id]['timeout'] < 0) { 54 | clearInterval(this.timers[id]['interval']); 55 | delete this.timers[id]; 56 | } 57 | }, 58 | }, 59 | }; 60 | 61 | function typingStatus(override = false, m = undefined) { 62 | if (!selectedChan) return; 63 | let dms = selectedChan.type == Discord.ChannelType.DM; 64 | let indicator = document.getElementById('typingIndicator'); 65 | let users = []; 66 | selectedChan._typing?.each((e) => 67 | users.push( 68 | dms 69 | ? bot.users.cache.get(e.user.id) 70 | : selectedChan.members.get(e.user.id) 71 | ) 72 | ); 73 | let text = ''; 74 | 75 | let self = users.find((member) => member.user == bot.user); 76 | if (self) users.splice(users.indexOf(self), 1); 77 | 78 | let length = users.length; 79 | 80 | if (length) { 81 | document.getElementById('typingDots').classList.add('enabled'); 82 | } else { 83 | document.getElementById('typingDots').classList.remove('enabled'); 84 | } 85 | 86 | for (let user in users) { 87 | let name = dms ? users[user].username : users[user].displayName; 88 | if (user != length - 1) text += name + ', '; 89 | else text += name; 90 | } 91 | 92 | let boldText = length > 4 ? `Several people are typing...` : `${text}`; 93 | let endText = `${ 94 | length == 0 ? '' : length == 1 ? ' is typing...' : ' are typing...' 95 | }`; 96 | 97 | if (indicator.innerText != boldText + endText) { 98 | indicator.innerHTML = ''; 99 | // Create the element for the bold text 100 | let boldTextElement = document.createElement('span'); 101 | boldTextElement.innerText = boldText; 102 | boldTextElement.classList.add('bold'); 103 | indicator.appendChild(boldTextElement); 104 | 105 | indicator.innerHTML += endText; 106 | } 107 | 108 | let shortestTime = 1000; 109 | if (selectedChan._typing?.size) { 110 | // Needs a set timer so it doesn't create 1000 timers at a time 111 | // The timings can be found in each users typing variable and checking by the smallest is the best bet 112 | selectedChan._typing.forEach((e) => { 113 | if (m != undefined) { 114 | let id = m.author.id; 115 | if (e.user.id == id && selectedChan._typing.has(id)) { 116 | clearTimeout(selectedChan._typing.get(id).timeout); 117 | selectedChan._typing.delete(id); 118 | m.author._typing.delete(selectedChan.id); 119 | return typingStatus(); 120 | } 121 | } 122 | if (lastTime < e.elapsedTime) { 123 | shortestTime = e.elapsedTime; 124 | let elapsedTime = 125 | new Date().getTime() - new Date(e.lastTimestamp).getTime(); 126 | if (shortestTime > shortestTime - elapsedTime) 127 | shortestTime = elapsedTime; 128 | } 129 | }); 130 | } 131 | 132 | typingTimer.indicator.check(shortestTime, selectedChan._typing?.size || 0); 133 | } 134 | 135 | let lastTime = 0; 136 | 137 | // setInterval(typingStatus, 1000); 138 | -------------------------------------------------------------------------------- /js/buildTeamMemberCards.js: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Sebastian Ouellette 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | 'use strict'; 16 | 17 | function buildTeamMemberCards(funky) { 18 | bot.application.owner.members.each((m) => { 19 | // Create the container 20 | let container = document.createElement('div'); 21 | container.classList.add('teamMember'); 22 | document.getElementById('selectMember').appendChild(container); 23 | 24 | // Create the area for the icon and name 25 | let userArea = document.createElement('div'); 26 | userArea.classList.add('userArea'); 27 | container.appendChild(userArea); 28 | 29 | // Create the icon and username 30 | let img = document.createElement('img'); 31 | img.src = m.user.displayAvatarURL(); 32 | img.classList.add('teamMemberIcon'); 33 | userArea.appendChild(img); 34 | 35 | let tag = document.createElement('div'); 36 | tag.classList.add('teamMemberTag'); 37 | userArea.appendChild(tag); 38 | 39 | // Username 40 | let username = document.createElement('span'); 41 | username.classList.add('teamMemberName'); 42 | username.innerText = m.user.username; 43 | tag.appendChild(username); 44 | 45 | // Discriminator 46 | let discrim = document.createElement('span'); 47 | discrim.classList.add('teamMemberDisc'); 48 | discrim.innerText = `#${m.user.discriminator}`; 49 | tag.appendChild(discrim); 50 | 51 | // Buttons 52 | let once = document.createElement('div'); 53 | once.classList.add('teamMemberButton'); 54 | container.appendChild(once); 55 | 56 | let onceSpan = document.createElement('span'); 57 | onceSpan.classList.add('oneline'); 58 | onceSpan.innerText = 'Sign in once'; 59 | once.appendChild(onceSpan); 60 | 61 | // Default button 62 | let defaultButton = document.createElement('div'); 63 | defaultButton.classList.add('teamMemberButton'); 64 | container.appendChild(defaultButton); 65 | 66 | let defaultSpan = document.createElement('span'); 67 | defaultSpan.innerText = 'Sign in and set as default'; 68 | defaultButton.appendChild(defaultSpan); 69 | 70 | // Event listeners 71 | once.addEventListener('click', () => { 72 | bot.owner = m.user; 73 | funky(); 74 | }); 75 | defaultButton.addEventListener('click', () => { 76 | bot.owner = m.user; 77 | settings.tokenSettings = { teamUser: m.user.id }; 78 | 79 | funky(); 80 | }); 81 | }); 82 | } 83 | 84 | function buildSplashToken() { 85 | let container = document.createElement('div'); 86 | container.classList.add('splashTokenContainer'); 87 | document.getElementById('selectMember').appendChild(container); 88 | 89 | // Create the token input field 90 | let input = document.createElement('input'); 91 | input.classList.add('splashScreenToken'); 92 | input.classList.add('tokenbox'); // Added cause of the animation 93 | input.type = 'password'; 94 | input.placeholder = 'Input your token'; 95 | container.appendChild(input); 96 | 97 | // One time button 98 | let oneTimeButton = document.createElement('div'); 99 | oneTimeButton.classList.add('tokenAddButton'); 100 | container.appendChild(oneTimeButton); 101 | 102 | let oneTimeSpan = document.createElement('span'); 103 | oneTimeSpan.innerText = 'One time login'; 104 | oneTimeButton.appendChild(oneTimeSpan); 105 | 106 | // Default button 107 | let defaultButton = document.createElement('div'); 108 | defaultButton.classList.add('tokenAddButton'); 109 | container.appendChild(defaultButton); 110 | 111 | let defaultSpan = document.createElement('span'); 112 | defaultSpan.innerText = 'Log in and save as default'; 113 | 10; 114 | defaultButton.appendChild(defaultSpan); 115 | 116 | // Event listeners 117 | oneTimeButton.addEventListener('click', async () => { 118 | let token = input.value; 119 | let error = [false, 'none']; 120 | 121 | if (global.bot === undefined) error = await load(token); 122 | else error = await setToken(token); 123 | 124 | if (!error[0]) { 125 | document.getElementById('selectMember').removeChild(container); 126 | setLoadingPerc(0); 127 | } else { 128 | errorHandler(error[1]); 129 | } 130 | }); 131 | defaultButton.addEventListener('click', async () => { 132 | let token = input.value; 133 | let error = await saveToken(token); 134 | if (!error[0]) { 135 | document.getElementById('selectMember').removeChild(container); 136 | setLoadingPerc(0.05); 137 | } else { 138 | errorHandler(error[1]); 139 | } 140 | }); 141 | } 142 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | --- 4 | 5 | **NOTICE** 6 | 7 | LiveBot is specifically designed for bots; it cannot be used for user accounts. The LiveBot developers take no responsibility for the actions executed through LiveBot. 8 | 9 | --- 10 | 11 | An app that allows you to be inside a bot! 12 | 13 | At the moment, LiveBot is still in alpha. Installation instructions can be found below. 14 | 15 | ## Support Server 16 | If you would like to get the latest information on the project, talk with the developers, post suggestions, get help, etc., join the official LiveBot [Discord server!](https://discord.gg/NG4rgqSgzx) 17 | 18 | ## Preview 19 | LiveBot image preview 20 | 21 | LiveBot is a program built with Electron and Discord.js that will allow you to control your bot from a client resembling the official one for users. You are able to view messages in channels, send messages, interact with people and see what is going on in the servers your bot is in. 22 | 23 | LiveBot has been built so that the style and flow is similar to discord, and while it is not exact, it is pretty close. It is also built from scratch using no frameworks, just straight HTML, CSS, and JavaScript. Due to this, and having a very small development team, additional features may take a while to be added. The point is to built from scratch, we know very well that it's not the fastest way to make something like this. However, LiveBot is slowly being filled with cool exclusive features like the pull-up user settings menu, the soon to come embed builder, native support for scripts, and more! 24 | 25 | ## Installation 26 | ### Release (recommended) 27 | 28 | To download a binary executable file, simply click on the latest [Release](../../releases) on the right side of the page, and download whichever zip or tar.gz file suits your needs. For example, if you use Windows, you should download the zip file that says "win32", if you use Linux, you will download the file that says "linux", and if you use macOS you will download the file that says "darwin". Once you download and decompress the file, just execute it as you would any other application. 29 | 30 | 31 | ### From Source 32 | 33 | LiveBot requires the [Node.js] JavaScript runtime to run. 34 | 35 | You can download the source with [Git] (and get updates easily) or you can download as a zipped (compressed) file from GitHub. If you wish to download as a zip file, GitHub provides a big green "Code" button which you can press to download the file. 36 | 37 | #### Step 1 38 | If you want to download with Git, a bash command to do so is found below: 39 | ```sh 40 | # with SSH (requires public key to be set) 41 | git clone "git@github.com:SebOuellette/LiveBot.git" 42 | 43 | # with HTTP 44 | git clone "https://github.com/SebOuellette/LiveBot.git" 45 | ``` 46 | 47 | #### Step 2 48 | After cloning with Git or decompressing the zip file, you must open the directory the source is located in and install the dependencies with npm (or your preferred package manager) with the following command: 49 | ```sh 50 | npm install 51 | ``` 52 | 53 | #### Step 3 54 | As of 2025, LiveBot requires a patch for internal libraries. This patch is included in the repository, and can be applied using the following command: 55 | ```sh 56 | tar -xvf patch.tar 57 | ``` 58 | 59 | #### Step 4 60 | When your package manager is finished installing, you may start the program with: 61 | ```sh 62 | npm start 63 | ``` 64 | 65 | ## Features 66 | ### Token switcher (User menu) 67 | This box is how you log into a profile after already logging in. To log into a token by default, put it into this box then press the save button. Otherwise if you are just logging into a token just for a look, input the token into the box then press `enter`. 68 | 69 | The token changer used to be at the top of the main screen, but has moved to the user options menu. 70 | 71 | Token switcher preview 72 | 73 | ### Barry 74 | Barry is the LiveBot version of discord's Clyde. Except Barry has some fun commands! Just use the prefix `/` and to see the commands write `/help`. Only you can see what barry says, so don't worry about interfering with any conversations.
75 | Barry's help message 76 | 77 | ### Profile card 78 | The profile card is able to tell you the username, the avatar image, the discriminator, and something discord does not have. The profile card will tell you if the account you are signed into is a bot or a user. This can be handy if you are not quite sure. As you can no longer login with a user account it's just legacy code and for the looks. This card also has the pull up menu on the right side, so you can get to all the settings.
79 | 80 | Profile card preview 81 | 82 | ### Generate Invite 83 | 84 | LiveBot gives you the ability to generate an invite quickly from within the app, instead of having to go to your bot's application page in the Discord Developer portal. It can be found in the user settings page in the pull up menu. Just toggle all the permissions you'd like to give the invite (by default, everything recommended to use LiveBot's features is enabled). 85 | 86 | Invite generator preview 87 | 88 | [node.js]: https://nodejs.org 89 | [git]: https://git-scm.com 90 | -------------------------------------------------------------------------------- /css/master.css: -------------------------------------------------------------------------------- 1 | /* Copyright 2017 Sebastian Ouellette 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. */ 14 | 15 | div > * { 16 | user-select: text; 17 | } 18 | 19 | div { 20 | user-select: none; 21 | } 22 | 23 | /* Some text styling */ 24 | p { 25 | font-size: 16px; 26 | overflow-wrap: break-word; 27 | word-wrap: break-word; 28 | white-space: pre-wrap; 29 | } 30 | 31 | img { 32 | -webkit-user-drag: none; 33 | } 34 | 35 | body { 36 | background-color: #202225; 37 | font-family: Whitney-light, "Helvetica Neue", Helvetica, Arial, sans-serif; 38 | text-rendering: optimizeLegibility; 39 | } 40 | 41 | .window-icon-bg { 42 | height: 20px !important; 43 | margin-top: 5px; 44 | } 45 | 46 | .button { 47 | z-index: 101; 48 | margin-top: -10px; 49 | } 50 | 51 | #max-btn { 52 | background-color: grey; 53 | height: 18px; 54 | width: 30px; 55 | margin-left: calc(100vw - 50px); 56 | position: absolute; 57 | z-index: 101; 58 | margin-top: -3px; 59 | } 60 | 61 | .drag { 62 | background-color: #202225; 63 | height: 30px; 64 | width: 100vw; 65 | margin-top: -10px; 66 | margin-left: -8px; 67 | z-index: 100; 68 | position: absolute; 69 | } 70 | 71 | #main { 72 | position: fixed; 73 | height: calc(100vh - 20px); 74 | } 75 | 76 | /* User card */ 77 | #userCard { 78 | height: 45px; 79 | border-bottom: 2px solid #202225; 80 | margin-bottom: 10px; 81 | } 82 | 83 | #userCardIcon { 84 | margin-top: 6px; 85 | margin-left: 45px; 86 | border-radius: 50%; 87 | } 88 | 89 | #userCardName { 90 | margin-top: -35px; 91 | margin-left: 80px; 92 | position: absolute; 93 | width: 140px; 94 | text-overflow: ellipsis; 95 | white-space: nowrap; 96 | overflow: hidden; 97 | } 98 | 99 | #userCardDiscrim { 100 | font-weight: 900; 101 | margin-top: -18px; 102 | font-size: 12px; 103 | margin-left: 80px; 104 | position: absolute; 105 | color: #999; 106 | } 107 | 108 | #userCardBot { 109 | position: absolute; 110 | font-size: 13px; 111 | margin-top: 13px; 112 | margin-left: 8px; 113 | background-color: #7289da; 114 | padding: 2px 3px 2px 2px; 115 | border-radius: 4px; 116 | } 117 | 118 | /* Channel stuff I think */ 119 | 120 | #serverName { 121 | height: 40px; 122 | width: 260px; 123 | background-color: grey; 124 | margin: 0px 0 0px 50px; 125 | background-color: #2f3136; 126 | font-family: Whitney, "Helvetica Neue", Helvetica, Arial, sans-serif; 127 | position: absolute; 128 | border-bottom: 1px solid #202225; 129 | border-radius: 10px 0 0 0; 130 | } 131 | 132 | #guildName { 133 | padding-left: 10px; 134 | padding-top: 3px; 135 | margin-top: 0; 136 | margin-bottom: 10px; 137 | color: #bbb; 138 | width: calc(100% - 70px); 139 | white-space: nowrap; 140 | overflow: hidden; 141 | text-overflow: ellipsis; 142 | } 143 | 144 | #guildImg { 145 | height: 35px; 146 | margin-top: -30px; 147 | margin-left: 220px; 148 | border-radius: 50%; 149 | position: absolute; 150 | } 151 | 152 | #members-text { 153 | margin-top: -10px; 154 | font-weight: bold; 155 | color: #666; 156 | } 157 | 158 | #members-count { 159 | margin-left: 70px; 160 | color: #999; 161 | } 162 | 163 | .members { 164 | font-size: 12px; 165 | margin-top: -25px; 166 | margin-left: 10px; 167 | } 168 | 169 | .directMsg { 170 | transform: translate(10px, 7px); 171 | font-weight: bold; 172 | } 173 | 174 | /* Message List */ 175 | #message-list { 176 | background-color: #36393e; 177 | height: calc(100vh - 80px); 178 | border-bottom: 1px solid #36393e; 179 | width: calc(100vw - 518px); 180 | position: absolute; 181 | margin-top: calc(-100vh + 60px); 182 | margin-left: 310px; 183 | overflow-y: auto; 184 | font-family: Whitney-light, "Helvetica Neue", Helvetica, Arial, sans-serif; 185 | overflow-x: hidden; 186 | position: absolute; 187 | padding-bottom: 60px; 188 | } 189 | 190 | #message-list::-webkit-scrollbar { 191 | background-color: transparent; 192 | width: 6px; 193 | } 194 | 195 | #message-list::-webkit-scrollbar-track { 196 | background-color: transparent; 197 | } 198 | 199 | #message-list::-webkit-scrollbar-thumb { 200 | background-color: #25272b; 201 | border-radius: 3px; 202 | } 203 | 204 | .timeSeparated { 205 | border-top: 1px solid #484b51; 206 | } 207 | 208 | /* Apology for not being able to view messages */ 209 | .sorryNoLoad { 210 | height: 50px; 211 | text-align: center; 212 | color: #b9bbbe; 213 | font-family: Whitney-light, "Helvetica Neue", Helvetica, Arial, sans-serif; 214 | /* border-bottom: 1px solid #484B51; */ 215 | } 216 | 217 | /* Settings stuff */ 218 | 219 | .optionBox { 220 | width: 150px; 221 | padding: 5px; 222 | background-color: #484b51; 223 | border-radius: 5px; 224 | margin-left: auto; 225 | margin-right: auto; 226 | display: block; 227 | color: white; 228 | margin-top: 5px; 229 | border: none; 230 | outline: none; 231 | } 232 | 233 | #inviteBtn { 234 | cursor: pointer; 235 | } 236 | 237 | #inviteBtn:hover { 238 | background-color: #393939; 239 | } 240 | -------------------------------------------------------------------------------- /js/messageGeneration.js: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Sebastian Ouellette 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | 'use strict'; 16 | 17 | function findTimeDiff(m, previousMessage, count) { 18 | let bunch = false; 19 | let timebunch = false; 20 | 21 | if (previousMessage && previousMessage.author.id == m.author.id) { 22 | bunch = true; 23 | 24 | if ( 25 | Math.floor( 26 | previousMessage.createdTimestamp / 1000 / 60 / 60 / 24 27 | ) != Math.floor(m.createdTimestamp / 1000 / 60 / 60 / 24) 28 | ) { 29 | bunch = false; 30 | timebunch = true; 31 | } 32 | } else { 33 | bunch = false; 34 | } 35 | 36 | return [bunch, timebunch]; 37 | } 38 | 39 | function generateMsgHTML( 40 | m, 41 | previousMessage, 42 | count = -1, 43 | fetchSize = undefined 44 | ) { 45 | // Check if the messages should be grouped or not 46 | let result = [false, false]; 47 | if (count == -1 || (count > 2 && count <= fetchSize)) { 48 | result = findTimeDiff(m, previousMessage); 49 | } 50 | 51 | // The bunched values 52 | let bunch = result[0]; 53 | let timebunch = result[1]; 54 | 55 | // Create the div for the dark background 56 | let darkBG = document.createElement('div'); 57 | darkBG.classList.add('messageBlock'); 58 | darkBG.id = m.id; 59 | 60 | // Create the messages 61 | let div; 62 | if (!bunch) { 63 | // Create message div 64 | div = document.createElement('div'); 65 | div.classList.add('messageCont'); 66 | div.classList.add(m.author.id); 67 | if (m.channel.type == Discord.ChannelType.DM) div.classList.add('dms'); 68 | if (timebunch) { 69 | div.classList.add('timeSeparated'); 70 | } 71 | 72 | // Inline message container 73 | // messageContainer = document.createElement("div"); 74 | // messageContainer.classList.add(m.author.id); 75 | // messageContainer.classList.add('inlineMsgCont'); 76 | // div.appendChild(messageContainer); 77 | 78 | // Create the dark background 79 | darkBG.classList.add('firstmsg'); 80 | div.appendChild(darkBG); 81 | 82 | // Create user image 83 | let img = document.createElement('img'); 84 | let userImg = (m.member || m.author).displayAvatarURL({ size: 64 }); 85 | if (m.author.avatar && m.author.avatar.startsWith('a_')) { 86 | let userGif = m.author.displayAvatarURL({ size: 128 }); 87 | img.src = userGif; 88 | darkBG.onmouseenter = (e) => { 89 | img.src = userGif; 90 | }; 91 | darkBG.onmouseleave = (e) => { 92 | img.src = userImg; 93 | }; 94 | } 95 | img.classList.add('messageImg'); 96 | img.src = userImg; 97 | img.height = '40'; 98 | img.width = '40'; 99 | darkBG.appendChild(img); 100 | 101 | // Create user's name 102 | let name = document.createElement('p'); 103 | name.innerText = m.member?.nickname || m.author.username; 104 | name.classList.add('messageUsername'); 105 | 106 | // Find the colour of their name 107 | // Use the highest role for their color 108 | name.style.color = m.member?.roles.color?.hexColor || '#fff'; 109 | 110 | darkBG.appendChild(name); 111 | 112 | // Create timestamp 113 | let timestamp = document.createElement('p'); 114 | timestamp.innerText = 115 | ' ' + 116 | m.createdAt.toLocaleString('en-US', { 117 | day: '2-digit', 118 | month: '2-digit', 119 | year: 'numeric', 120 | hour: '2-digit', 121 | minute: '2-digit', 122 | }); 123 | timestamp.classList.add('messageTimestamp'); 124 | darkBG.appendChild(timestamp); 125 | } else { 126 | div = document.getElementsByClassName(m.author.id); 127 | div = div[div.length - 1]; 128 | div.appendChild(darkBG); 129 | } 130 | 131 | // Prepend message text 132 | if (m.cleanContent.length) { 133 | // Render message text 134 | let text = document.createElement('p'); 135 | text.classList.add('messageText'); 136 | text.innerHTML = parseMessage(m.cleanContent, m, false); 137 | 138 | if (m.editedAt) 139 | text.innerHTML += ''; 140 | 141 | darkBG.appendChild(text); 142 | } 143 | 144 | // Prepend attachments 145 | if(m.attachments) { 146 | m.attachments.forEach((i) => { 147 | let attachText; 148 | if(i.contentType.includes("image")) { 149 | attachText = document.createElement('p'); 150 | showEmbed({ type: "image", thumbnail: { proxy_url: i.url, width: i.width, height: i.height } }, attachText, m); 151 | } 152 | if(i.contentType.includes("video")) { 153 | attachText = document.createElement('p'); 154 | showEmbed({ type: "video", video: { url: i.url, width: i.width, height: i.height } }, attachText, m); 155 | } 156 | if(attachText) { 157 | attachText.classList.add('messageText'); 158 | darkBG.appendChild(attachText) 159 | } 160 | }); 161 | } 162 | 163 | // Append embeds 164 | m.embeds.forEach((embed) => { 165 | showEmbed(embed.data, darkBG, m); 166 | }); 167 | return div; 168 | } 169 | -------------------------------------------------------------------------------- /js/channelSelect.js: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Sebastian Ouellette 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | 'use strict'; 16 | 17 | let channelSelect = (c, name) => { 18 | let messages = document.getElementById('message-list'); 19 | let fetchSize = 100; 20 | 21 | if (!Discord.Constants.TextBasedChannelTypes.includes(c.type)) { 22 | selectedVoice = c; 23 | return; 24 | } 25 | 26 | if (generatingMessages) { 27 | return; 28 | } 29 | 30 | selectedChan = c; 31 | selectedChanDiv = name; 32 | name.style.color = '#eee'; 33 | messageCreate(); 34 | 35 | // Refresh the typing indicator 36 | typingStatus(true); 37 | 38 | // Set the message bar placeholder 39 | document.getElementById('msgbox').placeholder = `Message #${c.name}`; 40 | 41 | // Remove the notification class 42 | name.classList.remove('newMsg'); 43 | 44 | // Clear the messages 45 | while (messages.firstChild) { 46 | messages.removeChild(messages.firstChild); 47 | } 48 | 49 | // Creates the loading dots 50 | var container = document.createElement('div'); // Centred container 51 | var loadingDots = document.createElement('div'); // Loading dots 52 | loadingDots.classList.add('dot-bricks'); 53 | container.style.position = 'absolute'; 54 | container.style.top = '50%'; 55 | container.style.left = '50%'; 56 | container.style.transform = 'translate(-50%, -50%)'; 57 | container.id = 'loading-container'; 58 | container.appendChild(loadingDots); 59 | messages.appendChild(container); 60 | 61 | // Set colour of the channel 62 | try { 63 | selectedChanDiv.style.color = '#606266'; 64 | name.addEventListener('mouseover', () => { 65 | if (name.style.color != 'rgb(238, 238, 238)') { 66 | name.style.color = '#B4B8BC'; 67 | } 68 | }); 69 | 70 | name.addEventListener('mouseleave', () => { 71 | if (name.style.color != 'rgb(238, 238, 238)') { 72 | name.style.color = '#606266'; 73 | } 74 | }); 75 | } catch (err) { 76 | console.log(err); 77 | } 78 | 79 | // Create the member list 80 | addMemberList(c.guild); 81 | 82 | // Create message 83 | async function messageCreate() { 84 | generatingMessages = true; 85 | // Loop through messages 86 | let count = 0; 87 | await c.messages.fetch({ limit: fetchSize }).then((messages) => { 88 | messages 89 | .toJSON() 90 | .reverse() 91 | .forEach((m) => { 92 | count++; 93 | let message = generateMsgHTML( 94 | m, 95 | messages.toJSON().reverse()[count - 2], 96 | count, 97 | fetchSize 98 | ); 99 | document 100 | .getElementById('message-list') 101 | .appendChild(message); 102 | }); 103 | }); 104 | // Add the no load apology 105 | let shell = document.createElement('div'); 106 | shell.classList.add('sorryNoLoad'); 107 | let text = document.createElement('p'); 108 | text.innerText = 109 | 'Sorry! No messages beyond this point can be displayed.'; 110 | shell.appendChild(text); 111 | document.getElementById('message-list').prepend(shell); 112 | 113 | messages.scrollTop = messages.scrollHeight; 114 | generatingMessages = false; 115 | 116 | // Remove the loading dots 117 | messages.removeChild(document.getElementById('loading-container')); 118 | } 119 | }; 120 | 121 | let dmChannelSelect = async (u, name = 'test') => { 122 | if (u.bot || bot.user == u) return; 123 | let messages = document.getElementById('message-list'); 124 | let fetchSize = 100; 125 | 126 | if (!u.dmChannel) { 127 | await u.createDM(); 128 | } 129 | 130 | let c = u.dmChannel; 131 | 132 | if (generatingMessages) { 133 | return; 134 | } 135 | if (!u.openDM) u.openDM = true; 136 | 137 | if (selectedChatDiv) { 138 | selectedChatDiv.classList.remove('selectedChan'); 139 | selectedChatDiv = undefined; 140 | } 141 | 142 | selectedChan = c; 143 | 144 | messageCreate(); 145 | 146 | // Refresh the typing indicator 147 | typingStatus(true); 148 | 149 | // Set the message bar placeholder 150 | document.getElementById( 151 | 'msgbox' 152 | ).placeholder = `Message #${c.recipient.username}`; 153 | 154 | // Clear the messages 155 | while (messages.firstChild) { 156 | messages.removeChild(messages.firstChild); 157 | } 158 | 159 | // Create message 160 | async function messageCreate() { 161 | generatingMessages = true; 162 | // Loop through messages 163 | let count = 0; 164 | await c.messages.fetch({ limit: fetchSize }).then((messages) => { 165 | messages 166 | .toJSON() 167 | .reverse() 168 | .forEach((m) => { 169 | count++; 170 | let message = generateMsgHTML( 171 | m, 172 | messages.toJSON().reverse()[count - 2], 173 | count, 174 | fetchSize 175 | ); 176 | document 177 | .getElementById('message-list') 178 | .appendChild(message); 179 | }); 180 | }); 181 | // Add the no load apology 182 | let shell = document.createElement('div'); 183 | shell.classList.add('sorryNoLoad'); 184 | let text = document.createElement('p'); 185 | text.innerText = 186 | 'Sorry! No messages beyond this point can be displayed.'; 187 | shell.appendChild(text); 188 | document.getElementById('message-list').prepend(shell); 189 | 190 | messages.scrollTop = messages.scrollHeight; 191 | generatingMessages = false; 192 | } 193 | }; 194 | -------------------------------------------------------------------------------- /scripts/how_to_write_livebot_scripts.md: -------------------------------------------------------------------------------- 1 | # How to Write LiveBot Scripts 2 | A reference guide for extending and customizing LiveBot using standalone JavaScript modules. 3 | 4 | LiveBot supports user-created scripts that can modify the UI, automate moderation tasks, add developer tools, and enhance the overall client experience. This guide explains how to write clean, safe, and maintainable scripts that integrate smoothly with LiveBot’s environment. 5 | 6 | --- 7 | 8 | ## ✅ 1. Overview 9 | LiveBot loads all `.js` files placed inside the `/scripts` directory. 10 | Each script runs in the LiveBot renderer process and has access to: 11 | 12 | - The Discord.js client (`window.bot`) 13 | - The DOM of the LiveBot UI 14 | - Standard browser APIs (MutationObserver, fetch, localStorage, etc.) 15 | - Node.js modules (via `require()`) 16 | 17 | Scripts are **modular**, **self-contained**, and **do not require modifying LiveBot’s core files**. 18 | 19 | --- 20 | 21 | ## ✅ 2. Basic Script Structure 22 | 23 | A typical LiveBot script looks like this: 24 | 25 | ```js 26 | (function() { 27 | console.log("My script loaded!"); 28 | 29 | // Your logic here 30 | })(); 31 | ``` 32 | 33 | Wrapping your code in an IIFE: 34 | - Prevents global namespace pollution 35 | - Ensures your script runs immediately 36 | - Keeps variables isolated and safe 37 | 38 | --- 39 | 40 | ## ✅ 3. Accessing the Discord Client 41 | 42 | LiveBot exposes the Discord.js client as: 43 | 44 | ```js 45 | window.bot 46 | ``` 47 | 48 | Example: 49 | 50 | ```js 51 | const guilds = window.bot.guilds.cache; 52 | console.log("Loaded guilds:", guilds.map(g => g.name)); 53 | ``` 54 | 55 | This gives you full access to: 56 | 57 | - Guilds 58 | - Channels 59 | - Roles 60 | - Members 61 | - Permissions 62 | - Events 63 | 64 | LiveBot typically uses Discord.js v14, so permission flags follow: 65 | 66 | ```js 67 | PermissionsBitField.Flags.SendMessages 68 | PermissionsBitField.Flags.CreateInstantInvite 69 | ``` 70 | 71 | --- 72 | 73 | ## ✅ 4. Detecting the Current Guild 74 | 75 | LiveBot does not expose a global “current guild” variable. 76 | The most reliable method is to detect the **selected channel** in the UI and resolve its guild: 77 | 78 | ```js 79 | function getCurrentGuild() { 80 | const selected = document.querySelector(".channel.selectedChan"); 81 | if (!selected) return null; 82 | 83 | const channelId = selected.id; 84 | 85 | return window.bot.guilds.cache.find(g => 86 | g.channels.cache.has(channelId) 87 | ); 88 | } 89 | ``` 90 | 91 | This works across all LiveBot versions. 92 | 93 | --- 94 | 95 | ## ✅ 5. Watching for UI Changes 96 | 97 | LiveBot’s UI re-renders frequently. 98 | To safely inject buttons, panels, or UI elements, use a `MutationObserver`: 99 | 100 | ```js 101 | new MutationObserver(() => { 102 | const list = document.getElementById("channel-list"); 103 | if (!list) return; 104 | 105 | if (!document.getElementById("myButton")) { 106 | const btn = document.createElement("button"); 107 | btn.id = "myButton"; 108 | btn.textContent = "Click Me"; 109 | list.prepend(btn); 110 | } 111 | }).observe(document.body, { childList: true, subtree: true }); 112 | ``` 113 | 114 | This ensures your UI elements persist even after LiveBot refreshes the DOM. 115 | 116 | --- 117 | 118 | ## ✅ 6. Editing Permissions (Discord.js v14) 119 | 120 | Example: Lock all channels for @everyone: 121 | 122 | ```js 123 | const { PermissionsBitField } = require("discord.js"); 124 | 125 | async function lockAllChannels(guild) { 126 | const everyone = guild.roles.everyone; 127 | 128 | guild.channels.cache.forEach(async channel => { 129 | try { 130 | await channel.permissionOverwrites.edit(everyone, { 131 | [PermissionsBitField.Flags.SendMessages]: false 132 | }); 133 | } catch (err) { 134 | console.error("Failed to lock:", channel.name, err); 135 | } 136 | }); 137 | } 138 | ``` 139 | 140 | --- 141 | 142 | ## ✅ 7. Adding Custom UI Panels 143 | 144 | You can create floating panels, toolbars, or menus: 145 | 146 | ```js 147 | function createPanel() { 148 | if (document.getElementById("myPanel")) return; 149 | 150 | const panel = document.createElement("div"); 151 | panel.id = "myPanel"; 152 | panel.style.cssText = ` 153 | position: absolute; 154 | top: 80px; 155 | left: 10px; 156 | width: 250px; 157 | background: #2f3136; 158 | padding: 12px; 159 | border-radius: 8px; 160 | color: white; 161 | z-index: 9999; 162 | `; 163 | 164 | panel.innerHTML = ` 165 |

My Tools

166 | 167 | `; 168 | 169 | document.body.appendChild(panel); 170 | } 171 | ``` 172 | 173 | --- 174 | 175 | ## ✅ 8. Best Practices 176 | 177 | ### ✅ Keep scripts modular 178 | Avoid modifying LiveBot’s core files. 179 | Scripts should be drop‑in and reversible. 180 | 181 | ### ✅ Avoid global variables 182 | Use IIFEs or namespaces. 183 | 184 | ### ✅ Use MutationObserver for UI injection 185 | LiveBot re-renders often. 186 | 187 | ### ✅ Fail gracefully 188 | If a guild or channel isn’t detected, log a warning instead of throwing. 189 | 190 | ### ✅ Document your script 191 | Include a header explaining what it does. 192 | 193 | ### ✅ Test on multiple servers 194 | Different guild structures help catch edge cases. 195 | 196 | --- 197 | 198 | ## ✅ 9. Example: A Complete Mini Script 199 | 200 | ```js 201 | (function() { 202 | const { PermissionsBitField } = require("discord.js"); 203 | 204 | function getGuild() { 205 | const selected = document.querySelector(".channel.selectedChan"); 206 | if (!selected) return null; 207 | return window.bot.guilds.cache.find(g => g.channels.cache.has(selected.id)); 208 | } 209 | 210 | function lock() { 211 | const guild = getGuild(); 212 | if (!guild) return console.warn("No guild detected"); 213 | 214 | const everyone = guild.roles.everyone; 215 | 216 | guild.channels.cache.forEach(ch => { 217 | ch.permissionOverwrites.edit(everyone, { 218 | [PermissionsBitField.Flags.SendMessages]: false 219 | }); 220 | }); 221 | } 222 | 223 | new MutationObserver(() => { 224 | const list = document.getElementById("channel-list"); 225 | if (!list || document.getElementById("lockBtn")) return; 226 | 227 | const btn = document.createElement("button"); 228 | btn.id = "lockBtn"; 229 | btn.textContent = "Lock All Channels"; 230 | btn.style.cssText = "width:100%;padding:8px;margin-bottom:8px;"; 231 | btn.onclick = lock; 232 | 233 | list.prepend(btn); 234 | }).observe(document.body, { childList: true, subtree: true }); 235 | })(); 236 | ``` 237 | 238 | --- 239 | 240 | ## ✅ 10. Contributing Your Scripts 241 | 242 | If you’d like to contribute your scripts to the official LiveBot repository: 243 | 244 | 1. Fork the repo 245 | 2. Add your script to `/scripts` 246 | 3. Add documentation to this file if needed 247 | 4. Submit a pull request 248 | 249 | Please ensure your scripts are: 250 | 251 | - Safe 252 | - Self-contained 253 | - Well-documented 254 | - Compatible with Discord.js v14 255 | 256 | --- 257 | 258 | ## ✅ Final Notes 259 | 260 | LiveBot scripting is powerful — you can build: 261 | 262 | - Moderation tools 263 | - Developer utilities 264 | - UI enhancements 265 | - Automation systems 266 | - Quality‑of‑life features 267 | 268 | This guide will continue to evolve as the community contributes new patterns and best practices. 269 | 270 | If you have improvements, feel free to submit a PR! 271 | -------------------------------------------------------------------------------- /js/errorHandler.js: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Sebastian Ouellette 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | 'use strict'; 16 | 17 | // Animations used in javascript if they can't be used in css 18 | let animations = { 19 | flashRed: [ 20 | { borderColor: '#313339' }, 21 | { borderColor: '#A00' }, 22 | { borderColor: '#F00' }, 23 | { borderColor: '#A00' }, 24 | { borderColor: '#313339' }, 25 | ], 26 | flashTextRed: [ 27 | { color: '#B4B8BC' }, 28 | { color: '#F00' }, 29 | { color: '#B4B8BC' }, 30 | ], 31 | }; 32 | 33 | // Custom error messages 34 | function errorHandler(err) { 35 | let code = err.code ? err.code : err; 36 | if (err.name == 'DiscordAPIError') discordApiErrors(code, err); 37 | else if (code.toString().includes('TOKEN')) { 38 | customTokenErrors(code, err); 39 | } else customErrors(code, err); 40 | } 41 | 42 | // Discord api errors that aren't caught with discord.js 43 | function discordApiErrors(code, err) { 44 | switch (err.message) { 45 | case 'Cannot send messages to this user': 46 | console.error( 47 | `This user either has direct messages disabled in the server or you've been blocked by the user` 48 | ); 49 | command( 50 | "I'm sorry but this user has you blocked or their direct messages are disabled!\n\nIf you wish to contact them through direct messages you'll have to ask them first." 51 | ); 52 | break; 53 | default: 54 | console.error(`Error code: ${err.code}\n${err.message}`); 55 | break; 56 | } 57 | } 58 | 59 | function tokenError() { 60 | // Flash red if the token is incorrect 61 | Array.from(document.getElementsByClassName('tokenbox')).forEach((box) => { 62 | box.animate(animations.flashRed, 400); 63 | box.value = ''; 64 | }); 65 | } 66 | 67 | // Errors that are caught by discord.js and converted to custom errors 68 | function customTokenErrors(code, err) { 69 | switch (code) { 70 | case 'TOKEN_INVALID': 71 | setLoadingPerc(-1, 'The token provided is invalid'); 72 | console.error('Invalid Token'); 73 | break; 74 | case 'NO-TOKEN': 75 | setLoadingPerc(-1, 'No tokens found in the cache'); 76 | console.error('There is no token stored'); 77 | break; 78 | case 'SAME-TOKEN': 79 | setLoadingPerc(-1, 'The token is the same as the current one'); 80 | console.error("The token is the same so it won't be switched"); 81 | break; 82 | case 'EMPTY-TOKEN': 83 | setLoadingPerc(-1, 'The token provided is empty'); 84 | console.error('The token is empty'); 85 | break; 86 | case 'TOKEN-SHORT': 87 | setLoadingPerc(-1, 'The token is too short'); 88 | console.error( 89 | "The token is too short, make sure you're not using the client secret\nIf you're sure it's not then contact the devs with the part of your token that is shorter\n\nRandomly generated example of a token:\n" + 90 | genFakeToken() 91 | ); 92 | break; 93 | case 'TOKEN-LONG': 94 | setLoadingPerc(-1, 'The token is too long'); 95 | console.error( 96 | "The token is too long\nIf you're sure it's not then contact the devs with the part of your token that is longer\n\nRandomly generated example of a token:\n" + 97 | genFakeToken() 98 | ); 99 | break; 100 | case 'TOKEN-WHITESPACE': 101 | setLoadingPerc(-1, 'There are spaces or newlines in the token'); 102 | console.error( 103 | 'The token cannot contain a space or a newline, make sure to paste the token correctly' 104 | ); 105 | break; 106 | case 'INVALID-TOKEN-CHARACTERS': 107 | setLoadingPerc(-1, 'There are invalid characters in the token'); 108 | console.error( 109 | 'The token contains invalid characters, please make sure the token is correct' 110 | ); 111 | break; 112 | case 'INVALID-TOKEN-FORMAT': 113 | setLoadingPerc(-1, 'The format of the token is invalid'); 114 | console.error( 115 | 'The token format is invalid\n\nRandomly generated example of a token:\n' + 116 | genFakeToken() 117 | ); 118 | break; 119 | default: 120 | setLoadingPerc(-1, `${err.toString()}`); 121 | console.error(`Error code: ${err.code}\n${err}`); 122 | break; 123 | } 124 | 125 | tokenError(); 126 | } 127 | 128 | // Errors that are caught by discord.js and converted to custom errors 129 | function customErrors(code, err) { 130 | switch (code) { 131 | case 'SHARDING_REQUIRED': 132 | setLoadingPerc( 133 | -1, 134 | "Sharding is required for your bot but it's not supported yet" 135 | ); 136 | console.error( 137 | "Sharding is required for your bot but it's not supported yet" 138 | ); 139 | tokenError(); 140 | break; 141 | case 'EMPTY-NAME': 142 | setLoadingPerc( 143 | -1, 144 | "Bot's username is empty or contains invalid characters. Please change it." 145 | ); 146 | console.error('Username is empty or contains invalid characters'); 147 | break; 148 | case 'SERVER_OFFLINE': 149 | setLoadingPerc( 150 | -1, 151 | "Guild(s) offline. Please wait for Discord to recover." 152 | ); 153 | console.error('Guild seems to be offline'); 154 | break; 155 | case 'NO_SERVERS': 156 | setLoadingPerc( 157 | -1, 158 | "The bot and owner MUST both be in at least 1 server together. Found 0 matching servers." 159 | ); 160 | console.error("The bot and owner MUST both be in at least 1 server together. Found 0 matching servers."); 161 | console.log("Automatically running FAQ 5:"); 162 | console.log(bot.guilds.cache.size + " server(s)"); 163 | bot.guilds.cache.forEach(g => { 164 | console.log(g.name+": "+g.members.cache.map(x => x.user.id).includes(bot.owner.id)+" - Members: "+g.members.cache.size) 165 | }); 166 | 167 | default: 168 | setLoadingPerc( 169 | -1, 170 | `${err.toString()}` // As a fallback, just show the user the raw error. 171 | ); 172 | console.error(`Error code: ${err.code}\n${err}`); 173 | break; 174 | } 175 | } 176 | 177 | // This just generates a random token that I'm sure 99% won't work 178 | function genFakeToken() { 179 | let characters = 180 | 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'; 181 | let gen = (length) => { 182 | let result = ''; 183 | var charLen = characters.length; 184 | for (var i = 0; i < length; i++) { 185 | result += characters.charAt(Math.floor(Math.random() * charLen)); 186 | } 187 | return result; 188 | }; 189 | 190 | return `${gen(24)}.${gen(6)}.${gen(27)}`; 191 | } 192 | -------------------------------------------------------------------------------- /js/parseMessage.js: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Sebastian Ouellette 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | 'use strict'; 16 | 17 | let parseSend = (text) => { 18 | // The regex used for the emojis 19 | let emojiRegex = 20 | /(?:\(|>:-\(|>=\(|>=-\(|:"\)|:-"\)|="\)|=-"\)|<\/3|:-\\|:-\/|=-\\|=-\/|:'\(|:'-\(|:,\(|:,-\(|='\(|='-\(|=,\(|=,-\(|:\(|:-\(|=\(|=-\(|<3|♡|]:\(|\]:-\(|]=\(|]=-\(|o:\)|O:\)|o:-\)|O:-\)|0:\)|0:-\)|o=\)|O=\)|o=-\)|O=-\)|0=\)|0=-\)|:'D|:'-D|:,D|:,-D|='D|='-D|=,D|=,-D|:\*|:-\*|=\*|=-\*|x-\)|X-\)|:\||:-\||=\||=-\||:o|:-o|:O|:-O|=o|=-o|=O|=-O|:@|:-@|=@|=-@|:D|:-D|=D|=-D|:'\)|:'-\)|:,\)|:,-\)|='\)|='-\)|=,\)|=,-\)|:\)|:-\)|=\)|=-\)|]:\)|]:-\)|]=\)|]=-\)|:,'\(|:,'-\(|;\(|;-\(|=,'\(|=,'-\(|:P|:-P|=P|=-P|8-\)|B-\)|,:\(|,:-\(|,=\(|,=-\(|,:\)|,:-\)|,=\)|,=-\)|:s|:-S|:z|:-Z|:\$|:-\$|=s|=-S|=z|=-Z|=\$|=-\$|;\)|;-\))(?!\S)/gm; 21 | 22 | // Replace all the shortcuts with actual emojis 23 | text = text.replace(emojiRegex, (a) => { 24 | let shortcut = shortcuts.find((s) => s.face === a); 25 | if (shortcut) return idToUni[shortcut.id]; 26 | return a; 27 | }); 28 | 29 | text = text.replace(/:(.*):/gm, (a, b) => { 30 | let shortcut = idToUni[b]; 31 | if (shortcut) return shortcut; 32 | return a; 33 | }); 34 | 35 | let customEmojiRegex = /^:([\d\w]+):|[^<]:([\d\w]+):/gm; 36 | text = text.replaceAll('::', ': :').replaceAll(customEmojiRegex, (name) => { 37 | console.log(name); 38 | if (name[1] === ':') 39 | return ( 40 | name[0] + 41 | bot.emojis.cache 42 | .find((emoji) => emoji.name == name.slice(2, -1)) 43 | .toString() 44 | ); 45 | else 46 | return bot.emojis.cache 47 | .find((emoji) => emoji.name == name.slice(1, -1)) 48 | .toString(); 49 | }); 50 | 51 | return text; 52 | }; 53 | 54 | let parseMessage = ( 55 | text, 56 | msg = null, 57 | embed = false, 58 | ping = false, 59 | embeddedLink 60 | ) => { 61 | let textContent = text; 62 | 63 | // Remove html <, > and & in the message 64 | textContent = parseHTML(textContent); 65 | 66 | // General message parsing 67 | // Format pings 68 | if (msg) parsePings(msg, textContent, embeddedLink, ping, embed); 69 | // Match links 70 | textContent = parseLinks(textContent); 71 | // Add html tags for markup 72 | textContent = parseStyling(textContent, embed); 73 | // Match all emojis 74 | textContent = parseUnicodeEmojis(textContent); 75 | // Render custom emojis 76 | textContent = parseCustomEmojis(textContent); 77 | // Parse the emojis to SVGs 78 | textContent = twemoji.parse(textContent); 79 | 80 | return textContent; 81 | }; 82 | 83 | function discoverSpoiler(spoiler) { 84 | spoiler.classList.toggle('discovered'); 85 | } 86 | 87 | // Ping formatting 88 | function formatPings(msg, text, dms) { 89 | let textContent = text; 90 | let keys = []; 91 | 92 | // Get all the mentions from users, roles and channels 93 | msg.mentions.users.each((user) => keys.push([user.id, 'user'])); 94 | msg.mentions.roles.each((role) => keys.push([role.id, 'role'])); 95 | msg.mentions.channels.each((channel) => keys.push([channel.id, 'channel'])); 96 | 97 | // Replace the ping with a span container 98 | keys.forEach((ping) => { 99 | let id = ping[0]; 100 | let type = ping[1]; 101 | 102 | let name = ''; 103 | let color = 0; 104 | if (type == 'user') { 105 | let user = dms 106 | ? bot.users.cache.get(id) 107 | : msg.guild.members.cache.get(id); 108 | name = user?.displayName || user?.username || id; 109 | } else if (type == 'role' && !dms) { 110 | let role = msg.guild.roles.cache.get(id); 111 | name = role ? role.name : id; 112 | color = role.color ? role.color.toString(16) : 0; 113 | color = color ? '#' + '0'.repeat(6 - color.length) + color : 0; 114 | } else if (type == 'channel' && !dms) { 115 | let channel = msg.guild.channels.cache.get(id); 116 | name = channel ? channel.name : 'deleted-channel'; 117 | } else { 118 | name = id; 119 | } 120 | 121 | name = name 122 | .replace(/(\[|\]|\(|\)|\\)/gm, (a) => '\\' + a) 123 | .replace(/\*/gm, '\\*'); 124 | let pingRegex = new RegExp(`(?:(<|>)?@!?(${name}))`, 'g'); 125 | let channelRegex = new RegExp(`(?:(<|>)?#(${name}))`, 'g'); 126 | textContent = textContent.replace(pingRegex, (a, b, c) => 127 | b == '<' || b == '>' 128 | ? a 129 | : `@${c.replace(/\*/gm, '*')}` 132 | ); 133 | if (!dms) { 134 | textContent = textContent.replace(channelRegex, (a, b, c) => 135 | b == '<' || b == '>' 136 | ? a 137 | : `#${c.replace( 138 | /\*/gm, 139 | '*' 140 | )}` 141 | ); 142 | } 143 | }); 144 | return textContent; 145 | } 146 | 147 | function formatEmbedPings(msg, text, dms) { 148 | let textContent = text; 149 | // console.log(text) 150 | let keys = []; 151 | 152 | // Replace user/role pings 153 | text.replace(/<@(!?[0-9]+)>/gm, (a, id) => keys.push(id)); 154 | 155 | // Replace channel pings 156 | text.replace(/<#(\d+)>/gm, (a, id) => keys.push(id)); 157 | 158 | // Replace the ping with a span container 159 | keys.forEach((id) => { 160 | let name = ''; 161 | let chanName = ''; 162 | let color = 0; 163 | 164 | let user = dms 165 | ? bot.users.cache.get(id.replace(/!/, '')) 166 | : msg.guild.members.cache.get(id.replace(/!/, '')); 167 | name = user?.displayName || user?.username || id; 168 | 169 | if (name == id && !dms) { 170 | let role = msg.guild.roles.cache.get(id); 171 | name = role ? role.name : id; 172 | color = role ? (role.color ? role.color.toString(16) : 0) : 0; 173 | color = color ? '#' + '0'.repeat(6 - color.length) + color : 0; 174 | } 175 | let channel; 176 | if (!dms) channel = msg.guild.channels.cache.get(id); 177 | chanName = channel ? channel.name : 'deleted-channel'; 178 | 179 | let pingRegex = new RegExp(`(?:(<|>)?<@!?(${id})>)`, 'g'); 180 | let channelRegex = new RegExp(`<#${id}>`, 'g'); 181 | 182 | textContent = textContent.replace(pingRegex, (a, b, c) => 183 | b == '<' || b == '>' 184 | ? a 185 | : `${ 188 | name.startsWith('!') ? `<@${c}>` : '@' + name 189 | }` 190 | ); 191 | if (!dms) 192 | textContent = textContent.replace( 193 | channelRegex, 194 | chanName == 'deleted-channel' 195 | ? '#deleted-channel' 196 | : `\#${chanName}` 197 | ); 198 | }); 199 | 200 | return textContent; 201 | } 202 | -------------------------------------------------------------------------------- /js/channelList.js: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Sebastian Ouellette 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | 'use strict'; 16 | 17 | function createChannels(g) { 18 | // Clear the channels list 19 | let channelList = document.getElementById('channel-elements'); 20 | while (channelList.firstChild) 21 | channelList.removeChild(channelList.firstChild); 22 | 23 | // The parent variable will change, realParent will not 24 | const realParent = document.getElementById('channel-elements'); 25 | let parent = realParent; 26 | let categoryParent; 27 | 28 | // Sort the channels and add them to the screen 29 | g.channels.cache 30 | .filter((c) => c.type == Discord.ChannelType.GuildCategory) 31 | .sort((c1, c2) => c1.rawPosition - c2.rawPosition) 32 | .each((c) => { 33 | let category = document.createElement('div'); 34 | category.classList.add('category'); 35 | category.classList.add('open'); 36 | category.id = c.id; 37 | realParent.appendChild(category); 38 | 39 | // Container for the category svg and name 40 | let nameCategory = document.createElement('div'); 41 | nameCategory.classList.add('categoryNameContainer'); 42 | category.appendChild(nameCategory); 43 | 44 | // Create the svg icon 45 | let svg = document.createElement('img'); 46 | // svg.type = "image/svg+xml"; 47 | // svg.data 48 | svg.src = './resources/icons/categoryArrow.svg'; 49 | svg.classList.add('categorySVG'); 50 | nameCategory.appendChild(svg); 51 | 52 | // Create the category name 53 | let text = document.createElement('h5'); 54 | text.classList.add('categoryText'); 55 | text.innerText = c.name; 56 | nameCategory.appendChild(text); 57 | 58 | // Create the container for all the channels 59 | let div = document.createElement('div'); 60 | div.classList.add('channelContainer'); 61 | category.appendChild(div); 62 | 63 | // Event listener for opening and closing 64 | nameCategory.addEventListener('click', (event) => { 65 | category.classList.toggle('open'); 66 | }); 67 | 68 | // Set the parent for the next added channels 69 | parent = div; 70 | categoryParent = c; 71 | }); 72 | 73 | let openedDefaultChannel = false; 74 | let defaultDiv; 75 | g.channels.cache 76 | .filter((c) => !Discord.Constants.ThreadChannelTypes.includes(c.type)) // Threads are currently not supported 77 | .map((c) => { 78 | c.rawPosition = 79 | c.type == Discord.ChannelType.GuildVoice 80 | ? c.rawPosition + g.channels.cache.size 81 | : c.rawPosition; 82 | return c; 83 | }) // Put voice channels after text channels 84 | .filter((c) => c.type != Discord.ChannelType.GuildCategory) 85 | .sort((c1, c2) => c1.rawPosition - c2.rawPosition) 86 | .forEach((c) => { 87 | // At this point, the channel is either text or voice 88 | let div = document.createElement('div'); 89 | div.classList.add('channel'); 90 | // div.classList.add(Discord.ChannelType[c.type]); 91 | div.id = c.id; 92 | 93 | // check if user can access the channel 94 | let blocked = false; 95 | if ( 96 | !g.members.me 97 | .permissionsIn(c) 98 | .has(Discord.PermissionFlagsBits.ViewChannel) || 99 | (bot.hideUnallowed && 100 | !g.members.cache 101 | .get(bot.owner.id) 102 | .permissionsIn(c) 103 | .has(Discord.PermissionFlagsBits.ViewChannel)) 104 | ) { 105 | blocked = true; 106 | div.classList.add('blocked'); 107 | } 108 | 109 | // Create the svg icon 110 | let svg = document.createElement('img'); 111 | // svg.type = "image/svg+xml"; 112 | // svg.data 113 | svg.src = `./resources/icons/${Discord.ChannelType[c.type]}Channel${ 114 | blocked ? 'Blocked' : '' 115 | }.svg`; 116 | svg.classList.add('channelSVG'); 117 | svg.classList.add(Discord.ChannelType[c.type]); 118 | div.appendChild(svg); 119 | 120 | // Add the text 121 | let channelName = document.createElement('h5'); 122 | channelName.classList.add('viewableText'); 123 | channelName.innerText = c.name; 124 | div.appendChild(channelName); 125 | 126 | // Finally, add it to the parent 127 | if (c.parentId) 128 | document 129 | .getElementById(c.parentId) 130 | .getElementsByTagName('div')[1] 131 | .appendChild(div); 132 | else 133 | realParent.insertBefore( 134 | div, 135 | realParent.querySelector('.category') 136 | ); 137 | 138 | if (!blocked) { 139 | // Open the channel if it's stored in the database as last opened 140 | if (settings.guilds[settings.lastGuild] == c.id) { 141 | channelSelect(c, div); 142 | openedDefaultChannel = true; 143 | } 144 | 145 | if (global.selectedChanDiv && div.id == selectedChanDiv.id) { 146 | div.classList.add('selectedChan'); 147 | selectedChanDiv = div; 148 | } 149 | div.addEventListener('click', (event) => { 150 | let previous = realParent.querySelector('.selectedChan'); 151 | let id; 152 | if (previous) { 153 | id = previous.id; 154 | if (id != c.id) 155 | previous.classList.remove('selectedChan'); 156 | } 157 | 158 | if (id != c.id) { 159 | // Set the channel as the last channel in the guild 160 | settings.guilds = (() => { 161 | let obj = {}; 162 | obj[c.guild.id] = c.id; 163 | return { ...obj }; 164 | })(); 165 | 166 | div.classList.add('selectedChan'); 167 | channelSelect(c, div); 168 | } 169 | }); 170 | } 171 | }); 172 | 173 | if (!openedDefaultChannel) { 174 | let chan = g.channels.cache 175 | .filter((c) => 176 | Discord.Constants.TextBasedChannelTypes.includes(c.type) 177 | ) 178 | .filter((c) => 179 | g.members.me 180 | .permissionsIn(c) 181 | .has(Discord.PermissionFlagsBits.ViewChannel) 182 | ) 183 | .sort((a, b) => a.rawPosition - b.rawPosition) 184 | .first(); 185 | 186 | // Check if chan exists 187 | if (chan === undefined) 188 | console.error('No available text channel to open'); 189 | else { 190 | // Select the first available channel 191 | let div = document.getElementById(chan.id); 192 | div.classList.add('selectedChan'); 193 | channelSelect(chan, div); 194 | } 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /js/guildList.js: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Sebastian Ouellette 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | 'use strict'; 16 | 17 | // Guilds that are in the guild list 18 | let cachedGuilds = []; 19 | 20 | async function addGuilds() { 21 | console.log('adding guilds'); 22 | // Get the first guild in the list if there is any 23 | let lastGuild = cachedGuilds.length ? cachedGuilds[0][1] : null; 24 | 25 | await bot.guilds.cache 26 | .filter((g) => 27 | bot.hideUnallowed ? g.members.cache.get(bot.owner.id) : g 28 | ) 29 | .forEach(async (g) => { 30 | // Check if the guild is available first, if it's not then remove it 31 | if (!g.available) { 32 | await g.fetch(); 33 | } 34 | if (!g.available) { 35 | errorHandler('SERVER_OFFLINE'); 36 | } 37 | let img; 38 | // console.log(g.name); 39 | 40 | // If there is no icon url for the server, create the letter icon 41 | if (g.iconURL() === null) { 42 | img = document.createElement('div'); 43 | 44 | img.style.backgroundColor = '#2F3136'; 45 | img.style.marginBottom = '4px'; 46 | 47 | let abrev = document.createElement('p'); 48 | abrev.id = 'guildAbrev'; 49 | abrev.appendChild(document.createTextNode(g.nameAcronym)); 50 | img.appendChild(abrev); 51 | } else { 52 | // The guild has an icon, create the image 53 | img = document.createElement('img'); 54 | 55 | let ico = `https://cdn.discordapp.com/icons/${g.id}/${g.icon}.webp?size=64`; 56 | 57 | // Check if the icon is animated and add the animation on hover or remove it 58 | if (g.icon.startsWith('a_')) { 59 | img.onmouseenter = () => { 60 | img.src = ico.replace('.webp', '.gif'); 61 | }; 62 | img.onmouseleave = () => { 63 | img.src = ico; 64 | }; 65 | } 66 | img.src = ico; 67 | 68 | img.alt = g.name; 69 | img.height = '40'; 70 | img.width = '40'; 71 | } 72 | 73 | // Styling for both image and letter icons 74 | img.style.height = '40px'; 75 | img.style.width = '40px'; 76 | img.classList.add('guild-icon'); 77 | img.id = g.id; 78 | 79 | // Add the events for the guild icons 80 | img.onclick = () => { 81 | settings.lastGuild = g.id; 82 | guildSelect(g, img); 83 | }; 84 | 85 | // Creating the container for the icon 86 | let guildIcon = document.createElement('div'); 87 | // Creating the container for the guilds name 88 | let guildNameContainer = document.createElement('div'); 89 | // Creating the text element which will display the name 90 | let guildName = document.createElement('p'); 91 | 92 | // Adding classes to elements 93 | guildNameContainer.classList.add('guildNameContainer'); 94 | guildName.classList.add('guildName'); 95 | guildIcon.classList.add('guildIconDiv'); 96 | 97 | // Setting the name 98 | guildName.innerText = g.name; 99 | // Appending the name to the name container 100 | guildNameContainer.appendChild(guildName); 101 | // Appending the image and the container in reverse order 102 | // so it could be manipulated with in css 103 | guildIcon.appendChild(img); 104 | guildIcon.appendChild(guildNameContainer); 105 | 106 | // Add image to the list of guilds 107 | if ( 108 | lastGuild == null || 109 | (lastGuild.parentElement && 110 | lastGuild.parentElement.lastElementChild == lastGuild) 111 | ) { 112 | // Append the guild to the last spot 113 | document 114 | .getElementById('guildContainer') 115 | .appendChild(guildIcon); 116 | cachedGuilds.push([g.id, guildIcon]); 117 | } else if (!cachedGuilds.find((e) => e[0] == g.id)) { 118 | // Insert the guild in the respectful place 119 | document 120 | .getElementById('guildContainer') 121 | .insertBefore(guildIcon, lastGuild.nextSibling); 122 | cachedGuilds.push([g.id, guildIcon]); 123 | } 124 | // Check if the guild is in the cache just in case 125 | if (cachedGuilds.find((e) => e[0] == g.id)) { 126 | // Get this guild out of the cache and store it 127 | lastGuild = cachedGuilds.find((e) => e[0] == g.id)[1]; 128 | } 129 | 130 | // Change location of the guild name indicator thingy 131 | img.onmouseover = () => { 132 | let top = img.getBoundingClientRect().top; 133 | guildNameContainer.style.top = `${top + 3}px`; 134 | }; 135 | 136 | // Changing the width of the name container so it fits the text 137 | guildNameContainer.style.width = 138 | guildName.getBoundingClientRect().width + 10 + 'px'; 139 | 140 | // Load the guild while starting up if it was the last guild stored 141 | // Doing it at the end so the indicator doesn't end up on the top 142 | if (g.id == settings.lastGuild) { 143 | guildSelect(g, img); 144 | } 145 | }); 146 | 147 | // Done loading. 148 | if (settings.options.splash) { 149 | setLoadingPerc(1); 150 | } 151 | } 152 | 153 | // Remove the guild 154 | function removeGuild(g) { 155 | // Find the guild in the cache 156 | let guild = cachedGuilds.find((e) => e[0] == g.id); 157 | // If the guild isn't in the cache then cancel the rest 158 | if (!guild) return; 159 | // Get the guilds respected element 160 | let guildElement = guild[1]; 161 | // If the guild is selected then hide the guild indicator 162 | if (selectedGuild && g.id == selectedGuild.id) { 163 | document.getElementById('guildIndicator').style.display = 'none'; 164 | selectedGuild = undefined; 165 | 166 | // Clear the list of channels 167 | let channels = document.getElementById('channel-elements'); 168 | while (channels.firstChild) { 169 | channels.removeChild(channels.firstChild); 170 | } 171 | 172 | // Clear the message list 173 | let messages = document.getElementById('message-list'); 174 | while (messages.firstChild) { 175 | messages.removeChild(messages.firstChild); 176 | } 177 | 178 | // Clear the member list 179 | let memberList = document.getElementById('memberBar'); 180 | memberList.innerHTML = ''; 181 | } 182 | // If the channel is deleted then remove it from the variable 183 | if ( 184 | selectedChan && 185 | selectedChan.type != Discord.ChannelType.DM && 186 | (selectedChan.guild.deleted || !selectedChan.guild.available) 187 | ) { 188 | selectedChan = undefined; 189 | selectedChatDiv = undefined; 190 | } 191 | // Remove the guild element from the guild list 192 | guildElement.parentElement.removeChild(guildElement); 193 | // Remove the guild out of the cache 194 | cachedGuilds.splice(cachedGuilds.indexOf(guildElement)); 195 | } 196 | -------------------------------------------------------------------------------- /js/dmList.js: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Sebastian Ouellette 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | 'use strict'; 16 | 17 | function mutualGuilds(u, g, remove) { 18 | if (u.bot) return; 19 | if (!u.mutualGuilds) { 20 | u.mutualGuilds = new Discord.Collection(); 21 | bot.guilds.cache.each((g) => { 22 | if (!g.available) return; 23 | let inGuild = g.members.cache.get(u.id); 24 | if (inGuild && !u.mutualGuilds.get(u.id)) { 25 | u.mutualGuilds.set(g.id, g); 26 | } else if (!inGuild && u.mutualGuilds.get(u.id)) { 27 | u.mutualGuilds.delete(g.id); 28 | } 29 | }); 30 | return; 31 | } 32 | 33 | let mutualGuild = u.mutualGuilds.get(g.id); 34 | 35 | if (remove && mutualGuild) u.mutualGuilds.delete(g.id); 36 | else if (!remove && !mutualGuild) u.mutualGuilds.set(g.id, g); 37 | } 38 | 39 | function updateUsers(bunch, m = undefined, remove = false) { 40 | if (bunch || !m) { 41 | bot.users.cache.each((u) => { 42 | u.openDM != true && u.openDM != false 43 | ? (u.openDM = false) 44 | : undefined; 45 | u.mutualGuilds ? undefined : mutualGuilds(u); 46 | u.received ? true : false; 47 | }); 48 | return; 49 | } 50 | 51 | if (m.user.openDM == undefined) m.user.openDM = false; 52 | 53 | if (m) mutualGuilds(m.user, m.guild, remove); 54 | } 55 | 56 | function updateUserDM(c, u) { 57 | if (c.type != Discord.ChannelType.DM || u.bot) return; 58 | return (u.received = selectedChan != c); 59 | } 60 | 61 | function dmList() { 62 | console.log('Switching to dms'); 63 | // If a guild is selected then hide the guild indicator 64 | if (selectedGuild) { 65 | document.getElementById('guildIndicator').style.display = 'none'; 66 | selectedGuild = undefined; 67 | 68 | // Clear guild card 69 | let children = document.getElementById('serverName').children; 70 | children[0].innerText = 'Direct Messages'; // Server name element 71 | if (!Array.from(children[0].classList).includes('directMsg')) { 72 | children[0].classList.add('directMsg'); // Toggle on the directMsg class for css 73 | } 74 | children[1].src = 'resources/icons/logo.svg'; // Server icon element 75 | children[2].style.display = 'none'; // Member text element 76 | children[3].innerText = ''; // Member count element 77 | } 78 | // Delete the selected chan variables 79 | selectedChan = undefined; 80 | selectedChatDiv = undefined; 81 | 82 | // Check if there are any users in the cache, if there aren't return 83 | if (!bot.users.cache.size) return; 84 | 85 | // Clear all the channels to make space for the users 86 | let channelList = document.getElementById('channel-elements'); 87 | while (channelList.firstChild) 88 | channelList.removeChild(channelList.firstChild); 89 | 90 | // Clear the member bar so it can be filled with something else 91 | let memberList = document.getElementById('memberBar'); 92 | memberList.innerHTML = ''; 93 | 94 | // Clear the message list 95 | let messages = document.getElementById('message-list'); 96 | while (messages.firstChild) { 97 | messages.removeChild(messages.firstChild); 98 | } 99 | 100 | // Categories that are going to be used to sort the users 101 | let categories = [ 102 | [undefined, `Open DM's`, 'openDM'], 103 | [undefined, `Received DM's`, 'receivedDM'], 104 | [undefined, `Other DM'S`, 'otherDM'], 105 | ]; 106 | 107 | // Create the categories so the users can be appended to them 108 | categories.forEach((c) => { 109 | let [element, name, id] = c; 110 | let category = document.createElement('div'); 111 | category.classList.add('category'); 112 | category.id = id; 113 | channelList.appendChild(category); 114 | 115 | // Container for the category svg and name 116 | let nameCategory = document.createElement('div'); 117 | nameCategory.classList.add('categoryNameContainer'); 118 | category.appendChild(nameCategory); 119 | 120 | // Create the svg icon 121 | let svg = document.createElement('img'); 122 | // svg.type = "image/svg+xml"; 123 | // svg.data 124 | svg.src = './resources/icons/categoryArrow.svg'; 125 | svg.classList.add('categorySVG'); 126 | nameCategory.appendChild(svg); 127 | 128 | // Create the category name 129 | let text = document.createElement('h5'); 130 | text.classList.add('categoryText'); 131 | text.innerText = name; 132 | nameCategory.appendChild(text); 133 | 134 | // Create the container for all the channels 135 | let div = document.createElement('div'); 136 | div.classList.add('channelContainer'); 137 | category.appendChild(div); 138 | 139 | // Event listener for opening and closing 140 | nameCategory.addEventListener('click', (event) => { 141 | category.classList.toggle('open'); 142 | }); 143 | 144 | categories[categories.indexOf(c)][0] = category; 145 | }); 146 | 147 | // Filter the users so there aren't any bots or users that aren't in the same guild as you 148 | // Sort them by name 149 | // Note: You can't message bots with a bot account, only users 150 | bot.users.cache 151 | .filter( 152 | (u) => u.mutualGuilds?.size && !u.bot && (u.openDM || u.received) 153 | ) 154 | .sort((u1, u2) => u1.username.localeCompare(u2.username)) 155 | .each((u) => { 156 | // Get the element for the user 157 | let [open, received, other] = categories; 158 | 159 | // Create the dm channel 160 | let div = document.createElement('div'); 161 | div.classList.add('dmChannel'); 162 | div.id = u.id; 163 | 164 | // Create the image for the user if they have one, otherwise use discords default and animate it on hover 165 | let img = document.createElement('img'); 166 | let userImg = u.displayAvatarURL({ 167 | size: 64, 168 | forceStatic: true, 169 | extension: 'webp', 170 | }); 171 | if (u.avatar?.startsWith('a_')) { 172 | let userGif = u.displayAvatarURL({ size: 64 }); 173 | img.src = userGif; 174 | div.onmouseenter = (e) => { 175 | img.src = userGif; 176 | }; 177 | div.onmouseleave = (e) => { 178 | img.src = userImg; 179 | }; 180 | } 181 | 182 | // Add the class for the image and set the size 183 | img.classList.add('dmChannelImage'); 184 | img.src = userImg; 185 | img.height = '25'; 186 | img.width = '25'; 187 | div.appendChild(img); 188 | 189 | // Add the name 190 | let channelName = document.createElement('h5'); 191 | channelName.classList.add('viewableText'); 192 | channelName.innerText = u.username; 193 | div.appendChild(channelName); 194 | 195 | // Open the dms when you click on the div 196 | div.addEventListener('click', (e) => { 197 | let previous = channelList.querySelector('.selectedChan'); 198 | let id; 199 | if (previous) { 200 | id = previous.id; 201 | if (id != u.id) previous.classList.remove('selectedChan'); 202 | } 203 | 204 | if (id != u.id) { 205 | div.classList.add('selectedChan'); 206 | dmChannelSelect(u, div); 207 | } 208 | }); 209 | 210 | // Check in which category the user should go in 211 | if (u.openDM) 212 | open[0].getElementsByTagName('div')[1].appendChild(div); 213 | else if (!u.openDM && u.received) { 214 | received[0].getElementsByTagName('div')[1].appendChild(div); 215 | } else { 216 | // other[0].getElementsByTagName('div')[1].appendChild(div); 217 | } 218 | }); 219 | } 220 | -------------------------------------------------------------------------------- /css/splashScreen.css: -------------------------------------------------------------------------------- 1 | /* Copyright 2017 Sebastian Ouellette 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. */ 14 | 15 | #splashScreen { 16 | width: 100vw; 17 | height: 100vh; 18 | background-color: #202225; 19 | position: fixed; 20 | z-index: 1000; 21 | display: block; 22 | margin: 0 0 0 -8px; 23 | transition-duration: 0.5s; 24 | text-align: center; 25 | } 26 | 27 | #splashScreen h1 { 28 | margin-top: 50px; 29 | font-family: UniSans; 30 | color: white; 31 | font-stretch: expanded; 32 | font-size: 70px; 33 | width: 100%; 34 | user-select: none; 35 | } 36 | 37 | /* Loading Circles */ 38 | #splashLoading { 39 | margin-left: auto; 40 | margin-right: auto; 41 | margin-top: calc(50vh - 300px); 42 | width: 200px; 43 | height: 120px; 44 | transition-duration: 0.5s; 45 | } 46 | 47 | .loadingCircle { 48 | height: 12px; 49 | width: 12px; 50 | margin: 6px 6px 6px 16px; 51 | border-radius: 50%; 52 | background-color: #36393f; 53 | transform: translateY(100px); 54 | display: inline-block; 55 | animation-name: loading; 56 | animation-iteration-count: infinite; 57 | animation-duration: 2s; 58 | animation-delay: 0s; 59 | } 60 | 61 | #c1 { 62 | animation-delay: 0ms; 63 | } 64 | 65 | #c2 { 66 | animation-delay: 0.2s; 67 | } 68 | 69 | #c3 { 70 | animation-delay: 0.4s; 71 | } 72 | 73 | #c4 { 74 | /* boom */ 75 | animation-delay: 0.6s; 76 | } 77 | 78 | #c5 { 79 | animation-delay: 0.8s; 80 | } 81 | 82 | @keyframes loading { 83 | 0% { 84 | height: 12px; 85 | width: 12px; 86 | margin: 6px 6px 6px 16px; 87 | transform: translateY(100px); 88 | background-color: #2f3136; 89 | } 90 | 20% { 91 | height: 24px; 92 | width: 24px; 93 | margin: 0 0 0 10px; 94 | transform: translateY(0px); 95 | background-color: #fff; 96 | } 97 | 45% { 98 | height: 12px; 99 | width: 12px; 100 | margin: 6px 6px 6px 16px; 101 | transform: translateY(100px); 102 | background-color: #2f3136; 103 | } 104 | 60% { 105 | height: 13, 9px; 106 | width: 13, 9px; 107 | margin: 5, 1px 5, 1px 5, 1px 15, 1px; 108 | transform: translateY(52px); 109 | background-color: #aaa; 110 | } 111 | 80% { 112 | height: 12px; 113 | width: 12px; 114 | margin: 6px 6px 6px 16px; 115 | transform: translateY(100px); 116 | background-color: #2f3136; 117 | } 118 | } 119 | 120 | /* Loading Bar */ 121 | #loadingBar { 122 | height: 20px; 123 | width: 50vw; 124 | margin-top: 50px; 125 | margin-left: auto; 126 | margin-right: auto; 127 | display: block; 128 | transition-duration: 0.5s; 129 | } 130 | 131 | #loadingIncomplete { 132 | width: 100%; 133 | height: 10px; 134 | background-color: #403e41; 135 | position: relative; 136 | border-radius: 5px; 137 | } 138 | 139 | #loadingComplete { 140 | height: 100%; 141 | width: 0; 142 | top: -15px; 143 | background-color: #2f3136; 144 | position: relative; 145 | transition-duration: 500ms; 146 | border-radius: 5px; 147 | } 148 | 149 | /* Loading text */ 150 | #percentageText { 151 | color: white; 152 | user-select: none; 153 | } 154 | 155 | /* Team members */ 156 | #selectMember { 157 | height: calc(100vh - 301px - calc(50vh - 300px) - 115px); 158 | margin-top: 50px; 159 | overflow-y: auto; 160 | } 161 | 162 | #selectMember::-webkit-scrollbar { 163 | background-color: transparent; 164 | width: 6px; 165 | } 166 | 167 | #selectMember::-webkit-scrollbar-track { 168 | background-color: transparent; 169 | } 170 | 171 | #selectMember::-webkit-scrollbar-thumb { 172 | background-color: #36393f; 173 | border-radius: 3px; 174 | } 175 | 176 | .teamMember { 177 | width: 250px; 178 | height: 280px; 179 | background-color: #36393f; 180 | display: inline-block; 181 | margin: 0 10px 20px 10px; 182 | border-radius: 5px; 183 | overflow: hidden; 184 | } 185 | 186 | .userArea { 187 | height: 200px; 188 | background-color: #18191c; 189 | } 190 | 191 | .teamMemberIcon { 192 | border-radius: 50%; 193 | height: 100px; 194 | width: 100px; 195 | margin-top: 20px; 196 | } 197 | 198 | .teamMemberTag { 199 | display: block; 200 | margin-top: 20px; 201 | font-size: 20px; 202 | } 203 | 204 | .teamMemberName { 205 | max-width: 180px; 206 | text-overflow: ellipsis; 207 | color: white; 208 | white-space: nowrap; 209 | overflow: hidden; 210 | display: inline-block; 211 | } 212 | 213 | .teamMemberDisc { 214 | color: #777; 215 | white-space: nowrap; 216 | overflow: hidden; 217 | display: inline-block; 218 | } 219 | 220 | .teamMemberButton { 221 | display: inline-block; 222 | height: 50px; 223 | width: calc(45% - 10px); 224 | margin: 0 4px; 225 | background-color: #28292c; 226 | text-align: center; 227 | margin-top: 10px; 228 | border-radius: 5px; 229 | padding: 5px; 230 | vertical-align: middle; 231 | cursor: pointer; 232 | transition-duration: 0.2s; 233 | } 234 | 235 | .teamMemberButton:hover { 236 | background-color: #222325; 237 | border-radius: 3px; 238 | } 239 | 240 | .teamMemberButton span { 241 | display: block; 242 | color: white; 243 | font-size: 18px; 244 | user-select: none; 245 | height: 100%; 246 | padding: 2px; 247 | } 248 | 249 | .oneline { 250 | line-height: 2.4; 251 | } 252 | 253 | /* Token styling */ 254 | .splashTokenContainer { 255 | width: 50vw; 256 | margin-left: auto; 257 | margin-right: auto; 258 | } 259 | 260 | .splashScreenToken { 261 | background-color: #40444b; 262 | padding: 10px; 263 | outline: none; 264 | border-radius: 3px; 265 | color: white; 266 | display: block; 267 | width: 100%; 268 | margin-left: -10px; 269 | font-size: 17px; 270 | border: 2px solid #40444b; 271 | } 272 | 273 | .tokenAddButton { 274 | display: inline-block; 275 | height: 25px; 276 | width: 40%; 277 | margin: 0 20px; 278 | background-color: #28292c; 279 | text-align: center; 280 | margin-top: 10px; 281 | border-radius: 5px; 282 | padding: 5px; 283 | vertical-align: middle; 284 | cursor: pointer; 285 | transition-duration: 0.2s; 286 | color: white; 287 | font-size: 17px; 288 | } 289 | 290 | .tokenAddButton:hover { 291 | background-color: #222325; 292 | border-radius: 3px; 293 | } 294 | 295 | /* Stuck on loading screen */ 296 | #stuckLoadingCont { 297 | min-height: 20px; 298 | width: 150px; 299 | background-color: rgba(0, 0, 0, 0); 300 | position: relative; 301 | float: right; 302 | right: 10px; 303 | top: 10px; 304 | transition-duration: 0.1s; 305 | color: #aaa; 306 | } 307 | 308 | #stuckLoadingCont:hover { 309 | color: white; 310 | } 311 | 312 | #faqMenu { 313 | display: none; 314 | background-color: #28292c; 315 | height: 500px; 316 | width: 390px; 317 | padding: 0 10px; 318 | position: absolute; 319 | right: 10px; 320 | z-index: 1000; 321 | border-radius: 3px; 322 | overflow-y: scroll; 323 | border: 1px solid #403e41; 324 | } 325 | 326 | #faqMenu img { 327 | width: 100%; 328 | } 329 | 330 | #faqMenu::-webkit-scrollbar { 331 | background-color: transparent; 332 | width: 6px; 333 | } 334 | 335 | #faqMenu::-webkit-scrollbar-track { 336 | background-color: transparent; 337 | } 338 | 339 | #faqMenu::-webkit-scrollbar-thumb { 340 | background-color: #1d1f22; 341 | border-radius: 3px; 342 | } 343 | 344 | p { 345 | color: #aaa; 346 | } 347 | 348 | #stuckLoadingCont:hover #faqMenu { 349 | display: block; 350 | } 351 | 352 | #stuckLoadingCont h2 { 353 | margin: 0; 354 | font-size: 15px; 355 | padding-bottom: 10px; 356 | transform: scaleX(1.1); 357 | } 358 | 359 | #clearCache { 360 | cursor: pointer; 361 | background-color: #40444b; 362 | border: none; 363 | color: white; 364 | padding: 10px; 365 | outline: none; 366 | border-radius: 4px; 367 | user-select: none; 368 | } 369 | 370 | #clearCache:hover { 371 | background-color: #484c53; 372 | } 373 | 374 | #clearCache:active { 375 | background-color: #2f3236; 376 | } 377 | 378 | .greenText { 379 | color: #00ff96; 380 | } 381 | --------------------------------------------------------------------------------