Compare commits

...

951 Commits

Author SHA1 Message Date
Alexei Stukov 835cc3134c
Merge pull request #318 from JsSucks/2.0.0-beta.6
beta6
2019-03-16 19:22:45 +02:00
Jiiks 02b30153ee beta6 2019-03-16 19:18:28 +02:00
Alexei Stukov d09dc5a2a5
Merge pull request #315 from Mega-Mewthree/patch-3
Fix #303, Try 3
2019-03-16 19:12:08 +02:00
Mega-Mewthree 98d9a30027
Fix #303, Try 3 2019-03-12 22:11:05 -07:00
Alexei Stukov 5cb4bc15bd
Merge pull request #312 from samuelthomas2774/component-patches
React component patches
2019-03-12 22:44:26 +02:00
Alexei Stukov bb2aba04d5
Missing FileUtils import 2019-03-12 22:40:21 +02:00
Samuel Elliott 82e9b0bd6a
Fix packed plugins 2019-03-12 19:47:55 +00:00
Samuel Elliott 47575d3449
Don’t watch packed themes 2019-03-12 19:22:27 +00:00
Samuel Elliott 32e2582ded
Use the system temporary directory for packed content 2019-03-12 19:10:09 +00:00
Samuel Elliott d58dda6f50
Fix E2EE message button patch 2019-03-12 17:23:56 +00:00
Samuel Elliott 648954d533
Rerender messages when disabling coloured text and fix jumbo Discord emoji in spoilers 2019-03-12 15:59:18 +00:00
Samuel Elliott 4aa38f4582
Move E2EE and emote module to their own directories 2019-03-12 15:27:05 +00:00
Samuel Elliott b3ba1aef13
Render emotes in spoilers 2019-03-11 17:56:29 +00:00
Samuel Elliott 2a6cbd39b7
Remove logging 2019-03-11 16:43:18 +00:00
Samuel Elliott ce5bcb9b85
Patch MessageAccessories instead of ImageWrapper for emotes sent as images 2019-03-10 21:29:17 +00:00
Samuel Elliott 5a3821ad3e
Package installer UploadArea patch 2019-03-10 20:29:55 +00:00
Samuel Elliott fd0032b24c
Autocomplete 2019-03-10 18:12:43 +00:00
Samuel Elliott 226719b36e
Rerender messages after loading emotes 2019-03-10 17:45:20 +00:00
Samuel Elliott fcfee53928
Move all component selectors + filters to ReactAutoPatcher 2019-03-10 16:58:24 +00:00
Samuel Elliott 285ae34b50
Fix component patches 2019-03-10 16:30:13 +00:00
Alexei Stukov a170a97688
Merge pull request #306 from samuelthomas2774/keytar-4.4.1
macOS + Linux keytar + node-sass bindings
2019-03-10 15:30:36 +02:00
Samuel Elliott a770f57b28
Linux keytar + node-sass bindings for Discord Canary 2019-03-10 00:04:02 +00:00
Samuel Elliott 5757fc20c9
macOS keytar + node-sass bindings for Discord Canary 2019-03-09 23:57:29 +00:00
Samuel Elliott 421289f63b
Linux keytar binding 2019-03-09 23:36:47 +00:00
Samuel Elliott ac85316578
.. 2019-03-09 22:02:40 +00:00
Samuel Elliott 08af9be061
Fix guilds wrapper position 2019-03-09 21:55:13 +00:00
Samuel Elliott ead0fbbd1e
Fix extra space on macOS in the plugins and themes panel and use BdMenuItems to add the super secret view 2019-03-09 21:43:41 +00:00
Samuel Elliott dc85a808f8
Fix undefined/storage file 2019-03-09 21:24:01 +00:00
Samuel Elliott aecfa814f9
Fix error Cannot find module ‘core/dist/package.json’ 2019-03-09 20:52:04 +00:00
Samuel Elliott 436f3d3c36
macOS keytar binding 2019-03-09 20:41:40 +00:00
Alexei Stukov 68a8187964
Merge pull request #300 from JsSucks/Jiiks-patch-1
Update issue templates
2019-03-08 21:54:49 +02:00
Alexei Stukov f30e4c12fe
Update issue templates 2019-03-08 21:51:44 +02:00
Alexei Stukov 686514ed1d
Merge pull request #299 from JsSucks/Jiiks-patch-1
Update issue templates
2019-03-08 21:43:50 +02:00
Alexei Stukov d795da1750
Update issue templates 2019-03-08 21:38:52 +02:00
Alexei Stukov 3219ff7c6e
Merge pull request #293 from JsSucks/2.0.0-beta.4
beta 4 to master
2019-03-08 14:11:13 +02:00
Alexei Stukov 33567a2cfd
Merge pull request #292 from JsSucks/updater
Updater
2019-03-08 14:08:07 +02:00
Jiiks 85310bfbff Stop updater thread 2019-03-08 14:00:54 +02:00
Jiiks d1d79a37b7 Start updater and set interval to 30 minutes 2019-03-08 13:58:46 +02:00
Jiiks d95592acc9 Styling and debug view 2019-03-08 13:57:02 +02:00
Jiiks f6a3fb65da v69 bindings 2019-03-08 13:33:52 +02:00
Alexei Stukov 6788cca363
Merge pull request #291 from XeonPowder/patch-1
fixed comma
2019-03-08 00:41:41 +02:00
Lars van der Zande 1fb442e096
fixed comma 2019-03-07 16:30:21 -06:00
Jiiks 6e64ff61c5 Copy csp.json 2019-03-07 23:50:17 +02:00
Jiiks dd8fe68a11 Better 2019-03-07 22:57:07 +02:00
Jiiks a3829089f9 Secrets for devs 2019-03-07 22:54:16 +02:00
Jiiks 174c1ee791 add some secrets 2019-03-07 22:44:29 +02:00
Jiiks b8793fd2b6 Package parsing for production 2019-03-07 21:09:48 +02:00
Jiiks 31986ca3a0 Try to rename first. 2019-03-07 20:22:11 +02:00
Jiiks 10ff740f75 Remove asar external from production 2019-03-07 19:34:32 +02:00
Jiiks 83fbab63c0 add release test script for faster release testing. copy extra files 2019-03-07 19:21:27 +02:00
Jiiks c4670946e6 Use replace to set production 2019-03-07 19:10:08 +02:00
Jiiks e5239d952e Send error event 2019-03-07 14:15:24 +02:00
Jiiks a57783a9d8 add updates available notification 2019-03-07 11:41:32 +02:00
Jiiks 001a6e4fda Functional updater 2019-03-07 11:35:24 +02:00
Jiiks dd621038f9 Reload and restart notifications 2019-03-06 12:57:19 +02:00
Jiiks 288c233447 tar.gz download 2019-03-06 12:11:45 +02:00
Jiiks 399c6e792b Updater ui 2019-03-06 09:29:23 +02:00
Jiiks 0be6facba4 assign old state, some updater checks. 2019-03-06 09:04:03 +02:00
Jiiks dc7247a12d Send updates to client 2019-03-06 08:36:54 +02:00
Jiiks 377c4fd104 Send updater events 2019-03-06 08:19:09 +02:00
Jiiks 252d496dc2 Get release from github, todo use common 2019-03-06 02:57:57 +02:00
Jiiks 83e334c3f8 Getters, debug 2019-03-06 02:16:27 +02:00
Jiiks e72ad10dfc add semver 2019-03-06 02:02:02 +02:00
Jiiks 9ef392c575 test updater 2019-03-05 23:46:42 +02:00
Jiiks 76057efbb7 Remove old updater code 2019-03-05 23:35:39 +02:00
Jiiks dcb121750a Use core version as the main version 2019-03-05 23:30:41 +02:00
Jiiks 13fa769e9e add versions to config 2019-03-05 23:26:17 +02:00
Jiiks 3143991239 Nevet test editor in production 2019-03-05 21:31:40 +02:00
Jiiks 1ae0c5aa4d Never ignore externals in production 2019-03-05 21:31:20 +02:00
Jiiks 5ea39f86f7 add axios to vendor 2019-03-05 21:28:32 +02:00
Jiiks d6a946e096 headers 2019-03-05 21:26:36 +02:00
Jiiks b68c1fbd04 Move csp and add some sources. 2019-03-05 21:02:06 +02:00
Jiiks 07d3629622 axios wrapper base 2019-03-05 20:38:21 +02:00
Jiiks 15daa9acef add axios because it's better 2019-03-05 20:22:26 +02:00
Jiiks 817a4a03b6 add versions to test args and version getters to config 2019-03-05 12:56:19 +02:00
Jiiks e63386e9eb Updater base 2019-03-05 12:53:08 +02:00
Jiiks 402acdfea9 Wrong main 2019-03-05 10:11:45 +02:00
Jiiks b440206d07 add webpack parallel build/watch 2019-03-05 10:07:49 +02:00
Jiiks c7bea4a743 remove csseditor 2019-03-05 09:43:25 +02:00
Jiiks 2528d87b8f beta.4 branch. all future pulls should go to the latest dev branch instead of master. 2019-03-05 08:46:27 +02:00
Alexei Stukov 405d74fada
Merge pull request #290 from JsSucks/Jiiks-patch-1
I like badges
2019-03-05 08:41:27 +02:00
Alexei Stukov b311220132
I like badges 2019-03-05 08:39:11 +02:00
Alexei Stukov 66b47457b1
Merge pull request #289 from JsSucks/snyk-badge
Snyk badge
2019-03-05 08:30:49 +02:00
Alexei Stukov 6e9f9f8bf8
put it in correct place 2019-03-05 07:46:02 +02:00
Alexei Stukov 88a113dc8f
snyk badge 2019-03-05 07:44:13 +02:00
Alexei Stukov a61f860466
Merge pull request #288 from JsSucks/update-versions
Update versions
2019-03-05 07:43:53 +02:00
Jiiks 729a4607bd Use mver for release tag instead. 2019-03-05 07:35:59 +02:00
Jiiks d81dcc9aa2 refresh lock, fix editor production config 2019-03-05 07:32:11 +02:00
Jiiks 1e4f3fa82b semver 2019-03-05 07:25:18 +02:00
Alexei Stukov 150a1d63c4
Merge pull request #287 from JsSucks/deps-and-scripts
Deps and scripts
2019-03-04 20:12:14 +02:00
Jiiks 6b481733b9 editor webpack merge 2019-03-04 20:08:48 +02:00
Jiiks e07b9b1550 bindings 2019-03-04 19:50:34 +02:00
Jiiks 99c2b53ec6 depcopy and configs 2019-03-04 17:02:52 +02:00
Jiiks 2a93e5d2a3 lint 2019-03-04 10:25:00 +02:00
Jiiks 3661207602 switch to babel.config.js 2019-03-04 10:12:42 +02:00
Jiiks 92845728cc command line pls 2019-03-04 10:09:58 +02:00
Jiiks c99753fc8c Use webpack merge to simplify configs 2019-03-04 10:07:32 +02:00
Jiiks 688e6022a0 pls 2019-03-04 09:55:34 +02:00
Jiiks 5408d994be babel config for client 2019-03-04 09:52:25 +02:00
Zack Rauen 003c9766bc remove uglifyjs plugin 2019-03-03 14:47:44 -05:00
Zack Rauen 82e9c257ce Update deps 2019-03-02 22:15:24 -05:00
Jiiks 6167cc7c4b remove installer build 2019-03-01 22:02:06 +02:00
Jiiks 1bacecf8d4 vtc 2019-03-01 22:01:37 +02:00
Jiiks d102686379 vue, vue-color, remove cm, remove csseditor scripts 2019-03-01 21:56:35 +02:00
Jiiks 1160955629 add chokidar 2019-03-01 20:15:30 +02:00
Jiiks e60f765a50 Move bdedit to devdeps 2019-03-01 20:14:33 +02:00
Alexei Stukov d3db696616
Merge pull request #284 from JsSucks/installer-compliance
Installer compliance
2019-02-28 19:25:39 +02:00
Jiiks b8d16c6e4d Load correct editor file 2019-02-28 17:07:53 +02:00
Jiiks b22923d12f editor packaging 2019-02-28 17:00:25 +02:00
Jiiks 1ea307efdd Editor release build 2019-02-28 16:51:45 +02:00
Jiiks d98cff878f uglify editor 2019-02-28 16:47:49 +02:00
Jiiks dd11708f9f return null if no path 2019-02-28 15:20:36 +02:00
Jiiks 665a7818c9 Make sure editor path is set 2019-02-28 15:12:17 +02:00
Jiiks d75b907ae4 ensure user.scss exists 2019-02-28 15:10:34 +02:00
Alexei Stukov f051bc4812
Merge pull request #286 from JsSucks/bdedit
Bdedit
2019-02-28 14:46:48 +02:00
Jiiks 9773f78506 Toasts and name conflict handler 2019-02-28 12:37:57 +02:00
Alexei Stukov f4b7c99c31
Merge pull request #283 from JsSucks/bdedit
Bdedit
2019-02-28 09:51:20 +02:00
Jiiks 97519b2307 lint 2019-02-28 09:48:45 +02:00
Jiiks 806ca5028a auto imports 2019-02-28 09:46:24 +02:00
Jiiks db89e3a1a0 sass doesn't like empty 2019-02-28 09:45:06 +02:00
Jiiks 6030a78b91 Delete and rename handlers 2019-02-28 06:40:33 +02:00
Jiiks 99ec82795c reveal, copy and copypath handlers 2019-02-28 06:05:54 +02:00
Jiiks b1e8b591ba Remove todo 2019-02-27 20:34:31 +02:00
Jiiks ac9e16632d Snippet save/load 2019-02-27 20:34:11 +02:00
Jiiks d8f977b57a Not a dir anymore 2019-02-27 16:43:13 +02:00
Jiiks c3283c47f6 Tons of smol things 2019-02-27 08:10:04 +02:00
Jiiks 2b86f2d741 File watchers 2019-02-27 01:33:23 +02:00
Jiiks 9f6389845a add live update 5 seconds after last change 2019-02-26 22:24:05 +02:00
Jiiks 7489fd0aaf Titlebar buttons fix 2019-02-26 20:30:07 +02:00
Jiiks 7e1c379fd3 Set user.scss caption 2019-02-26 20:19:57 +02:00
Jiiks c0765120e8 Juggle paths and keep user.scss in data 2019-02-26 20:18:33 +02:00
Jiiks 7e10de32cb add new paths and enable snippet import 2019-02-26 19:54:13 +02:00
Jiiks 145d61fe5e Enable importing other files 2019-02-26 18:32:06 +02:00
Jiiks 72b278de6e js should be javascript 2019-02-26 16:33:14 +02:00
Jiiks 5e977e8dc4 Hoist user.scss 2019-02-25 18:05:51 +02:00
Jiiks 109eb31aa5 custom css inject and file saved state 2019-02-25 17:56:56 +02:00
Jiiks 1b6f3005f5 No need to compile if not scss 2019-02-25 13:01:46 +02:00
Jiiks d02f894d4a Style inject 2019-02-25 12:41:22 +02:00
Jiiks 20561e2938 Set type 2019-02-25 12:10:00 +02:00
Jiiks a3724d739b Read files on load at least for now 2019-02-24 21:56:46 +02:00
Jiiks cf1da34e16 add editor file writing 2019-02-24 21:38:33 +02:00
Jiiks 5454950838 add writefile 2019-02-24 21:36:04 +02:00
Jiiks ed7abe3571 Don't need all these prefixes since they're automated 2019-02-24 21:29:54 +02:00
Jiiks b14bf93ef9 remove todo 2019-02-24 21:27:05 +02:00
Jiiks 3436bbe52b add editor test flag 2019-02-24 21:26:52 +02:00
Jiiks 1a26e77dd9 Move editor events to editor 2019-02-24 21:24:37 +02:00
Jiiks da1fc0a2f0 Smol things 2019-02-24 19:57:46 +02:00
Jiiks d1fd5ae881 imodule and base for new editor module in client 2019-02-24 19:46:11 +02:00
Jiiks 8c04e7d2d3 File/snippet saving signals 2019-02-24 18:43:58 +02:00
Jiiks 3fc1adc503 get files/snippets from core 2019-02-24 18:39:28 +02:00
Jiiks 691c9f378a new file/snippet 2019-02-24 18:29:00 +02:00
Jiiks a769385219 Run scripts from editor in Discord window 2019-02-24 16:04:25 +02:00
Jiiks f1f23fa220 Initial BDEdit addition. No logic yet. 2019-02-24 13:03:15 +02:00
Jiiks 99ef0d9f81 add bdedit 2019-02-24 12:28:29 +02:00
Jiiks 5597c485d1 oops 2019-02-24 12:12:13 +02:00
Jiiks 7f08ba27e6 Remove my test script 2019-02-24 12:02:30 +02:00
Jiiks 99614aecdc Fix linter warnings 2019-02-24 11:56:07 +02:00
Alexei Stukov 726db7f0c9
Merge pull request #279 from JsSucks/dnd-patch
Add dnd in non-channels and fix upload failover
2019-02-24 11:46:57 +02:00
Zack Rauen 67c39fd9d9 Add dnd in non-channels and fix upload failover 2019-02-23 09:42:53 -05:00
Jiiks 23d4eb77b2 Fix test mode 2019-02-20 20:13:27 +02:00
Jiiks 124ec078aa only warn if tag is not set 2019-02-20 16:12:56 +02:00
Jiiks e6c9bc447e allow tag to be set with args 2019-02-20 16:10:18 +02:00
Jiiks 7bc4b652fa add child process as external instead of asar 2019-02-20 16:03:03 +02:00
Alexei Stukov 6ab1f5c844
Merge pull request #278 from JsSucks/Jiiks-patch-1
Core version
2019-02-18 15:26:32 +02:00
Alexei Stukov 816ff2ed08
Core version 2019-02-18 15:23:05 +02:00
Jiiks 6ca36ebbb5 Should use core package 2019-02-18 15:21:51 +02:00
Jiiks f511e4556e keep stub 2019-02-18 15:20:44 +02:00
Alexei Stukov 36515432c7
Merge pull request #277 from JsSucks/Jiiks-patch-1
stub should be a file
2019-02-18 15:07:54 +02:00
Alexei Stukov 47782c0168
stub should be a file 2019-02-18 15:03:17 +02:00
Jiiks 3a370f2827 higher compression for core 2019-02-18 15:01:30 +02:00
Jiiks 90f5596cb2 files should be under respective dir 2019-02-18 14:57:00 +02:00
Jiiks 75439a600e stub should be a file 2019-02-18 14:54:27 +02:00
Alexei Stukov f46960fdd0
Merge pull request #276 from JsSucks/Jiiks-patch-1
Installer Test 2
2019-02-18 12:29:38 +02:00
Alexei Stukov f684780842
Installer Test 2 2019-02-18 12:26:29 +02:00
Jiiks 6c8631f1cd TODO editor packaging/renaming 2019-02-18 12:14:38 +02:00
Jiiks 0311602539 update scripts 2019-02-18 12:13:51 +02:00
Jiiks b5e8098d20 Not having asar in externals causes some weird error 2019-02-18 12:03:54 +02:00
Jiiks 111be57b59 release task 2019-02-18 11:42:34 +02:00
Jiiks 76e19c8469 wip script updates 2019-02-18 09:21:28 +02:00
Jiiks ccff016820 init comms module duh 2019-02-17 14:14:07 +02:00
Jiiks 021e282b1c add tests flag back 2019-02-17 12:26:07 +02:00
Jiiks 6699fcc1e7 add base for compatibility 2019-02-17 12:22:13 +02:00
Jiiks f3ea192974 Paths compatibility 2019-02-17 12:18:16 +02:00
Jiiks 9c1a93f4c1 Remove installer scripts, correct sparkplug external 2019-02-17 11:58:45 +02:00
Jiiks 2c23f18e89 New installer compliance 2019-02-17 11:12:12 +02:00
Jiiks c88d2cdae9 paths is an object now 2019-02-17 08:48:41 +02:00
Alexei Stukov 8235e55357
Merge pull request #275 from JsSucks/Jiiks-patch-1
Update releaseinfo.json
2019-02-16 03:43:40 +02:00
Alexei Stukov 754e6bbd72
Update releaseinfo.json 2019-02-16 03:40:17 +02:00
Alexei Stukov e27d8874ef
Merge pull request #274 from JsSucks/lodash-vuln
https://nvd.nist.gov/vuln/detail/CVE-2018-16487
2019-02-10 18:49:32 +02:00
Alexei Stukov c310cc52d5
Merge pull request #267 from BeardDesign1/master
Fixed a bunch of transitions and changed the arrow animation.
2019-02-10 18:47:59 +02:00
Jiiks 073cce825d https://nvd.nist.gov/vuln/detail/CVE-2018-16487 2019-02-10 18:45:57 +02:00
Alexei Stukov a04fb59db7
Merge pull request #273 from JsSucks/Jiiks-patch-1
Create README.MD
2019-02-06 01:53:39 +02:00
Alexei Stukov f8ed871f8e
Create README.MD 2019-02-06 01:50:53 +02:00
Alexei Stukov fae97613d1
Merge pull request #272 from JsSucks/installer-stuff
Installer stuff
2019-02-01 15:07:46 +02:00
Jiiks 27df24f369 Update stub 2019-02-01 15:00:03 +02:00
Jiiks 5bb95f59c0 update installer info 2019-02-01 14:58:50 +02:00
Alexei Stukov bcabaab14c
Merge pull request #271 from JsSucks/Jiiks-patch-1
Create stub.js
2019-02-01 09:20:45 +02:00
Alexei Stukov 033f3472cb
Create stub.js 2019-02-01 09:08:21 +02:00
Alexei Stukov 8fbd75ce94
Merge pull request #270 from JsSucks/Jiiks-patch-1
Update installer.json
2019-01-23 11:41:53 +02:00
Alexei Stukov 3a38b59a4d
Update installer.json 2019-01-23 11:38:57 +02:00
Alexei Stukov c29b659afb
Merge pull request #269 from JsSucks/installer-tests
Create installer.json
2019-01-20 21:31:30 +02:00
Alexei Stukov ece161fac0
Create installer.json 2019-01-20 21:28:12 +02:00
Lilian Tedone b57ab92269 Fixed a bunch of transitions and changed the arrow animation. 2019-01-08 08:41:24 +01:00
Alexei Stukov 5a3aa553cd
Merge pull request #266 from JsSucks/content-browser
Content browser
2018-12-06 11:26:26 +02:00
Jiiks a2f48dd007 lint 2018-12-06 11:23:31 +02:00
Jiiks 4f10f38581 Remote tests 2018-12-06 10:02:07 +02:00
Jiiks ea0dc0c6f1 Do the same for plugins 2018-12-05 10:58:43 +02:00
Jiiks 93f536bf21 Make tags clickable 2018-12-05 08:26:01 +02:00
Jiiks 497ab62ee6 Remove hover scaling 2018-12-05 08:09:15 +02:00
Jiiks 784a14f853 More neutral red 2018-12-05 08:08:52 +02:00
Jiiks 95447dd3f0 Array for sort buttons 2018-12-05 08:07:18 +02:00
Jiiks 2cff8edb59 Sorting by name is useless 2018-12-05 07:15:49 +02:00
Jiiks e4c0796e5e add rating sort 2018-12-05 00:43:40 +02:00
Jiiks e3010fcd43 Functional sorting 2018-12-05 00:40:57 +02:00
Jiiks 08b66d5e5a Functional sort buttons 2018-12-04 23:37:39 +02:00
Jiiks c618956639 flipy 2018-12-04 23:30:11 +02:00
Jiiks c6ddb75630 Remove sort by text 2018-12-04 23:25:49 +02:00
Jiiks 1fd7b97ce6 always have spinner container visible 2018-12-04 23:18:13 +02:00
Jiiks fa5be193ad add chevron to sort buttons and styling 2018-12-04 23:15:16 +02:00
Jiiks 63874c8b4e Sort styling 2018-12-04 23:02:36 +02:00
Jiiks b4dbfcd808 Pagination 2018-12-04 10:48:25 +02:00
Jiiks 7a9b72a33a Initial search 2018-12-04 09:30:49 +02:00
Jiiks 6db34b9cee Server emulator thing 2018-12-04 09:22:04 +02:00
Alexei Stukov ead51ae676
Merge pull request #265 from JsSucks/content-browser
Merge for now
2018-12-02 05:06:02 +02:00
Jiiks 9a77aae56b Lint 2018-12-02 04:13:57 +02:00
Jiiks 3182f28359 Layout and initial sort buttons 2018-12-02 02:23:03 +02:00
Jiiks a274b0c839 Styles 2018-12-02 02:20:16 +02:00
Jiiks 199b9249e4 Layout 2018-12-02 02:18:54 +02:00
Jiiks 08ba649a91 Sort box 2018-12-02 00:40:14 +02:00
Jiiks a0eb43188f add search results text 2018-12-02 00:30:20 +02:00
Jiiks b1bd5190c1 Initial loading logic for packed themes 2018-12-01 03:53:49 +02:00
Jiiks 7fb5c8a378 Theme packer 2018-11-30 22:13:36 +02:00
Jiiks 26b1fad5a4 Move packer to contentmanager 2018-11-30 22:08:05 +02:00
Jiiks f028aefab8 Source button 2018-11-30 21:14:52 +02:00
Jiiks f0d66300c4 Styling 2018-11-29 00:22:04 +02:00
Jiiks 12b291cf0b Temp online plugins, local content body scroller 2018-11-28 09:22:29 +02:00
Jiiks a8f686eb61 only scroll body 2018-11-28 08:50:14 +02:00
Jiiks 98bf4d21ac Load more on scroll 2018-11-26 17:15:26 +02:00
Jiiks d5a82ab2b3 Load more spinner 2018-11-26 17:04:20 +02:00
Jiiks d5d7709c9f Scrollend event 2018-11-26 11:32:22 +02:00
Jiiks cfc197cdc0 add spinner 2018-11-26 11:08:27 +02:00
Jiiks e8cfb840fe Styling 2018-11-26 10:56:35 +02:00
Jiiks f01f4926eb Random timestamps 2018-11-26 10:00:57 +02:00
Jiiks 83a4e0a114 Use moment 2018-11-26 09:55:59 +02:00
Jiiks 4b8a905d2a Dummy theme generation 2018-11-26 09:42:52 +02:00
Alexei Stukov 08ae8dceb5
Merge pull request #264 from JsSucks/emotemodule
Emotemodule
2018-11-26 07:31:53 +02:00
Jiiks ad28e7c638 Childpatch 2018-11-26 07:22:15 +02:00
Jiiks 9ba8047066 Fix MessageContent patch 2018-11-26 07:06:21 +02:00
Alexei Stukov 9ba9ec1556
Merge pull request #263 from JsSucks/package-updates
Package updates
2018-11-26 03:02:38 +02:00
Jiiks ce2658b6e2 electron 2018-11-26 02:56:31 +02:00
Jiiks f3cc033182 deepmerge 2018-11-26 02:54:50 +02:00
Jiiks 7b44d163ca node-sass 2018-11-26 02:53:12 +02:00
Alexei Stukov 72d24a1cb0
Merge pull request #262 from JsSucks/injection-patch
Injection patch
2018-11-26 02:50:34 +02:00
Alexei Stukov c2c520fe27
Merge pull request #260 from samuelthomas2774/electron-4.0.0-beta.7
Node Sass and keytar bindings for Electron 4.0.0 beta.7
2018-11-26 02:45:52 +02:00
Samuel Elliott ad55459649
Indentation 2018-11-25 02:19:10 +00:00
Samuel Elliott dfac3f1825
Add keytar and node-sass bindings from Jiiks 2018-11-25 02:13:14 +00:00
Samuel Elliott adecbf8945
Add Linux Node Sass binding 2018-11-25 02:10:19 +00:00
Samuel Elliott a371fabc70
Add keytar build scripts 2018-11-25 02:01:12 +00:00
Samuel Elliott c04ffdfc24
Working keytar binding for Linux 2018-11-25 01:58:35 +00:00
Samuel Elliott 572721c57f
Fix inject script for Canary on Linux 2018-11-25 01:04:07 +00:00
Samuel Elliott 4af09224d7
Update node-sass build script for Windows 2018-11-24 23:22:50 +00:00
Samuel Elliott 7f1ac9bea0
Update keytar and add bindings for Electron 4.0.0-beta.7 2018-11-24 23:20:25 +00:00
Samuel Elliott ce93b2b4c6
Updated node-sass build script for Electron 4.0.0-beta.7 2018-11-24 23:12:15 +00:00
Samuel Elliott d98ed6649e
Updated inject script (works on macOS and Linux) 2018-11-24 23:11:10 +00:00
Zack Rauen 468422084a fix injection for electron 4.0.0-beta.7 2018-11-24 17:44:28 -05:00
Alexei Stukov 395167aafa
Merge pull request #256 from JsSucks/reactcomponents
Fix for new context wrappers in Discord
2018-11-22 21:57:15 +02:00
Alexei Stukov 803cf59f11
Merge pull request #255 from JsSucks/message-content
Fix MessageContent not being found in multiple builtins
2018-11-22 21:57:00 +02:00
Alexei Stukov 133511aeec
Merge pull request #254 from JsSucks/electron-patch
fix for electron 3.x.x (now in discord canary)
2018-11-22 21:56:44 +02:00
Zack Rauen be024c1bbe quickfix array check 2018-10-10 23:58:16 -04:00
Zack Rauen 6f10c71623 add filter for MessageContent 2018-10-10 23:46:34 -04:00
Zack Rauen adb2430d71 fix for electron 3.x.x 2018-10-10 22:13:32 -04:00
Alexei Stukov 990453cdad
Merge pull request #253 from JsSucks/package-installer
Package installer
2018-08-31 08:36:45 +03:00
Jiiks 7cf69f6993 lint 2018-08-31 08:33:23 +03:00
Jiiks 5822110c87 Package updater fix 2018-08-31 05:16:42 +03:00
Jiiks 669a88c1ff Fix package installer and add remote loader 2018-08-31 05:01:05 +03:00
Jiiks 6a8c77b578 add error display 2018-08-31 00:42:05 +03:00
Jiiks a93d12df03 add support for updating unpacked to packed and packed with different package name 2018-08-30 21:24:09 +03:00
Jiiks 80096c5e71 Styling 2018-08-30 18:34:06 +03:00
Jiiks 307b084214 add success circle 2018-08-30 18:32:06 +03:00
Jiiks 1e0f3b69fb Success modal 2018-08-30 18:30:49 +03:00
Jiiks 550d0d3c40 Move uploadarea patcher to packageinstaller 2018-08-30 17:34:18 +03:00
Jiiks 96f8fe680f add verified text 2018-08-30 09:27:16 +03:00
Jiiks b48adc7a62 red outline file svg for unverified 2018-08-30 09:23:27 +03:00
Jiiks 6db016060b Small things 2018-08-30 09:08:52 +03:00
Jiiks ecb0b3e9d0 Verify step 2018-08-30 07:05:14 +03:00
Jiiks 8c060314da Functional installer/updater. Still needs lots of work. 2018-08-30 05:39:23 +03:00
Jiiks a040a605d4 Version is in info not in config 2018-08-29 23:09:30 +03:00
Jiiks 09fb76d148 Versions should be strings 2018-08-29 23:07:32 +03:00
Jiiks ddda5b3cc1 add checks and lint fixes 2018-08-29 23:02:41 +03:00
Jiiks cff425f916 Package installer & install modal 2018-08-29 06:19:18 +03:00
Jiiks 76d54f0a95 Fix plugin loading for packed plugins 2018-08-28 18:08:56 +03:00
Jiiks 2093c472b5 Move unsafe-content under security 2018-08-27 20:17:07 +03:00
Jiiks 1009c62ea0 duplicate asar external 2018-08-27 19:41:52 +03:00
Jiiks 2cd94fcd5f Remove package plugin button if plugin is already packaged 2018-08-27 19:39:29 +03:00
Jiiks cb35b14311 Remove debugs 2018-08-27 19:37:55 +03:00
Jiiks 6b3cb712c7 Block any nonpacked content by default if unsafe is not allowed 2018-08-27 19:36:06 +03:00
Jiiks cfbafffe46 add option for unsafe content 2018-08-27 19:17:33 +03:00
Jiiks 2913a85368 Packed content loading 2018-08-27 19:16:30 +03:00
Jiiks 5c160d75a6 add a packed test plugin 2018-08-27 17:32:21 +03:00
Jiiks f14b1b71e7 Use asar instead 2018-08-27 17:28:42 +03:00
Jiiks 4e3a56e466 Plugin packager 2018-08-26 22:39:04 +03:00
Alexei Stukov 30716b57e6
Merge pull request #251 from JsSucks/example-plugin
Render example
2018-08-26 17:56:22 +03:00
Jiiks b18559aa5e Rename to render example 2018-08-26 17:38:22 +03:00
Jiiks 6d8ef35bd6 add api's for easy generic components 2018-08-26 17:34:54 +03:00
Jiiks 8146e0e7f2 Make pluginmanager delete all relative modules from cache insted 2018-08-26 05:03:54 +03:00
Jiiks 23b8e2dd76 add and use the relative api 2018-08-26 03:48:23 +03:00
Jiiks e12dc28052 Vuewrap 2018-08-26 01:37:37 +03:00
Jiiks 6538442b0b Correct callback 2018-08-26 01:03:03 +03:00
Jiiks 4a9cc603d2 add vue component 2018-08-26 00:59:07 +03:00
Jiiks d4aa8fb377 Custom element example 2018-08-25 23:29:54 +03:00
Jiiks 977fd80d0c add walkers to pluginapi 2018-08-25 22:34:18 +03:00
Alexei Stukov ad64743874
Merge pull request #250 from JsSucks/builtin-refactor
E2EE Refactor
2018-08-25 19:24:30 +03:00
Jiiks 436c469b1d E2EE refactor for new builtin base 2018-08-25 19:19:19 +03:00
Alexei Stukov 7d05d0c884
Merge pull request #249 from JsSucks/builtin-refactor
Merge for now for e2ee fix.
2018-08-25 18:19:12 +03:00
Jiiks 1afbcacec3 add favourite contextmenu to emote autocomplete 2018-08-25 18:05:17 +03:00
Jiiks 9b5464b9a5 Remove unused imports 2018-08-25 17:45:52 +03:00
Jiiks da8e912a95 Emotemodule refactor to use new builtin base 2018-08-25 17:43:29 +03:00
Jiiks a4cbfd9235 Devtools 2018-08-25 17:03:46 +03:00
Jiiks 5e0c5e39f6 Unused imports 2018-08-25 17:01:37 +03:00
Jiiks e798e3d6e2 Voice Disconnect refactor to use new builtin base 2018-08-25 16:59:24 +03:00
Jiiks 937ab55456 Tracking protection refactor to use new builtin base 2018-08-25 16:58:27 +03:00
Jiiks aa0882c449 Kill clyde refactor to use new builtin base 2018-08-25 16:55:58 +03:00
Jiiks f5b70d5a23 Not much to change here 2018-08-25 16:51:45 +03:00
Jiiks 6d818f40d2 Get Message correctly 2018-08-25 16:50:48 +03:00
Jiiks 6a5f185c80 add some comments and consistent structure 2018-08-25 16:12:33 +03:00
Jiiks 62b7679408 Coloured text refactor to use new builtin base 2018-08-25 16:09:04 +03:00
Jiiks c11aa205a3 Remove unused imports 2018-08-25 16:03:13 +03:00
Jiiks 47af7a5da6 Blocked messages refactor to use new builtin base 2018-08-25 16:02:42 +03:00
Jiiks 89b645b334 24hour refactor to use new builtin base 2018-08-25 15:57:29 +03:00
Alexei Stukov 62c9fe6011
Merge pull request #247 from JsSucks/suggested-changes
Modules to Reflection
2018-08-25 15:51:11 +03:00
Alexei Stukov a45c39ed91
Merge branch 'master' into suggested-changes 2018-08-25 15:24:43 +03:00
Alexei Stukov f35d206044
Merge pull request #245 from samuelthomas2774/e2ee-popout
Add an option to move the E2EE popout over the button
2018-08-25 15:23:20 +03:00
Jiiks f46694f2d0 Use Reflection, deleted any wpm references from plugin api for now 2018-08-24 19:05:27 +03:00
Jiiks 5c493d413b Fix resolver and add helpers to it 2018-08-24 17:42:27 +03:00
Jiiks 32fc655326 camelCase 2018-08-24 15:04:10 +03:00
Jiiks 9cf63c2cfa allow non-array args 2018-08-24 15:03:41 +03:00
Jiiks b59c919733 Simplify names, have modules return byName 2018-08-24 14:17:13 +03:00
Jiiks e66af1e1d2 Export Reflection and put it in _bd 2018-08-24 13:50:59 +03:00
Jiiks f1cf802746 add ui reflection as property 2018-08-24 13:48:03 +03:00
Jiiks d1387f5183 Leave the original intact for now 2018-08-24 13:46:58 +03:00
Jiiks 9190b208eb Move classname stuff to resolver 2018-08-24 13:41:44 +03:00
Jiiks fdd934ae0c Change header 2018-08-24 13:38:21 +03:00
Jiiks b1c8070fc3 add deprecation notice 2018-08-24 13:37:55 +03:00
Jiiks 56753d3614 Move webpackmodules under reflection.modules 2018-08-24 13:35:16 +03:00
Alexei Stukov 66966e6729
Merge pull request #246 from Aphypnise/patch-1
Missing one round bracket, causes gulp build error
2018-08-24 12:59:15 +03:00
Aphypnise 0f7639da6d
Missing one round bracket, causes gulp build error 2018-08-24 03:25:04 +02:00
Samuel Elliott 2a81054f64
Add options for the popout’s position/trigger 2018-08-23 22:16:15 +01:00
Alexei Stukov 22ea3ff2ad
Merge pull request #244 from JsSucks/builtin-base
Builtin base
2018-08-23 22:57:55 +03:00
Jiiks d3e76a1c2d Typo 2018-08-23 22:51:35 +03:00
Jiiks 72326d220e Make builtin base more useful 2018-08-23 22:50:12 +03:00
Samuel Elliott 52dfaf18c1
Open the popout on hover 2018-08-23 20:44:51 +01:00
Samuel Elliott 40acb33bbd
Disable the popout when there’s no key for the channel and it’s not a DM 2018-08-23 20:44:14 +01:00
Samuel Elliott 74d5d3540f
Open the E2EE popout over the button 2018-08-23 20:38:48 +01:00
Alexei Stukov 3ba90705ff
Merge pull request #243 from samuelthomas2774/keytar
Add an option to store the master password in the system keychain
2018-08-23 22:30:57 +03:00
Samuel Elliott 6c14f80791
Fix “Unnecessary 'else' after 'return’” 2018-08-23 20:26:23 +01:00
Samuel Elliott 5fa964ed50
Install libsecret on Travis 2018-08-23 20:21:20 +01:00
Alexei Stukov da28828880
Merge pull request #242 from samuelthomas2774/plugin-api
Plugin API
2018-08-23 22:06:42 +03:00
Samuel Elliott 21c8c6d267
Update gulpfile to copy keytar bindings 2018-08-23 20:00:52 +01:00
Samuel Elliott 5868b4f0f9
Use a different hint on each platform 2018-08-23 19:50:09 +01:00
Samuel Elliott d53afe2538
Add an option to store the master password in the system keychain 2018-08-23 19:40:45 +01:00
Samuel Elliott 96b7e8d859
Add IPC events for keytar 2018-08-23 19:14:21 +01:00
Samuel Elliott 60e42cea07
Lock at v4.2.1 2018-08-23 18:28:30 +01:00
Samuel Elliott 43ec860bfe
Add keytar and bindings 2018-08-23 18:28:11 +01:00
Samuel Elliott 1f971c7490
Add a script to copy the node sass bindings on install 2018-08-23 18:20:34 +01:00
Samuel Elliott 5e7de49999
Update user profile modal badge size 2018-08-23 17:17:00 +01:00
Samuel Elliott 35c1ab93a0
Add Lucario 🌌 V5.0.0#7902 to contributors 2018-08-23 17:11:16 +01:00
Samuel Elliott a85107cc51
Fix GlobalAc.toggle and add it to the plugin API 2018-08-23 16:36:22 +01:00
Samuel Elliott 46dae11085
Use const 2018-08-23 04:02:01 +01:00
Samuel Elliott c885e4ceda
Add notification titles 2018-08-23 03:16:18 +01:00
Samuel Elliott 26bce739d7
Add bd-windowHasFrame and bd-windowIsTransparent classes 2018-08-23 02:54:01 +01:00
Samuel Elliott 3747417865
Register chrome-extension as privileged in the preload script 2018-08-23 02:39:33 +01:00
Samuel Elliott 4c0248ca00
Remove old favourite emotes storage 2018-08-22 23:45:25 +01:00
Samuel Elliott fd9c03ac2f
Update the emote module in the plugin API 2018-08-22 23:44:46 +01:00
Samuel Elliott d11e8d4fe2
Merge remote-tracking branch 'upstream/master' into plugin-api
# Conflicts:
#	client/src/index.js
#	client/src/ui/components/contextmenu/Group.vue
#	client/src/ui/contextmenus.js
2018-08-22 22:16:39 +01:00
Samuel Elliott 56a807f294
Add support for using submenus in submenus 2018-08-22 22:06:42 +01:00
Samuel Elliott cf71f89077
Use the CMGroup component to render sub menus 2018-08-22 21:35:50 +01:00
Samuel Elliott 3ff077ea04
Add BD button context menu 2018-08-22 21:04:08 +01:00
Samuel Elliott 697df8a7fe
Add v-contextmenu directive 2018-08-22 20:58:00 +01:00
Samuel Elliott c2ccfd02da
Move ReactComponent to Vue injector 2018-08-22 20:56:05 +01:00
Alexei Stukov 1e9b1618f1
Merge pull request #241 from JsSucks/emote-stuff
Support more then 10 items in autocomplete with scrolling
2018-08-22 21:49:51 +03:00
Jiiks d92da4b6e4 Support more then 10 items in autocomplete with scrolling 2018-08-22 21:45:32 +03:00
Samuel Elliott 67debca9d9
Add notifications to the plugin API 2018-08-22 19:42:22 +01:00
Samuel Elliott a0bee1846e
Add autocomplete to the plugin API 2018-08-22 19:07:06 +01:00
Samuel Elliott 73192f5762
Load external content after builtin modules 2018-08-22 18:37:39 +01:00
Alexei Stukov a1e32f8b89
Merge pull request #240 from JsSucks/emote-stuff
Emote stuff
2018-08-22 20:28:29 +03:00
Jiiks 2002db7807 bd-autocomplete -> bd-ac 2018-08-22 20:17:43 +03:00
Jiiks 07374ceeee add autocomplete hint text and float right for third title 2018-08-22 20:15:21 +03:00
Samuel Elliott d02be1521d
Fix modal closing animation and add a header slot 2018-08-22 18:02:07 +01:00
Jiiks 5c92f3e3e0 Toggle between favourite and most used 2018-08-22 20:00:00 +03:00
Jiiks 41a5a3a791 add toggle functionality to ac 2018-08-22 19:49:53 +03:00
Samuel Elliott 42a89129d4
Move unloading plugin main script to the plugin manager 2018-08-22 17:44:30 +01:00
Jiiks 2617925cd8 add actype setting 2018-08-22 19:34:26 +03:00
Jiiks 7a0b6e9262 Remove unused imports 2018-08-22 19:32:31 +03:00
Jiiks 3f5c168ffc Move emote autocomplete to it's own module 2018-08-22 19:31:09 +03:00
Jiiks 09d9f50160 z-indexes, shouldn't break any indexing? 2018-08-22 18:54:32 +03:00
Jiiks d274fda428 Save favourites to db 2018-08-22 18:49:20 +03:00
Jiiks 58a2b6714e add id to test cm 2018-08-22 18:39:47 +03:00
Jiiks 4c3659f857 add contextmenu patch removal 2018-08-22 18:39:09 +03:00
Jiiks b8dc9c0d25 No more ugly 2018-08-22 18:33:00 +03:00
Jiiks c4f28a6ac5 Initial favouriting and horrible toggle code fix it Zere 2018-08-22 17:17:37 +03:00
Jiiks 1ab52cb174 Correct targeting 2018-08-22 16:55:25 +03:00
Jiiks f315c8b134 pass target to toggle 2018-08-22 16:38:32 +03:00
Jiiks 8bfac0f6ff add favourite toggle 2018-08-22 16:31:49 +03:00
Samuel Elliott 4bed9f726c
Separate file/directory delete functions 2018-08-22 14:27:06 +01:00
Jiiks 0145492773 Wrap single emotes 2018-08-22 16:20:26 +03:00
Samuel Elliott 2188c22425
Update begin key exchange icon 2018-08-22 13:55:24 +01:00
Jiiks e9c419529d Change singular emote name to detect it in render 2018-08-22 15:50:20 +03:00
Samuel Elliott 6ee647b249
Fix SettingsWrapper padding on macOS 2018-08-22 13:32:10 +01:00
Samuel Elliott 5676435473
Fix channelcategory.channels 2018-08-22 13:23:22 +01:00
Samuel Elliott 8cf28fcaba
Add DiscordContextMenu to the plugin API 2018-08-22 13:19:15 +01:00
Samuel Elliott 237899c91a
Add BdContextMenu to the plugin API 2018-08-22 12:56:27 +01:00
Samuel Elliott 73e4e83ced
Add Maks#3712 to contributors 2018-08-22 12:54:55 +01:00
Alexei Stukov 5b63667bc2
Merge pull request #238 from JsSucks/context-menus
Context menus
2018-08-22 14:29:56 +03:00
Alexei Stukov 00ce5d70c9
Merge pull request #237 from JsSucks/windowpreferences-patch
fix window preferences
2018-08-22 14:29:47 +03:00
Alexei Stukov b3d14d853c
Merge pull request #236 from Maks-s/filetype
Add file functions for FileSetting
2018-08-22 14:29:35 +03:00
Alexei Stukov c1af0c6115
Merge pull request #234 from JsSucks/builtins
Add several builtins
2018-08-22 14:29:18 +03:00
Alexei Stukov f3bb7aac96
Merge pull request #233 from Maks-s/delete-intensifies
Added delete method to plugins / themes
2018-08-22 14:29:06 +03:00
Jiiks cd84f6329a add a test contextmenu item 2018-08-22 12:08:56 +03:00
Jiiks b4fa9c2934 add docs 2018-08-22 12:07:32 +03:00
Jiiks 9db6ddaf98 Make filter optional 2018-08-22 12:06:53 +03:00
Jiiks ed55e060aa Linter warnings 2018-08-22 10:55:18 +03:00
Jiiks bb37b89d35 Discord contextmenu patching 2018-08-22 10:53:20 +03:00
Jiiks 07fc98670c Hide cm when clicked outside 2018-08-22 02:03:18 +03:00
Jiiks 25aecf82a6 Fix chevron position 2018-08-22 01:42:14 +03:00
Jiiks 242fbfaaa7 Match notifications with context menu style 2018-08-22 01:35:46 +03:00
Jiiks ef5a1c223c Get position from passed event|object 2018-08-22 01:27:50 +03:00
Jiiks 8637f176c1 Lint 2018-08-22 00:34:13 +03:00
Jiiks 4c5300be12 Custom context menu 2018-08-22 00:23:08 +03:00
Zack Rauen 1ccd7766bc fix window preferences 2018-08-21 14:05:13 -04:00
Maks-s 9711c99217
Add filetype/read/readBuffer functions to FileSetting 2018-08-21 16:01:37 +02:00
Maks 10f45b6b4c
Fixed stupid errors 2018-08-21 10:09:38 +02:00
Maks 7c44b92bce
Changed path import 2018-08-21 09:57:09 +02:00
Zack Rauen 4784b13c86 proper color mixing, support light theme 2018-08-21 01:50:54 -04:00
Zack Rauen a8c8c017f6 add color intensity option 2018-08-21 01:19:43 -04:00
Zack Rauen c98e4430b1 forgot about midnight case in 24hr 2018-08-21 00:36:57 -04:00
Zack Rauen adf94990c2 add several builtins
- Colored Text
- 24 Hour
- Kill Clyde
- Prevent Blocked Messages
- Voice Disconnect
2018-08-20 23:42:34 -04:00
Maks-s e78021a538
Added delete method to plugins / themes 2018-08-21 02:45:54 +02:00
Alexei Stukov 3c38ecdd97
Merge pull request #232 from JsSucks/minor-things
Minor things
2018-08-20 18:58:06 +03:00
Jiiks 12021fcf15 Lint fix 2018-08-20 18:36:34 +03:00
Jiiks 1031ab3a18 zer-unit 2018-08-20 18:31:08 +03:00
Jiiks e75c575a32 add dummy notif 2018-08-20 18:30:41 +03:00
Jiiks 9c91222fff More styling 2018-08-20 18:23:18 +03:00
Jiiks d0eb2a441a Notification styling 2018-08-20 18:21:34 +03:00
Jiiks 5c6d544562 Use bd green for logger 2018-08-20 17:37:40 +03:00
Jiiks ceca53a75d Update CSS Editor Icon 2018-08-20 17:36:43 +03:00
Jiiks 6eaed063ec Move to bottom right 2018-08-20 17:21:01 +03:00
Jiiks bc5eb1b5f0 button logic 2018-08-20 16:52:26 +03:00
Jiiks 1a11eb6a6b Styling a bit, still uglyish 2018-08-20 16:48:29 +03:00
Jiiks cff67a3b1d Notification styling 2018-08-20 16:40:28 +03:00
Jiiks 3518ac7cb2 add base for notifications. These should be used for things such as "update available" only 2018-08-20 16:23:05 +03:00
Jiiks f2d7ef41da Use full white logo instead of filter 2018-08-20 15:28:31 +03:00
Jiiks 87e0f132d1 add full white small logo 2018-08-20 15:26:19 +03:00
Alexei Stukov 9306018111
Merge pull request #231 from JsSucks/editor-patch
fix module resolving for vscode
2018-08-20 06:42:11 +03:00
Zack Rauen 19d33a44ea fix module resolving for vscode 2018-08-19 22:43:40 -04:00
Alexei Stukov e755b60807
Merge pull request #230 from JsSucks/fix-zeres-crap
No typos
2018-08-20 05:13:18 +03:00
Jiiks f50bcd0785 Massive commit 2018-08-20 05:07:28 +03:00
Jiiks fc6481acae Big commit 2018-08-20 05:05:01 +03:00
Jiiks de0651bb47 Huge commit 2018-08-20 04:02:09 +03:00
Alexei Stukov 358b1e8bf3
Merge pull request #229 from JsSucks/emote-module
Emote module
2018-08-20 03:54:16 +03:00
Jiiks 84b94c614e Typos 2018-08-20 03:48:49 +03:00
Jiiks 3fa848bb5a add some comments 2018-08-20 03:26:56 +03:00
Jiiks 533566fde8 Don't store useless data in db 2018-08-20 03:16:09 +03:00
Jiiks 6ab1adfe5b Use findOne 2018-08-20 02:39:48 +03:00
Jiiks 205dcf6ff1 add findOne to db 2018-08-20 02:38:17 +03:00
Jiiks beaee8764c Persistent most used and favourites 2018-08-20 02:35:51 +03:00
Alexei Stukov d22188ea2a
Merge pull request #228 from JsSucks/emote-module
Emote module
2018-08-20 01:59:43 +03:00
Jiiks 53ef0be7b8 Minor changes 2018-08-20 01:56:57 +03:00
Jiiks e3edb09f38 Remove debugs 2018-08-19 23:16:49 +03:00
Jiiks 316f3c1bb0 Remove debug 2018-08-19 23:15:37 +03:00
Jiiks 6948335ea7 Fix lag and random no reason package change 2018-08-19 23:13:23 +03:00
Jiiks 25decd0514 Correct sort pls 2018-08-19 21:07:14 +03:00
Jiiks a35c75717e Most used emotes, only in memory right now 2018-08-19 21:00:53 +03:00
Jiiks 8d6d3c9e18 Autocomplete 2018-08-19 20:34:32 +03:00
Jiiks 91df5350ad I'll just remove them here 2018-08-17 17:06:38 +03:00
Jiiks 22740fb16c Autocomplete 2018-08-17 17:05:23 +03:00
Jiiks 0316bbc1cd add wrapper for getClassName 2018-08-17 10:40:19 +03:00
Jiiks 8bac53e9be Emote mofule refactoring, vrwrapper and hasOwnProperty treewalk check 2018-08-16 14:33:22 +03:00
Alexei Stukov 42e2569569
Merge pull request #226 from JsSucks/inject
Add inject script
2018-08-16 10:56:32 +03:00
Alexei Stukov dd81201a95
Merge pull request #227 from JsSucks/security-keyexchange
Fix key exchange
2018-08-16 10:55:55 +03:00
Jiiks 805e1ce771 Fix key exchange 2018-08-16 10:53:27 +03:00
Zack Rauen 322badd209 add inject script 2018-08-15 23:01:10 -04:00
Alexei Stukov 8438d32ce9
Merge pull request #225 from JsSucks/refactor-classnames
Refactor classnames, add sasslint, reorganize scss, change some classnames, update eslint rules etc
2018-08-15 13:00:44 +03:00
Jiiks 3446ed2d6b package-lock 2018-08-15 12:52:45 +03:00
Jiiks 6a51834f82 Cleaning and sass-lint 2018-08-15 12:42:43 +03:00
Jiiks 89461244bd Clean a little bit 2018-08-15 10:45:10 +03:00
Jiiks b1847b8c41 bit more 2018-08-15 10:14:45 +03:00
Jiiks 11eed2c89d Make it a bit more readable 2018-08-15 10:10:16 +03:00
Jiiks b971b716bf Update eslint 2018-08-15 09:10:11 +03:00
Jiiks 9f8a1f08be Update eslint rules 2018-08-15 09:01:47 +03:00
Jiiks c1ac349211 all fixed? 2018-08-15 07:13:37 +03:00
Jiiks 1804fcfbe1 Drawer open state 2018-08-15 06:48:59 +03:00
Jiiks 303addae42 Fix badges and add bd-v2 to body 2018-08-15 06:41:44 +03:00
Jiiks 9e80f7b051 Fix bugs caused by automated renaming 2018-08-15 06:33:42 +03:00
Jiiks 0e32d835ef camelCase 2018-08-15 05:03:56 +03:00
Alexei Stukov 462b198895
Merge pull request #223 from JsSucks/security
Security
2018-08-15 02:39:36 +03:00
Jiiks 34e2833555 Disable E2EE if master password is invalid 2018-08-15 02:34:35 +03:00
Alexei Stukov 8b180ea423
Merge pull request #222 from JsSucks/security-treewalk
add findInTree and adjust the render patch
2018-08-14 22:14:38 +03:00
Jiiks a90400c280 Check if we're not in a channel 2018-08-14 15:43:55 +03:00
Jiiks e49a1932f3 Remove debug 2018-08-14 15:37:14 +03:00
Jiiks abf83821d9 Typo exchange 2018-08-14 15:35:50 +03:00
Jiiks 6ed50c4206 TODO 2018-08-14 15:32:49 +03:00
Jiiks edab8dd93b Use MESSAGE_CREATE instead 2018-08-14 15:30:43 +03:00
Jiiks ab03633431 add password option to input modal 2018-08-14 14:49:50 +03:00
Jiiks 67b554c68f add input modal
TODO Option for masked input
2018-08-14 13:50:32 +03:00
Jiiks 68f3a280a5 TODO 2018-08-14 13:29:19 +03:00
Jiiks 5db4f8cf26 Remove debug 2018-08-14 13:27:45 +03:00
Jiiks 3e57d0092a Display user tag in exchange request modal 2018-08-14 13:27:20 +03:00
Jiiks 36408d74ad Change button tooltip 2018-08-14 13:03:47 +03:00
Jiiks 9aa9305755 Check for message state 2018-08-14 13:02:25 +03:00
Jiiks 6f9cdb9dda That obviously wouldn't work 2018-08-14 12:59:37 +03:00
Jiiks 0aa370aeb9 add custom text for confirm modal buttons 2018-08-14 12:57:39 +03:00
Jiiks 27fee8e10c Typo and set preExchangeState 2018-08-14 12:51:57 +03:00
Jiiks ac1e4ad9a2 Move storage outside 2018-08-14 11:16:39 +03:00
Jiiks 73f2fe4fd0 Reset encryptNewMessages to previous state 2018-08-14 11:08:41 +03:00
Jiiks 8b4318c06e Remove logging 2018-08-14 11:06:53 +03:00
Jiiks 7636ea480d Disable encryption for new exchanges 2018-08-14 11:06:02 +03:00
Jiiks 54e5f2149b Suite B 2018-08-14 11:02:29 +03:00
Jiiks b1f0a4247a Make sure we don't send public keys encrypted 2018-08-14 10:44:57 +03:00
Jiiks 8d6cdafc23 Push kvp to db 2018-08-14 10:41:22 +03:00
Jiiks 5711576a0d Semi automated key exchange process 2018-08-14 10:40:33 +03:00
Jiiks 6df9b1e019 add DraftActions 2018-08-14 06:20:02 +03:00
Jiiks c2e7abcfca nodecrypt 2018-08-14 04:21:48 +03:00
Jiiks b6c58f16e7 DiscordApi in data and remove contextmenu thing 2018-08-14 04:19:03 +03:00
Jiiks b47a3c65aa node-crypto 2018-08-14 04:04:57 +03:00
Alexei Stukov 0315d610e0
Merge pull request #216 from Mega-Mewthree/security
E2EE Secure ECDH key exchange for DMs
2018-08-14 04:03:11 +03:00
Zack Rauen 3714e71297 add findInTree 2018-08-13 16:38:13 -04:00
Mega-Mewthree 9f4b0aec6b
Forgot to save stuff. (#8) 2018-08-13 13:23:14 -07:00
Mega-Mewthree 0ea5bc2dcc
Merge pull request #7 from Mega-Mewthree/e2ee-dev
E2ee dev
2018-08-13 13:19:27 -07:00
Mega-Mewthree 64608c7e62 Switched context menu stuff to popover, fixed things. 2018-08-13 13:17:29 -07:00
Mega-Mewthree 2d8c0a5306
Merge pull request #6 from Mega-Mewthree/security
Security
2018-08-13 10:41:20 -07:00
Mega-Mewthree 81c451d31f
Merge branch 'security' into security 2018-08-13 10:35:16 -07:00
Jiiks d04aa313fc CSP Fix. fixes #215 2018-08-13 15:13:59 +03:00
Jiiks 45ab822fba add collection.scss that got lost somewhere somehow 2018-08-13 14:45:38 +03:00
Alexei Stukov c893216874
Merge branch 'master' into security 2018-08-13 14:38:25 +03:00
Alexei Stukov a1a869181b
Merge pull request #214 from samuelthomas2774/settings
Add an option to disable toasts and additional options for collections
2018-08-13 14:37:03 +03:00
Alexei Stukov f5cbd8d491
Merge pull request #220 from JsSucks/security-encrypted-images
Security encrypted images
2018-08-13 14:34:59 +03:00
Alexei Stukov 17917f3257
Merge branch 'security' into security-encrypted-images 2018-08-13 14:27:29 +03:00
Jiiks 19be33b17d Don't use encrypted key for hmac 2018-08-13 12:56:41 +03:00
Jiiks cfcca0d782 Remove debug 2018-08-13 12:35:19 +03:00
Jiiks 5592a8d376 Use security module 2018-08-13 12:33:44 +03:00
Jiiks 0c0ebb2ebb Pseudo needs content 2018-08-13 10:57:32 +03:00
Jiiks d6d78d1e99 Move master init to enabled 2018-08-13 10:45:52 +03:00
Jiiks b6da7019d0 Unused import 2018-08-12 22:12:26 +03:00
Jiiks a123e20b05 Move to Security 2018-08-12 22:11:41 +03:00
Jiiks 856334cf30 TODO 2018-08-12 22:09:34 +03:00
Jiiks 3f5b2afbd5 decrypting should be set to false as well 2018-08-12 22:05:44 +03:00
Jiiks ad35c89d7d Force update on failed auth instead 2018-08-12 21:59:54 +03:00
Jiiks 71a0ce8fb0 Set readyState on failed authentication 2018-08-12 21:58:26 +03:00
Jiiks d53a3b4d64 base classname 2018-08-12 21:56:57 +03:00
Jiiks bd0c66466f Use randomBytes for master encryption 2018-08-12 21:54:57 +03:00
Jiiks 0a751919aa Remove debug 2018-08-12 21:48:15 +03:00
Jiiks cae68947df add bad key handler 2018-08-12 21:46:59 +03:00
Jiiks 2b833b5141 add hmac 2018-08-12 21:43:59 +03:00
Jiiks b24c0ba2f3 Strip base64 prefix to not have static data in encrypted content 2018-08-12 20:57:02 +03:00
Jiiks 3b8126781d Default to channel id 2018-08-12 20:31:30 +03:00
Jiiks a1a63f2c35 Proper doc 2018-08-12 18:37:11 +03:00
Jiiks 334c9f852a Use cache module 2018-08-12 18:10:22 +03:00
Jiiks 17128a889b add cache module 2018-08-12 17:40:26 +03:00
Jiiks d2b99f200b add image plus material icon 2018-08-12 15:58:00 +03:00
Jiiks 5f4a77f2d8 Remove my leak tests 2018-08-12 15:48:26 +03:00
Jiiks 3614997fa9 No need to use this when it's already in the scope 2018-08-12 15:47:20 +03:00
Jiiks 5f76d304f6 Use predefined colours 2018-08-12 15:46:17 +03:00
Jiiks e8688c0ce4 User some colour variables 2018-08-12 15:41:25 +03:00
Jiiks 641a12d640 Move lock icon to images 2018-08-12 15:40:00 +03:00
Jiiks 169741d5a1 Image encryption, decryption, styling and other stuff 2018-08-12 15:39:00 +03:00
Alexei Stukov 4aec2a7ade
Merge pull request #218 from JsSucks/security-patch
fix all mentions in e2ee
2018-08-12 11:01:45 +03:00
Mega-Mewthree ee9cba328f
Merge pull request #5 from Mega-Mewthree/e2ee-dev
E2ee dev
2018-08-11 18:29:38 -07:00
Mega-Mewthree c421be9e91 Fix merge 2018-08-11 18:28:18 -07:00
Mega-Mewthree 7a1768ca87 Merge branch 'e2ee-dev' of https://github.com/Mega-Mewthree/BetterDiscordApp into e2ee-dev 2018-08-11 18:26:37 -07:00
Mega-Mewthree b9e9ab89f7 Rename variables to better describe what they represent. 2018-08-11 18:24:55 -07:00
Mega-Mewthree b9f3c3f4e2
Merge pull request #4 from Mega-Mewthree/e2ee-dev
E2ee dev
2018-08-11 17:24:46 -07:00
Mega-Mewthree dc842b479b
Merge branch 'security' into e2ee-dev 2018-08-11 17:24:22 -07:00
Mega-Mewthree 02e6fb88aa Prevent the context menu from appearing in Group DMs. 2018-08-11 16:37:58 -07:00
Mega-Mewthree c10310af3a Error toast on computeSecret error. 2018-08-11 16:28:34 -07:00
Mega-Mewthree 7e41fe73e7 Secure key exchange for DMs. 2018-08-11 16:21:08 -07:00
Mega-Mewthree c122457565 Style the lock context menu. 2018-08-11 10:11:56 -07:00
Mega-Mewthree 0ea0acfaa5
Merge pull request #3 from JsSucks/security-patch
fix all mentions
2018-08-11 07:55:19 -07:00
Jiiks 866ad8b13b Tons of stuff 2018-08-11 15:29:30 +03:00
Mega-Mewthree c5d4e199fc Very poorly implemented key exchange UI.
PLEASE HELP ME
2018-08-11 00:34:18 -07:00
Mega-Mewthree 1a8946b151 Merge branch 'e2ee-dev' of https://github.com/Mega-Mewthree/BetterDiscordApp into e2ee-dev 2018-08-10 23:06:28 -07:00
Zack Rauen 9c0e19e198 fix all mentions 2018-08-11 01:15:49 -04:00
Alexei Stukov 816f809ca7
Merge pull request #217 from samuelthomas2774/security
Add clicking the status icon to toggle encryption and add an indicator to encrypted messages
2018-08-11 06:00:56 +03:00
Samuel Elliott 32a995f678
Add an encrypted indicator to encrypted messages 2018-08-11 02:58:55 +01:00
Samuel Elliott dac3187866
Add clicking the status icon to toggle encryption 2018-08-11 02:43:02 +01:00
Jiiks 60cdec3ae4 and move everything around just to annoy Zere 2018-08-11 04:33:00 +03:00
Jiiks ed4adf21f4 Maybe encrypt instead 2018-08-11 04:31:57 +03:00
Jiiks a8f0e239cf Who wants to call them directly anyways 2018-08-11 04:31:13 +03:00
Jiiks be1a183e62 Empty prefixes 2018-08-11 04:28:07 +03:00
Jiiks d88cedb614 Security module 2018-08-11 04:27:18 +03:00
Jiiks 9880152290 Message parsing 2018-08-11 03:39:13 +03:00
Jiiks 0a1bd46ca9 Don't prefix everything by default 2018-08-10 21:17:52 +03:00
Jiiks 83ef17d302 Fix button patching 2018-08-10 20:38:02 +03:00
Mega-Mewthree cb6473920f
Update webpack.production.config.js 2018-08-10 10:30:42 -07:00
Mega-Mewthree d8c769433a
Update webpack.config.js 2018-08-10 10:30:16 -07:00
Mega-Mewthree fd7a0f4051
Functions to create an ECDH key exchange
Allows 2 users to securely obtain a shared key for usage with AES.
2018-08-10 09:56:55 -07:00
Mega-Mewthree 7899312e73
Merge pull request #1 from JsSucks/security
Security
2018-08-10 09:39:53 -07:00
Samuel Elliott 784d8223e8
Add a separate build-release task
So you can update the release version without deleting all user data
2018-08-10 15:48:43 +01:00
Samuel Elliott 344a9e6fe5
Fix escape key to close menu 2018-08-10 15:21:33 +01:00
Samuel Elliott 22e78c03e1
Add toasts enabled state to the plugin API 2018-08-10 15:17:57 +01:00
Jiiks b67ca5c42d Set master 2018-08-10 17:04:09 +03:00
Jiiks bdec8b842e Set master key 2018-08-10 17:02:41 +03:00
Jiiks 3bc7c28a97 initial master encrypt 2018-08-10 17:01:07 +03:00
Samuel Elliott a4992e905c
Fix class normaliser also normalising classes of elements outside the passed element 2018-08-10 14:29:31 +01:00
Samuel Elliott c8381eb808
Add disabled, min and max options for collections 2018-08-10 14:23:21 +01:00
Samuel Elliott 0aabc72652
Indentation 2018-08-10 14:22:37 +01:00
Jiiks 3d4204f080 Functional E2EE submitting 2018-08-10 16:22:30 +03:00
Samuel Elliott dc3fed3408
Hide collection setting dividers 2018-08-10 14:11:25 +01:00
Samuel Elliott a12a3c74f4
Hide the button properly 2018-08-10 13:57:27 +01:00
Jiiks 3c83dbe0ff add slight loading animation 2018-08-10 15:44:33 +03:00
Samuel Elliott 4970214324
Add an option to disable toasts 2018-08-10 13:37:47 +01:00
Jiiks a5ae5ad8e9 Warn style 2018-08-10 15:37:14 +03:00
Jiiks 54808f32ea Default to error state 2018-08-10 15:33:48 +03:00
Jiiks 4e4c532813 More styling 2018-08-10 15:33:34 +03:00
Jiiks aac4dba641 Styling 2018-08-10 15:32:04 +03:00
Jiiks 17f1b60a9b E2EE component and material lock icon 2018-08-10 15:30:24 +03:00
Jiiks 2a10191926 E2EE patches 2018-08-10 15:08:21 +03:00
Alexei Stukov 87ec7d8773
Merge pull request #213 from JsSucks/ui
Securekvp
2018-08-10 14:21:00 +03:00
Jiiks e518d4c117 package-lock 2018-08-10 14:18:08 +03:00
Jiiks c4dfc7b11d Remove debugs 2018-08-10 12:50:37 +03:00
Jiiks 0c04c04590 Secure kvp 2018-08-10 12:48:56 +03:00
Alexei Stukov 11b2b16233
Merge pull request #212 from samuelthomas2774/hide-button
Make the hide button setting functional
2018-08-10 02:35:07 +03:00
Alexei Stukov 4c6805510d
Merge pull request #211 from samuelthomas2774/vue-transition
Use Vue’s transition element to transition between menu panels
2018-08-10 02:34:25 +03:00
Samuel Elliott cb01d32e19
Fix position on macOS 2018-08-10 00:00:01 +01:00
Samuel Elliott 89a5bfba20
Make the hide button setting functional 2018-08-09 23:49:10 +01:00
Alexei Stukov 87a2db1745
Merge pull request #210 from JsSucks/drop-area
Add temporary .bd file check on drop
2018-08-10 00:59:14 +03:00
Samuel Elliott 387227cc1f
Use Vue’s transition element to transition between menu panels 2018-08-09 22:50:14 +01:00
Zack Rauen fdb3b308e1 add bd file check for upload drop area 2018-08-09 16:33:31 -04:00
Alexei Stukov b33aff51c1
Merge pull request #207 from JsSucks/normalizer
fix class search in normalizer
2018-08-09 23:29:28 +03:00
Alexei Stukov be09fbb5ac
Merge pull request #209 from JsSucks/settings
Collection setting and other minor things
2018-08-09 23:26:16 +03:00
Jiiks 2b4a804934 Use material icon 2018-08-09 23:21:57 +03:00
Jiiks fcfba41805 Styling and remove default from E2EE 2018-08-09 23:13:38 +03:00
Jiiks d14a06528c else 2018-08-09 23:07:53 +03:00
Alexei Stukov c336c8cb0d
Merge pull request #208 from samuelthomas2774/collection-settings
Extend ArraySetting for collections
2018-08-09 20:33:38 +03:00
Samuel Elliott 755d870a10
Merge remote-tracking branch 'upstream/settings' into collection-settings
# Conflicts:
#	client/src/structs/settings/types/collection.js
#	client/src/ui/components/bd/setting/Collection.vue
2018-08-09 18:27:06 +01:00
Jiiks 4ed301f531 Styling 2018-08-09 20:26:37 +03:00
Jiiks 263fcbe001 Collection fix 2018-08-09 20:22:48 +03:00
Samuel Elliott 883f760292
Extend ArraySetting for collections 2018-08-09 18:22:15 +01:00
Zack Rauen 5af8a2168d fix class search 2018-08-09 11:31:11 -04:00
Jiiks 50badaef2b add collection and make kvp functional 2018-08-09 17:20:06 +03:00
Alexei Stukov 4c20115705
Merge pull request #206 from JsSucks/security
Merge due to changes to core functionality to avoid conflicts
2018-08-09 13:24:00 +03:00
Jiiks e21583eafd add a manager for builtin modules 2018-08-09 13:21:17 +03:00
Jiiks 983763c48d Check for module existence 2018-08-09 13:09:35 +03:00
Jiiks 75ceee5449 Typo 2018-08-09 13:06:20 +03:00
Jiiks 572c57d3a7 add tracking protection 2018-08-09 13:04:19 +03:00
Jiiks 4f98eaf85b Rename to Security and Privacy 2018-08-09 11:40:48 +03:00
Jiiks 44a1c87484 Don't think we need test settings anymore 2018-08-09 11:37:13 +03:00
Jiiks b18dd7c01a KVP Component 2018-08-09 11:30:48 +03:00
Jiiks 356e8f0934 E2EE module base 2018-08-09 10:56:44 +03:00
Alexei Stukov 1288e0361a
Merge pull request #205 from JsSucks/patcher
Update Patcher
2018-08-09 09:07:39 +03:00
Alexei Stukov 8f95ba290a
Merge pull request #204 from JsSucks/normalizer
Fix class normalizer
2018-08-09 09:07:06 +03:00
Zack Rauen 5a603a084f normalize DOM on load 2018-08-09 01:18:49 -04:00
Zack Rauen f144934cd7 use const instead of let 2018-08-09 01:01:32 -04:00
Zack Rauen 9473e419b4 add documentation to patcher 2018-08-09 00:54:35 -04:00
Zack Rauen a081a8fdf9 make patcher more robust + fix collisions 2018-08-09 00:22:10 -04:00
Zack Rauen e660ce6a31 remove redundant array check, fix strings 2018-08-08 23:39:57 -04:00
Zack Rauen 5c7b10299b update normalizer 2018-08-08 23:29:54 -04:00
Jiiks 42957dbb16 add devtool hashes and move them to the top. Pushing to master since it's minor change. 2018-08-07 20:17:34 +03:00
Alexei Stukov c3df892f1c
Merge pull request #203 from JsSucks/devtools
React and Vue devtool modules
2018-08-07 16:12:04 +03:00
Jiiks 26f386ad31 Setting hints for devtools 2018-08-07 16:07:06 +03:00
Jiiks b861280548 Use path.join 2018-08-07 16:02:10 +03:00
Jiiks 9d22293927 Remove listener don't add another one 2018-08-07 13:37:23 +03:00
Jiiks c3d8f4090e try catch missing extension 2018-08-07 13:07:38 +03:00
Jiiks 3f00dba937 Devtool modules 2018-08-07 12:48:50 +03:00
Alexei Stukov 9f00aed4fa
Merge pull request #202 from JsSucks/bdapi
Online theme browser
2018-08-07 07:21:24 +03:00
Jiiks 685d0f4bac Dummy search 2018-08-07 07:17:44 +03:00
Alexei Stukov a805c8f75a
Merge pull request #201 from samuelthomas2774/channel-components
Channel components
2018-08-07 06:16:35 +03:00
Alexei Stukov ce23a39f83
Merge pull request #200 from samuelthomas2774/scheme-icons
Settings scheme icons
2018-08-07 06:15:21 +03:00
Samuel Elliott c0a971a27b
Fix element is not defined 2018-08-07 00:08:13 +01:00
Jiiks 7f567f6e9d Correct padding 2018-08-06 21:45:41 +03:00
Jiiks 865fcee12d Remote card styling 2018-08-06 21:42:09 +03:00
Jiiks 6b3fc39a1d Use dummy themes for now 2018-08-06 21:37:15 +03:00
Jiiks 7b1f36cb9c account-cicle material icon 2018-08-06 21:22:11 +03:00
Jiiks f6f308baa2 Remotecard component for online content 2018-08-06 20:52:10 +03:00
Jiiks e170ed608e online themes loading 2018-08-06 18:14:25 +03:00
Jiiks a432d78953 Structure 2018-08-06 17:05:36 +03:00
Jiiks b369dc31f2 Merge branch 'master' of https://github.com/JsSucks/BetterDiscordApp 2018-08-06 16:11:28 +03:00
Jiiks 778eb0474f Remove not so nice looking border 2018-08-06 16:04:50 +03:00
Jiiks 8c1f196675 New logos 2018-08-06 16:04:49 +03:00
Jiiks e25ac66abf Use bd green, override bdblue with it for now 2018-08-06 16:04:49 +03:00
Alexei Stukov a2a334bdac
Merge pull request #199 from JsSucks/branding
Branding
2018-08-06 15:30:57 +03:00
Jiiks 8331b30d73 Remove not so nice looking border 2018-08-06 15:25:53 +03:00
Jiiks 7083577db2 New logos 2018-08-06 15:23:29 +03:00
Jiiks 1b91c97c24 Use bd green, override bdblue with it for now 2018-08-06 15:17:57 +03:00
Alexei Stukov cca6cdf95d
Merge pull request #198 from JsSucks/bdapi
Connectivity and web api bases
2018-08-06 15:14:21 +03:00
Jiiks 7c02a51a46 Base connectivity stuff 2018-08-06 13:31:49 +03:00
Samuel Elliott 0e34fdaa8d
Fix scheme matching 2018-08-06 10:28:35 +01:00
Samuel Elliott e2f05d9a64
Patch all channel components 2018-08-06 04:48:10 +01:00
Samuel Elliott 13437c56d4
Search all matching elements instead of just the first and don’t fail if a filter function is passed 2018-08-06 04:30:48 +01:00
Samuel Elliott 4b4b3c341f
Update content/scheme icons properly 2018-08-06 02:55:08 +01:00
Samuel Elliott c2f8f5cfab
Use a file for all plugin/theme/scheme icons 2018-08-06 02:48:16 +01:00
Samuel Elliott d4c883a5d7
Add using a file as a scheme icon 2018-08-06 02:45:59 +01:00
Zack 520366c4ac
Merge pull request #191 from samuelthomas2774/discord-api
DiscordAPI, Toasts, Normalizer, Bugfixes, etc.
2018-08-05 15:08:53 -04:00
Samuel Elliott 342b4cddc4
Fix unread mentions indicator position 2018-08-04 00:54:44 +01:00
Samuel Elliott 6ea85341b9
Fix keybind settings not activating 2018-08-03 12:57:30 +01:00
Samuel Elliott 9af8a71e17
Add using a file as a plugin/theme’s icon 2018-08-02 20:24:56 +01:00
Samuel Elliott 33488d716d
Update dependencies 2018-08-01 21:41:45 +01:00
Samuel Elliott a5ff8bbdb6
Fix removing keybind-activated listeners 2018-08-01 21:17:36 +01:00
Samuel Elliott 16bb32b86c
Remove IterableWeakCollections and deactivate keybind settings when possible 2018-08-01 21:08:45 +01:00
Samuel Elliott 8826a7b984
Use an observer to detect new React components 2018-08-01 20:48:09 +01:00
Samuel Elliott 5b419cb8ab
Use ReactComponent.forceUpdateAll to rerender components after patching them 2018-08-01 20:41:53 +01:00
Samuel Elliott db21f4eb13
Fix Guild component selection 2018-08-01 20:40:03 +01:00
Samuel Elliott 3a7ac06ce3
Fix errors when rendering system messages 2018-08-01 20:28:35 +01:00
Samuel Elliott 0c8279311e
Fix Message component selection and add bd-isGuildOwner and bd-isGuildMember classes 2018-08-01 20:18:47 +01:00
Samuel Elliott 331b2b396a
Fix emotes 2018-08-01 19:58:52 +01:00
Samuel Elliott a20473d718
Add a function to force update all instances of a component 2018-08-01 19:31:48 +01:00
Samuel Elliott 1a12430322
Add getting all state nodes to Reflection 2018-08-01 15:50:10 +01:00
Samuel Elliott 7a416c0ff7
Fix MessageGroup component selection 2018-08-01 13:53:59 +01:00
Samuel Elliott 81e392c0f6
Fix request module not found 2018-07-21 23:10:33 +01:00
Samuel Elliott 71c2d3aaad
Fix error with process.binding 2018-07-21 21:03:47 +01:00
Samuel Elliott 19bbd4215c
Fix syntax error at `%` 2018-07-20 21:10:44 +01:00
Samuel Elliott 5301498978 Merge branch 'toasts' into discord-api 2018-07-20 12:04:51 +01:00
Samuel Elliott 1d887967d9
Add an option to add additional classes to toasts 2018-07-20 12:04:16 +01:00
Samuel Elliott a95b97b3c0
Requested changes from JsSucks/BetterDiscordApp#194 2018-07-19 22:21:20 +01:00
Samuel Elliott 007637a557
Merge remote-tracking branch 'upstream/toasts' into toasts
# Conflicts:
#	client/src/modules/pluginapi.js
#	client/src/ui/bdui.js
#	client/src/ui/dom.js
#	client/src/ui/ui.js
#	package-lock.json
#	package.json
2018-07-19 22:03:29 +01:00
Samuel Elliott 6ded1c62e5
Prevent modals from being added to the stack more than once 2018-07-19 00:14:03 +01:00
Samuel Elliott 27f1eec967
Use a counter instead of the current time for modal IDs to avoid duplicates when multiple modals are opened at the same time 2018-07-18 22:17:50 +01:00
Samuel Elliott 821f46481e
Add all Map and Set methods to IterableWeakMap and IterableWeakSet 2018-07-18 21:53:07 +01:00
Samuel Elliott 6b97505f66
Remove unused imports 2018-07-18 20:57:05 +01:00
Samuel Elliott df471161a7
Fix leaks in keybind settings 2018-07-18 20:55:57 +01:00
Samuel Elliott da627f71e5
Add functions to get the values of weak collections 2018-07-18 20:54:58 +01:00
Samuel Elliott 78079588ea Fix Message component selection 2018-07-18 19:18:06 +01:00
Samuel Elliott b4ebd6c08a Always activate keybind settings 2018-07-18 19:18:06 +01:00
Samuel Elliott e6f2f518e6 Properly unbind events for emote autocomplete and dropdowns 2018-07-18 19:18:06 +01:00
Samuel Elliott aff89371e6
Fix all modals closing on escape key press 2018-07-08 13:52:29 +01:00
Samuel Elliott 98a7a71b87
Fix updater incorrectly showing an update is available 2018-07-07 22:05:36 +01:00
Samuel Elliott 98a0717bba
Use webpack named modules and eval source map plugins in development and disable Babel’s debug info 2018-07-07 20:00:15 +01:00
Samuel Elliott 7c8c8a754f
Use request instead of axios 2018-07-07 19:43:17 +01:00
Samuel Elliott ab13e716ad
Fix custom CSS editor 2018-07-07 14:37:23 +01:00
Samuel Elliott f2c31d866d
Add axios export 2018-07-07 14:29:44 +01:00
Samuel Elliott e169206821
Add hosts that serve emotes to the content security policy 2018-07-07 14:26:48 +01:00
Samuel Elliott cd77d659a5
Use axios and http/https to check for updates 2018-07-07 14:23:43 +01:00
Samuel Elliott b6f38a73dc
Add function to get classes and fix emote autocomplete injection 2018-07-07 01:17:26 +01:00
Samuel Elliott c384920275
Add WebpackModules wait functions to the plugin API 2018-07-07 01:10:39 +01:00
Samuel Elliott 097fc9b3e9
Update dependencies 2018-07-05 01:49:40 +01:00
Samuel Elliott b8fbfdee55
Fix Travis 2018-07-05 01:43:05 +01:00
Samuel Elliott 8c0b73625d
Add a settings proxy so settings values can be updated/retrieved like a plain object 2018-07-05 01:39:30 +01:00
Samuel Elliott 55cd75b5f4
Add settings section to edit window options 2018-07-05 01:26:04 +01:00
Samuel Elliott f8f519e882
Add custom window preferences 2018-07-04 19:40:25 +01:00
Samuel Elliott 799dca7e1b
Update to Gulp v4 2018-07-04 18:48:45 +01:00
Samuel Elliott eae1bdf3a6
Fix Utils is not defined 2018-06-26 23:41:39 +01:00
Samuel Elliott ea27259a67
Add common to the ESLint path and fix stats is undefined 2018-06-26 23:39:46 +01:00
Samuel Elliott ebe195f078
Delete old logs on startup 2018-06-26 23:25:43 +01:00
Samuel Elliott e2b68788bd
Add functions to wait for modules to be loaded 2018-06-24 19:56:05 +01:00
Samuel Elliott c8ca4fcfce
Fix friend source flags 2018-06-24 19:50:21 +01:00
Samuel Elliott 2c38433a2b
Add extra classes to messages, channel members and channels 2018-06-24 19:48:45 +01:00
Samuel Elliott ee0b13dab2
Emit events for when webpack modules are loaded 2018-06-23 23:15:45 +01:00
Samuel Elliott 4d2347cf84
Make sure file settings are updated properly when removing items 2018-06-23 22:59:13 +01:00
Samuel Elliott 7cf40a9ee4
Add SettingsWrapper to CommonComponents 2018-06-22 23:57:07 +01:00
Samuel Elliott 70f678119a
Add support for ES6 modules in plugins 2018-06-22 23:55:53 +01:00
Samuel Elliott 0a2b967e36
Fix removing items from file settings 2018-06-22 23:16:42 +01:00
Samuel Elliott a2367299b7
Don’t load emotes unless they’re enabled 2018-06-12 20:56:40 +01:00
Samuel Elliott bbcc36647d
Add class normaliser
Normalises and prefixes all classes with `da-`
2018-06-12 20:56:07 +01:00
Samuel Elliott 906140686f
Fix Utils is not defined 2018-06-10 22:51:52 +01:00
Samuel Elliott cecaf314f9
Fix channel.setNsfw 2018-06-10 22:41:14 +01:00
Samuel Elliott 4a3bb89259
Restore common in main and add note about __non_webpack_require__ 2018-06-10 22:40:46 +01:00
Samuel Elliott d7db836e30
Update DOMObserver
- Changed observer.subscribe function signature - `observer.subscribe(id, filter, callback, type)` is now `id = observer.subscribe(callback, filter, bind, type === 'filter’)`
- Add a function to subscribe to mutations affecting elements matching a query selector
- Some internal changes
2018-06-10 22:35:44 +01:00
Samuel Elliott 1eca4bcec7
Add ReactHelpers and ReactAutoPatcher to global._bd 2018-06-10 22:21:09 +01:00
Samuel Elliott c02052a982
Remove emote.name 2018-06-10 22:20:17 +01:00
Samuel Elliott be841f0426
Add the Manip class as an export of ui/dom.js 2018-06-10 22:17:37 +01:00
Zack Rauen 6c4743d7ed update node-sass 2018-06-10 16:42:56 -04:00
Zack Rauen 726d6cf4b0 gulp v4 instead of node 8 2018-06-10 16:28:55 -04:00
Samuel Elliott 35cedbdbb3
Use a single badge component 2018-06-10 21:28:23 +01:00
Samuel Elliott 17597e2c30
Add Vue wrapper for React 2018-06-10 21:22:01 +01:00
Zack Rauen 1bb705014f friggin travis 2018-06-10 16:00:52 -04:00
Samuel Elliott 204dd76e7b
Use the correct name for the user popout component 2018-05-29 21:59:54 +01:00
Samuel Elliott 3e41a11197
Allow forcefully unloading plugins without reloading Discord 2018-05-29 21:48:58 +01:00
Samuel Elliott 68d4617e46
Use the same format as React.createElement for VueInjector.createReactElement
Allows props to flow down to Vue components cleanly
2018-05-29 21:47:44 +01:00
Samuel Elliott 4654025423
Properly destroy Vue components after they’re unmounted by React 2018-05-29 18:46:08 +01:00
Samuel Elliott 231afa8ed6
Add the option to mount Vue components at the top of the React component 2018-05-29 18:35:40 +01:00
Samuel Elliott c3ff71ff85
Hide the ; before emotes in autocomplete and add the case insensitive flag 2018-05-29 17:00:05 +01:00
Samuel Elliott 0e99b219c1
Move emote autocomplete injector to the emote module 2018-05-29 16:58:57 +01:00
Samuel Elliott 13508b449d
Add some libraries to Vendor 2018-05-29 16:49:37 +01:00
Samuel Elliott e57e1f51ea
Fix getModuleByDisplayName 2018-05-29 15:40:14 +01:00
Samuel Elliott c345ab1419
Indentation 2018-05-29 15:39:32 +01:00
Samuel Elliott bee869340d
Disable errors for __[non_]webpack_require__ 2018-05-28 04:17:38 +01:00
Samuel Elliott 1de4680ded
Use ES6 imports/exports 2018-05-28 01:52:12 +01:00
Samuel Elliott bba1165c77
Wrap Discord’s preload script instead of enabling Node integration 2018-05-28 01:43:25 +01:00
Samuel Elliott a5c7aaab3b
Allow patching arrays 2018-05-21 20:49:21 +01:00
Samuel Elliott 93cd1fd07d
Log proxy targets and handlers instead of the proxy object itself 2018-05-21 19:08:57 +01:00
Samuel Elliott cfaf581ae1
Don’t use Moment.js
Plugins can still use Discord’s Moment.js with WebpackModules
2018-05-21 16:41:41 +01:00
Samuel Elliott 665c1d5fe4
Use local Moment.js instead of Discord’s
Fixes a recursive dependency issue
2018-05-20 00:46:15 +01:00
Samuel Elliott 8d8c1a8080
Add getting webpack modules as properties of the WebpackModules object
`WebpackModules.React` instead of `WebpackModules.getModuleByName('React')`
2018-05-20 00:36:14 +01:00
Samuel Elliott 8827bdb8b0
Fix messages with emotes not being updated when emotes are disabled 2018-05-20 00:27:02 +01:00
Samuel Elliott cd81f925cb
Fix for webpack 4 2018-05-20 00:24:21 +01:00
Samuel Elliott d7f4c651b6
Fix errors 2018-05-15 20:07:34 +01:00
Samuel Elliott f2159f1031
Fix error when unmounting a component 2018-05-15 16:54:22 +01:00
Samuel Elliott 581c94f6b3
Add React wrapper for Vue 2018-05-14 17:56:23 +01:00
Samuel Elliott 9eb8eaa906
Add DOM, DOMObserver and VueInjector to the plugin API 2018-05-14 16:34:40 +01:00
Samuel Elliott 9915ef8b19 Fix for Linux 2018-05-13 16:16:51 +01:00
Zack Rauen 2d784b48f4 add toasts 2018-05-12 23:17:04 -04:00
Samuel Elliott cee2a6ec34
Fix badges not being rendered when jumping to a message without a flash 2018-05-05 11:51:08 +01:00
Samuel Elliott 64e855ab2d
Fix badges not being rendered when jumping to a message 2018-05-05 11:39:56 +01:00
Samuel Elliott 5aa921ae4b
Fix EventsWrapper not unsubscribing from events
Also changed it to use removeListener so it can be used with normal EventEmitters
2018-05-05 11:17:26 +01:00
Samuel Elliott 40a72458e2
Fix Channel component 2018-05-03 19:52:23 +01:00
Samuel Elliott dc2be5c410
Add ReactHelpers to the plugin API 2018-05-03 15:56:07 +01:00
Samuel Elliott 640ec66abc
Some comments and fix updateSystemChannel 2018-04-30 21:12:50 +01:00
Samuel Elliott 3aedcfdd06
Add updating a guild’s icon/splash image from a file 2018-04-30 20:46:55 +01:00
Samuel Elliott 71fe549ee6
Add some permission checks 2018-04-30 20:19:54 +01:00
Samuel Elliott cbe1de98a5
Add updating guilds 2018-04-30 20:18:25 +01:00
Samuel Elliott 5a58c1c6ce
Add updating channel attributes 2018-04-30 19:17:04 +01:00
Samuel Elliott 591d44dee4
Add creating channels 2018-04-30 19:16:06 +01:00
Samuel Elliott 6da58ef6bb
Move Channel.openSettings to GuildChannel.openSettings, rename defaultChannel to isDefaultChannel and add GroupChannel names 2018-04-30 19:14:34 +01:00
Samuel Elliott 85d5de9791
Fix ChannelMember returning wrong component 2018-04-30 19:12:21 +01:00
Samuel Elliott 5ce78f4b32
Update layer classes 2018-04-29 17:56:23 +01:00
Samuel Elliott 2499f8c0ab
Get class names from internal modules 2018-04-29 17:41:12 +01:00
Samuel Elliott 3f2eb1047e
Use top/left to position tooltips/popovers and remove BdMenuItems from global 2018-04-29 17:35:30 +01:00
Samuel Elliott ac79bbb99b
Update classes 2018-04-29 16:30:21 +01:00
Samuel Elliott 1c4fb0b5ea
Rename HTMLUtils to SimpleMarkdown 2018-04-29 02:19:08 +01:00
Samuel Elliott 2e2d0dd6f6
Make some Vue components available to plugins, add highlight.js module and some tidying 2018-04-29 01:57:09 +01:00
Samuel Elliott a6c91d5e27
Add updating a user’s roles 2018-04-28 00:15:17 +01:00
Samuel Elliott 949206738b
Add user notes 2018-04-27 23:51:37 +01:00
Samuel Elliott 4dc715a39a
Patch all components at once instead of waiting for the previous component to be found 2018-04-27 19:05:00 +01:00
Samuel Elliott 36d7554b46
Add user popout data attributes 2018-04-27 19:02:35 +01:00
Samuel Elliott d42e8f66ca
Fix badges not being rendered on update 2018-04-27 17:39:28 +01:00
Samuel Elliott 244b7e5bd1
Move filters to common 2018-04-27 17:33:05 +01:00
Samuel Elliott ca7a7c1f91
Fix injected profile badges being removed 2018-04-27 16:12:46 +01:00
Samuel Elliott 504221ce28
Fix getComponentByRegex and add Filters to the plugin API 2018-04-27 16:06:51 +01:00
Samuel Elliott 1e643b8164
Patch the UserProfileModal component instead of injecting badges when the modal is opened 2018-04-27 16:00:27 +01:00
Samuel Elliott 0944c0708f
Add user profile modal data attributes 2018-04-27 15:53:49 +01:00
Samuel Elliott 17575fc6a1
Add filtering all components to ReactComponents 2018-04-27 15:52:43 +01:00
Samuel Elliott 1354b884b4
Add getting all components to reflection 2018-04-27 15:51:59 +01:00
Samuel Elliott 1ace26b67f
Select the channel when sending messages 2018-04-26 19:52:56 +01:00
Samuel Elliott 117336a6b4
Switch to Node.js v8 for Travis CI builds 2018-04-26 18:35:06 +01:00
Samuel Elliott d1cce95abe
Fix colour picker/dropdown options being hidden when in a drawer 2018-04-26 01:08:14 +01:00
Samuel Elliott 22fbb7b5bd
Fixed favourite emotes array being undefined
https://cdn.discordapp.com/attachments/400734824740028419/438847211325882370/unknown.png
2018-04-26 00:47:42 +01:00
Samuel Elliott 34ebab7c21
Use APIModule directly to edit messages 2018-04-25 23:57:07 +01:00
Samuel Elliott 5e8579e4dc
Use APIModule directly to delete messages 2018-04-25 23:56:10 +01:00
Samuel Elliott 4721f66dab
Comments 2018-04-25 23:54:26 +01:00
Samuel Elliott 02b313f2e6
Add checks for whether the user can delete a message 2018-04-25 23:02:47 +01:00
Samuel Elliott b0ae424adb
Change all properties to camel case 2018-04-25 22:54:41 +01:00
Samuel Elliott 5a6fb990c7
Add sending local bot messages 2018-04-25 13:54:33 +01:00
Samuel Elliott c294df6b26
Fix nickname checks and add changing nicknames programmatically 2018-04-25 13:40:18 +01:00
Samuel Elliott c3e4563aeb
Add multi option radios 2018-04-25 13:21:18 +01:00
Samuel Elliott 525fd3145d Merge branch 'master' into discord-api 2018-04-17 20:17:04 +01:00
Samuel Elliott 32452d918a
Fix guild member methods and set the guild/channel settings window section before opening it 2018-04-15 02:27:01 +01:00
Samuel Elliott e524c50b65
Add opening the user’s profile modal 2018-04-15 02:26:21 +01:00
Samuel Elliott 96952c3fef
Fix guild.owner 2018-04-15 00:54:33 +01:00
Samuel Elliott ad710de008
Fixes and add file header to list 2018-04-14 21:49:16 +01:00
Samuel Elliott 21ff9d200c
Some comments 2018-04-14 19:42:58 +01:00
Samuel Elliott a1c6f5d5fd
Separate messages into their types 2018-04-14 19:13:26 +01:00
Samuel Elliott 6d64d17b7d
Add embeds 2018-04-14 18:43:18 +01:00
Samuel Elliott 695d088f60
Add reactions 2018-04-14 18:08:57 +01:00
Samuel Elliott bcb888100c
Fix development CSS editor build being copied instead of the release build 2018-04-14 17:51:02 +01:00
Samuel Elliott 146e108874
Add file headers 2018-04-14 17:37:41 +01:00
Samuel Elliott 4837a01812
Add guild emojis 2018-04-14 17:24:07 +01:00
Samuel Elliott 48f4837a24
Wrap User instead of extending it 2018-04-14 17:20:17 +01:00
Samuel Elliott 8b320d2f09
More modals 2018-04-14 17:03:39 +01:00
Samuel Elliott ac22b14358
Add opening channel/guild settings window 2018-04-14 17:02:06 +01:00
Samuel Elliott 4d5a403d6d
Add user settings 2018-04-14 17:00:33 +01:00
Samuel Elliott 9dc424cc75
Allow after patches to change the return value 2018-04-14 15:17:02 +01:00
Samuel Elliott 1b75753a3d
Add user status/activity 2018-04-14 15:15:13 +01:00
Samuel Elliott a7b300d233
Check if the member exists before creating a GuildMember 2018-04-14 15:14:31 +01:00
Samuel Elliott 68e42b8b36
Add channel permission overwrites 2018-04-14 14:06:32 +01:00
Samuel Elliott e061d95ae6
Add roles 2018-04-14 14:06:09 +01:00
Alexei Stukov 6b517adbe8
Merge pull request #190 from samuelthomas2774/emote-autocomplete
Emote autocomplete and other fixes
2018-04-14 04:57:01 +03:00
Samuel Elliott 33c0732c08
Store objects in a WeakMap
So comparisons like `DiscordApi.currentUser === DiscordApi.currentUser` can be true
2018-04-14 00:31:09 +01:00
Samuel Elliott f1e0350433
Move data types to separate files and further separate channels based of their type 2018-04-14 00:29:03 +01:00
Samuel Elliott d5f486d1ad
Add shortcut to plugin/theme/extmodule manager content 2018-04-13 19:50:31 +01:00
Samuel Elliott 7fb8337095
Fix for plain CSS themes 2018-04-09 21:12:53 +01:00
Samuel Elliott 7adf63dfa8
Prevent escape closing Discord and BetterDiscord menus/modals at the same time 2018-04-08 19:54:48 +01:00
Samuel Elliott d2f7e6142f
Fix dropdowns not showing the selected option 2018-04-08 17:00:26 +01:00
Samuel Elliott b62727b046
Fix channel members not always being rerendered to show badges 2018-04-08 01:37:15 +01:00
Samuel Elliott f0a337e0ed
Fix custom CSS file not being used if the custom editor CSS is empty 2018-04-08 00:07:23 +01:00
Samuel Elliott 5c755bc121
Update pump
Fixes release dependencies not being copied properly
2018-04-07 16:42:16 +01:00
Samuel Elliott 4d0631ba38
Add wait util 2018-04-04 21:53:02 +01:00
Samuel Elliott d4962bb2ab
Fix settings being partially saved while loading 2018-04-04 21:39:55 +01:00
Samuel Elliott cf319a2604
Separate load 2018-04-04 21:38:56 +01:00
Samuel Elliott 256db71aa4
Hide emote autocomplete when emotes are disabled 2018-04-04 21:08:50 +01:00
Samuel Elliott de8601ded4
Show favourite emotes in autocomplete 2018-04-04 20:51:25 +01:00
Samuel Elliott a96f3b1eb7
Fix border radius of the message input field 2018-04-04 20:36:11 +01:00
Samuel Elliott a7aa1fa5ec
Fix up/down at the end of the list when the list has less than 10 emotes 2018-04-04 20:34:54 +01:00
Alexei Stukov bbd12a0381
Merge pull request #187 from samuelthomas2774/tweaks-and-emote-favourites
Tweaks and favourite emotes
2018-04-02 16:25:45 -02:00
Samuel Elliott 027ca8d639
Change plugin API WebpackModules
getModule[…] returns a module (first = true)
getModules[…] returns an array (first = false)
2018-04-01 23:17:33 +01:00
Samuel Elliott a0f60dddb2
Update plugin API (fix Utils not being passed arguments) 2018-04-01 23:12:23 +01:00
Samuel Elliott a3038c46fd
Undo 10fd25607d (diff-e0997702acb90b93fb46357f7576ab03L423) 2018-04-01 23:02:02 +01:00
Samuel Elliott d98f152009
Refactor UserProfileModals badges 2018-04-01 22:44:01 +01:00
Samuel Elliott 68beee12d3
Add the BD badge before the server owner icon 2018-04-01 17:39:46 +01:00
Samuel Elliott 4b59b9f65d
Prevent opening the popout when the badge is clicked 2018-04-01 17:35:09 +01:00
Samuel Elliott 85d390e18d
Rerender messages and channel members after patching 2018-04-01 15:24:28 +01:00
Samuel Elliott 3f89286946
Refactor message badges 2018-04-01 15:21:31 +01:00
Samuel Elliott 554b8d195b
Rename ReactComponentHelpers to ReactHelpers 2018-04-01 15:16:50 +01:00
Samuel Elliott 7f5fa44fd3
Refactor ChannelMember badges 2018-04-01 15:15:31 +01:00
Samuel Elliott c95d60ab0f
Don’t toggle favourite when the emote itself is clicked 2018-04-01 02:39:58 +01:00
Samuel Elliott 60a82a3ad9
Add package script and fix release script not working when the release directory doesn’t already exist 2018-04-01 00:18:05 +01:00
Samuel Elliott 2f9af2e2d0
Use correct Electron version 2018-03-31 23:06:24 +01:00
Samuel Elliott 858fdcec59
Clean up and comment 2018-03-31 22:49:40 +01:00
Samuel Elliott 3eb1782a64
Fix for Discord Canary 2018-03-31 22:44:24 +01:00
Samuel Elliott 0e14d167dc
Fix event callbacks not being passed any arguments 2018-03-31 17:38:11 +01:00
Samuel Elliott c701a2b5f9
Add emotes to the plugin API 2018-03-31 16:51:14 +01:00
Samuel Elliott b3442ee108
Store emote database in a map 2018-03-31 04:47:30 +01:00
Samuel Elliott 1bde3b4ec9
Add setting to enable/disable emotes 2018-03-31 03:26:42 +01:00
Samuel Elliott e83c6f92cb
Add emote favourite button 2018-03-31 03:22:44 +01:00
Samuel Elliott 06cca44376
Make plugin/theme descriptions preformatted 2018-03-31 01:19:05 +01:00
Samuel Elliott 3f3898c774
Add favourite emotes (no UI yet) 2018-03-31 01:17:42 +01:00
Samuel Elliott f8a380fd59
Add more advanced content authors (links to Discord profile, GitHub, Twitter, etc) 2018-03-31 01:10:33 +01:00
Samuel Elliott f28525129e
Add menu API 2018-03-31 01:03:13 +01:00
Samuel Elliott d2cbbd309b
Fix plugin/themes view not updating after reloading content 2018-03-31 00:50:03 +01:00
Samuel Elliott 74d78f4bca
Remove old monkey patch 2018-03-31 00:47:25 +01:00
Samuel Elliott 68af931128
Record plugin/theme load/start/stop timestamp 2018-03-31 00:46:05 +01:00
Samuel Elliott 5331afb763
Tweak card styles and use events 2018-03-31 00:45:14 +01:00
Samuel Elliott 069b1ff689
Extend AsyncEventEmitter and allow changing of some properties 2018-03-30 04:23:32 +01:00
Samuel Elliott 009d6be057
All clicking through the transparent area when the menu is open but nothing is selected 2018-03-30 01:38:36 +01:00
Samuel Elliott da4b592e54
Extend AsyncEventEmitter 2018-03-30 01:34:37 +01:00
Samuel Elliott 81f3519408
Use events in the settings menu 2018-03-30 01:17:02 +01:00
Samuel Elliott 8c09112cff
Use events 2018-03-30 01:07:56 +01:00
Samuel Elliott 8983256ade
Use Logger 2018-03-30 01:06:37 +01:00
Samuel Elliott d323292162
Add textarea for updating custom setting’s values in debug mode 2018-03-30 01:04:58 +01:00
Samuel Elliott 1714a0225e
Change autocomplete class names 2018-03-30 01:03:04 +01:00
Samuel Elliott fd3b0a92ce
Use events on modal close button click 2018-03-30 00:57:01 +01:00
Samuel Elliott 9b1dd771ad
Use v-model/events instead of a change function
Custom settings still work the same
2018-03-29 21:48:06 +01:00
Samuel Elliott 441e80e0e8
Tweak colour picker and add animation 2018-03-29 21:23:15 +01:00
Samuel Elliott 4634266e14
Make the button look more built in 2018-03-29 20:16:06 +01:00
Samuel Elliott 10fd25607d
Remove installer from release build script 2018-03-29 20:04:01 +01:00
Samuel Elliott 946f68e4ef
Fix theme edit button 2018-03-25 18:31:17 +01:00
Alexei Stukov a4ceb8bd2c
Merge pull request #184 from samuelthomas2774/refactor
Refactor and comment
2018-03-25 09:52:32 -02:00
Samuel Elliott 545b72cc3e
Refactor logger (use util.inspect) and fix CSS editor not awaiting settings saving 2018-03-25 02:18:40 +01:00
Samuel Elliott 9e229a1c0f
Cleanup profile badges 2018-03-24 16:13:24 +00:00
Samuel Elliott ddfd0653ac
Fix emote injection 2018-03-24 16:06:34 +00:00
Samuel Elliott 6bb8c18857
Update repository URL 2018-03-22 17:13:47 +00:00
Samuel Elliott 66eee86eb3
Fix cloning settings sets with schemes 2018-03-22 16:46:42 +00:00
Samuel Elliott eaeae7ad98
Use a warning when settings failed to load 2018-03-22 16:41:20 +00:00
Samuel Elliott a3eeee9b57
Store content config with it’s type 2018-03-22 16:38:09 +00:00
Samuel Elliott 6d46ac67de
Remove SimplerFlat 2018-03-22 16:27:53 +00:00
Samuel Elliott db6be20864
Use correct path for logs 2018-03-22 16:25:06 +00:00
Samuel Elliott 5e8e9fa19f
CSS editor fixes 2018-03-22 02:37:30 +00:00
Samuel Elliott ebff286c31
Add release build configuration 2018-03-22 02:19:25 +00:00
Samuel Elliott 0f805d57e3
More comments 2018-03-22 02:13:32 +00:00
Samuel Elliott 33adb92902
Add updater view 2018-03-21 22:24:23 +00:00
Samuel Elliott a85422f29e
Fix tooltips and add Vue to vendor 2018-03-21 21:07:57 +00:00
Samuel Elliott aa933d9a09
Refactor IPC 2018-03-21 20:52:42 +00:00
Samuel Elliott 8a9c8edf39
Add patcher to the plugin API and fix error when rendering system messages 2018-03-21 20:50:49 +00:00
Samuel Elliott 74e3605ec6
Get version from package.json 2018-03-21 20:48:01 +00:00
Samuel Elliott dc44af6968
Cleanup core 2018-03-21 17:41:27 +00:00
Samuel Elliott a4a130bfc6
Write logs to a file 2018-03-21 15:57:37 +00:00
Samuel Elliott 2fb5d8fe11
Pause all keybinds while recording and add debugger keybind 2018-03-21 00:27:25 +00:00
Samuel Elliott 1772edd37c
Cleanup project root and update package-lock.json 2018-03-20 23:29:00 +00:00
Samuel Elliott b4bd9e9c7b
Comments and fix tooltip arrow positioning 2018-03-20 23:24:38 +00:00
Samuel Elliott 994faf94d6
Refactor Vue injector and sparkplug and move contributors to data 2018-03-20 21:11:11 +00:00
Alexei Stukov 048abaeeed
Merge pull request #183 from JsSucks/utils-load-latest
Utils load latest
2018-03-20 07:51:52 -03:00
Jiiks f3fa3c2ae2 Tests should be true other than for building a release 2018-03-20 07:49:19 -03:00
Jiiks ee87cbc25c lint before building release 2018-03-20 07:48:03 -03:00
Jiiks 4ce32c415e copy prebuilt bindings 2018-03-20 07:47:03 -03:00
Jiiks c60411c901 Don't have to resolve it twice 2018-03-20 07:46:34 -03:00
Jiiks 9f24d0d1ce Fix package ver and make sure we always load the latest client script 2018-03-20 07:45:11 -03:00
Jiiks 684f85b45e Update release scripts and packages 2018-03-20 03:54:33 -03:00
Jiiks 83d9419131 css editor path fix 2018-03-19 20:02:40 -03:00
Alexei Stukov 1fe233bc91
Merge pull request #182 from JsSucks/release-script
Release script
2018-03-19 19:09:16 -03:00
Jiiks 70ae8365fe Remove merge-stream devdep 2018-03-19 19:03:48 -03:00
Jiiks 038d15ecf3 add release gulpfile and deps for it 2018-03-19 19:02:44 -03:00
Alexei Stukov 86fc492af4
Merge pull request #181 from JsSucks/css-editor-path
Dist path for css editor
2018-03-19 18:28:53 -03:00
Jiiks eed634217e Dist path for css editor 2018-03-19 18:26:13 -03:00
461 changed files with 26844 additions and 14878 deletions

View File

@ -2,6 +2,7 @@ root = true
[*]
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space

42
.eslintrc Normal file
View File

@ -0,0 +1,42 @@
{
"parserOptions": {
"ecmaVersion": 8,
"sourceType": "module"
},
"env": {
"browser": true,
"node": true
},
"plugins": [
"vue"
],
"extends": [
"eslint:recommended",
"plugin:vue/recommended"
],
"rules": {
"no-unused-vars": "off",
"no-console": "off",
"no-empty": "off",
"quotes": [
"error",
"single",
{ "allowTemplateLiterals": true }
],
"prefer-template": "warn",
"no-return-await": "error",
"indent": [ "warn", 4 ],
"no-lonely-if": "error",
"no-multiple-empty-lines": [
"warn",
{ "max": 1 }
],
"no-tabs": "warn",
"no-trailing-spaces": "warn",
"no-unneeded-ternary": "warn",
"no-useless-constructor": "warn",
"no-var": "error",
"prefer-const": "error",
"no-else-return": "error"
}
}

View File

@ -1,21 +0,0 @@
---
parserOptions:
ecmaVersion: 8
sourceType: module
env:
browser: true
node: true
plugins:
- vue
extends:
- eslint:recommended
- plugin:vue/recommended
rules:
no-unused-vars: 'off'
no-console: 'off'
no-empty: 'off'

2
.gitattributes vendored
View File

@ -1 +1 @@
*.sh text=auto
*.sh text=auto

33
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@ -0,0 +1,33 @@
---
name: Bug report
about: Create a report to help us improve
title: "[Bug]"
labels: bug
assignees: ''
---
- [ ] Used appropriate template for the issue type
- [ ] Searched both open and closed issues for duplicates of this issue
- [ ] Title adequately and _concisely_ reflects the feature or the bug
**Describe the bug**
<!-- A clear and concise description of what the bug is. -->
**To Reproduce**
<!-- Steps to reproduce the behavior -->
**Expected behavior**
<!-- A clear and concise description of what you expected to happen. -->
**Screenshots**
<!-- If applicable, add screenshots to help explain your problem. -->
**System information**
<!-- os, discord branch etc -->
**Additional context**
<!-- Add any other context about the problem here. -->
**Are you willing and able to fix this?**
<!-- "Yes" or, if "no", what can current contributors do to help you create a PR? -->

12
.github/ISSUE_TEMPLATE/custom.md vendored Normal file
View File

@ -0,0 +1,12 @@
---
name: Custom issue template
about: Describe this issue template's purpose here.
title: ''
labels: ''
assignees: ''
---
- [ ] Used appropriate template for the issue type
- [ ] Searched both open and closed issues for duplicates of this issue
- [ ] Title adequately and _concisely_ reflects the feature or the bug

View File

@ -0,0 +1,27 @@
---
name: Feature request
about: Suggest an idea for this project
title: "[Feature]"
labels: feature
assignees: ''
---
- [ ] Used appropriate template for the issue type
- [ ] Searched both open and closed issues for duplicates of this issue
- [ ] Title adequately and _concisely_ reflects the feature or the bug
**Is your feature request related to a problem? Please describe.**
<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
**Describe the solution you'd like**
<!-- A clear and concise description of what you want to happen. -->
**Describe alternatives you've considered**
<!-- A clear and concise description of any alternative solutions or features you've considered. -->
**Additional context**
<!-- Add any other context or screenshots about the feature request here. -->
**Are you willing and able to implement this?**
<!-- "Yes" or, if "no", what can current contributors do to help you create a PR? -->

38
.gitignore vendored
View File

@ -1,26 +1,16 @@
.idea/*
*.name
devjs/.idea/devjs.iml
*.bak
*.bak.*
*.xpi
Firefox/data/js/jquery-2.1.4.min.js
*.dev.*
/nbproject/private/
# Generated files
node_modules
.sass-cache
/*.jiiks
Installers/dotNet/bin/
Installers/dotNet/packages/
Installers/dotNet/dlls/
v2/dist/vendor/
v2/lib/static.js
**/*.suo
Installers/**/*/bin
Installers/**/*/obj
Installers/**/*/packages
.vs
dist/
user.config.json
dist
etc
release
tests/tmp
tests/log.txt
# User data
tests/data
/tests/themes/SimplerFlat
user.config.json
/.vs
/npm-debug.log
/tests/ext/extensions
/tests/userdata

29
.sasslintrc Normal file
View File

@ -0,0 +1,29 @@
{
"rules": {
"no-transition-all": [ 0 ],
"nesting-depth": [
1,
{ "max-depth": 5 }
],
"no-vendor-prefixes": [ 0 ],
"property-sort-order": [ 0 ],
"indentation": [
1,
{ "size": 4 }
],
"no-color-literals": [ 0 ],
"class-name-format": [
1,
{
"convention": "^bd-[a-z]"
}
],
"variable-name-format": [
2,
{
"convention": "camelcase"
}
],
"url-quotes": [ 0 ]
}
}

View File

@ -1,6 +1,13 @@
language: node_js
node_js:
- stable
branches:
only:
- master
- master
addons:
apt:
packages:
- libsecret-1-dev

View File

@ -6,4 +6,4 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -1,3 +0,0 @@
# BetterDiscordApp [![Travis][build-badge]][build]
[build-badge]: https://img.shields.io/travis/JsSucks/BetterDiscordApp/master.svg
[build]: https://travis-ci.org/JsSucks/BetterDiscordApp

12
README.md Normal file
View File

@ -0,0 +1,12 @@
# BetterDiscordApp [![Travis][build-badge]][build] [![Snyk][snyk-badge]][snyk-url] [![deps][deps-badge]][deps-url] [![devdeps][devdeps-badge]][devdeps-url]
[build-badge]: https://img.shields.io/travis/JsSucks/BetterDiscordApp/master.svg
[build]: https://travis-ci.org/JsSucks/BetterDiscordApp
[snyk-badge]: https://snyk.io/test/github/JsSucks/BetterDiscordApp/badge.svg
[snyk-url]: https://snyk.io/test/github/JsSucks/BetterDiscordApp
[deps-badge]: https://david-dm.org/JsSucks/BetterDiscordApp.svg
[deps-url]: https://david-dm.org/JsSucks/BetterDiscordApp
[devdeps-badge]: https://david-dm.org/JsSucks/BetterDiscordApp/dev-status.svg
[devdeps-url]: https://david-dm.org/JsSucks/BetterDiscordApp?type=dev

17
babel.config.js Normal file
View File

@ -0,0 +1,17 @@
module.exports = function(api) {
api.cache(true);
const presets = [['@babel/env', {
targets: {
'node': '8.6.0'
}
}]];
const plugins = [];
return {
presets,
plugins
}
}

18
client/babel.config.js Normal file
View File

@ -0,0 +1,18 @@
module.exports = function(api) {
api.cache(true);
const presets = [['@babel/env', {
targets: {
'node': '8.6.0',
'chrome': '60'
}
}], '@babel/react'];
const plugins = [];
return {
presets,
plugins
}
}

16
client/jsconfig.json Normal file
View File

@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "es2017",
"allowSyntheticDefaultImports": false,
"baseUrl": "./",
"paths": {
"modules": ["./src/modules/modules.js"],
"structs": ["./src/structs/structs.js"],
"common": ["../common/modules/common.js"],
"ui": ["./src/ui/ui.js"],
"builtin": ["./src/builtin/builtin.js"],
"vue$": ["../node_modules/vue/dist/vue.esm.js"]
}
},
"exclude": ["node_modules", "dist"]
}

View File

@ -2,24 +2,22 @@
"name": "bdclient",
"description": "BetterDiscord client package",
"author": "Jiiks",
"version": "0.4.0",
"version": "2.0.0-beta.6",
"homepage": "https://betterdiscord.net",
"license": "MIT",
"main": "index.js",
"main": "dist/betterdiscord.client.js",
"contributors": [
"Jiiks",
"Pohky"
],
"repository": {
"type": "git",
"url": "https://github.com/Jiiks/BetterDiscordApp.git"
"url": "https://github.com/JsSucks/BetterDiscordApp.git"
},
"private": false,
"devDependencies": {
},
"scripts": {
"build": "webpack --progress --colors",
"watch": "webpack --progress --colors --watch"
"watch": "webpack --progress --colors --watch",
"release": "webpack --progress --colors --config=webpack.production.config.js"
}
}

View File

@ -0,0 +1,40 @@
/**
* BetterDiscord 24 Hour Timestamps Module
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import BuiltinModule from './BuiltinModule';
import { Reflection } from 'modules';
const twelveHour = new RegExp(`([0-9]{1,2}):([0-9]{1,2})\\s(AM|PM)`);
export default new class TwentyFourHour extends BuiltinModule {
/* Getters */
get moduleName() { return 'TwentyFourHour' }
get settingPath() { return ['ui', 'default', '24-hour'] }
/* Patches */
applyPatches() {
if (this.patches.length) return;
const { TimeFormatter } = Reflection.modules;
this.patch(TimeFormatter, 'calendarFormat', this.convertTimeStamps);
}
/**
* Convert 12 hours timestamps to 24 hour timestamps
*/
convertTimeStamps(that, args, returnValue) {
const matched = returnValue.match(twelveHour);
if (!matched || matched.length !== 4) return;
if (matched[3] === 'AM') return returnValue.replace(matched[0], `${matched[1] === '12' ? '00' : matched[1].padStart(2, '0')}:${matched[2]}`)
return returnValue.replace(matched[0], `${parseInt(matched[1]) + 12}:${matched[2]}`)
}
}

View File

@ -1,33 +0,0 @@
/**
* BetterDiscord Autocomplete Component
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
<template>
<div class="bd-autocomplete">
<div class="bd-autocomplete-inner">
<div class="bd-autocompleteRow">
<div class="bd-autocompleteSelector">
<div class="bd-autocompleteTitle">
Emotes Matching:
<strong>Kappa</strong>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import EmoteModule from '../../../builtin/Emotemodule.js';
export default {
props: ['title', 'emotes'],
beforeMount() {
console.log(EmoteModule);
}
}
</script>

View File

@ -0,0 +1,56 @@
/**
* BetterDiscord Blocked Messages Module
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import BuiltinModule from './BuiltinModule';
import { Reflection } from 'modules';
export default new class BlockedMessages extends BuiltinModule {
/* Getters */
get moduleName() { return 'BlockedMessages' }
get settingPath() { return ['ui', 'default', 'blocked-messages'] }
async enabled(e) {
const MessageListComponents = Reflection.module.byProps('BlockedMessageGroup');
MessageListComponents.OriginalBlockedMessageGroup = MessageListComponents.BlockedMessageGroup;
MessageListComponents.BlockedMessageGroup = () => null;
this.cancelBlockedMessages = () => {
MessageListComponents.BlockedMessageGroup = MessageListComponents.OriginalBlockedMessageGroup;
delete MessageListComponents.OriginalBlockedMessageGroup;
}
}
disabled(e) {
if (this.cancelBlockedMessages) this.cancelBlockedMessages();
}
/* Methods */
static isBlocked(id) {
const { RelationshipStore } = Reflection.modules;
return RelationshipStore.isBlocked(id);
}
/* Patches */
applyPatches() {
if (this.patches.length) return;
const { MessageActions } = Reflection.modules;
this.patch(MessageActions, 'receiveMessage', this.processMessage, 'instead');
}
/**
* Ignore blocked messages completely
*/
processMessage(that, args, originalFunction) {
if (args[1] && args[1].author && args[1].author.id && BlockedMessages.isBlocked(args[1].author.id)) return;
return originalFunction(...args);
}
}

View File

@ -0,0 +1,106 @@
/**
* BetterDiscord Builtin Module Base
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import { Settings } from 'modules';
import { Patcher, MonkeyPatch as Patch, Cache } from 'modules';
import { ClientLogger as Logger } from 'common';
export default class BuiltinModule {
constructor() {
this._settingUpdated = this._settingUpdated.bind(this);
if (this.enabled) this.enabled = this.enabled.bind(this);
if (this.disabled) this.disabled = this.disabled.bind(this);
if (this.applyPatches) this.applyPatches = this.applyPatches.bind(this);
this.patch = this.patch.bind(this);
}
async init() {
this.setting.on('setting-updated', this._settingUpdated);
if (this.setting.value) {
if (this.enabled) await this.enabled();
if (this.applyPatches) this.applyPatches();
}
}
get setting() {
return Settings.getSetting(...this.settingPath);
}
get patches() {
return Patcher.getPatchesByCaller(`BD:${this.moduleName}`);
}
async _settingUpdated(e) {
if (e.value) {
if (this.enabled) await this.enabled(e);
if (this.applyPatches) await this.applyPatches();
if (this.rerenderPatchedComponents) this.rerenderPatchedComponents();
} else {
if (this.disabled) await this.disabled(e);
this.unpatch();
if (this.rerenderPatchedComponents) this.rerenderPatchedComponents();
}
}
get cache() {
return {
push: data => Cache.push(this.moduleName, data),
find: filter => Cache.find(this.moduleName, filter)
}
}
/**
* By default unpatch everything.
* Override to do something else.
*/
unpatch() {
Patcher.unpatchAll(`BD:${this.moduleName}`);
}
/**
* Patch a function in a module
* @param {any} module Module to patch
* @param {String} fnName Name of the function to patch
* @param {Function} cb Callback
* @param {String} [when=after] before|after|instead
*/
patch(module, fnName, cb, when = 'after') {
if (!['before', 'after', 'instead'].includes(when)) when = 'after';
return Patch(`BD:${this.moduleName}`, module)[when](fnName, cb.bind(this));
}
childPatch(module, fnName, child, cb, when = 'after') {
const last = child.pop();
this.patch(module, fnName, (component, args, retVal) => {
const unpatch = this.patch(child.reduce((obj, key) => obj[key], retVal), last, function(...args) {unpatch(); return cb.call(this, component, ...args);}, when);
});
}
/**
* Logger wraps
*/
log(message) {
Logger.log(this.moduleName, message);
}
warn(message) {
Logger.warn(this.moduleName, message);
}
info(message) {
Logger.warn(this.moduleName, message);
}
debug(message) {
Logger.dbg(this.moduleName, message);
}
}

View File

@ -0,0 +1,73 @@
/**
* BetterDiscord Colored Text Module
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import BuiltinModule from './BuiltinModule';
import { Utils } from 'common';
import { Settings, Reflection, ReactComponents, DiscordApi } from 'modules';
export default new class ColoredText extends BuiltinModule {
/* Getters */
get moduleName() { return 'ColoredText' }
get settingPath() { return ['ui', 'default', 'colored-text'] }
get intensityPath() { return ['ui', 'advanced', 'colored-text-intensity'] }
get intensitySetting() { return Settings.getSetting(...this.intensityPath) }
get intensity() { return 100 - this.intensitySetting.value }
get defaultColor() { return DiscordApi.UserSettings.theme == 'light' ? '#747f8d' : '#dcddde' }
constructor() {
super();
this._intensityUpdated = this._intensityUpdated.bind(this);
}
async enabled(e) {
this.intensitySetting.off('setting-updated', this._intensityUpdated);
this.intensitySetting.on('setting-updated', this._intensityUpdated);
}
disabled(e) {
this.intensitySetting.off('setting-updated', this._intensityUpdated);
}
rerenderPatchedComponents() {
if (this.MessageContent) this.MessageContent.forceUpdateAll();
}
/* Methods */
_intensityUpdated() {
this.MessageContent.forceUpdateAll();
}
/* Patches */
async applyPatches() {
if (this.patches.length) return;
this.MessageContent = await ReactComponents.getComponent('MessageContent');
this.patch(this.MessageContent.component.prototype, 'render', this.injectColoredText);
}
/**
* Set markup text colour to match role colour
*/
injectColoredText(thisObject, args, originalReturn) {
const unpatch = this.patch(originalReturn.props, 'children', (obj, args, returnValue) => {
unpatch();
const { TinyColor } = Reflection.modules;
const markup = Utils.findInReactTree(returnValue, m => m && m.props && m.props.className && m.props.className.includes('da-markup'));
const roleColor = thisObject.props.message.colorString;
if (markup && roleColor) markup.props.style = {color: TinyColor.mix(roleColor, this.defaultColor, this.intensity)};
});
}
}

View File

@ -0,0 +1,396 @@
/**
* BetterDiscord E2EE Module
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import { Settings, Cache, Events } from 'modules';
import BuiltinModule from '../BuiltinModule';
import { Reflection, ReactComponents, DiscordApi, Security } from 'modules';
import { VueInjector, Modals, Toasts } from 'ui';
import { ClientLogger as Logger, ClientIPC } from 'common';
import { request } from 'vendor';
import { Utils } from 'common';
import E2EEComponent from './E2EEComponent.vue';
import E2EEMessageButton from './E2EEMessageButton.vue';
import nodecrypto from 'node-crypto';
const userMentionPattern = new RegExp(`<@!?([0-9]{10,})>`, 'g');
const roleMentionPattern = new RegExp(`<@&([0-9]{10,})>`, 'g');
const everyoneMentionPattern = new RegExp(`(?:\\s+|^)@everyone(?:\\s+|$)`);
const START_DATE = new Date();
const TEMP_KEY = 'temporarymasterkey';
const ECDH_STORAGE = {};
let seed;
export default new class E2EE extends BuiltinModule {
/* Getters */
get moduleName() { return 'E2EE' }
get settingPath() { return ['security', 'default', 'e2ee'] }
get database() { return Settings.getSetting('security', 'e2eedb', 'e2ekvps').value }
constructor() {
super();
this.encryptNewMessages = true;
this.ecdhDate = START_DATE;
this.handlePublicKey = this.handlePublicKey.bind(this);
this.fetchMasterKey = this.fetchMasterKey.bind(this);
}
async enabled(e) {
await this.fetchMasterKey();
Events.on('discord:MESSAGE_CREATE', this.handlePublicKey);
Settings.getSetting('security', 'default', 'use-keytar').on('setting-updated', this.fetchMasterKey);
}
async disabled(e) {
Settings.getSetting('security', 'default', 'use-keytar').off('setting-updated', this.fetchMasterKey);
Events.off('discord:MESSAGE_CREATE', this.handlePublicKey);
const ctaComponent = await ReactComponents.getComponent('ChannelTextArea');
ctaComponent.forceUpdateAll();
}
/* Methods */
async fetchMasterKey() {
try {
if (Settings.get('security', 'default', 'use-keytar')) {
const master = await ClientIPC.getPassword('betterdiscord', 'master');
if (master) return this.setMaster(master);
if (Settings.getSetting('security', 'e2eedb', 'e2ekvps').items.length) {
// Ask the user for their current password to save to the system keychain
const currentMaster = await Modals.input('Save to System Keychain', 'Master Password', true).promise;
await ClientIPC.setPassword('betterdiscord', 'master', currentMaster);
return this.setMaster(currentMaster);
}
// Generate a new master password and save it to the system keychain
const newMaster = Security.randomBytes();
await ClientIPC.setPassword('betterdiscord', 'master', newMaster);
return this.setMaster(newMaster);
}
const newMaster = await Modals.input('Open Database', 'Master Password', true).promise;
return this.setMaster(newMaster);
} catch (err) {
Settings.getSetting(...this.settingPath).value = false;
Toasts.error('Invalid master password! E2EE Disabled');
Logger.err('E2EE', ['Error fetching master password', err]);
}
}
setMaster(key) {
seed = Security.randomBytes();
const newMaster = Security.encrypt(seed, key);
// TODO re-encrypt everything with new master
return (this.master = newMaster);
}
encrypt(key, content, prefix = '') {
if (!key) {
// Encrypt something with master
return Security.encrypt(Security.decrypt(seed, this.master), content);
}
if (!content) {
// Get key for current channel and encrypt
const haveKey = this.getKey(DiscordApi.currentChannel.id);
if (!haveKey) return 'nokey';
return Security.encrypt(Security.decrypt(seed, [this.master, haveKey]), key);
}
return prefix + Security.encrypt(key, content);
}
decrypt(key, content, prefix = '') {
return Security.decrypt(key, content, prefix);
}
async createHmac(data) {
const haveKey = this.getKey(DiscordApi.currentChannel.id);
if (!haveKey) return null;
return Security.createHmac(Security.decrypt(seed, [this.master, haveKey]), data);
}
getKey(channelId) {
const haveKey = this.database.find(kvp => kvp.value.key === channelId);
if (!haveKey) return null;
return haveKey.value.value;
}
setKey(channelId, key) {
const items = Settings.getSetting('security', 'e2eedb', 'e2ekvps').items;
const index = items.findIndex(kvp => kvp.value.key === channelId);
if (index > -1) {
items[index].value = { key: channelId, value: key };
return;
}
Settings.getSetting('security', 'e2eedb', 'e2ekvps').addItem({ value: { key: channelId, value: key } });
}
createKeyExchange(dmChannelID) {
if (ECDH_STORAGE.hasOwnProperty(dmChannelID)) return null;
ECDH_STORAGE[dmChannelID] = Security.createECDH();
setTimeout(() => {
if (ECDH_STORAGE.hasOwnProperty(dmChannelID)) {
delete ECDH_STORAGE[dmChannelID];
Toasts.error('Key exchange expired!');
if (this.preExchangeState) this.encryptNewMessages = this.preExchangeState;
this.preExchangeState = null;
}
}, 30000);
return Security.generateECDHKeys(ECDH_STORAGE[dmChannelID]);
}
publicKeyFor(dmChannelID) {
return Security.getECDHPublicKey(ECDH_STORAGE[dmChannelID]);
}
computeSecret(dmChannelID, otherKey) {
try {
const secret = Security.computeECDHSecret(ECDH_STORAGE[dmChannelID], otherKey);
delete ECDH_STORAGE[dmChannelID];
return Security.hash('sha384', secret, 'hex');
} catch (e) {
throw e;
}
}
/* Patches */
async applyPatches() {
if (this.patches.length) return;
const { Dispatcher } = Reflection.modules;
this.patch(Dispatcher, 'dispatch', this.dispatcherPatch, 'before');
this.patchMessageContent();
const ChannelTextArea = await ReactComponents.getComponent('ChannelTextArea');
this.patchChannelTextArea(ChannelTextArea);
this.patchChannelTextAreaSubmit(ChannelTextArea);
ChannelTextArea.forceUpdateAll();
}
dispatcherPatch(_, [event]) {
if (!event || event.type !== 'MESSAGE_CREATE') return;
const key = this.getKey(event.message.channel_id);
if (!key) return; // We don't have a key for this channel
if (typeof event.message.content !== 'string') return; // Ignore any non string content
if (!event.message.content.startsWith('$:')) return; // Not an encrypted string
let decrypt;
try {
decrypt = this.decrypt(this.decrypt(this.decrypt(seed, this.master), key), event.message.content);
} catch (err) { return } // Ignore errors such as non empty
const { MessageParser, Permissions, DiscordConstants } = Reflection.modules;
const currentChannel = DiscordApi.Channel.fromId(event.message.channel_id).discordObject;
// Create a generic message object to parse mentions with
const parsed = MessageParser.parse(currentChannel, decrypt).content;
if (userMentionPattern.test(parsed))
event.message.mentions = parsed.match(userMentionPattern).map(m => { return { id: m.replace(/[^0-9]/g, '') } });
if (roleMentionPattern.test(parsed))
event.message.mention_roles = parsed.match(roleMentionPattern).map(m => m.replace(/[^0-9]/g, ''));
if (everyoneMentionPattern.test(parsed))
event.message.mention_everyone = Permissions.can(DiscordConstants.Permissions.MENTION_EVERYONE, currentChannel);
}
// TODO Received exchange should also expire if not accepted in time
async handlePublicKey(e) {
if (!DiscordApi.currentChannel) return;
if (DiscordApi.currentChannel.type !== 'DM') return;
const { id, content, author, channelId } = e.args;
if (author.id === DiscordApi.currentUser.id || channelId !== DiscordApi.currentChannel.id) return;
const [tagstart, begin, key, end, tagend] = content.split('\n');
if (begin !== '-----BEGIN PUBLIC KEY-----' || end !== '-----END PUBLIC KEY-----') return;
try {
await Modals.confirm('Key Exchange', `Key exchange request from: ${author.username}#${author.discriminator}`, 'Accept', 'Reject').promise;
// We already sent our key
if (!ECDH_STORAGE.hasOwnProperty(channelId)) {
const publicKeyMessage = `\`\`\`\n-----BEGIN PUBLIC KEY-----\n${this.createKeyExchange(channelId)}\n-----END PUBLIC KEY-----\n\`\`\``;
if (this.encryptNewMessages) this.encryptNewMessages = false;
Reflection.modules.DraftActions.saveDraft(channelId, publicKeyMessage);
}
const secret = this.computeSecret(channelId, key);
this.setKey(channelId, secret);
Toasts.success('Key exchange complete!');
if (this.preExchangeState) this.encryptNewMessages = this.preExchangeState;
this.preExchangeState = null;
} catch (err) {
console.log(err);
return;
}
}
async patchMessageContent() {
const MessageContent = await ReactComponents.getComponent('MessageContent');
this.patch(MessageContent.component.prototype, 'render', this.beforeRenderMessageContent, 'before');
this.childPatch(MessageContent.component.prototype, 'render', ['props', 'children'], this.afterRenderMessageContent);
MessageContent.forceUpdateAll();
const ImageWrapper = await ReactComponents.getComponent('ImageWrapper');
this.patch(ImageWrapper.component.prototype, 'render', this.beforeRenderImageWrapper, 'before');
ImageWrapper.forceUpdateAll();
}
beforeRenderMessageContent(component) {
if (!component.props || !component.props.message) return;
const key = this.getKey(component.props.message.channel_id);
if (!key) return; // We don't have a key for this channel
const Message = Reflection.module.byPrototypes('isMentioned');
const { MessageParser, Permissions, DiscordConstants } = Reflection.modules;
const currentChannel = DiscordApi.Channel.fromId(component.props.message.channel_id).discordObject;
if (typeof component.props.message.content !== 'string') return; // Ignore any non string content
if (!component.props.message.content.startsWith('$:')) return; // Not an encrypted string
let decrypt;
try {
decrypt = Security.decrypt(seed, [this.master, key, component.props.message.content]);
} catch (err) { return } // Ignore errors such as non empty
component.props.message.bd_encrypted = true; // signal as encrypted
// Create a generic message object to parse mentions with
const message = MessageParser.createMessage(currentChannel.id, MessageParser.parse(currentChannel, decrypt).content);
if (userMentionPattern.test(message.content))
message.mentions = message.content.match(userMentionPattern).map(m => { return { id: m.replace(/[^0-9]/g, '') } });
if (roleMentionPattern.test(message.content))
message.mention_roles = message.content.match(roleMentionPattern).map(m => m.replace(/[^0-9]/g, ''));
if (everyoneMentionPattern.test(message.content))
message.mention_everyone = Permissions.can(DiscordConstants.Permissions.MENTION_EVERYONE, currentChannel);
// Create a new message to parse it properly
const create = Message.create(message);
if (!create.content || !create.contentParsed) return;
component.props.message.mentions = create.mentions;
component.props.message.mentionRoles = create.mentionRoles;
component.props.message.mentionEveryone = create.mentionEveryone;
component.props.message.mentioned = create.mentioned;
component.props.message.content = create.content;
component.props.message.contentParsed = create.contentParsed;
}
afterRenderMessageContent(component, _childrenObject, args, retVal) {
if (!component.props.message.bd_encrypted) return;
const { className } = Reflection.resolve('buttonContainer', 'avatar', 'username');
const buttonContainer = Utils.findInReactTree(retVal, m => m && m.className && m.className.indexOf(className) !== -1);
if (!buttonContainer) return;
const buttons = buttonContainer.children.props.children;
if (!buttons) return;
try {
buttons.unshift(VueInjector.createReactElement(E2EEMessageButton));
} catch (err) {
Logger.err('E2EE', err.message);
}
}
beforeRenderImageWrapper(component, args, retVal) {
if (!component.props || !component.props.src) return;
if (component.props.decrypting) return;
component.props.decrypting = true;
const src = component.props.original || component.props.src.split('?')[0];
if (!src.includes('bde2ee')) return;
component.props.className = 'bd-encryptedImage';
const haveKey = this.getKey(DiscordApi.currentChannel.id);
if (!haveKey) return;
const cached = Cache.find('e2ee:images', item => item.src === src);
if (cached) {
if (cached.invalidKey) { // TODO If key has changed we should recheck all with invalid key
component.props.className = 'bd-encryptedImage bd-encryptedImageBadKey';
component.props.readyState = 'READY';
return;
}
Logger.info('E2EE', 'Returning encrypted image from cache');
try {
const decrypt = Security.decrypt(seed, [this.master, haveKey, cached.image]);
component.props.className = 'bd-decryptedImage';
component.props.src = component.props.original = `data:;base64,${decrypt}`;
} catch (err) { return } finally { component.props.readyState = 'READY' }
return;
}
component.props.readyState = 'LOADING';
Logger.info('E2EE', `Decrypting image: ${src}`);
(async () => {
try {
const res = await request.get(src, { encoding: 'binary' });
const arr = new Uint8Array(new ArrayBuffer(res.length));
for (let i = 0; i < res.length; i++) arr[i] = res.charCodeAt(i);
const aobindex = Utils.aobscan(arr, [73, 69, 78, 68]) + 8;
const sliced = arr.slice(aobindex);
const image = new TextDecoder().decode(sliced);
const hmac = image.slice(-64);
const data = image.slice(0, -64);
const validateHmac = await this.createHmac(data);
if (hmac !== validateHmac) {
Cache.push('e2ee:images', { src, invalidKey: true });
if (component && component.props) {
component.props.decrypting = false;
component.forceUpdate();
}
return;
}
Cache.push('e2ee:images', { src, image: data });
if (!component || !component.props) {
Logger.warn('E2EE', 'Component seems to be gone');
return;
}
component.props.decrypting = false;
component.forceUpdate();
} catch (err) {
console.log('request error', err);
}
})();
}
patchChannelTextArea(cta) {
this.patch(cta.component.prototype, 'render', this.renderChannelTextArea);
}
renderChannelTextArea(component, args, retVal) {
if (!(retVal.props.children instanceof Array)) retVal.props.children = [retVal.props.children];
const inner = retVal.props.children.find(child => child.props.className && child.props.className.includes('inner'));
inner.props.children.splice(0, 0, VueInjector.createReactElement(E2EEComponent));
}
patchChannelTextAreaSubmit(cta) {
this.patch(cta.component.prototype, 'handleSubmit', this.handleChannelTextAreaSubmit, 'before');
}
handleChannelTextAreaSubmit(component, args, retVal) {
const key = this.getKey(DiscordApi.currentChannel.id);
if (!this.encryptNewMessages || !key) return;
component.props.value = Security.encrypt(Security.decrypt(seed, [this.master, key]), component.props.value, '$:');
}
}

View File

@ -0,0 +1,120 @@
/**
* BetterDiscord E2EE Component
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
<template>
<div class="bd-e2eeTaContainer">
<v-popover :popoverClass="['bd-popover', 'bd-e2eePopover', {'bd-e2eePopoverOver': popoutPositionSetting.value === 'over'}]"
:trigger="popoutPositionSetting.value === 'over' && popoutTriggerSetting.value === 'hover' ? 'hover' : 'click'"
:placement="popoutPositionSetting.value === 'over' ? 'top-start' : 'top'"
:disabled="error && DiscordApi.currentChannel.type !== 'DM'">
<div v-if="error" class="bd-e2eeTaBtn bd-e2eeLock bd-error">
<MiLock v-tooltip="error" />
</div>
<div v-else-if="state === 'loading'" class="bd-e2eeTaBtn bd-e2eeLock bd-loading bd-warn">
<MiLock v-tooltip="'Loading'" />
</div>
<div v-else-if="!E2EE.encryptNewMessages" class="bd-e2eeTaBtn bd-e2eeLock bd-warn">
<MiLock v-tooltip="'New messages will not be encrypted.'" />
</div>
<div v-else class="bd-e2eeTaBtn bd-e2eeLock bd-ok">
<MiLock v-tooltip="'Ready!'" />
</div>
<template slot="popover">
<div @click="toggleEncrypt" :class="{'bd-warn': !E2EE.encryptNewMessages, 'bd-ok': E2EE.encryptNewMessages}"><MiLock v-tooltip="'Toggle Encryption'" /></div>
<div v-close-popover @click="showUploadDialog" v-if="!error"><MiImagePlus v-tooltip="'Upload Encrypted Image'" /></div>
<div v-close-popover @click="generatePublicKey" v-if="DiscordApi.currentChannel.type === 'DM'"><MiIcVpnKey v-tooltip="'Begin Key Exchange'" /></div>
</template>
</v-popover>
<div class="bd-taDivider"></div>
</div>
</template>
<script>
import { Utils, FileUtils, ClientIPC } from 'common';
import { E2EE } from 'builtin';
import { Settings, DiscordApi, Reflection } from 'modules';
import { Toasts } from 'ui';
import { MiLock, MiImagePlus, MiIcVpnKey } from 'commoncomponents';
export default {
components: {
MiLock, MiImagePlus, MiIcVpnKey
},
data() {
return {
E2EE,
DiscordApi,
state: 'loading',
error: null,
popoutPositionSetting: Settings.getSetting('security', 'e2ee-popout', 'position'),
popoutTriggerSetting: Settings.getSetting('security', 'e2ee-popout', 'trigger')
};
},
methods: {
async showUploadDialog() {
const dialogResult = await ClientIPC.send('bd-native-open', {properties: ['openFile']});
if (!dialogResult || !dialogResult.length) return;
const readFile = await FileUtils.readFileBuffer(dialogResult[0]);
const FileActions = Reflection.module.byProps('makeFile');
const Uploader = Reflection.module.byProps('instantBatchUpload');
const img = await Utils.getImageFromBuffer(readFile);
const canvas = document.createElement('canvas');
canvas.height = img.height;
canvas.width = img.width;
const arrBuffer = await Utils.canvasToArrayBuffer(canvas);
const encrypted = E2EE.encrypt(img.src.replace('data:;base64,', ''));
const hmac = await E2EE.createHmac(encrypted);
const encodedBytes = new TextEncoder().encode(encrypted + hmac);
Uploader.upload(DiscordApi.currentChannel.id, FileActions.makeFile(new Uint8Array([...new Uint8Array(arrBuffer), ...encodedBytes]), 'bde2ee.png'));
},
toggleEncrypt() {
const newState = !E2EE.encryptNewMessages;
E2EE.encryptNewMessages = newState;
if (!newState) {
Toasts.warning('New messages will not be encrypted');
return;
}
Toasts.success('New messages will be encrypted');
},
generatePublicKey() {
const keyExchange = E2EE.createKeyExchange(DiscordApi.currentChannel.id);
if (keyExchange === null) {
Toasts.warning('Key exchange for channel already in progress!');
return;
}
E2EE.preExchangeState = E2EE.encryptNewMessages;
E2EE.encryptNewMessages = false; // Disable encrypting new messages so we won't encrypt public keys
const publicKeyMessage = `\`\`\`\n-----BEGIN PUBLIC KEY-----\n${keyExchange}\n-----END PUBLIC KEY-----\n\`\`\``;
Reflection.modules.DraftActions.saveDraft(DiscordApi.currentChannel.id, publicKeyMessage);
Toasts.info('Key exchange started. Expires in 30 seconds');
}
},
mounted() {
if (!E2EE.master) {
this.error = 'No master key set!';
return;
}
const haveKey = E2EE.getKey(DiscordApi.currentChannel.id);
if (!haveKey) {
this.error = 'No key for channel!';
return;
}
this.state = 'OK';
this.error = null;
}
}
</script>

View File

@ -0,0 +1,28 @@
/**
* BetterDiscord E2EE Component
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
<template>
<div class="bd-e2eeMessageButtonWrap">
<div class="bd-e2eeMessageButton">
<MiLock v-tooltip="'Encrypted'" :size="16" />
</div>
</div>
</template>
<script>
import { MiLock } from 'commoncomponents';
export default {
components: {
MiLock
},
props: ['message']
}
</script>

View File

@ -0,0 +1,3 @@
export { default as default } from './E2EE';
export { default as E2EEComponent } from './E2EEComponent.vue';
export { default as E2EEMessageButton } from './E2EEMessageButton.vue';

View File

@ -1,8 +0,0 @@
<template>
<span class="edited" v-tooltip="ets">(edited)</span>
</template>
<script>
export default {
props: ['ets']
}
</script>

View File

@ -1,21 +0,0 @@
<template>
<span class="bd-emotewrapper" v-tooltip="name">
<img class="bd-emote" :src="src" :alt="`;${name};`"/>
</span>
</template>
<script>
export default {
data() {
return {
favourite: false
}
},
props: ['src', 'name'],
methods: {
},
beforeMount() {
// Check favourite state
}
}
</script>

View File

@ -1,182 +0,0 @@
/**
* BetterDiscord Emote Module
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import { FileUtils, ClientLogger as Logger } from 'common';
import { Events, Globals, WebpackModules, ReactComponents, MonkeyPatch } from 'modules';
import { DOM, VueInjector, Reflection } from 'ui';
import EmoteComponent from './EmoteComponent.vue';
let emotes = null;
const emotesEnabled = true;
export default class {
static get searchCache() {
return this._searchCache || (this._searchCache = {});
}
static get emoteDb() {
return emotes;
}
static get React() {
return WebpackModules.getModuleByName('React');
}
static get ReactDOM() {
return WebpackModules.getModuleByName('ReactDOM');
}
static processMarkup(markup) {
if (!emotesEnabled) return markup; // TODO Get it from setttings
const newMarkup = [];
for (const child of markup) {
if ('string' !== typeof child) {
newMarkup.push(child);
continue;
}
if (!this.testWord(child)) {
newMarkup.push(child);
continue;
}
const words = child.split(/([^\s]+)([\s]|$)/g);
if (!words) continue;
let text = null;
for (const [wordIndex, word] of words.entries()) {
const isEmote = this.isEmote(word);
if (isEmote) {
if (text !== null) {
newMarkup.push(text);
text = null;
}
newMarkup.push(this.React.createElement('span', { className: 'bd-emote-outer', 'data-bdemote-name': isEmote.name, 'data-bdemote-src': isEmote.src }));
continue;
}
if (text === null) {
text = word;
} else {
text += word;
}
if (wordIndex === words.length - 1) {
newMarkup.push(text);
}
}
}
return newMarkup;
}
static testWord(word) {
if (!/;[\w]+;/gmi.test(word)) return false;
return true;
}
static injectAll() {
if (!emotesEnabled) return;
const all = document.getElementsByClassName('bd-emote-outer');
for (const ec of all) {
if (ec.children.length) continue;
this.injectEmote(ec);
}
}
static findByProp(obj, what, value) {
if (obj.hasOwnProperty(what) && obj[what] === value) return obj;
if (obj.props && !obj.children) return this.findByProp(obj.props, what, value);
if (!obj.children || !obj.children.length) return null;
for (const child of obj.children) {
if (!child) continue;
const findInChild = this.findByProp(child, what, value);
if (findInChild) return findInChild;
}
return null;
}
static async observe() {
const dataPath = Globals.getObject('paths').find(path => path.id === 'data').path;
try {
emotes = await FileUtils.readJsonFromFile(dataPath + '/emotes.json');
const Message = await ReactComponents.getComponent('Message');
this.unpatchRender = MonkeyPatch('BD:EmoteModule', Message.component.prototype).after('render', (component, args, retVal) => {
try {
const markup = this.findByProp(retVal, 'className', 'markup'); // First child has all the actual text content, second is the edited timestamp
if (!markup) return;
markup.children[0] = this.processMarkup(markup.children[0]);
} catch (err) {
Logger.err('EmoteModule', err);
}
});
for (const message of document.querySelectorAll('.message')) {
Reflection(message).forceUpdate();
}
this.injectAll();
this.unpatchMount = MonkeyPatch('BD:EmoteModule', Message.component.prototype).after('componentDidMount', component => {
const element = this.ReactDOM.findDOMNode(component);
if (!element) return;
this.injectEmotes(element);
});
this.unpatchUpdate = MonkeyPatch('BD:EmoteModule', Message.component.prototype).after('componentDidUpdate', component => {
const element = this.ReactDOM.findDOMNode(component);
if (!element) return;
this.injectEmotes(element);
});
} catch (err) {
Logger.err('EmoteModule', err);
}
}
static injectEmote(root) {
if (!emotesEnabled) return;
while (root.firstChild) {
root.removeChild(root.firstChild);
}
const { bdemoteName, bdemoteSrc } = root.dataset;
if (!bdemoteName || !bdemoteSrc) return;
VueInjector.inject(
root,
DOM.createElement('span'),
{ EmoteComponent },
`<EmoteComponent src="${bdemoteSrc}" name="${bdemoteName}"/>`
);
root.classList.add('bd-is-emote');
}
static injectEmotes(element) {
if (!emotesEnabled || !element) return;
for (const beo of element.getElementsByClassName('bd-emote-outer')) this.injectEmote(beo);
}
static isEmote(word) {
if (!emotes) return null;
const name = word.replace(/;/g, '');
const emote = emotes.find(emote => emote.id === name);
if (!emote) return null;
let { id, value } = emote;
if (value.id) value = value.id;
const uri = emote.type === 2 ? 'https://cdn.betterttv.net/emote/:id/1x' : emote.type === 1 ? 'https://cdn.frankerfacez.com/emoticon/:id/1' : 'https://static-cdn.jtvnw.net/emoticons/v1/:id/1.0';
return { name, src: uri.replace(':id', value) };
}
static filterTest() {
const re = new RegExp('Kappa', 'i');
const filtered = emotes.filter(emote => re.test(emote.id));
return filtered.slice(0, 10);
}
static filter(regex, limit, start = 0) {
const key = `${regex}:${limit}:${start}`;
if (this.searchCache.hasOwnProperty(key)) return this.searchCache[key];
let index = 0;
let startIndex = 0;
return this.searchCache[key] = emotes.filter(emote => {
if (index >= limit) return false;
if (regex.test(emote.id)) {
if (startIndex < start) {
startIndex++;
return false;
}
index++;
return true;
}
});
}
}

View File

@ -0,0 +1,104 @@
/**
* BetterDiscord Emote Autocomplete Module
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import { Settings } from 'modules';
import BuiltinModule from '../BuiltinModule';
import EmoteModule, { EMOTE_SOURCES } from './EmoteModule';
import GlobalAc from 'autocomplete';
import { BdContextMenu } from 'ui';
export default new class EmoteAc extends BuiltinModule {
/* Getters */
get moduleName() { return 'EmoteAC' }
get settingPath() { return ['emotes', 'default', 'emoteac'] }
async enabled(e) {
GlobalAc.add(';', this);
window.removeEventListener('contextmenu', this.acCm);
window.addEventListener('contextmenu', this.acCm);
}
disabled(e) {
GlobalAc.remove(';');
window.removeEventListener('contextmenu', this.acCm);
}
/* Methods */
acCm(e) {
const row = e.target.closest('.bd-emotAc');
if (!row) return;
const img = row.querySelector('img');
if (!img || !img.alt) return;
BdContextMenu.show(e, [
{
text: 'Test',
items: [
{
text: 'Favourite',
type: 'toggle',
checked: EmoteModule.isFavourite(img.alt.replace(/;/g, '')),
onChange: checked => {
if (!img || !img.alt) return;
const emote = img.alt.replace(/;/g, '');
if (!checked) return EmoteModule.removeFavourite(emote), false;
return EmoteModule.addFavourite(emote), true;
}
}
]
}
]);
}
/**
* Search for autocomplete
* @param {any} regex
*/
acsearch(regex) {
const acType = Settings.getSetting('emotes', 'default', 'emoteactype').value;
if (regex.length <= 0) {
return {
type: 'imagetext',
title: [`Your ${acType ? 'most used' : 'favourite'} emotes`, '', `${acType ? 'Favourites' : 'Most Used'}`],
items: EmoteModule[acType ? 'mostUsed' : 'favourites'].sort((a, b) => b.useCount - a.useCount).map(mu => {
return {
key: acType ? mu.key : mu.name,
value: {
src: EMOTE_SOURCES[mu.type].replace(':id', mu.id),
replaceWith: `;${acType ? mu.key : mu.name};`,
hint: mu.useCount ? `Used ${mu.useCount} times` : null
}
}
}),
extraClasses: ['bd-emotAc']
}
}
const results = EmoteModule.search(regex);
return {
type: 'imagetext',
title: ['Matching', regex.length],
items: results.map(result => {
result.value.src = EMOTE_SOURCES[result.value.type].replace(':id', result.value.id);
result.value.replaceWith = `;${result.key};`;
return result;
}),
extraClasses: ['bd-emotAc']
}
}
toggle(sterm) {
if (sterm.length > 1) return false;
Settings.getSetting('emotes', 'default', 'emoteactype').value = !Settings.getSetting('emotes', 'default', 'emoteactype').value;
return true;
}
}

View File

@ -0,0 +1,41 @@
/**
* BetterDiscord Emote Component
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import VrWrapper from '../../ui/vrwrapper';
import { EMOTE_SOURCES } from '.';
import EmoteComponent from './EmoteComponent.vue';
export default class Emote extends VrWrapper {
constructor(type, id, name) {
super();
this.jumboable = false;
this.type = type;
this.id = id;
this.name = name;
}
get component() { return EmoteComponent }
get props() {
return {
src: this.parseSrc(),
name: this.name,
jumboable: this.jumboable
}
}
parseSrc() {
const { type, id } = this;
if (type > 2 || type < 0) return '';
return EMOTE_SOURCES[type].replace(':id', id);
}
}

View File

@ -0,0 +1,32 @@
<template>
<img class="bd-emote emoji" :class="{jumboable}" :src="src" :alt="`;${name};`" v-tooltip="{ content: `;${name};`, delay: { show: 750, hide: 0 } }" />
</template>
<script>
import { ClientLogger as Logger } from 'common';
import EmoteModule from './EmoteModule';
import { MiStar } from 'commoncomponents';
export default {
components: {
MiStar
},
props: ['src', 'name', 'hasWrapper', 'jumboable'],
data() {
return {
EmoteModule
};
},
computed: {
favourite() {
return false;
}
},
methods: {
async toggleFavourite() {
await EmoteModule.setFavourite(this.name, !this.favourite);
Logger.log('EmoteComponent', `Set emote ${this.name} as ${this.favourite ? '' : 'un'}favourite`);
}
}
}
</script>

View File

@ -0,0 +1,406 @@
/**
* BetterDiscord Emote Module
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import BuiltinModule from '../BuiltinModule';
import path from 'path';
import { request } from 'vendor';
import { Utils, FileUtils, ClientLogger as Logger } from 'common';
import { DiscordApi, Settings, Globals, Reflection, ReactComponents, Database } from 'modules';
import { DiscordContextMenu } from 'ui';
import Emote from './EmoteComponent.js';
export const EMOTE_SOURCES = [
'https://static-cdn.jtvnw.net/emoticons/v1/:id/1.0',
'https://cdn.frankerfacez.com/emoticon/:id/1',
'https://cdn.betterttv.net/emote/:id/1x'
];
export default new class EmoteModule extends BuiltinModule {
/* Getters */
get moduleName() { return 'EmoteModule' }
/**
* @returns {String} Path to local emote database
*/
get dbpath() { return path.join(Globals.getPath('data'), 'emotes.json') }
/**
* @returns {Map} Cached raw emote database
*/
get database() { return this._db || (this._db = new Map()) }
/**
* @returns {Array} Favourite emotes
*/
get favourites() { return this._favourites || (this._favourites = []) }
/**
* @returns {Array} Most used emotes
*/
get mostUsed() { return this._mostUsed || (this._mostUsed = []) }
get settingPath() { return ['emotes', 'default', 'enable'] }
async enabled() {
// Add favourite button to context menu
this.favCm = DiscordContextMenu.add(target => [
{
text: 'Favourite',
type: 'toggle',
checked: target && target.alt && this.isFavourite(target.alt.replace(/;/g, '')),
onChange: (checked, target) => {
const { alt } = target;
if (!alt) return false;
const emote = alt.replace(/;/g, '');
if (!checked) return this.removeFavourite(emote), false;
return this.addFavourite(emote), true;
}
}
], target => target.closest('.bd-emote'));
if (!this.database.size) {
await this.loadLocalDb();
}
// Read favourites and most used from database
await this.loadUserData();
}
async disabled() {
DiscordContextMenu.remove(this.favCm);
}
/* Methods */
/**
* Adds an emote to favourites.
* @param {Object|String} emote
* @return {Promise}
*/
addFavourite(emote) {
if (this.isFavourite(emote)) return;
if (typeof emote === 'string') emote = this.findByName(emote, true);
this.favourites.push(emote);
return this.saveUserData();
}
/**
* Removes an emote from favourites.
* @param {Object|String} emote
* @return {Promise}
*/
removeFavourite(emote) {
if (!this.isFavourite(emote)) return;
Utils.removeFromArray(this.favourites, e => e.name === emote || e.name === emote.name, true);
return this.saveUserData();
}
/**
* Checks if an emote is in favourites.
* @param {Object|String} emote
* @return {Boolean}
*/
isFavourite(emote) {
return !!this.favourites.find(e => e.name === emote || e.name === emote.name);
}
/**
* Load emotes from local database
*/
async loadLocalDb() {
const emotes = await FileUtils.readJsonFromFile(this.dbpath);
for (const [index, emote] of emotes.entries()) {
const { type, id, src, value } = emote;
if (index % 10000 === 0) await Utils.wait();
this.database.set(id, { id: emote.value.id || value, type });
}
Logger.log('EmoteModule', ['Loaded emote database']);
}
async loadUserData() {
const userData = await Database.findOne({ type: 'builtin', id: 'EmoteModule' });
if (!userData) return;
if (userData.hasOwnProperty('favourites')) this._favourites = userData.favourites;
if (userData.hasOwnProperty('mostused')) this._mostUsed = userData.mostused;
}
async saveUserData() {
await Database.insertOrUpdate({ type: 'builtin', id: 'EmoteModule' }, {
type: 'builtin',
id: 'EmoteModule',
favourites: this.favourites,
mostused: this.mostUsed
});
}
/**
* Add/update emote to most used
* @param {Object} emote emote to add/update
* @return {Promise}
*/
addToMostUsed(emote) {
const isMostUsed = this.mostUsed.find(mu => mu.key === emote.name);
if (isMostUsed) {
isMostUsed.useCount += 1;
} else {
this.mostUsed.push({
key: emote.name,
id: emote.id,
type: emote.type,
useCount: 1
});
}
// Save most used to database
// TODO only save first n
return this.saveUserData();
}
/**
* Find an emote by name
* @param {String} name Emote name
* @param {Boolean} simple Simple object or Emote instance
* @returns {Object|Emote}
*/
findByName(name, simple = false) {
const emote = this.database.get(name);
if (!emote) return null;
return this.parseEmote(name, emote, simple);
}
/**
* Parse emote object
* @param {String} name Emote name
* @param {Object} emote Emote object
* @param {Boolean} simple Simple object or Emote instance
* @returns {Object|Emote}
*/
parseEmote(name, emote, simple = false) {
const { type, id } = emote;
if (type < 0 || type > 2) return null;
return simple ? { type, id, name } : new Emote(type, id, name);
}
/**
* Search for anything else
* @param {any} regex
* @param {any} limit
*/
search(regex, limit = 10) {
if (typeof regex === 'string') regex = new RegExp(regex, 'i');
const matching = [];
for (const [key, value] of this.database.entries()) {
if (matching.length >= limit) break;
if (regex.test(key)) matching.push({ key, value })
}
return matching;
}
/* Patches */
async applyPatches() {
this.patchMessageContent();
this.patchSendAndEdit();
this.patchSpoiler();
const MessageAccessories = await ReactComponents.getComponent('MessageAccessories');
this.patch(MessageAccessories.component.prototype, 'render', this.afterRenderMessageAccessories, 'after');
MessageAccessories.forceUpdateAll();
}
/**
* Patches MessageContent render method
*/
async patchMessageContent() {
const MessageContent = await ReactComponents.getComponent('MessageContent');
this.childPatch(MessageContent.component.prototype, 'render', ['props', 'children'], this.afterRenderMessageContent);
MessageContent.forceUpdateAll();
}
/**
* Patches MessageActions send and edit
*/
patchSendAndEdit() {
const { MessageActions } = Reflection.modules;
this.patch(MessageActions, 'sendMessage', this.handleSendMessage, 'instead');
this.patch(MessageActions, 'editMessage', this.handleEditMessage, 'instead');
}
async patchSpoiler() {
const Spoiler = await ReactComponents.getComponent('Spoiler');
this.childPatch(Spoiler.component.prototype, 'render', ['props', 'children', 'props', 'children'], this.afterRenderSpoiler);
Spoiler.forceUpdateAll();
}
afterRenderSpoiler(component, _childrenObject, args, retVal) {
const markup = Utils.findInReactTree(retVal, filter =>
filter &&
filter.className &&
filter.className.includes('inlineContent'));
if (!markup) return;
markup.children = this.processMarkup(markup.children);
}
/**
* Handle message render
*/
afterRenderMessageContent(component, _childrenObject, args, retVal) {
const markup = Utils.findInReactTree(retVal, filter =>
filter &&
filter.className &&
filter.className.includes('markup') &&
filter.children.length >= 2);
if (!markup) return;
markup.children[1] = this.processMarkup(markup.children[1]);
}
/**
* Handle send message
*/
async handleSendMessage(MessageActions, args, orig) {
if (!args.length) return orig(...args);
const { content } = args[1];
if (!content) return orig(...args);
Logger.log('EmoteModule', ['Sending message', MessageActions, args, orig]);
const emoteAsImage = Settings.getSetting('emotes', 'default', 'emoteasimage').value &&
(DiscordApi.currentChannel.type === 'DM' || DiscordApi.currentChannel.type === 'GROUP_DM' || DiscordApi.currentChannel.checkPermissions(DiscordApi.modules.DiscordPermissions.ATTACH_FILES));
if (!emoteAsImage || content.split(' ').length > 1) {
args[1].content = args[1].content.split(' ').map(word => {
const isEmote = /;(.*?);/g.exec(word);
if (isEmote) {
const emote = this.findByName(isEmote[1], true);
if (!emote) return word;
this.addToMostUsed(emote);
return emote ? `;${isEmote[1]};` : word;
}
return word;
}).join(' ');
return orig(...args);
}
const isEmote = /;(.*?);/g.exec(content);
if (!isEmote) return orig(...args);
const emote = this.findByName(isEmote[1]);
if (!emote) return orig(...args);
this.addToMostUsed(emote);
const FileActions = Reflection.module.byProps('makeFile');
const Uploader = Reflection.module.byProps('instantBatchUpload');
request.get(emote.props.src, { encoding: 'binary' }).then(res => {
const arr = new Uint8Array(new ArrayBuffer(res.length));
for (let i = 0; i < res.length; i++) arr[i] = res.charCodeAt(i);
const suffix = arr[0] === 71 && arr[1] === 73 && arr[2] === 70 ? '.gif' : '.png';
Uploader.upload(args[0], FileActions.makeFile(arr, `${emote.name}.bdemote${suffix}`));
});
}
/**
* Handle edit message
*/
handleEditMessage(component, args, orig) {
if (!args.length) return orig(...args);
const { content } = args[2];
if (!content) return orig(...args);
args[2].content = args[2].content.split(' ').map(word => {
const isEmote = /;(.*?);/g.exec(word);
return isEmote ? `;${isEmote[1]};` : word;
}).join(' ');
return orig(...args);
}
/**
* Handle MessageAccessories render
*/
afterRenderMessageAccessories(component, args, retVal) {
if (!component.props || !component.props.message) return;
if (!component.props.message.attachments || component.props.message.attachments.length !== 1) return;
const filename = component.props.message.attachments[0].filename;
const match = filename.match(/([^/]*)\.bdemote\.(gif|png)$/i);
if (!match) return;
const emote = this.findByName(match[1]);
if (!emote) return;
emote.jumboable = true;
retVal.props.children[2] = emote.render();
}
/**
* Inject emotes into markup
*/
processMarkup(markup) {
const newMarkup = [];
if (!(markup instanceof Array)) return markup;
const jumboable = !markup.some(child => {
if (typeof child !== 'string') return false;
return / \w+/g.test(child);
});
for (const child of markup) {
if (typeof child !== 'string') {
if (typeof child === 'object') {
const isEmoji = Utils.findInReactTree(child, filter => filter && filter.emojiName);
if (isEmoji) isEmoji.jumboable = jumboable;
}
newMarkup.push(child);
continue;
}
if (!/;(\w+);/g.test(child)) {
newMarkup.push(child);
continue;
}
const words = child.split(/([^\s]+)([\s]|$)/g).filter(f => f !== '');
let s = '';
for (const word of words) {
const isemote = /;(.*?);/g.exec(word);
if (!isemote) {
s += word;
continue;
}
const emote = this.findByName(isemote[1]);
if (!emote) {
s += word;
continue;
}
newMarkup.push(s);
s = '';
emote.jumboable = jumboable;
newMarkup.push(emote.render());
}
if (s !== '') newMarkup.push(s);
}
if (newMarkup.length === 1) return newMarkup[0];
return newMarkup;
}
}

View File

@ -0,0 +1,4 @@
export { default as EmoteModule, EMOTE_SOURCES } from './EmoteModule';
export { default as Emote } from './EmoteComponent';
export { default as EmoteComponent } from './EmoteComponent.vue';
export { default as EmoteAc } from './EmoteAc';

View File

@ -0,0 +1,28 @@
/**
* BetterDiscord Kill Clyde Module
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import BuiltinModule from './BuiltinModule';
import { Reflection } from 'modules';
export default new class KillClyde extends BuiltinModule {
/* Getters */
get moduleName() { return 'KillClyde' }
get settingPath() { return ['ui', 'default', 'kill-clyde'] }
/* Patches */
applyPatches() {
if (this.patches.length) return;
const { MessageActions } = Reflection.modules;
this.patch(MessageActions, 'sendBotMessage', () => void 0, 'instead');
}
}

View File

@ -0,0 +1,30 @@
import { EmoteModule, EmoteAc } from './Emotes';
import ReactDevtoolsModule from './ReactDevtoolsModule';
import VueDevtoolsModule from './VueDevToolsModule';
import TrackingProtection from './TrackingProtection';
import E2EE from './E2EE';
import ColoredText from './ColoredText';
import TwentyFourHour from './24Hour';
import KillClyde from './KillClyde';
import BlockedMessages from './BlockedMessages';
import VoiceDisconnect from './VoiceDisconnect';
export default class {
static get modules() {
return require('./builtin');
}
static initAll() {
EmoteModule.init();
ReactDevtoolsModule.init();
VueDevtoolsModule.init();
TrackingProtection.init();
E2EE.init();
ColoredText.init();
TwentyFourHour.init();
KillClyde.init();
BlockedMessages.init();
VoiceDisconnect.init();
EmoteAc.init();
}
}

View File

@ -0,0 +1,55 @@
/**
* BetterDiscord React Devtools Module
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import electron from 'electron';
import path from 'path';
import BuiltinModule from './BuiltinModule';
import { Globals } from 'modules';
import { Toasts } from 'ui';
export default new class ReactDevtoolsModule extends BuiltinModule {
/* Getters */
get moduleName() { return 'ReactDevTools' }
get settingPath() { return ['core', 'advanced', 'react-devtools'] }
constructor() {
super();
this.devToolsOpened = this.devToolsOpened.bind(this);
}
enabled(e) {
electron.remote.getCurrentWindow().webContents.on('devtools-opened', this.devToolsOpened);
if (electron.remote.getCurrentWindow().isDevToolsOpened) this.devToolsOpened();
}
disabled(e) {
electron.remote.BrowserWindow.removeDevToolsExtension('React Developer Tools');
electron.remote.getCurrentWindow().webContents.removeListener('devtools-opened', this.devToolsOpened);
}
devToolsOpened() {
electron.remote.BrowserWindow.removeDevToolsExtension('React Developer Tools');
try {
const res = electron.remote.BrowserWindow.addDevToolsExtension(path.join(Globals.getPath('ext'), 'extensions', 'rdt'));
if (res !== undefined) {
Toasts.success(`${res} Installed`);
return;
}
Toasts.error('React Developer Tools install failed');
} catch (err) {
Toasts.error('React Developer Tools install failed');
}
}
}

View File

@ -0,0 +1,36 @@
/**
* BetterDiscord Tracking Protection Module
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import BuiltinModule from './BuiltinModule';
import { Reflection } from 'modules';
export default new class TrackingProtection extends BuiltinModule {
/* Getters */
get moduleName() { return 'TrackingProtection' }
get settingPath() { return ['security', 'default', 'tracking-protection'] }
/* Patches */
applyPatches() {
if (this.patches.length) return;
const TrackingModule = Reflection.module.byProps('track');
if (!TrackingModule) {
this.warn('Tracking module not found!');
return;
}
this.patch(TrackingModule, 'track', this.track, 'instead');
}
track(e) {
this.debug('Tracking blocked');
}
}

View File

@ -0,0 +1,35 @@
/**
* BetterDiscord Voice Disconnect Timestamps Module
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import BuiltinModule from './BuiltinModule';
import { Reflection } from 'modules';
export default new class VoiceDisconnect extends BuiltinModule {
/* Getters */
get moduleName() { return 'VoiceDisconnect' }
get settingPath() { return ['core', 'default', 'voice-disconnect'] }
async enabled(e) {
window.addEventListener('beforeunload', this.listener);
}
disabled(e) {
window.removeEventListener('beforeunload', this.listener);
}
/* Methods */
listener() {
const { VoiceChannelActions } = Reflection.modules;
VoiceChannelActions.selectVoiceChannel(null, null);
}
}

View File

@ -0,0 +1,55 @@
/**
* BetterDiscord Vue Devtools Module
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import electron from 'electron';
import path from 'path';
import BuiltinModule from './BuiltinModule';
import { Globals } from 'modules';
import { Toasts } from 'ui';
export default new class VueDevtoolsModule extends BuiltinModule {
/* Getters */
get moduleName() { return 'VueDevTools' }
get settingPath() { return ['core', 'advanced', 'vue-devtools'] }
constructor() {
super();
this.devToolsOpened = this.devToolsOpened.bind(this);
}
enabled(e) {
electron.remote.getCurrentWindow().webContents.on('devtools-opened', this.devToolsOpened);
if (electron.remote.getCurrentWindow().isDevToolsOpened) this.devToolsOpened();
}
disabled(e) {
electron.remote.BrowserWindow.removeDevToolsExtension('Vue.js devtools');
electron.remote.getCurrentWindow().webContents.removeListener('devtools-opened', this.devToolsOpened);
}
devToolsOpened() {
electron.remote.BrowserWindow.removeDevToolsExtension('Vue.js devtools');
try {
const res = electron.remote.BrowserWindow.addDevToolsExtension(path.join(Globals.getPath('ext'), 'extensions', 'vdt'));
if (res !== undefined) {
Toasts.success(`${res} Installed`);
return;
}
Toasts.error('Vue.js devtools install failed');
} catch (err) {
Toasts.error('Vue.js devtools install failed');
}
}
}

View File

@ -1 +1,11 @@
export { default as EmoteModule } from './EmoteModule';
export { EmoteModule, EmoteAc } from './Emotes';
export { default as ReactDevtoolsModule } from './ReactDevtoolsModule';
export { default as VueDevtoolsModule } from './VueDevToolsModule';
export { default as TrackingProtection } from './TrackingProtection';
export { default as BuiltinManager } from './Manager';
export { default as E2EE } from './E2EE';
export { default as ColoredText } from './ColoredText';
export { default as TwentyFourHour } from './24Hour';
export { default as KillClyde } from './KillClyde';
export { default as BlockedMessages } from './BlockedMessages';
export { default as VoiceDisconnect } from './VoiceDisconnect';

View File

@ -0,0 +1,79 @@
[
{
"__user": "Jiiks#5000",
"id": "81388395867156480",
"developer": true,
"webdev": true,
"contributor": true
},
{
"__user": "Pohky#0156",
"id": "98003542823944192",
"developer": true,
"webdev": false,
"contributor": true
},
{
"__user": "Hammock#3110",
"id": "138850472541814784",
"developer": false,
"webdev": true,
"contributor": true
},
{
"__user": "Zerebos#7790",
"id": "249746236008169473",
"developer": true,
"webdev": false,
"contributor": true
},
{
"__user": "Pierce#1337",
"id": "125367412370440192",
"developer": true,
"webdev": false,
"contributor": true
},
{
"__user": "Samuel Elliott#2764",
"id": "284056145272766465",
"developer": true,
"webdev": false,
"contributor": true
},
{
"__user": "Lilian Tedone#6223",
"id": "184021060562321419",
"developer": false,
"webdev": false,
"contributor": true
},
{
"__user": "samfun123#8972",
"id": "76052829285916672",
"developer": false,
"webdev": false,
"contributor": true
},
{
"__user": "samogot#4379",
"id": "171005991272316937",
"developer": false,
"webdev": false,
"contributor": true
},
{
"__user": "Lucario 🌌 V5.0.0#7902",
"id": "438469378418409483",
"developer": false,
"webdev": false,
"contributor": true
},
{
"__user": "Maks#3712",
"id": "340975736037048332",
"developer": false,
"webdev": false,
"contributor": true
}
]

View File

@ -5,31 +5,14 @@
"headertext": "Core Settings",
"settings": [
{
"category": "default",
"id": "default",
"settings": [
{
"id": "test-setting",
"type": "bool",
"text": "Test Setting",
"hint": "Test Setting",
"value": false,
"disabled": false
},
{
"id": "test-setting2",
"type": "bool",
"text": "Test Setting 2",
"hint": "Test Setting 2",
"value": true,
"disabled": false
},
{
"id": "voice-disconnect",
"type": "bool",
"text": "Voice Disconnect",
"hint": "Disconnect from voice server when Discord closes",
"value": false,
"disabled": true
"value": false
},
{
"id": "menu-keybind",
@ -40,8 +23,8 @@
]
},
{
"category": "advanced",
"category_name": "Advanced",
"id": "advanced",
"name": "Advanced",
"type": "drawer",
"settings": [
{
@ -51,12 +34,44 @@
"hint": "Adds some of BetterDiscord's internal modules to `global._bd`.",
"value": false
},
{
"id": "debugger-keybind",
"type": "keybind",
"text": "Debugger keybind",
"hint": "Open developer tools and pause"
},
{
"id": "ignore-content-manager-errors",
"type": "bool",
"text": "Ignore content manager errors",
"hint": "Only when starting Discord. It gets annoying when you're reloading Discord often and have plugins that are meant to fail.",
"value": false
},
{
"id": "react-devtools",
"type": "bool",
"text": "React Developer Tools",
"hint": "Place extension in ext/extensions/rdt",
"value": false
},
{
"id": "vue-devtools",
"type": "bool",
"text": "Vue Developer Tools",
"hint": "Place extension in ext/extensions/vdt",
"value": false
}
]
},
{
"id": "window-preferences",
"name": "Window Preferences",
"type": "drawer",
"settings": [
{
"id": "window-preferences",
"type": "custom",
"component": "WindowPreferences"
}
]
}
@ -68,7 +83,7 @@
"headertext": "UI Settings",
"settings": [
{
"category": "default",
"id": "default",
"settings": [
{
"id": "hide-button",
@ -77,6 +92,59 @@
"hint": "When this is enabled you can use Ctrl/Cmd + B to open the BetterDiscord settings menu.",
"value": false,
"disabled": false
},
{
"id": "enable-toasts",
"type": "bool",
"text": "Enable Toasts",
"hint": "Allows plugins to show toasts.",
"value": true
},
{
"id": "colored-text",
"type": "bool",
"text": "Colored Text",
"hint": "Colors messages to match the user's role color.",
"value": false
},
{
"id": "24-hour",
"type": "bool",
"text": "24 Hour Timestamps",
"hint": "Replaces 12 hour timestamps with proper ones.",
"value": false
},
{
"id": "kill-clyde",
"type": "bool",
"text": "Kill Clyde",
"hint": "Prevents Clyde from sending you error messages.",
"value": false
},
{
"id": "blocked-messages",
"type": "bool",
"text": "Prevent Blocked Messages",
"hint": "Hides blocked messages in chat and even hides the new message notification.",
"value": false
}
]
},
{
"id": "advanced",
"name": "Advanced",
"type": "drawer",
"settings": [
{
"id": "colored-text-intensity",
"type": "slider",
"text": "Colored Text Intensity",
"hint": "Intensity of the colored text setting.",
"value": 100,
"min": 0,
"max": 100,
"step": 1,
"unit": "%"
}
]
}
@ -86,15 +154,47 @@
"id": "emotes",
"text": "Emotes",
"headertext": "Emote Settings",
"settings": []
"settings": [
{
"id": "default",
"settings": [
{
"id": "enable",
"type": "bool",
"text": "Enable emotes",
"value": true
},
{
"id": "emoteasimage",
"type": "bool",
"text": "Image Emote",
"hint": "Send single emotes as images if you have the permission",
"value": true
},
{
"id": "emoteac",
"type": "bool",
"text": "Emote Autocomplete",
"hint": "Autocomplete emotes when typing with ; prefix",
"value": true
},
{
"id": "emoteactype",
"type": "bool",
"text": "Show most used instead of favourites",
"hint": "Toggle with arrow keys in autocomplete menu",
"value": true
}
]
}
]
},
{
"id": "css",
"text": "CSS Editor",
"hidden": true,
"settings": [
{
"category": "default",
"id": "default",
"settings": [
{
"id": "live-update",
@ -116,10 +216,96 @@
},
{
"id": "security",
"text": "Security",
"headertext": "Security Settings",
"text": "Security and Privacy",
"headertext": "Security and Privacy Settings",
"settings": [
{
"id": "default",
"settings": [
{
"id": "unsafe-content",
"type": "bool",
"text": "Allow unverified content",
"hint": "Allow loading unverified plugins/themes",
"value": "false"
},
{
"id": "tracking-protection",
"type": "bool",
"text": "Tracking Protection",
"hint": "Disable any Discord tracking",
"value": false
},
{
"id": "e2ee",
"type": "bool",
"text": "E2EE",
"hint": "End-to-end encryption",
"value": false
},
{
"id": "use-keytar",
"type": "bool",
"text": "Use system keychain",
"hint": "Store the master password in the system keychain",
"value": true
}
]
},
{
"id": "e2eedb",
"name": "E2EE Database",
"type": "drawer",
"settings": [
{
"id": "e2ekvps",
"type": ["securekvp"],
"value": []
}
]
},
{
"id": "e2ee-popout",
"name": "E2EE Popout",
"type": "drawer",
"settings": [
{
"id": "position",
"type": "radio",
"text": "Position",
"value": "above",
"fullwidth": true,
"options": [
{
"text": "Above the lock icon",
"value": "above"
},
{
"text": "Over the lock icon",
"value": "over"
}
]
},
{
"id": "trigger",
"type": "radio",
"text": "Trigger",
"hint": "Only applies when the popout is over the lock icon",
"value": "click",
"fullwidth": true,
"options": [
{
"text": "After clicking the lock icon",
"value": "click"
},
{
"text": "When hovering over the lock icon",
"value": "hover"
}
]
}
]
}
]
}
]

232
client/src/dev/serveremu.js Normal file
View File

@ -0,0 +1,232 @@
const dummyTags = ['dark', 'light', 'simple', 'minimal', 'extra', 'something', 'tag', 'whatever', 'another', 'transparent'];
export default class ServerEmu {
static async themes(args) {
if (!this._themes) this._themes = this.generateThemes();
await new Promise(r => setTimeout(r, Math.random() * 3000));
let docs = [];
if (args && args.sterm) {
const { sterm } = args;
const reg = new RegExp(sterm, 'gi');
docs = this._themes.filter(doc => doc.tags.includes(sterm) || reg.exec(doc.name) || reg.exec(doc.description));
} else {
docs = this._themes;
}
if (args.sort) {
switch (args.sort) {
case 'updated':
if (args.ascending) docs = docs.sort((docA, docB) => new Date(docA.updated).getTime() - new Date(docB.updated).getTime());
else docs = docs.sort((docA, docB) => new Date(docB.updated).getTime() - new Date(docA.updated).getTime());
break;
case 'installs':
if (args.ascending) docs = docs.sort((docA, docB) => docA.installs - docB.installs);
else docs = docs.sort((docA, docB) => docB.installs - docA.installs);
break;
case 'users':
if (args.ascending) docs = docs.sort((docA, docB) => docA.activeUsers - docB.activeUsers);
else docs = docs.sort((docA, docB) => docB.activeUsers - docA.activeUsers);
break;
case 'rating':
if (args.ascending) docs = docs.sort((docA, docB) => docA.rating - docB.rating);
else docs = docs.sort((docA, docB) => docB.rating - docA.rating);
break;
}
}
const total = docs.length;
const pages = Math.ceil(total / 9);
let page = 1;
if (args && args.page) {
page = args.page;
docs = docs.slice((page - 1) * 9, page * 9);
} else {
docs = docs.slice(0, 9);
}
return {
docs,
filters: {
sterm: args.sterm || '',
ascending: args.ascending || false,
sort: args.sort || 'name'
},
pagination: {
total,
pages,
limit: 9,
page
}
}
}
static async plugins(args) {
if (!this._plugins) this._plugins = this.generatePlugins();
await new Promise(r => setTimeout(r, Math.random() * 3000));
let docs = [];
if (args && args.sterm) {
const { sterm } = args;
const reg = new RegExp(sterm, 'gi');
docs = this._plugins.filter(doc => doc.tags.includes(sterm) || reg.exec(doc.name) || reg.exec(doc.description));
} else {
docs = this._plugins;
}
if (args.sort) {
switch (args.sort) {
case 'updated':
if (args.ascending) docs = docs.sort((docA, docB) => new Date(docA.updated).getTime() - new Date(docB.updated).getTime());
else docs = docs.sort((docA, docB) => new Date(docB.updated).getTime() - new Date(docA.updated).getTime());
break;
case 'installs':
if (args.ascending) docs = docs.sort((docA, docB) => docA.installs - docB.installs);
else docs = docs.sort((docA, docB) => docB.installs - docA.installs);
break;
case 'users':
if (args.ascending) docs = docs.sort((docA, docB) => docA.activeUsers - docB.activeUsers);
else docs = docs.sort((docA, docB) => docB.activeUsers - docA.activeUsers);
break;
case 'rating':
if (args.ascending) docs = docs.sort((docA, docB) => docA.rating - docB.rating);
else docs = docs.sort((docA, docB) => docB.rating - docA.rating);
break;
}
}
const total = docs.length;
const pages = Math.ceil(total / 9);
let page = 1;
if (args && args.page) {
page = args.page;
docs = docs.slice((page - 1) * 9, page * 9);
} else {
docs = docs.slice(0, 9);
}
return {
docs,
filters: {
sterm: args.sterm || '',
ascending: args.ascending || false,
sort: args.sort || 'name'
},
pagination: {
total,
pages,
limit: 9,
page
}
}
}
static generateThemes() {
const docs = [];
const count = Math.floor(Math.random() * 50 + 30);
for (let i = 0; i < count; i++) {
const id = `theme${i}-${this.randomId()}`;
const name = `Dummy Theme ${i}`;
const tags = dummyTags.sort(() => .5 - Math.random()).slice(0, 3);
docs.push({
id,
name,
tags,
installs: Math.floor(Math.random() * 5000) + 5000,
updated: this.randomTimestamp(),
rating: Math.floor(Math.random() * 500) + 500,
activeUsers: Math.floor(Math.random() * 1000) + 1000,
rated: Math.random() > .5,
version: this.randomVersion(),
repository: this.dummyThemeRepo,
files: this.dummyFiles,
author: this.dummyAuthor,
description: ''
});
}
return docs;
}
static generatePlugins() {
const docs = [];
const count = Math.floor(Math.random() * 50 + 30);
for (let i = 0; i < count; i++) {
const id = `plugin${i}-${this.randomId()}`;
const name = `Dummy Plugin ${i}`;
const tags = dummyTags.sort(() => .5 - Math.random()).slice(0, 3);
docs.push({
id,
name,
tags,
installs: Math.floor(Math.random() * 5000) + 5000,
updated: this.randomTimestamp(),
rating: Math.floor(Math.random() * 500) + 500,
activeUsers: Math.floor(Math.random() * 1000) + 1000,
rated: Math.random() > .5,
version: this.randomVersion(),
repository: this.dummyPluginRepo,
files: this.dummyFiles,
author: this.dummyAuthor,
description: ''
});
}
return docs;
}
static get dummyThemeRepo() {
return {
name: 'ExampleRepository',
baseUri: 'https://github.com/Jiiks/ExampleRepository',
rawUri: 'https://github.com/Jiiks/ExampleRepository/raw/master',
assetUri: 'https://api.github.com/repos/Jiiks/ExampleRepository/releases/assets/10023264'
}
}
static get dummyPluginRepo() {
return {
name: 'ExampleRepository',
baseUri: 'https://github.com/Jiiks/ExampleRepository',
rawUri: 'https://github.com/Jiiks/ExampleRepository/raw/master',
assetUri: 'https://api.github.com/repos/Jiiks/ExampleRepository/releases/assets/10023265'
}
}
static get dummyFiles() {
return {
readme: 'Example/readme.md',
previews: [{
large: 'Example/preview1-big.png',
thumb: 'Example/preview1-small.png'
}]
}
}
static get dummyAuthor() {
return 'Someone';
}
static randomId() {
return btoa(Math.random()).substring(3, 9);
}
static randomTimestamp() {
return `2018-${Math.floor((Math.random() * 12) + 1).toString().padStart(2, '0')}-${Math.floor((Math.random() * 30) + 1).toString().padStart(2, '0')}T14:51:32.057Z`;
}
static randomVersion() {
return `${Math.round(Math.random() * 3)}.${Math.round(Math.random() * 10)}.${Math.round(Math.random() * 10)}`;
}
}

View File

@ -8,41 +8,47 @@
* LICENSE file in the root directory of this source tree.
*/
import { DOM, BdUI, Modals, Reflection } from 'ui';
import { DOM, BdUI, BdMenu, Modals, Toasts, Notifications, BdContextMenu, DiscordContextMenu, Autocomplete } from 'ui';
import BdCss from './styles/index.scss';
import { Patcher, MonkeyPatch, Vendor, Events, CssEditor, Globals, ExtModuleManager, PluginManager, ThemeManager, ModuleManager, WebpackModules, Settings, Database, ReactComponents, ReactAutoPatcher, DiscordApi } from 'modules';
import { ClientLogger as Logger, ClientIPC, Utils } from 'common';
import { EmoteModule } from 'builtin';
const ignoreExternal = false;
const DEV = true;
import { Events, Globals, Settings, Database, Updater, ModuleManager, PluginManager, ThemeManager, ExtModuleManager, Vendor, Patcher, MonkeyPatch, ReactComponents, ReactHelpers, ReactAutoPatcher, DiscordApi, BdWebApi, Connectivity, Cache, Reflection, PackageInstaller } from 'modules';
import { ClientLogger as Logger, ClientIPC, Utils, Axi } from 'common';
import { BuiltinManager, EmoteModule, ReactDevtoolsModule, VueDevtoolsModule, TrackingProtection, E2EE } from 'builtin';
import electron from 'electron';
import path from 'path';
const tests = typeof PRODUCTION === 'undefined';
const ignoreExternal = tests && true;
class BetterDiscord {
constructor() {
Logger.file = tests ? path.resolve(__dirname, '..', '..', 'tests', 'log.txt') : path.join(__dirname, 'log.txt');
Logger.trimLogFile();
Logger.log('main', 'BetterDiscord starting');
this._bd = {
DOM,
BdUI,
Modals,
Reflection,
Patcher,
MonkeyPatch,
DOM, BdUI, BdMenu, Modals, Reflection, Toasts, Notifications, BdContextMenu, DiscordContextMenu, Autocomplete,
Events, Globals, Settings, Database, Updater,
ModuleManager, PluginManager, ThemeManager, ExtModuleManager, PackageInstaller,
Vendor,
Events,
CssEditor,
Globals,
ExtModuleManager,
PluginManager,
ThemeManager,
ModuleManager,
WebpackModules,
Settings,
Database,
ReactComponents,
DiscordApi,
Logger,
ClientIPC,
Utils,
EmoteModule
Patcher, MonkeyPatch, ReactComponents, ReactHelpers, ReactAutoPatcher, DiscordApi,
BuiltinManager, EmoteModule,
BdWebApi,
Connectivity,
Cache,
Logger, ClientIPC, Utils, Axi,
plugins: PluginManager.localContent,
themes: ThemeManager.localContent,
extmodules: ExtModuleManager.localContent,
__filename, __dirname,
module: Globals.require.cache[__filename],
require: Globals.require,
webpack_require: __webpack_require__, // eslint-disable-line no-undef
get discord_require() { return Reflection.require }
};
const developermode = Settings.getSetting('core', 'advanced', 'developer-mode');
@ -52,45 +58,84 @@ class BetterDiscord {
else if (window._bd) delete window._bd;
});
const debuggerkeybind = Settings.getSetting('core', 'advanced', 'debugger-keybind');
debuggerkeybind.on('keybind-activated', () => {
const currentWindow = electron.remote.getCurrentWindow();
if (currentWindow.isDevToolsOpened()) return eval('debugger;');
currentWindow.openDevTools();
setTimeout(() => eval('debugger;'), 1000);
});
DOM.injectStyle(BdCss, 'bdmain');
this.globalReady = this.globalReady.bind(this);
Events.on('global-ready', this.globalReady);
Globals.initg();
}
async init() {
try {
await Database.init();
await Settings.loadSettings();
await ModuleManager.initModules();
Modals.showContentManagerErrors();
if (!ignoreExternal) {
await ExtModuleManager.loadAllModules(true);
await PluginManager.loadAllPlugins(true);
await ThemeManager.loadAllThemes(true);
}
if (!Settings.get('core', 'advanced', 'ignore-content-manager-errors'))
Modals.showContentManagerErrors();
Events.emit('ready');
Events.emit('discord-ready');
EmoteModule.observe();
} catch (err) {
Logger.err('main', ['FAILED TO LOAD!', err]);
}
}
globalReady() {
BdUI.initUiEvents();
this.vueInstance = BdUI.injectUi();
this.init();
}
async init() {
try {
await Database.init();
await Settings.loadSettings();
await ModuleManager.initModules();
BuiltinManager.initAll();
if (tests) this.initTests();
if (!ignoreExternal) {
await ExtModuleManager.loadAllModules(true);
await PluginManager.loadAllPlugins(true);
await ThemeManager.loadAllThemes(true);
}
Events.emit('ready');
Events.emit('discord-ready');
if (!Settings.get('core', 'advanced', 'ignore-content-manager-errors'))
Modals.showContentManagerErrors();
} catch (err) {
Logger.err('main', ['FAILED TO LOAD!', err]);
}
}
initTests() {
let notifications = 0;
function showDummyNotif() { // eslint-disable-line no-inner-declarations
Notifications.add(notifications++ ? `Notification ${notifications}` : undefined, 'Dummy Notification', [
{
text: 'Show Again', onClick: function () {
setTimeout(showDummyNotif, 5000);
return true;
}
},
{
text: 'Ignore', onClick: function () {
return true;
}
}
]);
}
showDummyNotif();
DiscordContextMenu.add([
{
text: 'Hello',
onClick: () => { Toasts.info('Hello!'); }
}
]);
}
}
if (window.BetterDiscord) {
Logger.log('main', 'Attempting to inject again?');
} else {
let instance = null;
let instance;
Events.on('autopatcher', () => instance = new BetterDiscord());
ReactAutoPatcher.autoPatch().then(() => Events.emit('autopatcher'));
}

View File

@ -0,0 +1,82 @@
/**
* BetterDiscord Web Apis
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import ServerEmu from '../dev/serveremu';
import { request } from 'vendor';
const APIBASE = 'ifyouareinwebtestthenyouknowwhatthisshouldbe'; // Do not push
const ENDPOINTS = {
'themes': `${APIBASE}/themes`,
'theme': id => `${APIBASE}/theme/${id}`,
'users': `${APIBASE}/users`,
'user': id => `${APIBASE}/user/${id}`,
'statistics': `${APIBASE}/statistics`
};
export default class BdWebApi {
static get themes() {
return {
get: this.getThemes
};
}
static get plugins() {
return {
get: this.getPlugins
};
}
static get users() {
return {
get: this.getUsers
};
}
static get statistics() {
return {
get: this.getStatistics,
patch: this.patchStatistics
};
}
static getThemes(args) {
return ServerEmu.themes(args);
// return dummyThemes();
/*
if (!args) return request.get(ENDPOINTS.themes);
const { id } = args;
if (id) return request.get(ENDPOINTS.theme(id));
return request.get(ENDPOINTS.themes);
*/
}
static getPlugins(args) {
return ServerEmu.plugins(args);
}
static getUsers(args) {
if (!args) return request.get(ENDPOINTS.users);
const { id } = args;
if (id) return request.get(ENDPOINTS.user(id));
return request.get(ENDPOINTS.users);
}
static getStatistics() {
return request.get(ENDPOINTS.statistics);
}
static patchStatistics(json) {
return request({ method: 'PATCH', url: ENDPOINTS.statistics, json });
}
}

View File

@ -0,0 +1,38 @@
/**
* BetterDiscord Cache Module
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
const CACHE = [];
export default class Cache {
static get cache() {
return CACHE;
}
/**
* Push something to cache
* @param {String} where Cache identifier
* @param {any} data Data to push
*/
static push(where, data) {
if (!this.cache[where]) this.cache[where] = [];
this.cache[where].push(data);
}
/**
* Find something in cache
* @param {String} where Cache identifier
* @param {Function} what Find callback
*/
static find(where, what) {
if (!this.cache[where]) this.cache[where] = [];
return this.cache[where].find(what);
}
}

View File

@ -0,0 +1,25 @@
/**
* BetterDiscord Connectivity Module
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import BdWebApi from './bdwebapi';
import { ClientLogger as Logger } from 'common';
export default class Connectivity {
static start() {
Logger.info('Connectivity', `Patching anonymous statistics`);
BdWebApi.statistics.patch({ themes: [], plugins: [] });
setInterval(() => {
Logger.info('Connectivity', `Patching anonymous statistics`);
BdWebApi.statistics.patch({ themes: [], plugins: [] });
}, 15*60*1000);
}
}

View File

@ -8,20 +8,26 @@
* LICENSE file in the root directory of this source tree.
*/
import { Utils, FileUtils, ClientLogger as Logger, AsyncEventEmitter } from 'common';
import { Utils, ClientLogger as Logger, AsyncEventEmitter } from 'common';
import { Modals } from 'ui';
import Database from './database';
export default class Content {
export default class Content extends AsyncEventEmitter {
constructor(internals) {
super();
internals.loaded = Date.now();
internals.started = undefined;
internals.stopped = undefined;
Utils.deepfreeze(internals.info);
Object.freeze(internals.paths);
this.__internals = internals;
this.settings.on('setting-updated', event => this.events.emit('setting-updated', event));
this.settings.on('settings-updated', event => this.events.emit('settings-updated', event));
this.settings.on('setting-updated', event => this.emit('setting-updated', event));
this.settings.on('settings-updated', event => this.emit('settings-updated', event));
this.settings.on('settings-updated', event => this.__settingsUpdated(event));
// Add hooks
@ -49,16 +55,23 @@ export default class Content {
get description() { return this.info.description }
get authors() { return this.info.authors }
get version() { return this.info.version }
get loadedTimestamp() { return this.__internals.loaded }
get startedTimestamp() { return this.__internals.started }
get stoppedTimestamp() { return this.__internals.stopped }
get contentPath() { return this.paths.contentPath }
get dirName() { return this.paths.dirName }
get enabled() { return this.userConfig.enabled }
get settings() { return this.userConfig.config }
get config() { return this.settings.categories }
get data() { return this.userConfig.data || (this.userConfig.data = {}) }
get events() { return this.EventEmitter || (this.EventEmitter = new AsyncEventEmitter()) }
get packed() { return this.dirName.packed }
get packagePath() { return this.dirName.packagePath }
get packageName() { return this.dirName.pkg }
/**
* Opens a settings modal for this content.
* @return {Modal}
*/
showSettingsModal() {
return Modals.contentSettings(this);
@ -73,27 +86,20 @@ export default class Content {
/**
* Saves the content's current configuration.
* @return {Promise}
*/
async saveConfiguration() {
try {
/*
await FileUtils.writeFile(`${this.contentPath}/user.config.json`, JSON.stringify({
enabled: this.enabled,
config: this.settings.strip().settings,
data: this.data
}));
*/
Database.insertOrUpdate({ type: 'contentconfig', $or: [{ id: this.id }, { name: this.name }] }, {
type: 'contentconfig',
Database.insertOrUpdate({ type: `${this.type}-config`, id: this.id }, {
type: `${this.type}-config`,
id: this.id,
name: this.name,
enabled: this.enabled,
config: this.settings.strip().settings,
data: this.data
});
this.settings.setSaved();
this.settings.setSaved();
} catch (err) {
Logger.err(this.name, ['Failed to save configuration', err]);
Logger.err(this.name, ['Failed to save configuration', err]);
throw err;
}
}
@ -116,6 +122,8 @@ export default class Content {
await this.emit('enable');
await this.emit('start');
this.__internals.started = Date.now();
this.__internals.stopped = undefined;
this.userConfig.enabled = true;
if (save) await this.saveConfiguration();
}
@ -130,48 +138,12 @@ export default class Content {
await this.emit('stop');
await this.emit('disable');
this.__internals.started = undefined;
this.__internals.stopped = Date.now();
this.userConfig.enabled = false;
if (save) await this.saveConfiguration();
}
/**
* Adds an event listener.
* @param {String} event The event to add the listener to
* @param {Function} callback The function to call when the event is emitted
*/
on(...args) {
return this.events.on(...args);
}
/**
* Removes an event listener.
* @param {String} event The event to remove the listener from
* @param {Function} callback The bound callback (optional)
*/
off(...args) {
return this.events.removeListener(...args);
}
/**
* Adds an event listener that removes itself when called, therefore only being called once.
* @param {String} event The event to add the listener to
* @param {Function} callback The function to call when the event is emitted
* @return {Promise|undefined}
*/
once(...args) {
return this.events.once(...args);
}
/**
* Emits an event.
* @param {String} event The event to emit
* @param {Any} data Data to be passed to listeners
* @return {Promise|undefined}
*/
emit(...args) {
return this.events.emit(...args);
}
}
Object.freeze(Content.prototype);

View File

@ -8,15 +8,19 @@
* LICENSE file in the root directory of this source tree.
*/
import asar from 'asar';
import path, { dirname } from 'path';
import rimraf from 'rimraf';
import { remote } from 'electron';
import Content from './content';
import Globals from './globals';
import Database from './database';
import { Utils, FileUtils, ClientLogger as Logger } from 'common';
import { Events } from 'modules';
import { SettingsSet, ErrorEvent } from 'structs';
import { Modals } from 'ui';
import path from 'path';
import Combokeys from 'combokeys';
import Settings from './settings';
/**
* Base class for managing external content
@ -24,31 +28,74 @@ import Combokeys from 'combokeys';
export default class {
/**
* Any errors that happened
* returns {Array}
* Any errors that happened.
* @return {Array}
*/
static get errors() {
return this._errors || (this._errors = []);
}
/**
* Locallly stored content
* returns {Array}
* Locally stored content.
* @return {Array}
*/
static get localContent() {
return this._localContent ? this._localContent : (this._localContent = []);
}
/**
* Local path for content
* returns {String}
* The type of content this content manager manages.
*/
static get contentType() {
return undefined;
}
/**
* The name of this content manager.
*/
static get moduleName() {
return undefined;
}
/**
* The path used to store this content manager's content.
*/
static get pathId() {
return undefined;
}
/**
* Local path for content.
* @return {String}
*/
static get contentPath() {
return Globals.getPath(this.pathId);
}
static async packContent(path, contentPath) {
return new Promise((resolve, reject) => {
remote.dialog.showSaveDialog({
title: 'Save Package',
defaultPath: path,
filters: [
{
name: 'BetterDiscord Package',
extensions: ['bd']
}
]
}, filepath => {
if (!filepath) return;
asar.uncache(filepath);
asar.createPackage(contentPath, filepath, () => {
resolve(filepath);
});
});
});
}
/**
* Load all locally stored content
* Load all locally stored content.
* @param {bool} suppressErrors Suppress any errors that occur during loading of content
*/
static async loadAllContent(suppressErrors = false) {
@ -56,13 +103,21 @@ export default class {
await FileUtils.ensureDirectory(this.contentPath);
const directories = await FileUtils.listDirectory(this.contentPath);
for (let dir of directories) {
try {
await FileUtils.directoryExists(path.join(this.contentPath, dir));
} catch (err) { continue; }
for (const dir of directories) {
const packed = dir.endsWith('.bd');
if (!packed) {
try {
await FileUtils.directoryExists(path.join(this.contentPath, dir));
} catch (err) { continue; }
}
try {
await this.preloadContent(dir);
if (packed) {
await this.preloadPackedContent(dir);
} else {
await this.preloadContent(dir);
}
} catch (err) {
this.errors.push(new ErrorEvent({
module: this.moduleName,
@ -83,8 +138,6 @@ export default class {
});
this._errors = [];
}
return this.localContent;
} catch (err) {
throw err;
}
@ -101,8 +154,10 @@ export default class {
await FileUtils.ensureDirectory(this.contentPath);
const directories = await FileUtils.listDirectory(this.contentPath);
for (let dir of directories) {
// If content is already loaded this should resolve.
for (const dir of directories) {
const packed = dir.endsWith('.bd');
// If content is already loaded this should resolve
if (this.getContentByDirName(dir)) continue;
try {
@ -124,7 +179,7 @@ export default class {
}
}
for (let content of this.localContent) {
for (const content of this.localContent) {
if (directories.includes(content.dirName)) continue;
try {
@ -150,13 +205,36 @@ export default class {
});
this._errors = [];
}
return this.localContent;
} catch (err) {
throw err;
}
}
static async preloadPackedContent(pkg, reload = false, index) {
try {
const packagePath = path.join(this.contentPath, pkg);
const packageName = pkg.replace('.bd', '');
await FileUtils.fileExists(packagePath);
const config = JSON.parse(asar.extractFile(packagePath, 'config.json').toString());
const unpackedPath = path.join(Globals.getPath('tmp'), packageName);
asar.extractAll(packagePath, unpackedPath);
return this.preloadContent({
config,
contentPath: unpackedPath,
packagePath: packagePath,
pkg,
packageName,
packed: true
}, reload, index);
} catch (err) {
Logger.log('ContentManager', ['Error extracting packed content', err]);
throw err;
}
}
/**
* Common loading procedure for loading content before passing it to the actual loader
* @param {any} dirName Base directory for content
@ -165,19 +243,24 @@ export default class {
*/
static async preloadContent(dirName, reload = false, index) {
try {
const contentPath = path.join(this.contentPath, dirName);
const unsafeAllowed = Settings.getSetting('security', 'default', 'unsafe-content').value;
const packed = typeof dirName === 'object' && dirName.packed;
// Block any unpacked content as they can't be verified
if (!packed && !unsafeAllowed) {
throw 'Blocked unsafe content';
}
const contentPath = packed ? dirName.contentPath : path.join(this.contentPath, dirName);
await FileUtils.directoryExists(contentPath);
if (!reload) {
const loaded = this.localContent.find(content => content.contentPath === contentPath);
if (loaded) {
throw { 'message': `Attempted to load already loaded user content: ${path}` };
}
}
if (!reload && this.getContentByPath(contentPath))
throw { 'message': `Attempted to load already loaded user content: ${path}` };
const readConfig = await this.readConfig(contentPath);
const mainPath = path.join(contentPath, readConfig.main);
const configPath = path.resolve(contentPath, 'config.json');
const readConfig = packed ? dirName.config : await FileUtils.readJsonFromFile(configPath);
const mainPath = path.join(contentPath, readConfig.main || 'index.js');
const defaultConfig = new SettingsSet({
settings: readConfig.defaultConfig,
@ -191,30 +274,31 @@ export default class {
};
try {
//const readUserConfig = await this.readUserConfig(contentPath);
const readUserConfig = await Database.find({ type: 'contentconfig', name: readConfig.info.name });
const id = readConfig.info.id || readConfig.info.name.toLowerCase().replace(/[^a-zA-Z0-9-]/g, '-').replace(/--/g, '-');
const readUserConfig = await Database.find({ type: `${this.contentType}-config`, id });
if (readUserConfig.length) {
userConfig.enabled = readUserConfig[0].enabled || false;
// await userConfig.config.merge({ settings: readUserConfig.config });
// userConfig.config.setSaved();
// userConfig.config = userConfig.config.clone({ settings: readUserConfig.config });
userConfig.config = readUserConfig[0].config;
userConfig.data = readUserConfig[0].data || {};
}
} catch (err) { /*We don't care if this fails it either means that user config doesn't exist or there's something wrong with it so we revert to default config*/
Logger.info(this.moduleName, `Failed reading config for ${this.contentType} ${readConfig.info.name} in ${dirName}`);
Logger.err(this.moduleName, err);
} catch (err) {
// We don't care if this fails it either means that user config doesn't exist or there's something wrong with it so we revert to default config
Logger.warn(this.moduleName, [`Failed reading config for ${this.contentType} ${readConfig.info.name} in ${packed ? dirName.pkg : dirName}`, err]);
}
userConfig.config = defaultConfig.clone({ settings: userConfig.config });
userConfig.config.setSaved();
for (let setting of userConfig.config.findSettings(() => true)) {
for (const setting of userConfig.config.findSettings(() => true)) {
// This will load custom settings
// Setting the content's path on only the live config (and not the default config) ensures that custom settings will not be loaded on the default settings
setting.setContentPath(contentPath);
}
for (const scheme of userConfig.config.schemes) {
scheme.setContentPath(contentPath);
}
Utils.deepfreeze(defaultConfig, object => object instanceof Combokeys);
const configs = {
@ -229,45 +313,43 @@ export default class {
mainPath
};
const content = await this.loadContent(paths, configs, readConfig.info, readConfig.main, readConfig.dependencies, readConfig.permissions);
const content = await this.loadContent(paths, configs, readConfig.info, readConfig.main, readConfig.dependencies, readConfig.permissions, readConfig.mainExport, packed ? dirName : false);
if (!content) return undefined;
if (!reload && this.getContentById(content.id))
throw {message: `A ${this.contentType} with the ID ${content.id} already exists.`};
throw { message: `A ${this.contentType} with the ID ${content.id} already exists.` };
if (reload) this.localContent[index] = content;
if (reload) this.localContent.splice(index, 1, content);
else this.localContent.push(content);
return content;
} catch (err) {
throw err;
}
}
/**
* Unload content
* @param {any} content Content to unload
* @param {bool} reload Whether to reload the content after
* Delete content.
* @param {Content|String} content Content to delete
* @param {Boolean} force If true the content will be deleted even if an exception is thrown when disabling/unloading/deleting
*/
static async unloadContent(content, reload) {
static async deleteContent(content, force) {
content = this.findContent(content);
if (!content) throw {message: `Could not find a ${this.contentType} from ${content}.`};
try {
await content.disable(false);
await content.emit('unload', reload);
await Modals.confirm(`Delete ${this.contentType}?`, `Are you sure you want to delete ${content.info.name} ?`, 'Delete').promise;
} catch (err) {
return false;
}
const index = this.getContentIndex(content);
try {
const unload = this.unloadContent(content, force, false);
delete window.require.cache[window.require.resolve(content.paths.mainPath)];
if (!force)
await unload;
if (reload) {
const newcontent = await this.preloadContent(content.dirName, true, index);
if (newcontent.enabled) {
newcontent.userConfig.enabled = false;
newcontent.start(false);
}
return newcontent;
} else this.localContent.splice(index, 1);
await FileUtils.recursiveDeleteDirectory(content.paths.contentPath);
if (content.packed) await FileUtils.recursiveDeleteDirectory(content.packagePath);
return true;
} catch (err) {
Logger.err(this.moduleName, err);
throw err;
@ -275,34 +357,52 @@ export default class {
}
/**
* Reload content
* @param {any} content Content to reload
* Unload content.
* @param {Content|String} content Content to unload
* @param {Boolean} force If true the content will be unloaded even if an exception is thrown when disabling/unloading
* @param {Boolean} reload Whether to reload the content after
* @return {Content}
*/
static reloadContent(content) {
return this.unloadContent(content, true);
static async unloadContent(content, force, reload) {
content = this.findContent(content);
if (!content) throw {message: `Could not find a ${this.contentType} from ${content}.`};
try {
const disablePromise = content.disable(false);
const unloadPromise = content.emit('unload', reload);
if (!force) {
await disablePromise;
await unloadPromise;
}
const index = this.getContentIndex(content);
if (this.unloadContentHook) this.unloadContentHook(content);
if (reload) return content.packed ? this.preloadPackedContent(content.packagePath, true, index) : this.preloadContent(content.dirName, true, index);
this.localContent.splice(index, 1);
} catch (err) {
Logger.err(this.moduleName, err);
throw err;
}
}
/**
* Read content config file
* @param {any} configPath Config file path
* Reload content.
* @param {Content|String} content Content to reload
* @param {Boolean} force If true the content will be unloaded even if an exception is thrown when disabling/unloading
* @return {Content}
*/
static async readConfig(configPath) {
configPath = path.resolve(configPath, 'config.json');
return FileUtils.readJsonFromFile(configPath);
}
/**
* Read content user config file
* @param {any} configPath User config file path
*/
static async readUserConfig(configPath) {
configPath = path.resolve(configPath, 'user.config.json');
return FileUtils.readJsonFromFile(configPath);
static reloadContent(content, force) {
return this.unloadContent(content, force, true);
}
/**
* Checks if the passed object is an instance of this content type.
* @param {any} content Object to check
* @param {Any} content Object to check
* @return {Boolean}
*/
static isThisContent(content) {
return content instanceof Content;
@ -318,8 +418,9 @@ export default class {
/**
* Wildcard content finder
* @param {any} wild Content ID / directory name / path / name
* @param {bool} nonunique Allow searching attributes that may not be unique
* @param {String} wild Content ID / directory name / path / name
* @param {Boolean} nonunique Allow searching attributes that may not be unique
* @return {Content}
*/
static findContent(wild, nonunique) {
if (this.isThisContent(wild)) return wild;
@ -338,18 +439,11 @@ export default class {
/**
* Wait for content to load
* @param {any} content_id
* @param {String} content_id
* @return {Promise => Content}
*/
static waitForContent(content_id) {
return new Promise((resolve, reject) => {
const check = () => {
const content = this.getContentById(content_id);
if (content) return resolve(content);
setTimeout(check, 100);
};
check();
});
return Utils.until(() => this.getContentById(content_id), 100);
}
}

View File

@ -8,12 +8,12 @@
* LICENSE file in the root directory of this source tree.
*/
import { FileUtils, ClientLogger as Logger, ClientIPC } from 'common';
import Settings from './settings';
import { DOM } from 'ui';
import filewatcher from 'filewatcher';
import { FileUtils, ClientLogger as Logger, ClientIPC } from 'common';
import path from 'path';
import electron from 'electron';
import filewatcher from 'filewatcher';
import Settings from './settings';
/**
* Custom css editor communications
@ -32,24 +32,32 @@ export default new class {
}
/**
* Init css editor
* Init css editor.
*/
init() {
ClientIPC.on('bd-get-scss', () => this.sendToEditor('set-scss', { scss: this.scss }));
ClientIPC.on('bd-get-scss', () => this.scss, true);
ClientIPC.on('bd-update-scss', (e, scss) => this.updateScss(scss));
ClientIPC.on('bd-save-csseditor-bounds', (e, bounds) => this.saveEditorBounds(bounds));
ClientIPC.on('bd-editor-runScript', (e, script) => {
try {
new Function(script)();
e.reply('ok');
} catch (err) {
e.reply({ err: err.stack || err });
}
});
ClientIPC.on('bd-save-scss', async (e, scss) => {
await this.updateScss(scss);
await this.save();
});
}, true);
this.liveupdate = Settings.getSetting('css', 'default', 'live-update');
this.liveupdate.on('setting-updated', event => {
this.sendToEditor('set-liveupdate', event.value);
});
ClientIPC.on('bd-get-liveupdate', () => this.sendToEditor('set-liveupdate', this.liveupdate.value));
ClientIPC.on('bd-get-liveupdate', () => this.liveupdate.value, true);
ClientIPC.on('bd-set-liveupdate', (e, value) => this.liveupdate.value = value);
this.watchfilessetting = Settings.getSetting('css', 'default', 'watch-files');
@ -60,22 +68,22 @@ export default new class {
}
/**
* Show css editor, flashes if already visible
* Show css editor, flashes if already visible.
*/
async show() {
await ClientIPC.send('openCssEditor', this.editor_bounds);
}
/**
* Update css in client
* @param {String} scss scss to compile
* @param {bool} sendSource send to css editor instance
* Update css in client.
* @param {String} scss SCSS to compile
* @param {bool} sendSource Whether to send to css editor instance
*/
async updateScss(scss, sendSource) {
if (sendSource)
this.sendToEditor('set-scss', { scss });
this.sendToEditor('set-scss', scss);
if (!scss) {
if (!scss && !await this.fileExists()) {
this._scss = this.css = '';
this.sendToEditor('scss-error', null);
return;
@ -97,51 +105,55 @@ export default new class {
}
/**
* Save css to file
* Save css to file.
* @return {Promise}
*/
async save() {
Settings.saveSettings();
save() {
return Settings.saveSettings();
}
/**
* Save current editor bounds
* @param {Rectangle} bounds editor bounds
* Save current editor bounds.
* @param {Rectangle} bounds Editor bounds
* @return {Promise}
*/
saveEditorBounds(bounds) {
this.editor_bounds = bounds;
Settings.saveSettings();
return Settings.saveSettings();
}
/**
* Send scss to core for compilation
* @param {String} scss scss string
* Send SCSS to core for compilation.
* @param {String} scss SCSS string
*/
async compile(scss) {
return await ClientIPC.send('bd-compileSass', {
return ClientIPC.send('bd-compileSass', {
data: scss,
path: await this.fileExists() ? this.filePath : undefined
});
}
/**
* Recompile the current SCSS
* Recompile the current SCSS.
* @return {Promise}
*/
async recompile() {
return await this.updateScss(this.scss);
return this.updateScss(this.scss);
}
/**
* Send data to open editor
* @param {any} channel
* @param {any} data
* Send data to open editor.
* @param {String} channel
* @param {Any} data
* @return {Promise}
*/
async sendToEditor(channel, data) {
return await ClientIPC.send('sendToCssEditor', { channel, data });
return ClientIPC.sendToCssEditor(channel, data);
}
/**
* Opens an SCSS file in a system editor
* Opens an SCSS file in a system editor.
* @return {Promise}
*/
async openSystemEditor() {
try {
@ -156,11 +168,12 @@ export default new class {
// For some reason this doesn't work
// if (!electron.shell.openItem(this.filePath))
if (!electron.shell.openExternal('file://' + this.filePath))
if (!electron.shell.openExternal(`file://${this.filePath}`))
throw {message: 'Failed to open system editor.'};
}
/** Set current state
/**
* Set current state
* @param {String} scss Current uncompiled SCSS
* @param {String} css Current compiled CSS
* @param {String} files Files imported in the SCSS
@ -168,36 +181,35 @@ export default new class {
*/
setState(scss, css, files, err) {
this._scss = scss;
this.sendToEditor('set-scss', { scss });
this.sendToEditor('set-scss', scss);
this.css = css;
this.files = files;
this.error = err;
}
/**
* Current uncompiled scss
* Current uncompiled scss.
*/
get scss() {
return this._scss || '';
}
/**
* Set current scss
* Set current scss.
*/
set scss(scss) {
this.updateScss(scss, true);
}
/**
* Current compiled css
* Current compiled css.
*/
get css() {
return this._css || '';
}
/**
* Inject compiled css to head
* {DOM}
* Inject compiled css to head.
*/
set css(css) {
this._css = css;
@ -205,15 +217,14 @@ export default new class {
}
/**
* Current error
* Current error.
*/
get error() {
return this._error || undefined;
}
/**
* Set current error
* {DOM}
* Set current error.
*/
set error(err) {
this._error = err;
@ -264,7 +275,7 @@ export default new class {
* @param {Array} files Files to watch
*/
set watchfiles(files) {
for (let file of files) {
for (const file of files) {
if (!this.watchfiles.includes(file)) {
this.filewatcher.add(file);
this.watchfiles.push(file);
@ -272,7 +283,7 @@ export default new class {
}
}
for (let index in this.watchfiles) {
for (const index in this.watchfiles) {
let file = this.watchfiles[index];
while (file && !files.find(f => f === file)) {
this.filewatcher.remove(file);
@ -293,7 +304,7 @@ export default new class {
/**
* Checks if the system editor's file exists.
* @return {Boolean}
* @return {Promise}
*/
async fileExists() {
try {

View File

@ -8,7 +8,7 @@
* LICENSE file in the root directory of this source tree.
*/
import { ClientIPC } from 'bdipc';
import { ClientIPC } from 'common';
export default class {
@ -16,6 +16,12 @@ export default class {
return true;
}
/**
* Inserts or updates data in the database.
* @param {Object} args The record to find
* @param {Object} data The new record
* @return {Promise}
*/
static async insertOrUpdate(args, data) {
try {
return ClientIPC.send('bd-dba', { action: 'update', args, data });
@ -24,6 +30,11 @@ export default class {
}
}
/**
* Finds data in the database.
* @param {Object} args The record to find
* @return {Promise}
*/
static async find(args) {
try {
return ClientIPC.send('bd-dba', { action: 'find', args });
@ -31,4 +42,20 @@ export default class {
throw err;
}
}
/**
* Find first in the database
* @param {Object} args The record to find
* @return {Promise} null if record was not found
*/
static async findOne(args) {
try {
const find = await this.find(args);
if (find && find.length) return find[0];
return null;
} catch (err) {
throw err;
}
}
}

View File

@ -1,47 +1,25 @@
import { WebpackModules } from './webpackmodules';
import { $ } from 'vendor';
/**
* BetterDiscord Discord API
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
class List extends Array {
import { List } from 'structs';
import { User, Channel, Guild, Message } from 'discordstructs';
import Reflection from './reflection/index';
constructor() {
super(...arguments);
}
get(...filters) {
return this.find(item => {
for (let filter of filters) {
for (let key in filter) {
if (filter.hasOwnProperty(key)) {
if (item[key] !== filter[key]) return false;
}
}
}
return true;
});
}
}
class PermissionsError extends Error {
constructor(message) {
super(message);
this.name = 'PermissionsError';
}
}
class InsufficientPermissions extends PermissionsError {
constructor(message) {
super(`Missing Permission — ${message}`)
this.name = 'InsufficientPermissions';
}
}
const Modules = {
export const Modules = {
_getModule(name) {
const foundModule = WebpackModules.getModuleByName(name);
const foundModule = Reflection.module.byName(name);
if (!foundModule) return null;
delete this[name];
return this[name] = foundModule;
},
get ChannelSelector() { return this._getModule('ChannelSelector'); },
get MessageActions() { return this._getModule('MessageActions'); },
get MessageParser() { return this._getModule('MessageParser'); },
@ -64,356 +42,294 @@ const Modules = {
get UserStore() { return this._getModule('UserStore'); },
get RelationshipStore() { return this._getModule('RelationshipStore'); },
get RelationshipManager() { return this._getModule('RelationshipManager'); },
get ChangeNicknameModal() { return this._getModule('ChangeNicknameModal'); },
get UserSettingsStore() { return this._getModule('UserSettingsStore'); },
get UserSettingsWindow() { return this._getModule('UserSettingsWindow'); },
get UserStatusStore() { return this._getModule('UserStatusStore'); },
get ChannelSettingsWindow() { return this._getModule('ChannelSettingsWindow'); },
get GuildSettingsWindow() { return this._getModule('GuildSettingsWindow'); },
get CreateChannelModal() { return this._getModule('CreateChannelModal'); },
get PruneMembersModal() { return this._getModule('PruneMembersModal'); },
get NotificationSettingsModal() { return this._getModule('NotificationSettingsModal'); },
get PrivacySettingsModal() { return this._getModule('PrivacySettingsModal'); },
get UserProfileModal() { return this._getModule('UserProfileModal'); },
get APIModule() { return this._getModule('APIModule'); },
get UserNoteStore() { return this._getModule('UserNoteStore'); },
get DiscordPermissions() { return this.DiscordConstants.Permissions; }
};
class User {
constructor(data) {
for (let key in data)
if (data.hasOwnProperty(key))
this[key] = data[key];
this.discordObject = data;
}
static fromId(id) {
return new User(Modules.UserStore.getUser(id));
}
async sendMessage(content, parse = true) {
const id = await Modules.PrivateChannelActions.ensurePrivateChannel(DiscordApi.currentUser.id, this.id);
const channel = new PrivateChannel(Modules.ChannelStore.getChannel(id));
channel.sendMessage(content, parse);
}
get isFriend() {
return Modules.RelationshipStore.isFriend(this.id);
}
get isBlocked() {
return Modules.RelationshipStore.isBlocked(this.id);
}
addFriend() {
Modules.RelationshipManager.addRelationship(this.id, {location: 'Context Menu'});
}
removeFriend() {
Modules.RelationshipManager.removeRelationship(this.id, {location: 'Context Menu'});
}
block() {
Modules.RelationshipManager.addRelationship(this.id, {location: 'Context Menu'}, Modules.DiscordConstants.RelationshipTypes.BLOCKED);
}
unblock() {
Modules.RelationshipManager.removeRelationship(this.id, {location: 'Context Menu'});
}
}
class Member extends User {
constructor(data, guild) {
super(data);
const userData = Modules.UserStore.getUser(data.userId);
for (let key in userData)
if (userData.hasOwnProperty(key))
this[key] = userData[key];
this.guild_id = guild;
}
checkPermissions(perms) {
return Modules.PermissionUtils.can(perms, DiscordApi.currentUser, Modules.GuildStore.getGuild(this.guild_id));
}
kick(reason = '') {
if (!this.checkPermissions(Modules.DiscordPermissions.KICK_MEMBERS)) throw new InsufficientPermissions('KICK_MEMBERS');
Modules.GuildActions.kickUser(this.guild_id, this.id, reason);
}
ban(daysToDelete = '1', reason = '') {
if (!this.checkPermissions(Modules.DiscordPermissions.BAN_MEMBERS)) throw new InsufficientPermissions('BAN_MEMBERS');
Modules.GuildActions.banUser(this.guild_id, this.id, daysToDelete, reason);
}
unban() {
if (!this.checkPermissions(Modules.DiscordPermissions.BAN_MEMBERS)) throw new InsufficientPermissions('BAN_MEMBERS');
Modules.GuildActions.unbanUser(this.guild_id, this.id);
}
move(channel_id) {
if (!this.checkPermissions(Modules.DiscordPermissions.MOVE_MEMBERS)) throw new InsufficientPermissions('MOVE_MEMBERS');
Modules.GuildActions.setChannel(this.guild_id, this.id, channel_id);
}
mute(active = true) {
if (!this.checkPermissions(Modules.DiscordPermissions.MUTE_MEMBERS)) throw new InsufficientPermissions('MUTE_MEMBERS');
Modules.GuildActions.setServerMute(this.guild_id, this.id, active);
}
unmute(active = true) {
this.mute(false);
}
deafen(active = true) {
if (!this.checkPermissions(Modules.DiscordPermissions.DEAFEN_MEMBERS)) throw new InsufficientPermissions('DEAFEN_MEMBERS');
Modules.GuildActions.setServerDeaf(this.guild_id, this.id, active);
}
undeafen(active = true) {
this.deafen(false);
}
}
class Guild {
constructor(data) {
for (let key in data)
if (data.hasOwnProperty(key))
this[key] = data[key];
this.discordObject = data;
}
get channels() {
const channels = Modules.GuildChannelsStore.getChannels(this.id);
const returnChannels = new List();
for (const category in channels) {
if (channels.hasOwnProperty(category)) {
if (!Array.isArray(channels[category])) continue;
const channelList = channels[category];
for (const channel of channelList) {
returnChannels.push(new GuildChannel(channel.channel));
}
}
}
return returnChannels;
}
get defaultChannel() {
return new GuildChannel(Modules.GuildChannelsStore.getDefaultChannel(this.id));
}
get members() {
const members = Modules.GuildMemberStore.getMembers(this.id);
const returnMembers = new List();
for (const member of members) returnMembers.push(new Member(member, this.id));
return returnMembers;
}
get memberCount() {
return Modules.MemberCountStore.getMemberCount(this.id);
}
get emojis() {
return Modules.EmojiUtils.getGuildEmoji(this.id);
}
get permissions() {
return Modules.GuildPermissions.getGuildPermissions(this.id);
}
getMember(userId) {
return Modules.GuildMemberStore.getMember(this.id, userId);
}
isMember(userId) {
return Modules.GuildMemberStore.isMember(this.id, userId);
}
markAsRead() {
Modules.GuildActions.markGuildAsRead(this.id);
}
select() {
Modules.GuildActions.selectGuild(this.id);
}
nsfwAgree() {
Modules.GuildActions.nsfwAgree(this.id);
}
nsfwDisagree() {
Modules.GuildActions.nsfwDisagree(this.id);
}
changeSortLocation(index) {
Modules.GuildActions.move(DiscordApi.guildPositions.indexOf(this.id), index);
}
}
class Channel {
constructor(data) {
for (let key in data)
if (data.hasOwnProperty(key))
this[key] = data[key];
this.discordObject = data;
}
checkPermissions(perms) {
return Modules.PermissionUtils.can(perms, DiscordApi.currentUser, this.discordObject) || this.isPrivate();
}
async sendMessage(content, parse = true) {
if (!this.checkPermissions(Modules.DiscordPermissions.VIEW_CHANNEL | Modules.DiscordPermissions.SEND_MESSAGES)) throw new InsufficientPermissions('SEND_MESSAGES');
let response = {};
if (parse) response = await Modules.MessageActions._sendMessage(this.id, Modules.MessageParser.parse(this.discordObject, content));
else response = await Modules.MessageActions._sendMessage(this.id, {content});
return new Message(Modules.MessageStore.getMessage(this.id, response.body.id));
}
get messages() {
const messages = Modules.MessageStore.getMessages(this.id).toArray();
for (let i in messages)
if (messages.hasOwnProperty(i))
messages[i] = new Message(messages[i]);
return new List(...messages);
}
jumpToPresent() {
if (!this.checkPermissions(Modules.DiscordPermissions.VIEW_CHANNEL)) throw new InsufficientPermissions('VIEW_CHANNEL');
if (this.hasMoreAfter) Modules.MessageActions.jumpToPresent(this.id, Modules.DiscordConstants.MAX_MESSAGES_PER_CHANNEL);
else this.messages[this.messages.length - 1].jumpTo(false);
}
get hasMoreAfter() {
return Modules.MessageStore.getMessages(this.id).hasMoreAfter;
}
sendInvite(inviteId) {
if (!this.checkPermissions(Modules.DiscordPermissions.VIEW_CHANNEL | Modules.DiscordPermissions.SEND_MESSAGES)) throw new InsufficientPermissions('SEND_MESSAGES');
Modules.MessageActions.sendInvite(this.id, inviteId);
}
select() {
if (!this.checkPermissions(Modules.DiscordPermissions.VIEW_CHANNEL)) throw new InsufficientPermissions('VIEW_CHANNEL');
Modules.NavigationUtils.transitionToGuild(this.guild_id ? this.guild_id : Modules.DiscordConstants.ME, this.id);
}
}
class GuildChannel extends Channel {
constructor(data) {
super(data);
}
get permissions() {
return Modules.GuildPermissions.getChannelPermissions(this.id);
}
get guild() {
return new Guild(Modules.GuildStore.getGuild(this.guild_id));
}
isDefaultChannel() {
return Modules.GuildChannelsStore.getDefaultChannel(this.guild_id).id === this.id;
}
}
class PrivateChannel extends Channel {
constructor(data) {
super(data);
}
}
class Message {
constructor(data) {
for (let key in data)
if (data.hasOwnProperty(key))
this[key] = data[key];
this.discordObject = data;
}
delete() {
Modules.MessageActions.deleteMessage(this.channel_id, this.id);
}
// programmatically update the content
edit(content, parse = false) {
if (this.author.id !== DiscordApi.currentUser.id) return;
if (parse) Modules.MessageActions.editMessage(this.channel_id, this.id, Modules.MessageParser.parse(this.discordObject, content));
else Modules.MessageActions.editMessage(this.channel_id, this.id, {content});
}
// start the editing mode of GUI
startEdit() {
if (this.author.id !== DiscordApi.currentUser.id) return;
Modules.MessageActions.startEditMessage(this.channel_id, this.id, this.content);
}
// end editing mode of GUI
endEdit() {
Modules.MessageActions.endEditMessage();
}
jumpTo(flash = true) {
Modules.MessageActions.jumpToMessage(this.channel_id, this.id, flash);
}
}
export default class DiscordApi {
static get channels() {
const channels = Modules.ChannelStore.getChannels();
const returnChannels = new List();
for (const [key, value] of Object.entries(channels)) {
returnChannels.push(value.isPrivate() ? new PrivateChannel(value) : new GuildChannel(value));
}
return returnChannels;
}
static get modules() { return Modules }
static get User() { return User }
static get Channel() { return Channel }
static get Guild() { return Guild }
static get Message() { return Message }
/**
* A list of loaded guilds.
* @type {List<Guild>}
*/
static get guilds() {
const guilds = Modules.GuildStore.getGuilds();
const returnGuilds = new List();
for (const [key, value] of Object.entries(guilds)) {
returnGuilds.push(new Guild(value));
}
return returnGuilds;
return List.from(Object.entries(guilds), ([i, g]) => Guild.from(g));
}
/**
* A list of loaded channels.
* @type {List<Channel>}
*/
static get channels() {
const channels = Modules.ChannelStore.getChannels();
return List.from(Object.entries(channels), ([i, c]) => Channel.from(c));
}
/**
* A list of loaded users.
* @type {List<User>}
*/
static get users() {
const users = Modules.UserStore.getUsers();
const returnUsers = new List();
for (const [key, value] of Object.entries(users)) {
returnUsers.push(new User(value));
}
return returnUsers;
return List.from(Object.entries(users), ([i, u]) => User.from(u));
}
/**
* An object mapping guild IDs to their member counts.
* @type {Object}
*/
static get memberCounts() {
return Modules.MemberCountStore.getMemberCounts();
}
/**
* A list of guilds in the order they appear in the server list.
* @type {List<Guild>}
*/
static get sortedGuilds() {
const guilds = Modules.SortedGuildStore.getSortedGuilds();
const returnGuilds = new List();
for (const guild of guilds) {
returnGuilds.push(new Guild(guild));
}
return returnGuilds;
return List.from(guilds, g => Guild.from(g));
}
/**
* An array of guild IDs in the order they appear in the server list.
* @type {Number[]}
*/
static get guildPositions() {
return Modules.SortedGuildStore.guildPositions;
}
/**
* The currently selected guild.
* @type {Guild}
*/
static get currentGuild() {
return new Guild(Modules.GuildStore.getGuild(Modules.SelectedGuildStore.getGuildId()));
const guild = Modules.GuildStore.getGuild(Modules.SelectedGuildStore.getGuildId());
if (guild) return Guild.from(guild);
return null;
}
/**
* The currently selected channel.
* @type {Channel}
*/
static get currentChannel() {
const channel = Modules.ChannelStore.getChannel(Modules.SelectedChannelStore.getChannelId());
return channel.isPrivate ? new PrivateChannel(channel) : new GuildChannel(channel);
if (channel) return Channel.from(channel);
return null;
}
/**
* The current user.
* @type {User}
*/
static get currentUser() {
return Modules.UserStore.getCurrentUser();
const user = Modules.UserStore.getCurrentUser();
if (user) return User.from(user);
return null;
}
/**
* A list of the current user's friends.
* @type {List<User>}
*/
static get friends() {
const friends = Modules.RelationshipStore.getFriendIDs();
const returnUsers = new List();
for (const id of friends) returnUsers.push(User.fromId(id));
return returnUsers;
return List.from(friends, id => User.fromId(id));
}
/**
* User settings
*/
static get UserSettings() {
return UserSettings;
}
}
export class UserSettings {
/**
* Opens Discord's settings UI.
*/
static open(section = 'ACCOUNT') {
Modules.UserSettingsWindow.setSection(section);
Modules.UserSettingsWindow.open();
}
/**
* The user's current status. Either "online", "idle", "dnd" or "invisible".
* @type {String}
*/
static get status() { return Modules.UserSettingsStore.status }
/**
* The user's selected explicit content filter level.
* 0 == off, 1 == everyone except friends, 2 == everyone
* Configurable in the privacy and safety panel.
* @type {Number}
*/
static get explicitContentFilter() { return Modules.UserSettingsStore.explicitContentFilter }
/**
* Whether to disallow direct messages from server members by default.
* @type {Boolean}
*/
static get defaultGuildsRestricted() { return Modules.UserSettingsStore.defaultGuildsRestricted }
/**
* An array of guilds to disallow direct messages from their members.
* This is bypassed if the member is has another mutual guild with this disabled, or the member is friends with the current user.
* Configurable in each server's privacy settings.
* @type {Guild[]}
*/
static get restrictedGuildIds() { return Modules.UserSettingsStore.restrictedGuilds }
static get restrictedGuilds() {
return List.from(this.restrictedGuildIds, id => Guild.fromId(id) || id);
}
/**
* An array of flags specifying who should be allowed to add the current user as a friend.
* If everyone is checked, this will only have one item, "all". Otherwise it has either "mutual_friends", "mutual_guilds", both or neither.
* Configurable in the privacy and safety panel.
* @type {Array}
*/
static get friendSourceFlags() { return Object.keys(Modules.UserSettingsStore.friendSourceFlags) }
static get friendSourceEveryone() { return this.friendSourceFlags.include('all') }
static get friendSourceMutual_friends() { return this.friendSourceFlags.include('all') || this.friendSourceFlags.include('mutual_friends') }
static get friendSourceMutual_guilds() { return this.friendSourceFlags.include('all') || this.friendSourceFlags.include('mutual_guilds') }
static get friendSourceAnyone() { return this.friendSourceFlags.length > 0 }
/**
* Whether to automatically add accounts from other platforms running on the user's computer.
* Configurable in the connections panel.
* @type {Boolean}
*/
static get detectPlatformAccounts() { return Modules.UserSettingsStore.detectPlatformAccounts }
/**
* The number of seconds Discord will wait for activity before sending mobile push notifications.
* Configurable in the notifications panel.
* @type {Number}
*/
static get afkTimeout() { return Modules.UserSettingsStore.afkTimeout }
/**
* Whether to display the currently running game as a status message.
* Configurable in the games panel.
* @type {Boolean}
*/
static get showCurrentGame() { return Modules.UserSettingsStore.showCurrentGame }
/**
* Whether to show images uploaded directly to Discord.
* Configurable in the text and images panel.
* @type {Boolean}
*/
static get inlineAttachmentMedia() { return Modules.UserSettingsStore.inlineAttachmentMedia }
/**
* Whether to show images linked in Discord.
* Configurable in the text and images panel.
* @type {Boolean}
*/
static get inlineEmbedMedia() { return Modules.UserSettingsStore.inlineEmbedMedia }
/**
* Whether to automatically play GIFs when the Discord window is active without having to hover the mouse over the image.
* Configurable in the text and images panel.
* @type {Boolean}
*/
static get autoplayGifs() { return Modules.UserSettingsStore.gifAutoPlay }
/**
* Whether to show content from HTTP[s] links as embeds.
* Configurable in the text and images panel.
* @type {Boolean}
*/
static get showEmbeds() { return Modules.UserSettingsStore.renderEmbeds }
/**
* Whether to show a message's reactions.
* Configurable in the text and images panel.
* @type {Boolean}
*/
static get showReactions() { return Modules.UserSettingsStore.renderReactions }
/**
* Whether to play animated emoji.
* Configurable in the text and images panel.
* @type {Boolean}
*/
static get animateEmoji() { return Modules.UserSettingsStore.animateEmoji }
/**
* Whether to convert ASCII emoticons to emoji.
* Configurable in the text and images panel.
* @type {Boolean}
*/
static get convertEmoticons() { return Modules.UserSettingsStore.convertEmoticons }
/**
* Whether to allow playing text-to-speech messages.
* Configurable in the text and images panel.
* @type {Boolean}
*/
static get allowTts() { return Modules.UserSettingsStore.enableTTSCommand }
/**
* The user's selected theme. Either "dark" or "light".
* Configurable in the appearance panel.
* @type {String}
*/
static get theme() { return Modules.UserSettingsStore.theme }
/**
* Whether the user has enabled compact mode.
* `true` if compact mode is enabled, `false` if cozy mode is enabled.
* Configurable in the appearance panel.
* @type {Boolean}
*/
static get displayCompact() { return Modules.UserSettingsStore.messageDisplayCompact }
/**
* Whether the user has enabled developer mode.
* Currently only adds a "Copy ID" option to the context menu on users, guilds and channels.
* Configurable in the appearance panel.
* @type {Boolean}
*/
static get developerMode() { return Modules.UserSettingsStore.developerMode }
/**
* The user's selected language code.
* Configurable in the language panel.
* @type {String}
*/
static get locale() { return Modules.UserSettingsStore.locale }
/**
* The user's timezone offset in hours.
* This is not configurable.
* @type {Number}
*/
static get timezoneOffset() { return Modules.UserSettingsStore.timezoneOffset }
}

View File

@ -0,0 +1,66 @@
/**
* BetterDiscord Editor Module
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import Module from './imodule';
import { DOM } from 'ui';
export default new class extends Module {
get name() { return 'Editor' }
get delay() { return false; }
setInitialState(state) {
return {
editorBounds: undefined
};
}
initialize() {
super.initialize();
(async () => {
try {
// TODO this is temporary
const userScss = await this.send('readDataFile', 'user.scss');
const compiled = await this.send('compileSass', { data: userScss });
this.injectStyle('customcss', compiled.css.toString());
} catch (err) {
console.warn('SCSS Compilation error', err);
}
})();
}
events(ipc) {
ipc.on('editor-runScript', (e, script) => {
try {
new Function(script)();
e.reply('ok');
} catch (err) {
e.reply({ err: err.stack || err });
}
});
ipc.on('editor-injectStyle', (e, { id, style }) => {
this.injectStyle(id, style);
e.reply('ok');
});
}
injectStyle(id, style) {
return DOM.injectStyle(style, `userstyle-${id}`);
}
/**
* Show editor, flashes if already visible.
*/
async show() {
await this.send('editor-open', this.state.editorBounds);
}
}

View File

@ -1,5 +1,5 @@
/**
* BetterDiscord WebpackModules Module
* BetterDiscord Event Hook
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
@ -8,14 +8,13 @@
* LICENSE file in the root directory of this source tree.
*/
import EventListener from './eventlistener';
import { Utils } from 'common';
import Reflection from './reflection/index';
import { MonkeyPatch } from './patcher';
import Events from './events';
import { WebpackModules } from './webpackmodules';
import EventListener from './eventlistener';
import * as SocketStructs from '../structs/socketstructs';
/**
* Discord socket event hook
* @extends {EventListener}
@ -23,7 +22,7 @@ import * as SocketStructs from '../structs/socketstructs';
export default class extends EventListener {
init() {
console.log(SocketStructs);
this.ignoreMultiple = -1;
this.hook();
}
@ -38,53 +37,64 @@ export default class extends EventListener {
}
hook() {
const self = this;
const orig = this.eventsModule.prototype.emit;
this.eventsModule.prototype.emit = function (...args) {
const { Events } = Reflection.modules;
MonkeyPatch('BD:EVENTS', Events.prototype).after('emit', (obj, args, retVal) => {
const eventId = args.length >= 3 ? args[2].id || -1 : -1;
if (eventId === this.ignoreMultiple) return;
this.ignoreMultiple = eventId;
if (obj.webSocket) this.wsc = obj.webSocket;
this.emit(...args);
});
/*
const orig = Events.prototype.emit;
Events.prototype.emit = function (...args) {
orig.call(this, ...args);
self.wsc = this;
self.emit(...args);
}
}
get eventsModule() {
return WebpackModules.getModuleByPrototypes(['setMaxListeners', 'emit']);
};*/
}
/**
* Discord emit overload
* @param {any} e
* @param {any} event
* @param {any} action
* @param {any} data
*/
emit(e, action, data) {
switch (e) {
case 'dispatch':
return this.dispatch(action, data);
case 'dispatch':
return this.dispatch(action, data);
}
}
/**
* Emit callback
* @param {any} e Event Action
* @param {any} d Event Args
* @param {any} event Event
* @param {any} data Event data
*/
dispatch(e, d) {
Events.emit('raw-event', { type: e, data: d });
if (e === this.actions.READY || e === this.actions.RESUMED) {
Events.emit(e, d);
dispatch(type, data) {
Events.emit('raw-event', { type, data });
if (type === this.actions.READY || type === this.actions.RESUMED) {
Events.emit(type, data);
return;
}
if (!Object.keys(SocketStructs).includes(e)) return;
const evt = new SocketStructs[e](d);
Events.emit(`discord:${e}`, evt);
if (!Object.keys(SocketStructs).includes(type)) return;
Events.emit(`discord:${type}`, new SocketStructs[type](data));
}
get SocketStructs() {
return SocketStructs;
}
/**
* All known socket actions
*/
get actions() {
return {
if (this._actions) return this._actions;
return this._actions = {
READY: 'READY', // Socket ready
RESUMED: 'RESUMED', // Socket resumed
TYPING_START: 'TYPING_START', // User typing start
@ -143,7 +153,7 @@ export default class extends EventListener {
LFG_LISTING_CREATE: 'LFG_LISTING_CREATE', // No groups here
LFG_LISTING_DELETE: 'LFG_LISTING_DELETE', // Thank you
BRAINTREE_POPUP_BRIDGE_CALLBACK: 'BRAINTREE_POPUP_BRIDGE_CALLBACK' // What
}
};
}
}

View File

@ -14,7 +14,7 @@ import Events from './events';
export default class extends Module {
events() {
for (let event of this.eventBindings) {
for (const event of this.eventBindings) {
Events.on(event.id, event.callback);
}
}

View File

@ -12,16 +12,43 @@ import { EventEmitter } from 'events';
const emitter = new EventEmitter();
export default class {
static on(eventName, callBack) {
emitter.on(eventName, callBack);
/**
* Adds an event listener.
* @param {String} event The event to listen for
* @param {Function} callback The function to call when the event is emitted
*/
static on(event, callback) {
emitter.on(event, callback);
}
static off(eventName, callBack) {
emitter.removeListener(eventName, callBack);
/**
* Adds an event listener that is only called once.
* @param {String} event The event to listen for
* @param {Function} callback The function to call when the event is emitted
*/
static once(event, callback) {
emitter.once(event, callback);
}
static emit(...args) {
emitter.emit(...args);
/**
* Removes an event listener.
* @param {String} event The event to remove
* @param {Function} callback The listener to remove
*/
static off(event, callback) {
emitter.removeListener(event, callback);
}
static get removeListener() { return this.off }
/**
* Emits an event
* @param {String} event The event to emit
* @param {Any} ...data Data to pass to the event listeners
*/
static emit(event, ...data) {
emitter.emit(event, ...data);
}
}

View File

@ -11,35 +11,45 @@
const eventemitters = new WeakMap();
export default class EventsWrapper {
constructor(eventemitter) {
constructor(eventemitter, bind) {
eventemitters.set(this, eventemitter);
this.bind = bind || this;
}
get eventSubs() {
return this._eventSubs || (this._eventSubs = []);
}
get on() { return this.subscribe }
subscribe(event, callback) {
if (this.eventSubs.find(e => e.event === event && e.callback === callback)) return;
this.eventSubs.push({
event,
callback
});
eventemitters.get(this).on(event, callback);
const boundCallback = (...args) => callback.apply(this.bind, args);
this.eventSubs.push({ event, callback, boundCallback });
eventemitters.get(this).on(event, boundCallback);
}
once(event, callback) {
if (this.eventSubs.find(e => e.event === event && e.callback === callback)) return;
const boundCallback = (...args) => this.off(event, callback) && callback.apply(this.bind, args);
this.eventSubs.push({ event, callback, boundCallback });
eventemitters.get(this).on(event, boundCallback);
}
get off() { return this.unsubscribe }
unsubscribe(event, callback) {
for (let index of this.eventSubs) {
if (this.eventSubs[index].event !== event || (callback && this.eventSubs[index].callback === callback)) return;
eventemitters.get(this).off(event, this.eventSubs[index].callback);
for (const index in this.eventSubs) {
if (this.eventSubs[index].event !== event || (callback && this.eventSubs[index].callback === callback)) continue;
eventemitters.get(this).removeListener(event, this.eventSubs[index].boundCallback);
this.eventSubs.splice(index, 1);
}
}
unsubscribeAll() {
for (let event of this.eventSubs) {
eventemitters.get(this).off(event.event, event.callback);
for (const event of this.eventSubs) {
eventemitters.get(this).removeListener(event.event, event.boundCallback);
}
this.eventSubs.splice(0, this.eventSubs.length);
}
}

View File

@ -8,13 +8,14 @@
* LICENSE file in the root directory of this source tree.
*/
import Globals from './globals';
import Content from './content';
export default class ExtModule extends Content {
constructor(internals) {
super(internals);
this.__require = window.require(this.paths.mainPath);
this.__require = Globals.require(this.paths.mainPath);
}
get type() { return 'module' }

View File

@ -10,8 +10,6 @@
import ContentManager from './contentmanager';
import ExtModule from './extmodule';
import { ClientLogger as Logger } from 'common';
import { Events } from 'modules';
export default class extends ContentManager {

View File

@ -8,14 +8,20 @@
* LICENSE file in the root directory of this source tree.
*/
import path from 'path';
import sparkplug from 'sparkplug';
import { ClientIPC } from 'common';
import Module from './module';
import Events from './events';
import { ClientIPC } from 'bdipc';
export default new class extends Module {
constructor(args) {
super(args);
// webpack replaces this with the normal require function
// eslint-disable-next-line no-undef
this.require = __non_webpack_require__;
}
initg() {
@ -28,28 +34,24 @@ export default new class extends Module {
this.getObject = this.getObject.bind(this);
}
first() {
(async() => {
const config = await ClientIPC.send('getConfig');
this.setState(config);
async first() {
const config = await ClientIPC.send('getConfig');
this.setState({ config });
// This is for Discord to stop error reporting :3
window.BetterDiscord = {
'version': config.version,
'v': config.version
};
window.jQuery = {};
// This is for Discord to stop error reporting :3
window.BetterDiscord = {
version: config.version,
v: config.version
};
window.jQuery = {};
if (window.__bd) {
this.setState(window.__bd);
window.__bd = {
setWS: this.setWS
}
}
if (sparkplug.bd) {
this.setState({ bd: sparkplug.bd });
sparkplug.bd.setWS = this.setWS;
}
Events.emit('global-ready');
Events.emit('socket-created', this.state.wsHook);
})();
Events.emit('global-ready');
Events.emit('socket-created', this.state.wsHook);
}
setWS(wSocket) {
@ -60,19 +62,43 @@ export default new class extends Module {
}
getObject(name) {
return this.state[name];
return this.config[name] || this.bd[name];
}
get bd() {
return this.state.bd;
}
get localStorage() {
return this.bd.localStorage;
}
get webSocket() {
return this.bd.wsHook;
}
get WebSocket() {
return this.bd.wsOrig;
}
get ignited() {
return this.bd.ignited;
}
get config() {
return this.state.config;
}
get paths() {
return this.config.paths;
}
getPath(id) {
return this.state.paths.find(path => path.id === id).path;
return this.paths.find(path => path.id === id).path;
}
static get paths() {
return this.state.paths;
}
static get version() {
return this.state.version;
get version() {
return this.config.versions.core;
}
}

View File

@ -0,0 +1,73 @@
/**
* BetterDiscord Module Base
* Copyright (c) 2015-present JsSucks - https://github.com/JsSucks
* All rights reserved.
* https://github.com/JsSucks - https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* Base Module that every non-static module should extend
*/
import { ClientLogger as Logger, ClientIPC } from 'common';
export default class Module {
constructor(args) {
this.__ = {
state: args || {},
args
};
this.setState = this.setState.bind(this);
if (this.delay) { // If delay is set then module is set to load delayed from modulemanager
this.initialize = this.initialize.bind(this);
this.init = this.initialize;
} else {
this.initialize();
this.init = () => { };
}
}
initialize() {
if (this.bindings) this.bindings();
if (this.setInitialState) this.setState(this.setInitialState(this.state));
if (this.events) this.events(ClientIPC);
}
setState(newState) {
const oldState = Object.assign({}, this.state);
Object.assign(this.state, newState);
if (this.stateChanged) this.stateChanged(oldState, newState);
}
set args(t) { }
get args() { return this.__.args; }
set state(state) { return this.__.state = state; }
get state() { return this.__.state; }
async send(channel, message) {
return ClientIPC.send(channel, message);
}
log(msg) {
Logger.log(this.name, msg);
}
warn(msg) {
Logger.log(this.name, msg);
}
err(msg) {
Logger.log(this.name, msg);
}
info(msg) {
Logger.log(this.name, msg);
}
}

View File

@ -5,20 +5,19 @@
* https://github.com/JsSucks - https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/*
Base Module that every non-static module should extend
* LICENSE file in the root directory of this source tree.
*/
/**
* Base Module that every non-static module should extend
*/
export default class Module {
constructor(args) {
this.__ = {
state: args || {},
args
}
};
this.setState = this.setState.bind(this);
this.initialize();
}
@ -38,7 +37,6 @@ export default class Module {
set args(t) { }
get args() { return this.__.args; }
set state(state) { return this.__.state = state; }
get state() { return this.__.state; }

View File

@ -8,30 +8,39 @@
* LICENSE file in the root directory of this source tree.
*/
/*Module Manager initializes all modules when everything is ready*/
import { Events, SocketProxy, EventHook, CssEditor } from 'modules';
import { ProfileBadges } from 'ui';
import { ClientLogger as Logger } from 'common';
import { SocketProxy, EventHook } from 'modules';
import { ProfileBadges, ClassNormaliser } from 'ui';
import Updater from './updater';
/**
* Module Manager initializes all modules when everything is ready
*/
export default class {
/**
* An array of modules.
*/
static get modules() {
return this._modules ? this._modules : (this._modules = [
new ProfileBadges(),
new ClassNormaliser(),
new SocketProxy(),
new EventHook(),
CssEditor,
new Updater()
Updater
]);
}
/**
* Initializes all modules.
* @return {Promise}
*/
static async initModules() {
for (let module of this.modules) {
for (const module of this.modules) {
try {
if (module.init && module.init instanceof Function) module.init();
} catch (err) {
console.log(`Failed to initialize module: ${err}`);
Logger.err('Module Manager', ['Failed to initialize module:', err]);
}
}
return true;

View File

@ -1,19 +1,31 @@
export { default as Events } from './events';
export { default as Settings } from './settings';
export { default as CssEditor } from './csseditor';
export { default as ExtModuleManager } from './extmodulemanager';
export { default as Editor } from './editor';
export { default as Globals } from './globals';
export { default as Settings } from './settings';
export { default as Database } from './database';
export { default as Updater } from './updater';
export { default as ModuleManager } from './modulemanager';
export { default as PluginManager } from './pluginmanager';
export { default as ThemeManager } from './thememanager';
export { default as Globals } from './globals';
export { default as ExtModuleManager } from './extmodulemanager';
export { default as Permissions } from './permissionmanager';
export { default as EventsWrapper } from './eventswrapper';
export { default as Vendor } from './vendor';
export * from './webpackmodules';
export { default as ModuleManager } from './modulemanager';
export * from './patcher';
export * from './reactcomponents';
export { default as Module } from './module';
export { default as EventListener } from './eventlistener';
export { default as SocketProxy } from './socketproxy';
export { default as EventHook } from './eventhook';
export { default as Permissions } from './permissionmanager';
export { default as Database } from './database';
export { default as EventsWrapper } from './eventswrapper';
export { default as DiscordApi } from './discordapi';
export * from './patcher';
export * from './reactcomponents';
export { default as DiscordApi, Modules as DiscordApiModules } from './discordapi';
export { default as BdWebApi } from './bdwebapi';
export { default as Connectivity } from './connectivity';
export { default as Security } from './security';
export { default as Cache } from './cache';
export { default as Reflection } from './reflection/index';
export { default as PackageInstaller } from './packageinstaller';

View File

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

View File

@ -5,41 +5,73 @@
* https://github.com/JsSucks - https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
* LICENSE file in the root directory of this source tree.
*/
import { WebpackModules } from './webpackmodules';
import { ClientLogger as Logger, Utils } from 'common';
import Reflection from './reflection/index';
import { ClientLogger as Logger } from 'common';
/**
* Function with no arguments and no return value that may be called to revert changes made by {@link Patcher}, restoring (unpatching) original method.
* @callback Patcher~unpatch
*/
/**
* A callback that modifies method logic. This callback is called on each call of the original method and is provided all data about original call. Any of the data can be modified if necessary, but do so wisely.
*
* The third argument for the callback will be `undefined` for `before` patches. `originalFunction` for `instead` patches and `returnValue` for `after` patches.
*
* @callback Patcher~patchCallback
* @param {object} thisObject - `this` in the context of the original function.
* @param {arguments} arguments - The original arguments of the original function.
* @param {(function|*)} extraValue - For `instead` patches, this is the original function from the module. For `after` patches, this is the return value of the function.
* @return {*} Makes sense only when using an `instead` or `after` patch. If something other than `undefined` is returned, the returned value replaces the value of `returnValue`. If used for `before` the return value is ignored.
*/
export class Patcher {
static get patches() { return this._patches || (this._patches = {}) }
static get patches() { return this._patches || (this._patches = []) }
/**
* Returns all the patches done by a specific caller
* @param {string} id - Name of the patch caller
* @method
*/
static getPatchesByCaller(id) {
if (!id) return [];
const patches = [];
for (const patch in this.patches) {
if (this.patches.hasOwnProperty(patch)) {
if (this.patches[patch].caller === id) patches.push(this.patches[patch]);
for (const patch of this.patches) {
for (const childPatch of patch.children) {
if (childPatch.caller === id) patches.push(childPatch);
}
}
return patches;
}
/**
* Unpatches all patches passed, or when a string is passed unpatches all
* patches done by that specific caller.
* @param {Array|string} patches - Either an array of patches to unpatch or a caller name
*/
static unpatchAll(patches) {
if (typeof patches === 'string')
patches = this.getPatchesByCaller(patches);
for (const patch of patches) {
for (const child of patch.children) {
child.unpatch();
}
patch.unpatch();
}
}
static resolveModule(module) {
if (module instanceof Function || (module instanceof Object && !(module instanceof Array))) return module;
if ('string' === typeof module) return WebpackModules.getModuleByName(module);
if (module instanceof Array) return WebpackModules.getModuleByProps(module);
if (module instanceof Function || (module instanceof Object)) return module;
if (typeof module === 'string') return Reflection.module.byName(module);
return null;
}
static overrideFn(patch) {
return function () {
let retVal = null;
if (!patch.children) return patch.originalFunction.apply(this, arguments);
let retVal = undefined;
if (!patch.children || !patch.children.length) return patch.originalFunction.apply(this, arguments);
for (const superPatch of patch.children.filter(c => c.type === 'before')) {
try {
superPatch.callback(this, arguments);
@ -54,7 +86,8 @@ export class Patcher {
} else {
for (const insteadPatch of insteads) {
try {
retVal = insteadPatch.callback(this, arguments);
const tempReturn = insteadPatch.callback(this, arguments, patch.originalFunction.bind(this));
if (typeof tempReturn !== 'undefined') retVal = tempReturn;
} catch (err) {
Logger.err(`Patcher:${patch.id}`, err);
}
@ -63,7 +96,8 @@ export class Patcher {
for (const slavePatch of patch.children.filter(c => c.type === 'after')) {
try {
slavePatch.callback(this, arguments, retVal);
const tempReturn = slavePatch.callback(this, arguments, retVal, r => retVal = r);
if (typeof tempReturn !== 'undefined') retVal = tempReturn;
} catch (err) {
Logger.err(`Patcher:${patch.id}`, err);
}
@ -73,7 +107,9 @@ export class Patcher {
}
static rePatch(patch) {
patch.proxyFunction = patch.module[patch.functionName] = this.overrideFn(patch);
if (patch.module instanceof Array && typeof patch.functionName === 'number')
patch.module.splice(patch.functionName, 1, patch.proxyFunction = this.overrideFn(patch));
else patch.proxyFunction = patch.module[patch.functionName] = this.overrideFn(patch);
}
static pushPatch(caller, id, module, functionName) {
@ -87,25 +123,75 @@ export class Patcher {
revert: () => { // Calling revert will destroy any patches added to the same module after this
patch.module[patch.functionName] = patch.originalFunction;
patch.proxyFunction = null;
patch.slaves = patch.supers = [];
patch.children = [];
},
counter: 0,
children: []
};
patch.proxyFunction = module[functionName] = this.overrideFn(patch);
return this.patches[id] = patch;
return this.patches.push(patch), patch;
}
static before() { return this.pushChildPatch(...arguments, 'before') }
static after() { return this.pushChildPatch(...arguments, 'after') }
static instead() { return this.pushChildPatch(...arguments, 'instead') }
/**
* This method patches onto another function, allowing your code to run beforehand.
* Using this, you are also able to modify the incoming arguments before the original method is run.
*
* @param {string} caller - Name of the caller of the patch function. Using this you can undo all patches with the same name using {@link Patcher#unpatchAll}.
* @param {object} unresolvedModule - Object with the function to be patched. Can also patch an object's prototype.
* @param {string} functionName - Name of the method to be patched
* @param {Patcher~patchCallback} callback - Function to run before the original method
* @param {string} [displayName] You can provide meaningful name for class/object provided in `what` param for logging purposes. By default, this function will try to determine name automatically.
* @return {Patcher~unpatch} Function with no arguments and no return value that should be called to cancel (unpatch) this patch. You should save and run it when your plugin is stopped.
*/
static before(caller, unresolvedModule, functionName, callback, displayName) { return this.pushChildPatch(caller, unresolvedModule, functionName, callback, displayName, 'before') }
/**
* This method patches onto another function, allowing your code to run afterwards.
* Using this, you are also able to modify the return value, using the return of your code instead.
*
* @param {string} caller - Name of the caller of the patch function. Using this you can undo all patches with the same name using {@link Patcher#unpatchAll}.
* @param {object} unresolvedModule - Object with the function to be patched. Can also patch an object's prototype.
* @param {string} functionName - Name of the method to be patched
* @param {Patcher~patchCallback} callback - Function to run after the original method
* @param {string} [displayName] You can provide meaningful name for class/object provided in `what` param for logging purposes. By default, this function will try to determine name automatically.
* @return {Patcher~unpatch} Function with no arguments and no return value that should be called to cancel (unpatch) this patch. You should save and run it when your plugin is stopped.
*/
static after(caller, unresolvedModule, functionName, callback, displayName) { return this.pushChildPatch(caller, unresolvedModule, functionName, callback, displayName, 'after') }
/**
* This method patches onto another function, allowing your code to run instead, preventing the running of the original code.
* Using this, you are also able to modify the return value, using the return of your code instead.
*
* @param {string} caller - Name of the caller of the patch function. Using this you can undo all patches with the same name using {@link Patcher#unpatchAll}.
* @param {object} unresolvedModule - Object with the function to be patched. Can also patch an object's prototype.
* @param {string} functionName - Name of the method to be patched
* @param {Patcher~patchCallback} callback - Function to run instead of the original method
* @param {string} [displayName] You can provide meaningful name for class/object provided in `what` param for logging purposes. By default, this function will try to determine name automatically.
* @return {Patcher~unpatch} Function with no arguments and no return value that should be called to cancel (unpatch) this patch. You should save and run it when your plugin is stopped.
*/
static instead(caller, unresolvedModule, functionName, callback, displayName) { return this.pushChildPatch(caller, unresolvedModule, functionName, callback, displayName, 'instead') }
/**
* This method patches onto another function, allowing your code to run before, instead or after the original function.
* Using this you are able to modify the incoming arguments before the original function is run as well as the return
* value before the original function actually returns.
*
* @param {string} caller - Name of the caller of the patch function. Using this you can undo all patches with the same name using {@link Patcher#unpatchAll}.
* @param {object} unresolvedModule - Object with the function to be patched. Can also patch an object's prototype.
* @param {string} functionName - Name of the method to be patched
* @param {Patcher~patchCallback} callback - Function to run after the original method
* @param {string} [displayName] You can provide meaningful name for class/object provided in `what` param for logging purposes. By default, this function will try to determine name automatically.
* @param {string} [type=after] - Determines whether to run the function `before`, `instead`, or `after` the original.
* @return {Patcher~unpatch} Function with no arguments and no return value that should be called to cancel (unpatch) this patch. You should save and run it when your plugin is stopped.
*/
static pushChildPatch(caller, unresolvedModule, functionName, callback, displayName, type = 'after') {
const module = this.resolveModule(unresolvedModule);
if (!module || !module[functionName] || !(module[functionName] instanceof Function)) return null;
displayName = 'string' === typeof unresolvedModule ? unresolvedModule : displayName || module.displayName || module.name || module.constructor.displayName || module.constructor.name;
displayName = typeof unresolvedModule === 'string' ? unresolvedModule :
displayName || module.displayName || module.name || module.constructor.displayName || module.constructor.name;
const patchId = `${displayName}:${functionName}:${caller}`;
const patch = this.patches[patchId] || this.pushPatch(caller, patchId, module, functionName);
const patch = this.patches.find(p => p.module == module && p.functionName == functionName) || this.pushPatch(caller, patchId, module, functionName);
if (!patch.proxyFunction) this.rePatch(patch);
const child = {
caller,
@ -114,7 +200,11 @@ export class Patcher {
callback,
unpatch: () => {
patch.children.splice(patch.children.findIndex(cpatch => cpatch.id === child.id && cpatch.type === type), 1);
if (patch.children.length <= 0) delete this.patches[patchId];
if (patch.children.length <= 0) {
const patchNum = this.patches.findIndex(p => p.module == module && p.functionName == functionName);
this.patches[patchNum].revert();
this.patches.splice(patchNum, 1);
}
}
};
patch.children.push(child);

View File

@ -15,15 +15,15 @@ export default class Plugin extends Content {
get type() { return 'plugin' }
// Don't use - these will eventually be removed!
get pluginPath() { return this.contentPath }
get pluginConfig() { return this.config }
get start() { return this.enable }
get stop() { return this.disable }
get stop() { return this.disable }
unload() {
PluginManager.unloadPlugin(this);
reload(force) {
return PluginManager.reloadPlugin(this, force);
}
unload(force) {
return PluginManager.unloadPlugin(this, force);
}
}

View File

@ -1,5 +1,5 @@
/**
* BetterDiscord Plugin Api
* BetterDiscord Plugin API
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
@ -8,43 +8,43 @@
* LICENSE file in the root directory of this source tree.
*/
import { Utils, ClientLogger as Logger, ClientIPC } from 'common';
import { EmoteModule } from 'builtin';
import { SettingsSet, SettingsCategory, Setting, SettingsScheme } from 'structs';
import { BdMenu, Modals, DOM, DOMObserver, VueInjector, Toasts, Notifications, BdContextMenu, DiscordContextMenu } from 'ui';
import * as CommonComponents from 'commoncomponents';
import { default as Components } from '../ui/components/generic';
import { Utils, Filters, ClientLogger as Logger, ClientIPC, AsyncEventEmitter } from 'common';
import Settings from './settings';
import ExtModuleManager from './extmodulemanager';
import PluginManager from './pluginmanager';
import ThemeManager from './thememanager';
import Events from './events';
import EventsWrapper from './eventswrapper';
import { WebpackModules } from './webpackmodules';
import { SettingsSet, SettingsCategory, Setting, SettingsScheme } from 'structs';
import { BdMenuItems, Modals, DOM, Reflection } from 'ui';
import Reflection from './reflection/index';
import DiscordApi from './discordapi';
import { ReactComponents } from './reactcomponents';
import { MonkeyPatch } from './patcher';
import { ReactComponents, ReactHelpers } from './reactcomponents';
import { Patcher, MonkeyPatch } from './patcher';
import GlobalAc from '../ui/autocomplete';
import Vue from 'vue';
import path from 'path';
import Globals from './globals';
export default class PluginApi {
constructor(pluginInfo) {
constructor(pluginInfo, pluginPath) {
this.pluginInfo = pluginInfo;
this.pluginPath = pluginPath;
this.Events = new EventsWrapper(Events);
Utils.defineSoftGetter(this.Events, 'bind', () => this.plugin);
this._menuItems = undefined;
this._injectedStyles = undefined;
this._modalStack = undefined;
}
get Discord() {
return DiscordApi;
}
get ReactComponents() {
return ReactComponents;
}
get Reflection() {
return Reflection;
}
get MonkeyPatch() {
return module => MonkeyPatch(this.pluginInfo.id, module);
}
get plugin() {
return PluginManager.getPluginById(this.pluginInfo.id || this.pluginInfo.name.toLowerCase().replace(/[^a-zA-Z0-9-]/g, '-').replace(/--/g, '-'));
return PluginManager.getPluginByPath(this.pluginPath);
}
async bridge(plugin_id) {
@ -61,22 +61,37 @@ export default class PluginApi {
get Api() { return this }
get AsyncEventEmitter() { return AsyncEventEmitter }
get EventsWrapper() { return EventsWrapper }
get CommonComponents() { return CommonComponents }
get Components() { return Components }
get Filters() { return Filters }
get Discord() { return DiscordApi }
get DiscordApi() { return DiscordApi }
get ReactComponents() { return ReactComponents }
get ReactHelpers() { return ReactHelpers }
get Reflection() { return Reflection }
get DOM() { return DOM }
get VueInjector() { return VueInjector }
get observer() {
return this._observer || (this._observer = new DOMObserver());
}
/**
* Logger
*/
loggerLog(...message) { Logger.log(this.pluginInfo.name, message) }
loggerErr(...message) { Logger.err(this.pluginInfo.name, message) }
loggerWarn(...message) { Logger.warn(this.pluginInfo.name, message) }
loggerInfo(...message) { Logger.info(this.pluginInfo.name, message) }
loggerDbg(...message) { Logger.dbg(this.pluginInfo.name, message) }
get Logger() {
return {
log: this.loggerLog.bind(this),
err: this.loggerErr.bind(this),
warn: this.loggerWarn.bind(this),
info: this.loggerInfo.bind(this),
dbg: this.loggerDbg.bind(this)
log: (...message) => Logger.log(this.plugin.name, message),
error: (...message) => Logger.err(this.plugin.name, message),
err: (...message) => Logger.err(this.plugin.name, message),
warn: (...message) => Logger.warn(this.plugin.name, message),
info: (...message) => Logger.info(this.plugin.name, message),
debug: (...message) => Logger.dbg(this.plugin.name, message),
dbg: (...message) => Logger.dbg(this.plugin.name, message)
};
}
@ -86,15 +101,18 @@ export default class PluginApi {
get Utils() {
return {
overload: () => Utils.overload.apply(Utils, arguments),
monkeyPatch: () => Utils.monkeyPatch.apply(Utils, arguments),
monkeyPatchOnce: () => Utils.monkeyPatchOnce.apply(Utils, arguments),
compatibleMonkeyPatch: () => Utils.monkeyPatchOnce.apply(Utils, arguments),
tryParseJson: () => Utils.tryParseJson.apply(Utils, arguments),
toCamelCase: () => Utils.toCamelCase.apply(Utils, arguments),
compare: () => Utils.compare.apply(Utils, arguments),
deepclone: () => Utils.deepclone.apply(Utils, arguments),
deepfreeze: () => Utils.deepfreeze.apply(Utils, arguments)
overload: (...args) => Utils.overload.apply(Utils, args),
tryParseJson: (...args) => Utils.tryParseJson.apply(Utils, args),
toCamelCase: (...args) => Utils.toCamelCase.apply(Utils, args),
compare: (...args) => Utils.compare.apply(Utils, args),
deepclone: (...args) => Utils.deepclone.apply(Utils, args),
deepfreeze: (...args) => Utils.deepfreeze.apply(Utils, args),
removeFromArray: (...args) => Utils.removeFromArray.apply(Utils, args),
defineSoftGetter: (...args) => Utils.defineSoftGetter.apply(Utils, args),
wait: (...args) => Utils.wait.apply(Utils, args),
until: (...args) => Utils.until.apply(Utils, args),
findInTree: (...args) => Utils.findInTree.apply(Utils, args),
findInReactTree: (...args) => Utils.findInReactTree.apply(Utils, args)
};
}
@ -142,6 +160,9 @@ export default class PluginApi {
get BdMenu() {
return {
open: BdMenu.open.bind(BdMenu),
close: BdMenu.close.bind(BdMenu),
items: this.BdMenuItems,
BdMenuItems: this.BdMenuItems
};
}
@ -154,23 +175,23 @@ export default class PluginApi {
return this._menuItems || (this._menuItems = []);
}
addMenuItem(item) {
return BdMenuItems.add(item);
return BdMenu.items.add(item);
}
addMenuSettingsSet(category, set, text) {
const item = BdMenuItems.addSettingsSet(category, set, text);
const item = BdMenu.items.addSettingsSet(category, set, text);
return this.menuItems.push(item);
}
addMenuVueComponent(category, text, component) {
const item = BdMenuItems.addVueComponent(category, text, component);
const item = BdMenu.items.addVueComponent(category, text, component);
return this.menuItems.push(item);
}
removeMenuItem(item) {
BdMenuItems.remove(item);
BdMenu.items.remove(item);
Utils.removeFromArray(this.menuItems, item);
}
removeAllMenuItems() {
for (let item of this.menuItems)
BdMenuItems.remove(item);
for (const item of this.menuItems)
BdMenu.items.remove(item);
}
get BdMenuItems() {
return Object.defineProperty({
@ -184,6 +205,25 @@ export default class PluginApi {
});
}
/**
* BdContextMenu
*/
showContextMenu(event, groups) {
BdContextMenu.show(event, groups);
this.activeMenu.menu = BdContextMenu.activeMenu.menu;
}
get activeMenu() {
return this._activeMenu || (this._activeMenu = { menu: null });
}
get BdContextMenu() {
return Object.defineProperty({
show: this.showContextMenu.bind(this)
}, 'activeMenu', {
get: () => this.activeMenu
});
}
/**
* CssUtils
*/
@ -218,8 +258,8 @@ export default class PluginApi {
this.injectedStyles.splice(this.injectedStyles.indexOf(styleid), 1);
DOM.deleteStyle(styleid);
}
deleteAllStyles(id, css) {
for (let id of this.injectedStyles) {
deleteAllStyles(id) {
for (const id of this.injectedStyles) {
this.deleteStyle(id);
}
}
@ -242,16 +282,9 @@ export default class PluginApi {
get modalStack() {
return this._modalStack || (this._modalStack = []);
}
get baseModalComponent() {
return Modals.baseComponent;
}
addModal(_modal, component) {
const modal = Modals.add(_modal, component);
modal.on('close', () => {
let index;
while ((index = this.modalStack.findIndex(m => m === modal)) > -1)
this.modalStack.splice(index, 1);
});
modal.on('close', () => Utils.removeFromArray(this.modalStack, modal));
this.modalStack.push(modal);
return modal;
}
@ -260,7 +293,7 @@ export default class PluginApi {
}
closeAllModals(force) {
const promises = [];
for (let modal of this.modalStack)
for (const modal of this.modalStack)
promises.push(modal.close(force));
return Promise.all(promises);
}
@ -269,10 +302,10 @@ export default class PluginApi {
return this.modalStack[this.modalStack.length - 1].close(force);
}
basicModal(title, text) {
return this.addModal(Modals.basic(title, text));
return this.addModal(Modals.createBasicModal(title, text));
}
settingsModal(settingsset, headertext, options) {
return this.addModal(Modals.settings(settingsset, headertext, options));
return this.addModal(Modals.createSettingsModal(settingsset, headertext, options));
}
get Modals() {
return Object.defineProperties({
@ -287,7 +320,177 @@ export default class PluginApi {
get: () => this.modalStack
},
baseComponent: {
get: () => this.baseModalComponent
get: () => Modals.baseComponent
}
});
}
/**
* Toasts
*/
showToast(message, options = {}) {
return Toasts.push(message, options);
}
showSuccessToast(message, options = {}) {
return Toasts.success(message, options);
}
showInfoToast(message, options = {}) {
return Toasts.info(message, options);
}
showErrorToast(message, options = {}) {
return Toasts.error(message, options);
}
showWarningToast(message, options = {}) {
return Toasts.warning(message, options);
}
get Toasts() {
return {
push: this.showToast.bind(this),
success: this.showSuccessToast.bind(this),
error: this.showErrorToast.bind(this),
info: this.showInfoToast.bind(this),
warning: this.showWarningToast.bind(this),
get enabled() { return Toasts.enabled }
};
}
/**
* Notifications
*/
get notificationStack() {
return this._notificationStack || (this._notificationStack = []);
}
addNotification(title, text, buttons = []) {
if (arguments.length <= 1) text = title, title = undefined;
if (arguments[1] instanceof Array) [text, buttons] = arguments, title = undefined;
const notification = Notifications.add(title, text, buttons, () => Utils.removeFromArray(this.notificationStack, notification));
this.notificationStack.push(notification);
return notification;
}
dismissNotification(index) {
index = Notifications.stack.indexOf(this.notificationStack[index]);
if (index) Notifications.dismiss(index);
}
dismissAllNotifications() {
for (const index in this.notificationStack) {
this.dismissNotification(index);
}
}
get Notifications() {
return Object.defineProperty({
add: this.addNotification.bind(this),
dismiss: this.dismissNotification.bind(this),
dismissAll: this.dismissAllNotifications.bind(this)
}, 'stack', {
get: () => this.notificationStack
});
}
/**
* Autocomplete
*/
get autocompleteSets() {
return this._autocompleteSets || (this._autocompleteSets = new Map());
}
addAutocompleteController(prefix, controller) {
if (!controller) controller = this.plugin;
if (GlobalAc.validPrefix(prefix)) return;
GlobalAc.add(prefix, controller);
this.autocompleteSets.set(prefix, controller);
}
removeAutocompleteController(prefix) {
if (this.autocompleteSets.get(prefix) !== GlobalAc.sets.get(prefix)) return;
GlobalAc.remove(prefix);
this.autocompleteSets.delete(prefix);
}
removeAllAutocompleteControllers() {
for (const [prefix] of this.autocompleteSets) {
this.removeAutocompleteController(prefix);
}
}
validAutocompletePrefix(prefix) {
return GlobalAc.validPrefix(prefix);
}
toggleAutocompleteMode(prefix, sterm) {
return GlobalAc.toggle(prefix, sterm);
}
searchAutocomplete(prefix, sterm) {
return GlobalAc.items(prefix, sterm);
}
get Autocomplete() {
return Object.defineProperty({
add: this.addAutocompleteController.bind(this),
remove: this.removeAutocompleteController.bind(this),
removeAll: this.removeAllAutocompleteControllers.bind(this),
validPrefix: this.validAutocompletePrefix.bind(this),
toggle: this.toggleAutocompleteMode.bind(this),
search: this.searchAutocomplete.bind(this)
}, 'sets', {
get: () => this.autocompleteSets
});
}
/**
* Emotes
*/
get emotes() {
return EmoteModule.database;
}
get favouriteEmotes() {
return EmoteModule.favourites;
}
get mostUsedEmotes() {
return EmoteModule.mostUsed;
}
setFavouriteEmote(emote, favourite) {
return EmoteModule[favourite ? 'removeFavourite' : 'addFavourite'](emote);
}
addFavouriteEmote(emote) {
return EmoteModule.addFavourite(emote);
}
removeFavouriteEmote(emote) {
return EmoteModule.removeFavourite(emote);
}
isFavouriteEmote(emote) {
return EmoteModule.isFavourite(emote);
}
getEmote(emote) {
return EmoteModule.findByName(emote, true);
}
getEmoteUseCount(emote) {
const mostUsed = EmoteModule.mostUsed.find(mu => mu.key === emote.name);
return mostUsed ? mostUsed.useCount : 0;
}
incrementEmoteUseCount(emote) {
return EmoteModule.addToMostUsed(emote);
}
searchEmotes(regex, limit) {
return EmoteModule.search(regex, limit);
}
get Emotes() {
return Object.defineProperties({
setFavourite: this.setFavouriteEmote.bind(this),
addFavourite: this.addFavouriteEmote.bind(this),
removeFavourite: this.removeFavouriteEmote.bind(this),
isFavourite: this.isFavouriteEmote.bind(this),
getEmote: this.getEmote.bind(this),
getUseCount: this.getEmoteUseCount.bind(this),
incrementUseCount: this.incrementEmoteUseCount.bind(this),
search: this.searchEmotes.bind(this)
}, {
emotes: {
get: () => this.emotes
},
favourites: {
get: () => this.favouriteEmotes
},
mostused: {
get: () => this.mostUsedEmotes
}
});
}
@ -298,7 +501,7 @@ export default class PluginApi {
async getPlugin(plugin_id) {
// This should require extra permissions
return await PluginManager.waitForPlugin(plugin_id);
return PluginManager.waitForPlugin(plugin_id);
}
listPlugins() {
return PluginManager.localContent.map(plugin => plugin.id);
@ -316,7 +519,7 @@ export default class PluginApi {
async getTheme(theme_id) {
// This should require extra permissions
return await ThemeManager.waitForContent(theme_id);
return ThemeManager.waitForContent(theme_id);
}
listThemes() {
return ThemeManager.localContent.map(theme => theme.id);
@ -334,7 +537,7 @@ export default class PluginApi {
async getModule(module_id) {
// This should require extra permissions
return await ExtModuleManager.waitForContent(module_id);
return ExtModuleManager.waitForContent(module_id);
}
listModules() {
return ExtModuleManager.localContent.map(module => module.id);
@ -347,39 +550,71 @@ export default class PluginApi {
}
/**
* WebpackModules
* Patcher
*/
get webpackRequire() {
return WebpackModules.require;
get patches() {
return Patcher.getPatchesByCaller(this.plugin.id);
}
getWebpackModule(filter, first = true) {
return WebpackModules.getModule(filter, first);
patchBefore(...args) { return this.pushChildPatch(...args, 'before') }
patchAfter(...args) { return this.pushChildPatch(...args, 'after') }
patchInstead(...args) { return this.pushChildPatch(...args, 'instead') }
pushChildPatch(...args) {
return Patcher.pushChildPatch(this.plugin.id, ...args);
}
getWebpackModuleByName(name, fallback) {
return WebpackModules.getModuleByName(name, fallback);
unpatchAll(patches) {
return Patcher.unpatchAll(patches || this.plugin.id);
}
getWebpackModuleByRegex(regex, first = true) {
return WebpackModules.getModuleByRegex(regex, first);
}
getWebpackModuleByProperties(props, first = true) {
return WebpackModules.getModuleByProps(props, first);
}
getWebpackModuleByPrototypeFields(props, first = true) {
return WebpackModules.getModuleByPrototypes(props, first);
}
get WebpackModules() {
get Patcher() {
return Object.defineProperty({
getModule: this.getWebpackModule.bind(this),
getModuleByName: this.getWebpackModuleByName.bind(this),
getModuleByDisplayName: this.getWebpackModuleByName.bind(this),
getModuleByRegex: this.getWebpackModuleByRegex.bind(this),
getModuleByProperties: this.getWebpackModuleByProperties.bind(this),
getModuleByPrototypeFields: this.getWebpackModuleByPrototypeFields.bind(this)
}, 'require', {
get: () => this.webpackRequire
before: this.patchBefore.bind(this),
after: this.patchAfter.bind(this),
instead: this.patchInstead.bind(this),
pushChildPatch: this.pushChildPatch.bind(this),
unpatchAll: this.unpatchAll.bind(this),
monkeyPatch: this.monkeyPatch.bind(this)
}, 'patches', {
get: () => this.patches
});
}
get monkeyPatch() {
return m => MonkeyPatch(this.plugin.id, m);
}
/**
* DiscordContextMenu
*/
get discordContextMenus() {
return this._discordContextMenus || (this._discordContextMenus = []);
}
addDiscordContextMenu(items, filter) {
const menu = DiscordContextMenu.add(items, filter);
this.discordContextMenus.push(menu);
return menu;
}
removeDiscordContextMenu(menu) {
DiscordContextMenu.remove(menu);
Utils.removeFromArray(this.discordContextMenus, menu);
}
removeAllDiscordContextMenus() {
for (const menu of this.discordContextMenus) {
this.removeDiscordContextMenu(menu);
}
}
get DiscordContextMenu() {
return Object.defineProperty({
add: this.addDiscordContextMenu.bind(this),
remove: this.removeDiscordContextMenu.bind(this),
removeAll: this.removeAllDiscordContextMenus.bind(this)
}, 'menus', {
get: () => this.discordContextMenus
});
}
Vuewrap(id, component, props) {
return VueInjector.createReactElement(Vue.component(id, component), props);
}
}

View File

@ -8,15 +8,16 @@
* LICENSE file in the root directory of this source tree.
*/
import { Permissions } from 'modules';
import { Modals } from 'ui';
import { ErrorEvent } from 'structs';
import { ClientLogger as Logger } from 'common';
import Globals from './globals';
import ContentManager from './contentmanager';
import ExtModuleManager from './extmodulemanager';
import Plugin from './plugin';
import PluginApi from './pluginapi';
import Vendor from './vendor';
import { ClientLogger as Logger } from 'common';
import { Events, Permissions } from 'modules';
import { Modals } from 'ui';
import { ErrorEvent } from 'structs';
export default class extends ContentManager {
@ -40,7 +41,7 @@ export default class extends ContentManager {
this.loaded = false;
const loadAll = await this.loadAllContent(true);
this.loaded = true;
for (let plugin of this.localPlugins) {
for (const plugin of this.localPlugins) {
if (!plugin.enabled) continue;
plugin.userConfig.enabled = false;
@ -73,42 +74,42 @@ export default class extends ContentManager {
static get refreshPlugins() { return this.refreshContent }
static get loadContent() { return this.loadPlugin }
static async loadPlugin(paths, configs, info, main, dependencies, permissions) {
static async loadPlugin(paths, configs, info, main, dependencies, permissions, mainExport, packed = false) {
if (permissions && permissions.length > 0) {
for (let perm of permissions) {
console.log(`Permission: ${Permissions.permissionText(perm).HEADER} - ${Permissions.permissionText(perm).BODY}`);
for (const perm of permissions) {
Logger.log(this.moduleName, `Permission: ${Permissions.permissionText(perm).HEADER} - ${Permissions.permissionText(perm).BODY}`);
}
try {
const allowed = await Modals.permissions(`${info.name} wants to:`, info.name, permissions).promise;
} catch (err) {
return null;
return;
}
}
const deps = [];
const deps = {};
if (dependencies) {
for (const [key, value] of Object.entries(dependencies)) {
const extModule = ExtModuleManager.findModule(key);
if (!extModule) {
throw {
'message': `Dependency: ${key}:${value} is not loaded`
};
throw {message: `Dependency ${key}:${value} is not loaded.`};
}
deps[key] = extModule.__require;
}
}
const plugin = window.require(paths.mainPath)(Plugin, new PluginApi(info), Vendor, deps);
if (!(plugin.prototype instanceof Plugin))
const pluginExports = Globals.require(paths.mainPath);
const pluginFunction = mainExport ? pluginExports[mainExport]
: pluginExports.__esModule ? pluginExports.default : pluginExports;
if (typeof pluginFunction !== 'function')
throw {message: `Plugin ${info.name} did not export a function.`};
const plugin = pluginFunction.call(pluginExports, Plugin, new PluginApi(info, paths.contentPath), Vendor, deps);
if (!plugin || !(plugin.prototype instanceof Plugin))
throw {message: `Plugin ${info.name} did not return a class that extends Plugin.`};
const instance = new plugin({
configs, info, main,
paths: {
contentPath: paths.contentPath,
dirName: paths.dirName,
mainPath: paths.mainPath
}
configs, info, main, paths
});
if (instance.enabled && this.loaded) {
@ -118,27 +119,38 @@ export default class extends ContentManager {
return instance;
}
static get deletePlugin() { return this.deleteContent }
static get unloadPlugin() { return this.unloadContent }
static get reloadPlugin() { return this.reloadContent }
static stopPlugin(name) {
const plugin = name instanceof Plugin ? name : this.getPluginByName(name);
try {
if (plugin) return plugin.stop();
} catch (err) {
// Logger.err('PluginManager', err);
static unloadContentHook(content, reload) {
delete Globals.require.cache[Globals.require.resolve(content.paths.mainPath)];
const uncache = [];
for (const required in Globals.require.cache) {
if (!required.includes(content.paths.contentPath)) continue;
uncache.push(Globals.require.resolve(required));
}
return true; //Return true anyways since plugin doesn't exist
for (const u of uncache) delete Globals.require.cache[u];
}
static startPlugin(name) {
const plugin = name instanceof Plugin ? name : this.getPluginByName(name);
try {
if (plugin) return plugin.start();
} catch (err) {
// Logger.err('PluginManager', err);
}
return true; //Return true anyways since plugin doesn't exist
/**
* Stops a plugin.
* @param {Plugin|String} plugin
* @return {Promise}
*/
static stopPlugin(plugin) {
plugin = this.isPlugin(plugin) ? plugin : this.getPluginById(plugin);
return plugin.stop();
}
/**
* Starts a plugin.
* @param {Plugin|String} plugin
* @return {Promise}
*/
static startPlugin(plugin) {
plugin = this.isPlugin(plugin) ? plugin : this.getPluginById(plugin);
return plugin.start();
}
static get isPlugin() { return this.isThisContent }

View File

@ -1,24 +1,27 @@
/**
* BetterDiscord React Component Manipulations
* original concept and some code by samogot - https://github.com/samogot / https://github.com/samogot/betterdiscord-plugins/tree/master/v2/1Lib%20Discord%20Internals
* Original concept and some code by samogot - https://github.com/samogot / https://github.com/samogot/betterdiscord-plugins/tree/master/v2/1Lib%20Discord%20Internals
*
* Copyright (c) 2015-present JsSucks - https://github.com/JsSucks
* All rights reserved.
* https://github.com/JsSucks - https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
* LICENSE file in the root directory of this source tree.
*/
import { MonkeyPatch, Patcher } from './patcher';
import { WebpackModules, Filters } from './webpackmodules';
import { DOM, Modals } from 'ui';
import { Utils, Filters, ClientLogger as Logger } from 'common';
import { MonkeyPatch } from './patcher';
import Reflection from './reflection/index';
import DiscordApi from './discordapi';
import { EmoteModule } from 'builtin';
import { Reflection } from 'ui';
import PackageInstaller from './packageinstaller';
class Helpers {
static get plannedActions() {
return this._plannedActions || (this._plannedActions = new Map());
}
static recursiveArray(parent, key, count = 1) {
let index = 0;
function* innerCall(parent, key) {
@ -34,30 +37,34 @@ class Helpers {
return innerCall(parent, key);
}
static recursiveArrayCount(parent, key) {
let count = 0;
// eslint-disable-next-line no-empty-pattern
for (let { } of this.recursiveArray(parent, key))
for (let {} of this.recursiveArray(parent, key))
++count;
return this.recursiveArray(parent, key, count);
}
static get recursiveChildren() {
return function* (parent, key, index = 0, count = 1) {
const item = parent[key];
yield { item, parent, key, index, count };
if (item && item.props && item.props.children) {
for (let { parent, key, index, count } of this.recursiveArrayCount(item.props, 'children')) {
for (const { parent, key, index, count } of this.recursiveArrayCount(item.props, 'children')) {
yield* this.recursiveChildren(parent, key, index, count);
}
}
}
}
static returnFirst(iterator, process) {
for (let child of iterator) {
for (const child of iterator) {
const retVal = process(child);
if (retVal !== undefined) return retVal;
}
}
static getFirstChild(rootParent, rootKey, selector) {
const getDirectChild = (item, selector) => {
if (item && item.props && item.props.children) {
@ -105,26 +112,27 @@ class Helpers {
if (match) {
if (selector.child) {
return getDirectChild(item, selector.child);
}
else if (selector.successor) {
} else if (selector.successor) {
return this.getFirstChild(parent, key, selector.successor);
}
else {
return { item, parent, key };
}
return { item, parent, key };
}
};
return this.returnFirst(this.recursiveChildren(rootParent, rootKey), checkFilter.bind(null, selector)) || {};
}
static parseSelector(selector) {
if (selector.startsWith('.')) return { className: selector.substr(1) }
if (selector.startsWith('#')) return { id: selector.substr(1) }
return {}
}
static findByProp(obj, what, value) {
if (obj.hasOwnProperty(what) && obj[what] === value) return obj;
if (obj.props && !obj.children) return this.findByProp(obj.props, what, value);
if (!obj.children || !obj.children.length) return null;
if (!obj.children) return null;
if (!(obj.children instanceof Array)) return this.findByProp(obj.children, what, value);
for (const child of obj.children) {
if (!child) continue;
const findInChild = this.findByProp(child, what, value);
@ -132,6 +140,7 @@ class Helpers {
}
return null;
}
static findProp(obj, what) {
if (obj.hasOwnProperty(what)) return obj[what];
if (obj.props && !obj.children) return this.findProp(obj.props, what);
@ -144,25 +153,40 @@ class Helpers {
}
return null;
}
static get React() {
return Reflection.modules.React;
}
static get ReactDOM() {
return WebpackModules.getModuleByName('ReactDOM');
return Reflection.modules.ReactDOM;
}
}
export { Helpers as ReactHelpers };
class ReactComponent {
constructor(id, component, retVal) {
this._id = id;
this._component = component;
this._retVal = retVal;
constructor(id, component, retVal, important) {
this.id = id;
this.component = component;
this.retVal = retVal;
this.important = important;
}
get id() {
return this._id;
get elements() {
if (!this.important || !this.important.selector) return [];
return document.querySelectorAll(this.important.selector);
}
get component() {
return this._component;
get stateNodes() {
return [...this.elements].map(e => Reflection.DOM(e).getComponentStateNode(this));
}
get retVal() {
return this._retVal;
forceUpdateAll() {
for (const e of this.elements) {
Reflection.DOM(e).forceUpdate(this);
}
}
}
@ -171,61 +195,119 @@ export class ReactComponents {
static get unknownComponents() { return this._unknownComponents || (this._unknownComponents = []) }
static get listeners() { return this._listeners || (this._listeners = []) }
static get nameSetters() { return this._nameSetters || (this._nameSetters = []) }
static get componentAliases() { return this._componentAliases || (this._componentAliases = []) }
static push(component, retVal) {
static get ReactComponent() { return ReactComponent }
static push(component, retVal, important) {
if (!(component instanceof Function)) return null;
const { displayName } = component;
if (!displayName) {
return this.processUnknown(component, retVal);
}
const have = this.components.find(comp => comp.id === displayName);
if (have) return component;
const c = new ReactComponent(displayName, component, retVal);
this.components.push(c);
const listener = this.listeners.find(listener => listener.id === displayName);
if (!listener) return c;
for (const l of listener.listeners) {
l(c);
if (have) {
if (!have.important) have.important = important;
return component;
}
this.listeners.splice(this.listeners.findIndex(listener => listener.id === displayName), 1);
const c = new ReactComponent(displayName, component, retVal, important);
this.components.push(c);
const listener = this.listeners.find(listener => listener.id === displayName);
if (listener) {
for (const l of listener.listeners) l(c);
Utils.removeFromArray(this.listeners, listener);
}
return c;
}
static async getComponent(name, important) {
/**
* Finds a component from the components array or by waiting for it to be mounted.
* @param {String} name The component's name
* @param {Object} important An object containing a selector to look for
* @param {Function} filter A function to filter components if a single element is rendered by multiple components
* @return {Promise => ReactComponent}
*/
static async getComponent(name, important, filter) {
name = this.getComponentName(name);
const have = this.components.find(c => c.id === name);
if (have) return have;
if (important) {
const importantInterval = setInterval(() => {
const callback = () => {
if (this.components.find(c => c.id === name)) {
console.info(`Important component ${name} already found`);
clearInterval(importantInterval);
Logger.info('ReactComponents', `Important component ${name} already found`);
DOM.observer.unsubscribe(observerSubscription);
return;
}
const select = document.querySelector(important.selector);
if (!select) return;
const reflect = Reflection(select);
if (!reflect.component) {
clearInterval(importantInterval);
console.error(`FAILED TO GET IMPORTANT COMPONENT ${name} WITH REFLECTION FROM`, select);
const elements = document.querySelectorAll(important.selector);
if (!elements.length) return;
let component, reflect;
for (const element of elements) {
reflect = Reflection.DOM(element);
component = filter ? reflect.components.find(component => {
try {
return filter.call(undefined, component);
} catch (err) {
return false;
}
}) : reflect.component;
if (component) break;
}
if (!component && filter) {
Logger.log('ReactComponents', ['Found elements matching the query selector but no components passed the filter']);
return;
}
if (!reflect.component.displayName) reflect.component.displayName = name;
console.info(`Found important component ${name} with reflection.`);
this.push(reflect.component);
clearInterval(importantInterval);
}, 50);
DOM.observer.unsubscribe(observerSubscription);
if (!component) {
Logger.err('ReactComponents', [`FAILED TO GET IMPORTANT COMPONENT ${name} WITH REFLECTION FROM`, elements]);
return;
}
if (!component.displayName) component.displayName = name;
Logger.info('ReactComponents', [`Found important component ${name} with reflection`, reflect]);
important.filter = filter;
this.push(component, undefined, important);
};
const observerSubscription = DOM.observer.subscribeToQuerySelector(callback, important.selector, null, true);
setTimeout(callback, 0);
}
const listener = this.listeners.find(l => l.id === name);
if (!listener) this.listeners.push({
let listener = this.listeners.find(l => l.id === name);
if (!listener) this.listeners.push(listener = {
id: name,
listeners: []
});
return new Promise(resolve => {
this.listeners.find(l => l.id === name).listeners.push(c => resolve(c));
listener.listeners.push(resolve);
});
}
static setName(name, filter, callback) {
static getComponentName(name) {
const resolvedAliases = [];
while (this.componentAliases[name]) {
resolvedAliases.push(name);
name = this.componentAliases[name];
if (resolvedAliases.includes(name)) break;
}
return name;
}
static setName(name, filter) {
const have = this.components.find(c => c.id === name);
if (have) return have;
@ -243,7 +325,7 @@ export class ReactComponents {
const have = this.unknownComponents.find(c => c.component === component);
for (const [fi, filter] of this.nameSetters.entries()) {
if (filter.filter.filter(component)) {
console.log('filter match!');
Logger.log('ReactComponents', 'Filter match!');
component.displayName = filter.name;
this.nameSetters.splice(fi, 1);
return this.push(component, retVal);
@ -256,36 +338,34 @@ export class ReactComponents {
}
export class ReactAutoPatcher {
/**
* Wait for React to be loaded and patch it's createElement to store all unknown components.
* Also patches some known components.
*/
static async autoPatch() {
await this.ensureReact();
this.React = {};
this.React.unpatchCreateElement = MonkeyPatch('BD:ReactComponents:createElement', 'React').before('createElement', (component, args) => {
ReactComponents.push(args[0]);
});
const React = await Reflection.module.waitForModuleByName('React');
this.unpatchCreateElement = MonkeyPatch('BD:ReactComponents:createElement', React).before('createElement', (component, args) => ReactComponents.push(args[0]));
this.patchComponents();
return 1;
}
static async ensureReact() {
while (!window.webpackJsonp || !WebpackModules.getModuleByName('React')) await new Promise(resolve => setTimeout(resolve, 10));
return 1;
}
static async patchComponents() {
await this.patchMessage();
await this.patchMessageGroup();
await this.patchChannelMember();
await this.patchGuild();
await this.patchChannel();
await this.patchChannelList();
this.forceUpdate();
/**
* Patches a few known components.
*/
static patchComponents() {
const componentPatchFunctions = Object.getOwnPropertyNames(this).filter(p => p.startsWith('patch') && p !== 'patchComponents');
return Promise.all(componentPatchFunctions.map(p => this[p].call(this)));
}
static async patchMessage() {
this.Message = await ReactComponents.getComponent('Message', { selector: '.message' });
const { selector } = Reflection.resolve('message', 'messageCozy', 'messageCompact');
this.Message = await ReactComponents.getComponent('Message', {selector}, m => m.prototype && m.prototype.renderCozy);
this.unpatchMessageRender = MonkeyPatch('BD:ReactComponents', this.Message.component.prototype).after('render', (component, args, retVal) => {
const { message } = component.props;
const { message, jumpSequenceId, canFlash } = component.props;
const { id, colorString, bot, author, attachments, embeds } = message;
if (jumpSequenceId && canFlash) retVal = retVal.props.children;
retVal.props['data-message-id'] = id;
retVal.props['data-colourstring'] = colorString;
if (author && author.id) retVal.props['data-user-id'] = author.id;
@ -293,62 +373,214 @@ export class ReactAutoPatcher {
if (attachments && attachments.length) retVal.props.className += ' bd-hasAttachments';
if (embeds && embeds.length) retVal.props.className += ' bd-hasEmbeds';
if (author && author.id === DiscordApi.currentUser.id) retVal.props.className += ' bd-isCurrentUser';
const dapiMessage = DiscordApi.Message.from(message);
if (dapiMessage.guild && author.id === dapiMessage.guild.ownerId) retVal.props.className += ' bd-isGuildOwner';
if (dapiMessage.guild && dapiMessage.guild.isMember(author.id)) retVal.props.className += ' bd-isGuildMember';
});
this.Message.forceUpdateAll();
}
static async patchMessageContent() {
const { selector } = Reflection.resolve('container', 'containerCozy', 'containerCompact', 'edited');
this.MessageContent = await ReactComponents.getComponent('MessageContent', {selector}, c => c.defaultProps && c.defaultProps.hasOwnProperty('disableButtons'));
}
static async patchSpoiler() {
const { selector } = Reflection.resolve('spoilerText', 'spoilerContainer');
this.Spoiler = await ReactComponents.getComponent('Spoiler', {selector}, c => c.prototype.renderSpoilerText);
}
static async patchMessageAccessories() {
const { selector } = Reflection.resolve('container', 'containerCozy', 'embedWrapper');
this.MessageAccessories = await ReactComponents.getComponent('MessageAccessories', {selector});
}
static async patchMessageGroup() {
this.MessageGroup = await ReactComponents.getComponent('MessageGroup', { selector: '.message-group' });
const { selector } = Reflection.resolve('container', 'message', 'messageCozy');
this.MessageGroup = await ReactComponents.getComponent('MessageGroup', {selector});
this.unpatchMessageGroupRender = MonkeyPatch('BD:ReactComponents', this.MessageGroup.component.prototype).after('render', (component, args, retVal) => {
const { author, type } = component.props.messages[0];
retVal.props['data-author-id'] = author.id;
if (author.id === DiscordApi.currentUser.id) retVal.props.className += ' bd-isCurrentUser';
if (type !== 0) retVal.props.className += ' bd-isSystemMessage';
const dapiMessage = DiscordApi.Message.from(component.props.messages[0]);
if (dapiMessage.guild && author.id === dapiMessage.guild.ownerId) retVal.props.className += ' bd-isGuildOwner';
if (dapiMessage.guild && dapiMessage.guild.isMember(author.id)) retVal.props.className += ' bd-isGuildMember';
});
this.MessageGroup.forceUpdateAll();
}
static async patchImageWrapper() {
ReactComponents.componentAliases.ImageWrapper = 'Image';
const { selector } = Reflection.resolve('imageWrapper');
this.ImageWrapper = await ReactComponents.getComponent('ImageWrapper', {selector}, c => typeof c.defaultProps.children === 'function');
}
static async patchChannelMember() {
this.ChannelMember = await ReactComponents.getComponent('ChannelMember', { selector: '.member.member-status' });
ReactComponents.componentAliases.ChannelMember = 'MemberListItem';
const { selector } = Reflection.resolve('member', 'memberInner', 'activity');
this.ChannelMember = await ReactComponents.getComponent('ChannelMember', {selector}, m => m.prototype.renderActivity);
this.unpatchChannelMemberRender = MonkeyPatch('BD:ReactComponents', this.ChannelMember.component.prototype).after('render', (component, args, retVal) => {
if (!retVal.props || !retVal.props.children || !retVal.props.children.length) return;
if (!retVal.props || !retVal.props.children) return;
const user = Helpers.findProp(component, 'user');
if (!user) return;
retVal.props['data-user-id'] = user.id;
retVal.props['data-colourstring'] = component.props.colorString;
if (component.props.isOwner) retVal.props.className += ' bd-isGuildOwner';
});
this.ChannelMember.forceUpdateAll();
}
static async patchNameTag() {
const { selector } = Reflection.resolve('nameTag', 'username', 'discriminator', 'ownerIcon');
this.NameTag = await ReactComponents.getComponent('NameTag', {selector});
}
static async patchGuild() {
this.Guild = await ReactComponents.getComponent('Guild');
const selector = `div.${Reflection.resolve('container', 'guildIcon', 'selected', 'unread').className}:not(:first-child)`;
this.Guild = await ReactComponents.getComponent('Guild', {selector}, m => m.prototype.renderBadge);
this.unpatchGuild = MonkeyPatch('BD:ReactComponents', this.Guild.component.prototype).after('render', (component, args, retVal) => {
const { guild } = component.props;
if (!guild) return;
retVal.props['data-guild-id'] = guild.id;
retVal.props['data-guild-name'] = guild.name;
});
this.Guild.forceUpdateAll();
}
/**
* The Channel component contains the header, message scroller, message form and member list.
*/
static async patchChannel() {
this.Channel = await ReactComponents.getComponent('Channel');
const { selector } = Reflection.resolve('chat', 'title', 'channelName');
this.Channel = await ReactComponents.getComponent('Channel', {selector});
this.unpatchChannel = MonkeyPatch('BD:ReactComponents', this.Channel.component.prototype).after('render', (component, args, retVal) => {
const channel = component.props.channel || component.state.channel;
if (!channel) return;
retVal.props['data-channel-id'] = channel.id;
retVal.props['data-channel-name'] = channel.name;
if ([0, 2, 4].includes(channel.type)) retVal.props.className += ' bd-isGuildChannel';
if ([1, 3].includes(channel.type)) retVal.props.className += ' bd-isPrivateChannel';
if (channel.type === 3) retVal.props.className += ' bd-isGroupChannel';
});
this.Channel.forceUpdateAll();
}
static async patchChannelList() {
this.GuildChannel = await ReactComponents.getComponent('GuildChannel', { selector: '.containerDefault-7RImuF' });
this.unpatchGuildChannel = MonkeyPatch('BD:ReactComponents', this.GuildChannel.component.prototype).after('render', (component, args, retVal) => {
const { channel } = component.props;
if (!channel) return;
retVal.props['data-channel-id'] = channel.id;
retVal.props['data-channel-name'] = channel.name;
});
static async patchChannelTextArea() {
const { selector } = Reflection.resolve('channelTextArea', 'autocomplete');
this.ChannelTextArea = await ReactComponents.getComponent('ChannelTextArea', {selector});
}
static forceUpdate() {
for (const e of document.querySelectorAll('.message,.message-group,.guild,.containerDefault-7RImuF,.channel-members .member')) {
Reflection(e).forceUpdate();
}
/**
* The GuildTextChannel component represents a text channel in the guild channel list.
*/
static async patchGuildTextChannel() {
ReactComponents.componentAliases.GuildTextChannel = 'TextChannel';
const { selector } = Reflection.resolve('containerDefault', 'actionIcon');
this.GuildTextChannel = await ReactComponents.getComponent('GuildTextChannel', {selector}, c => c.prototype.renderMentionBadge);
this.unpatchGuildTextChannel = MonkeyPatch('BD:ReactComponents', this.GuildTextChannel.component.prototype).after('render', this._afterChannelRender);
this.GuildTextChannel.forceUpdateAll();
}
/**
* The GuildVoiceChannel component represents a voice channel in the guild channel list.
*/
static async patchGuildVoiceChannel() {
ReactComponents.componentAliases.GuildVoiceChannel = 'VoiceChannel';
const { selector } = Reflection.resolve('containerDefault', 'actionIcon');
this.GuildVoiceChannel = await ReactComponents.getComponent('GuildVoiceChannel', {selector}, c => c.prototype.handleVoiceConnect);
this.unpatchGuildVoiceChannel = MonkeyPatch('BD:ReactComponents', this.GuildVoiceChannel.component.prototype).after('render', this._afterChannelRender);
this.GuildVoiceChannel.forceUpdateAll();
}
/**
* The DirectMessage component represents a channel in the direct messages list.
*/
static async patchDirectMessage() {
ReactComponents.componentAliases.DirectMessage = 'PrivateChannel';
const { selector } = Reflection.resolve('channel', 'avatar', 'name');
this.DirectMessage = await ReactComponents.getComponent('DirectMessage', {selector}, c => c.prototype.renderAvatar);
this.unpatchDirectMessage = MonkeyPatch('BD:ReactComponents', this.DirectMessage.component.prototype).after('render', this._afterChannelRender);
this.DirectMessage.forceUpdateAll();
}
static _afterChannelRender(component, args, retVal) {
const { channel } = component.props;
if (!channel) return;
retVal.props['data-channel-id'] = channel.id;
retVal.props['data-channel-name'] = channel.name;
if ([0, 2, 4].includes(channel.type)) retVal.props.className += ' bd-isGuildChannel';
if (channel.type === 2) retVal.props.className += ' bd-isVoiceChannel';
// if (channel.type === 4) retVal.props.className += ' bd-isChannelCategory';
if ([1, 3].includes(channel.type)) retVal.props.className += ' bd-isPrivateChannel';
if (channel.type === 3) retVal.props.className += ' bd-isGroupChannel';
}
static async patchUserProfileModal() {
ReactComponents.componentAliases.UserProfileModal = 'UserProfileBody';
const { selector } = Reflection.resolve('root', 'topSectionNormal');
this.UserProfileModal = await ReactComponents.getComponent('UserProfileModal', {selector}, c => c.prototype.renderHeader && c.prototype.renderBadges);
this.unpatchUserProfileModal = MonkeyPatch('BD:ReactComponents', this.UserProfileModal.component.prototype).after('render', (component, args, retVal) => {
const root = retVal.props.children[0] || retVal.props.children;
const { user } = component.props;
if (!user) return;
root.props['data-user-id'] = user.id;
if (user.bot) root.props.className += ' bd-isBot';
if (user.id === DiscordApi.currentUser.id) root.props.className += ' bd-isCurrentUser';
});
this.UserProfileModal.forceUpdateAll();
}
static async patchUserPopout() {
const { selector } = Reflection.resolve('userPopout', 'headerNormal');
this.UserPopout = await ReactComponents.getComponent('UserPopout', {selector}, c => c.prototype.renderHeader);
this.unpatchUserPopout = MonkeyPatch('BD:ReactComponents', this.UserPopout.component.prototype).after('render', (component, args, retVal) => {
const root = retVal.props.children[0] || retVal.props.children;
const { user, guild, guildMember } = component.props;
if (!user) return;
root.props['data-user-id'] = user.id;
if (user.bot) root.props.className += ' bd-isBot';
if (user.id === DiscordApi.currentUser.id) root.props.className += ' bd-isCurrentUser';
if (guild) root.props['data-guild-id'] = guild.id;
if (guild && user.id === guild.ownerId) root.props.className += ' bd-isGuildOwner';
if (guild && guildMember) root.props.className += ' bd-isGuildMember';
if (guildMember && guildMember.roles.length) root.props.className += ' bd-hasRoles';
});
this.UserPopout.forceUpdateAll();
}
static async patchUploadArea() {
const { selector } = Reflection.resolve('uploadArea');
this.UploadArea = await ReactComponents.getComponent('UploadArea', {selector});
PackageInstaller.uploadAreaPatch(this.UploadArea);
}
}

View File

@ -0,0 +1,44 @@
/**
* BetterDiscord Reflection Module
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import { Module, Modules } from './modules';
import { Reflection as DOM } from 'ui';
import Resolver from './resolver';
export default class Reflection {
static get module() {
return Module;
}
static get modules() {
return Modules;
}
static get resolve() {
return Resolver.resolve;
}
static get resolveAsync() {
return Resolver.resolveAsync;
}
static get resolver() {
return Resolver;
}
static get DOM() {
return DOM;
}
static get require() {
return Module.require;
}
}

View File

@ -0,0 +1,433 @@
/**
* BetterDiscord Reflection Modules
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import { Utils, Filters } from 'common';
import Events from '../events';
const KnownModules = {
React: Filters.byProperties(['createElement', 'cloneElement']),
ReactDOM: Filters.byProperties(['render', 'findDOMNode']),
Events: Filters.byPrototypeFields(['setMaxListeners', 'emit']),
/* Guild Info, Stores, and Utilities */
GuildStore: Filters.byProperties(['getGuild']),
SortedGuildStore: Filters.byProperties(['getSortedGuilds']),
SelectedGuildStore: Filters.byProperties(['getLastSelectedGuildId']),
GuildSync: Filters.byProperties(['getSyncedGuilds']),
GuildInfo: Filters.byProperties(['getAcronym']),
GuildChannelsStore: Filters.byProperties(['getChannels', 'getDefaultChannel']),
GuildMemberStore: Filters.byProperties(['getMember']),
MemberCountStore: Filters.byProperties(['getMemberCounts']),
GuildEmojiStore: Filters.byProperties(['getEmojis']),
GuildActions: Filters.byProperties(['markGuildAsRead']),
GuildPermissions: Filters.byProperties(['getGuildPermissions']),
/* Channel Store & Actions */
ChannelStore: Filters.byProperties(['getChannels', 'getDMFromUserId']),
SelectedChannelStore: Filters.byProperties(['getLastSelectedChannelId']),
ChannelActions: Filters.byProperties(['selectChannel']),
PrivateChannelActions: Filters.byProperties(['openPrivateChannel']),
ChannelSelector: Filters.byProperties(['selectGuild', 'selectChannel']),
VoiceChannelActions: Filters.byProperties(['selectVoiceChannel']),
/* Current User Info, State and Settings */
UserInfoStore: Filters.byProperties(['getToken']),
UserSettingsStore: Filters.byProperties(['guildPositions']),
AccountManager: Filters.byProperties(['register', 'login']),
UserSettingsUpdater: Filters.byProperties(['updateRemoteSettings']),
OnlineWatcher: Filters.byProperties(['isOnline']),
CurrentUserIdle: Filters.byProperties(['getIdleTime']),
RelationshipStore: Filters.byProperties(['isBlocked', 'isFriend']),
RelationshipManager: Filters.byProperties(['addRelationship']),
MentionStore: Filters.byProperties(['getMentions']),
/* User Stores and Utils */
UserStore: Filters.byProperties(['getCurrentUser']),
UserStatusStore: Filters.byProperties(['getStatuses']),
UserTypingStore: Filters.byProperties(['isTyping']),
UserActivityStore: Filters.byProperties(['getActivity']),
UserNameResolver: Filters.byProperties(['getName']),
UserNoteStore: Filters.byProperties(['getNote']),
UserNoteActions: Filters.byProperties(['updateNote']),
DraftActions: Filters.byProperties(['changeDraft']),
/* Emoji Store and Utils */
EmojiInfo: Filters.byProperties(['isEmojiDisabled']),
EmojiUtils: Filters.byProperties(['getGuildEmoji']),
EmojiStore: Filters.byProperties(['getByCategory', 'EMOJI_NAME_RE']),
/* Invite Store and Utils */
InviteStore: Filters.byProperties(['getInvites']),
InviteResolver: Filters.byProperties(['findInvite']),
InviteActions: Filters.byProperties(['acceptInvite']),
/* Discord Objects & Utils */
DiscordConstants: Filters.byProperties(['Permissions', 'ActivityTypes', 'StatusTypes']),
Permissions: Filters.byProperties(['getHighestRole']),
ColorConverter: Filters.byProperties(['hex2int']),
ColorShader: Filters.byProperties(['darken']),
TinyColor: Filters.byPrototypeFields(['toRgb']),
ClassResolver: Filters.byProperties(['getClass']),
ButtonData: Filters.byProperties(['ButtonSizes']),
IconNames: Filters.byProperties(['IconNames']),
NavigationUtils: Filters.byProperties(['transitionTo', 'replaceWith', 'getHistory']),
/* Discord Messages */
MessageStore: Filters.byProperties(['getMessages']),
MessageActions: Filters.byProperties(['jumpToMessage', '_sendMessage']),
MessageQueue: Filters.byProperties(['enqueue']),
MessageParser: Filters.byProperties(['createMessage', 'parse', 'unparse']),
/* In-Game Overlay */
OverlayUserPopoutSettings: Filters.byProperties(['openUserPopout']),
OverlayUserPopoutInfo: Filters.byProperties(['getOpenedUserPopout']),
/* Experiments */
ExperimentStore: Filters.byProperties(['getExperimentOverrides']),
ExperimentsManager: Filters.byProperties(['isDeveloper']),
CurrentExperiment: Filters.byProperties(['getExperimentId']),
/* Images, Avatars and Utils */
ImageResolver: Filters.byProperties(['getUserAvatarURL']),
ImageUtils: Filters.byProperties(['getSizedImageSrc']),
AvatarDefaults: Filters.byProperties(['getUserAvatarURL', 'DEFAULT_AVATARS']),
/* Drag & Drop */
DNDActions: Filters.byProperties(['beginDrag']),
DNDSources: Filters.byProperties(['addTarget']),
DNDObjects: Filters.byProperties(['DragSource']),
/* Electron & Other Internals with Utils */
ElectronModule: Filters.byProperties(['_getMainWindow']),
Dispatcher: Filters.byProperties(['dirtyDispatch']),
PathUtils: Filters.byProperties(['hasBasename']),
NotificationModule: Filters.byProperties(['showNotification']),
RouterModule: Filters.byProperties(['Router']),
APIModule: Filters.byProperties(['getAPIBaseURL']),
AnalyticEvents: Filters.byProperties(['AnalyticEventConfigs']),
KeyGenerator: Filters.byCode(/"binary"/),
Buffers: Filters.byProperties(['Buffer', 'kMaxLength']),
DeviceStore: Filters.byProperties(['getDevices']),
SoftwareInfo: Filters.byProperties(['os']),
CurrentContext: Filters.byProperties(['setTagsContext']),
/* Media Stuff (Audio/Video) */
MediaDeviceInfo: Filters.byProperties(['Codecs', 'SUPPORTED_BROWSERS']),
MediaInfo: Filters.byProperties(['getOutputVolume']),
MediaEngineInfo: Filters.byProperties(['MediaEngineFeatures']),
VoiceInfo: Filters.byProperties(['EchoCancellation']),
VideoStream: Filters.byProperties(['getVideoStream']),
SoundModule: Filters.byProperties(['playSound']),
/* Window, DOM, HTML */
WindowInfo: Filters.byProperties(['isFocused', 'windowSize']),
TagInfo: Filters.byProperties(['VALID_TAG_NAMES']),
DOMInfo: Filters.byProperties(['canUseDOM']),
/* Locale/Location and Time */
LocaleManager: Filters.byProperties(['setLocale']),
Moment: Filters.byProperties(['parseZone']),
LocationManager: Filters.byProperties(['createLocation']),
Timestamps: Filters.byProperties(['fromTimestamp']),
TimeFormatter: Filters.byProperties(['dateFormat']),
/* Strings and Utils */
Strings: Filters.byProperties(['TEXT', 'TEXTAREA_PLACEHOLDER']),
StringFormats: Filters.byProperties(['a', 'z']),
StringUtils: Filters.byProperties(['toASCII']),
/* URLs and Utils */
URLParser: Filters.byProperties(['Url', 'parse']),
ExtraURLs: Filters.byProperties(['getArticleURL']),
/* Text Processing */
hljs: Filters.byProperties(['highlight', 'highlightBlock']),
SimpleMarkdown: Filters.byProperties(['parseBlock', 'parseInline', 'defaultOutput']),
/* DOM/React Components */
/* ==================== */
LayerManager: Filters.byProperties(['popLayer', 'pushLayer']),
UserSettingsWindow: Filters.byProperties(['open', 'updateAccount']),
ChannelSettingsWindow: Filters.byProperties(['open', 'updateChannel']),
GuildSettingsWindow: Filters.byProperties(['open', 'updateGuild']),
/* Modals */
ModalStack: Filters.byProperties(['push', 'update', 'pop', 'popWithKey']),
ConfirmModal: Filters.byPrototypeFields(['handleCancel', 'handleSubmit', 'handleMinorConfirm']),
UserProfileModal: Filters.byProperties(['fetchMutualFriends', 'setSection']),
ChangeNicknameModal: Filters.byProperties(['open', 'changeNickname']),
CreateChannelModal: Filters.byProperties(['open', 'createChannel']),
PruneMembersModal: Filters.byProperties(['open', 'prune']),
NotificationSettingsModal: Filters.byProperties(['open', 'updateNotificationSettings']),
PrivacySettingsModal: Filters.byCode(/PRIVACY_SETTINGS_MODAL_OPEN/, m => m.open),
CreateInviteModal: Filters.byProperties(['open', 'createInvite']),
/* Popouts */
PopoutStack: Filters.byProperties(['open', 'close', 'closeAll']),
PopoutOpener: Filters.byProperties(['openPopout']),
EmojiPicker: Filters.byPrototypeFields(['onHoverEmoji', 'selectEmoji']),
/* Context Menus */
ContextMenuActions: Filters.byCode(/CONTEXT_MENU_CLOSE/, c => c.close),
ContextMenuItemsGroup: Filters.byCode(/itemGroup/),
ContextMenuItem: Filters.byCode(/\.label\b.*\.hint\b.*\.action\b/),
/* In-Message Links */
ExternalLink: Filters.byCode(/\.trusted\b/)
};
class Module {
/**
* Finds a module using a filter function.
* @param {Function} filter A function to use to filter modules
* @param {Boolean} first Whether to return only the first matching module
* @param {Array} modules An array of modules to search in
* @return {Any}
*/
static getModule(filter, first = true, _modules) {
const modules = _modules || this.getAllModules();
const rm = [];
for (const index in modules) {
if (!modules.hasOwnProperty(index)) continue;
const module = modules[index];
const { exports } = module;
let foundModule = null;
if (!exports) continue;
if (exports.__esModule && exports.default && filter(exports.default)) foundModule = exports.default;
if (filter(exports)) foundModule = exports;
if (!foundModule) continue;
if (first) return foundModule;
rm.push(foundModule);
}
return first ? undefined : rm;
}
/**
* Finds a module by it's name.
* @param {String} name The name of the module
* @param {Function} fallback A function to use to filter modules if not finding a known module
* @return {Any}
*/
static byName(name, fallback) {
if (Cache.hasOwnProperty(name)) return Cache[name];
if (KnownModules.hasOwnProperty(name)) fallback = KnownModules[name];
if (!fallback) return undefined;
const module = this.getModule(fallback, true);
return module ? Cache[name] = module : undefined;
}
/**
* Finds a module by it's display name.
* @param {String} name The display name of the module
* @return {Any}
*/
static byDisplayName(name) {
return this.getModule(Filters.byDisplayName(name), true);
}
/**
* Finds a module using it's code.
* @param {RegEx} regex A regular expression to use to filter modules
* @param {Boolean} first Whether to return the only the first matching module
* @return {Any}
*/
static byRegex(regex, first = true) {
return this.getModule(Filters.byCode(regex), first);
}
/**
* Finds the first module using properties on it's prototype.
* @param {any} props Properties to use to filter modules
* @return {Any}
*/
static byPrototypes(...prototypes) {
return this.getModule(Filters.byPrototypeFields(prototypes), true);
}
/**
* Finds all modules using properties on it's prototype.
* @param {any} props Properties to use to filter modules
* @return {Any}
*/
static allByPrototypes(...prototypes) {
return this.getModule(Filters.byPrototypeFields(prototypes), false);
}
/**
* Finds the first module using it's own properties.
* @param {any} props Properties to use to filter modules
* @return {Any}
*/
static byProps(...props) {
return this.getModule(Filters.byProperties(props), true);
}
/**
* Finds all modules using it's own properties.
* @param {any} props Properties to use to filter modules
* @return {Any}
*/
static allByProps(...props) {
return this.getModule(Filters.byProperties(props), false);
}
/**
* Discord's __webpack_require__ function.
*/
static get require() {
if (this._require) return this._require;
const __webpack_require__ = this.getWebpackRequire();
if (!__webpack_require__) return null;
this.hookWebpackRequireCache(__webpack_require__);
return this._require = __webpack_require__;
}
static getWebpackRequire() {
const id = 'bd-webpackmodules';
if (typeof window.webpackJsonp === 'function') {
const __webpack_require__ = window['webpackJsonp']([], {
[id]: (module, exports, __webpack_require__) => exports.default = __webpack_require__
}, [id]).default;
delete __webpack_require__.m[id];
delete __webpack_require__.c[id];
return __webpack_require__;
} else if (window.webpackJsonp && window.webpackJsonp.push) {
const __webpack_require__ = window['webpackJsonp'].push([[], {
[id]: (module, exports, req) => exports.default = req
}, [[id]]]).default;
window['webpackJsonp'].pop();
delete __webpack_require__.m[id];
delete __webpack_require__.c[id];
return __webpack_require__;
}
}
static hookWebpackRequireCache(__webpack_require__) {
__webpack_require__.c = new Proxy(__webpack_require__.c, {
set(module_cache, module_id, module) {
// Add it to our emitter cache and emit a module-loading event
this.moduleLoading(module_id, module);
Events.emit('module-loading', module);
// Add the module to the cache as normal
module_cache[module_id] = module;
}
});
}
static moduleLoading(module_id, module) {
if (this.require.c[module_id]) return;
if (!this.moduleLoadedEventTimeout) {
this.moduleLoadedEventTimeout = setTimeout(() => {
this.moduleLoadedEventTimeout = undefined;
// Emit a module-loaded event for every module
for (const module of this.modulesLoadingCache) {
Events.emit('module-loaded', module);
}
// Emit a modules-loaded event
Events.emit('modules-loaded', this.modulesLoadingCache);
this.modulesLoadedCache = [];
}, 0);
}
// Add this to our own cache
if (!this.modulesLoadingCache) this.modulesLoadingCache = [];
this.modulesLoadingCache.push(module);
}
static waitForWebpackRequire() {
return Utils.until(() => this.require, 10);
}
/**
* Waits for a module to load.
* This only returns a single module, as it can't guarentee there are no more modules that could
* match the filter, which is pretty much what that would be asking for.
* @param {Function} filter The name of a known module or a filter function
* @return {Any}
*/
static async waitForModule(filter) {
const module = this.getModule(filter);
if (module) return module;
while (this.require.m.length > this.require.c.length) {
const additionalModules = await Events.once('modules-loaded');
const module = this.getModule(filter, true, additionalModules);
if (module) return module;
}
throw new Error('All modules have now been loaded. None match the passed filter.');
}
/**
* Finds a module by it's name.
* @param {String} name The name of the module
* @param {Function} fallback A function to use to filter modules if not finding a known module
* @return {Any}
*/
static async waitForModuleByName(name, fallback) {
if (Cache.hasOwnProperty(name)) return Cache[name];
if (KnownModules.hasOwnProperty(name)) fallback = KnownModules[name];
if (!fallback) return undefined;
const module = await this.waitForModule(fallback, true);
return module ? Cache[name] = module : undefined;
}
static waitForModuleByDisplayName(props) {
return this.waitForModule(Filters.byDisplayName(props));
}
static waitForModuleByRegex(props) {
return this.waitForModule(Filters.byCode(props));
}
static waitForModuleByProps(props) {
return this.waitForModule(Filters.byProperties(props));
}
static waitForModuleByPrototypes(props) {
return this.waitForModule(Filters.byPrototypeFields(props));
}
/**
* Returns all loaded modules.
* @return {Array}
*/
static getAllModules() {
return this.require.c;
}
/**
* Returns an array of known modules.
* @return {Array}
*/
static listKnownModules() {
return Object.keys(KnownModules);
}
static get KnownModules() { return KnownModules }
}
const Modules = new Proxy(Module, {
get(Module, name) {
return Module.byName(name);
}
});
export { Module, Modules }

View File

@ -0,0 +1,59 @@
/**
* BetterDiscord Reflection Resolver
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import { Module } from './modules';
class Resolved {
constructor(module, ...classes) {
this.module = Module.byProps(...classes);
this.classes = classes;
}
get className() {
return this.module && this.module[this.classes[0]] ? this.module[this.classes[0]].split(' ')[0] : this.classes[0];
}
get selector() {
return `.${this.className}`;
}
}
export default class Resolver {
static resolve(...classes) {
return new Resolved(Module.byProps(...classes), ...classes);
}
static async resolveAsync(...classes) {
const module = await Module.waitForModuleByProps([...classes]);
return new Resolved(module, ...classes);
}
/**
* Searches for a class module and returns a class from it.
* @param {String} base The first part of the class to find
* @param {String} ...additional_classes Additional classes to look for to filter duplicate class modules
* @return {String}
*/
static getClassName(base, ...additional_classes) {
const class_module = Module.byProps([base, ...additional_classes]);
if (class_module && class_module[base]) return class_module[base].split(' ')[0];
}
static async waitForClassName(base, ...additional_classes) {
const class_module = await Module.waitForModuleByProps([base, ...additional_classes]);
if (class_module && class_module[base]) return class_module[base].split(' ')[0];
}
static getSelector(base, ...additional_classes) {
const gcn = this.getClassName(base, ...additional_classes);
if (gcn) return `.${gcn}`;
}
}

View File

@ -0,0 +1,430 @@
/**
* BetterDiscord WebpackModules Module
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import { Utils, Filters } from 'common';
import Events from '../events';
const KnownModules = {
React: Filters.byProperties(['createElement', 'cloneElement']),
ReactDOM: Filters.byProperties(['render', 'findDOMNode']),
Events: Filters.byPrototypeFields(['setMaxListeners', 'emit']),
/* Guild Info, Stores, and Utilities */
GuildStore: Filters.byProperties(['getGuild']),
SortedGuildStore: Filters.byProperties(['getSortedGuilds']),
SelectedGuildStore: Filters.byProperties(['getLastSelectedGuildId']),
GuildSync: Filters.byProperties(['getSyncedGuilds']),
GuildInfo: Filters.byProperties(['getAcronym']),
GuildChannelsStore: Filters.byProperties(['getChannels', 'getDefaultChannel']),
GuildMemberStore: Filters.byProperties(['getMember']),
MemberCountStore: Filters.byProperties(['getMemberCounts']),
GuildEmojiStore: Filters.byProperties(['getEmojis']),
GuildActions: Filters.byProperties(['markGuildAsRead']),
GuildPermissions: Filters.byProperties(['getGuildPermissions']),
/* Channel Store & Actions */
ChannelStore: Filters.byProperties(['getChannels', 'getDMFromUserId']),
SelectedChannelStore: Filters.byProperties(['getLastSelectedChannelId']),
ChannelActions: Filters.byProperties(['selectChannel']),
PrivateChannelActions: Filters.byProperties(['openPrivateChannel']),
ChannelSelector: Filters.byProperties(['selectGuild', 'selectChannel']),
VoiceChannelActions: Filters.byProperties(['selectVoiceChannel']),
/* Current User Info, State and Settings */
UserInfoStore: Filters.byProperties(['getToken']),
UserSettingsStore: Filters.byProperties(['guildPositions']),
AccountManager: Filters.byProperties(['register', 'login']),
UserSettingsUpdater: Filters.byProperties(['updateRemoteSettings']),
OnlineWatcher: Filters.byProperties(['isOnline']),
CurrentUserIdle: Filters.byProperties(['getIdleTime']),
RelationshipStore: Filters.byProperties(['isBlocked', 'isFriend']),
RelationshipManager: Filters.byProperties(['addRelationship']),
MentionStore: Filters.byProperties(['getMentions']),
/* User Stores and Utils */
UserStore: Filters.byProperties(['getCurrentUser']),
UserStatusStore: Filters.byProperties(['getStatuses']),
UserTypingStore: Filters.byProperties(['isTyping']),
UserActivityStore: Filters.byProperties(['getActivity']),
UserNameResolver: Filters.byProperties(['getName']),
UserNoteStore: Filters.byProperties(['getNote']),
UserNoteActions: Filters.byProperties(['updateNote']),
DraftActions: Filters.byProperties(['changeDraft']),
/* Emoji Store and Utils */
EmojiInfo: Filters.byProperties(['isEmojiDisabled']),
EmojiUtils: Filters.byProperties(['getGuildEmoji']),
EmojiStore: Filters.byProperties(['getByCategory', 'EMOJI_NAME_RE']),
/* Invite Store and Utils */
InviteStore: Filters.byProperties(['getInvites']),
InviteResolver: Filters.byProperties(['findInvite']),
InviteActions: Filters.byProperties(['acceptInvite']),
/* Discord Objects & Utils */
DiscordConstants: Filters.byProperties(['Permissions', 'ActivityTypes', 'StatusTypes']),
Permissions: Filters.byProperties(['getHighestRole']),
ColorConverter: Filters.byProperties(['hex2int']),
ColorShader: Filters.byProperties(['darken']),
TinyColor: Filters.byPrototypeFields(['toRgb']),
ClassResolver: Filters.byProperties(['getClass']),
ButtonData: Filters.byProperties(['ButtonSizes']),
IconNames: Filters.byProperties(['IconNames']),
NavigationUtils: Filters.byProperties(['transitionTo', 'replaceWith', 'getHistory']),
/* Discord Messages */
MessageStore: Filters.byProperties(['getMessages']),
MessageActions: Filters.byProperties(['jumpToMessage', '_sendMessage']),
MessageQueue: Filters.byProperties(['enqueue']),
MessageParser: Filters.byProperties(['createMessage', 'parse', 'unparse']),
/* In-Game Overlay */
OverlayUserPopoutSettings: Filters.byProperties(['openUserPopout']),
OverlayUserPopoutInfo: Filters.byProperties(['getOpenedUserPopout']),
/* Experiments */
ExperimentStore: Filters.byProperties(['getExperimentOverrides']),
ExperimentsManager: Filters.byProperties(['isDeveloper']),
CurrentExperiment: Filters.byProperties(['getExperimentId']),
/* Images, Avatars and Utils */
ImageResolver: Filters.byProperties(['getUserAvatarURL']),
ImageUtils: Filters.byProperties(['getSizedImageSrc']),
AvatarDefaults: Filters.byProperties(['getUserAvatarURL', 'DEFAULT_AVATARS']),
/* Drag & Drop */
DNDActions: Filters.byProperties(['beginDrag']),
DNDSources: Filters.byProperties(['addTarget']),
DNDObjects: Filters.byProperties(['DragSource']),
/* Electron & Other Internals with Utils */
ElectronModule: Filters.byProperties(['_getMainWindow']),
Dispatcher: Filters.byProperties(['dirtyDispatch']),
PathUtils: Filters.byProperties(['hasBasename']),
NotificationModule: Filters.byProperties(['showNotification']),
RouterModule: Filters.byProperties(['Router']),
APIModule: Filters.byProperties(['getAPIBaseURL']),
AnalyticEvents: Filters.byProperties(['AnalyticEventConfigs']),
KeyGenerator: Filters.byCode(/"binary"/),
Buffers: Filters.byProperties(['Buffer', 'kMaxLength']),
DeviceStore: Filters.byProperties(['getDevices']),
SoftwareInfo: Filters.byProperties(['os']),
CurrentContext: Filters.byProperties(['setTagsContext']),
/* Media Stuff (Audio/Video) */
MediaDeviceInfo: Filters.byProperties(['Codecs', 'SUPPORTED_BROWSERS']),
MediaInfo: Filters.byProperties(['getOutputVolume']),
MediaEngineInfo: Filters.byProperties(['MediaEngineFeatures']),
VoiceInfo: Filters.byProperties(['EchoCancellation']),
VideoStream: Filters.byProperties(['getVideoStream']),
SoundModule: Filters.byProperties(['playSound']),
/* Window, DOM, HTML */
WindowInfo: Filters.byProperties(['isFocused', 'windowSize']),
TagInfo: Filters.byProperties(['VALID_TAG_NAMES']),
DOMInfo: Filters.byProperties(['canUseDOM']),
/* Locale/Location and Time */
LocaleManager: Filters.byProperties(['setLocale']),
Moment: Filters.byProperties(['parseZone']),
LocationManager: Filters.byProperties(['createLocation']),
Timestamps: Filters.byProperties(['fromTimestamp']),
TimeFormatter: Filters.byProperties(['dateFormat']),
/* Strings and Utils */
Strings: Filters.byProperties(['TEXT', 'TEXTAREA_PLACEHOLDER']),
StringFormats: Filters.byProperties(['a', 'z']),
StringUtils: Filters.byProperties(['toASCII']),
/* URLs and Utils */
URLParser: Filters.byProperties(['Url', 'parse']),
ExtraURLs: Filters.byProperties(['getArticleURL']),
/* Text Processing */
hljs: Filters.byProperties(['highlight', 'highlightBlock']),
SimpleMarkdown: Filters.byProperties(['parseBlock', 'parseInline', 'defaultOutput']),
/* DOM/React Components */
/* ==================== */
LayerManager: Filters.byProperties(['popLayer', 'pushLayer']),
UserSettingsWindow: Filters.byProperties(['open', 'updateAccount']),
ChannelSettingsWindow: Filters.byProperties(['open', 'updateChannel']),
GuildSettingsWindow: Filters.byProperties(['open', 'updateGuild']),
/* Modals */
ModalStack: Filters.byProperties(['push', 'update', 'pop', 'popWithKey']),
ConfirmModal: Filters.byPrototypeFields(['handleCancel', 'handleSubmit', 'handleMinorConfirm']),
UserProfileModal: Filters.byProperties(['fetchMutualFriends', 'setSection']),
ChangeNicknameModal: Filters.byProperties(['open', 'changeNickname']),
CreateChannelModal: Filters.byProperties(['open', 'createChannel']),
PruneMembersModal: Filters.byProperties(['open', 'prune']),
NotificationSettingsModal: Filters.byProperties(['open', 'updateNotificationSettings']),
PrivacySettingsModal: Filters.byCode(/PRIVACY_SETTINGS_MODAL_OPEN/, m => m.open),
CreateInviteModal: Filters.byProperties(['open', 'createInvite']),
/* Popouts */
PopoutStack: Filters.byProperties(['open', 'close', 'closeAll']),
PopoutOpener: Filters.byProperties(['openPopout']),
EmojiPicker: Filters.byPrototypeFields(['onHoverEmoji', 'selectEmoji']),
/* Context Menus */
ContextMenuActions: Filters.byCode(/CONTEXT_MENU_CLOSE/, c => c.close),
ContextMenuItemsGroup: Filters.byCode(/itemGroup/),
ContextMenuItem: Filters.byCode(/\.label\b.*\.hint\b.*\.action\b/),
/* In-Message Links */
ExternalLink: Filters.byCode(/\.trusted\b/)
};
class WebpackModules {
/**
* Finds a module using a filter function.
* @param {Function} filter A function to use to filter modules
* @param {Boolean} first Whether to return only the first matching module
* @param {Array} modules An array of modules to search in
* @return {Any}
*/
static getModule(filter, first = true, _modules) {
const modules = _modules || this.getAllModules();
const rm = [];
for (const index in modules) {
if (!modules.hasOwnProperty(index)) continue;
const module = modules[index];
const { exports } = module;
let foundModule = null;
if (!exports) continue;
if (exports.__esModule && exports.default && filter(exports.default)) foundModule = exports.default;
if (filter(exports)) foundModule = exports;
if (!foundModule) continue;
if (first) return foundModule;
rm.push(foundModule);
}
return first ? undefined : rm;
}
/**
* Finds a module by it's name.
* @param {String} name The name of the module
* @param {Function} fallback A function to use to filter modules if not finding a known module
* @return {Any}
*/
static getModuleByName(name, fallback) {
if (Cache.hasOwnProperty(name)) return Cache[name];
if (KnownModules.hasOwnProperty(name)) fallback = KnownModules[name];
if (!fallback) return undefined;
const module = this.getModule(fallback, true);
return module ? Cache[name] = module : undefined;
}
/**
* Finds a module by it's display name.
* @param {String} name The display name of the module
* @return {Any}
*/
static getModuleByDisplayName(name) {
return this.getModule(Filters.byDisplayName(name), true);
}
/**
* Finds a module using it's code.
* @param {RegEx} regex A regular expression to use to filter modules
* @param {Boolean} first Whether to return the only the first matching module
* @return {Any}
*/
static getModuleByRegex(regex, first = true) {
return this.getModule(Filters.byCode(regex), first);
}
/**
* Finds a module using properties on it's prototype.
* @param {Array} props Properties to use to filter modules
* @param {Boolean} first Whether to return only the first matching module
* @return {Any}
*/
static getModuleByPrototypes(prototypes, first = true) {
return this.getModule(Filters.byPrototypeFields(prototypes), first);
}
/**
* Finds a module using it's own properties.
* @param {Array} props Properties to use to filter modules
* @param {Boolean} first Whether to return only the first matching module
* @return {Any}
*/
static getModuleByProps(props, first = true) {
return this.getModule(Filters.byProperties(props), first);
}
/**
* Discord's __webpack_require__ function.
*/
static get require() {
if (this._require) return this._require;
const __webpack_require__ = this.getWebpackRequire();
if (!__webpack_require__) return null;
this.hookWebpackRequireCache(__webpack_require__);
return this._require = __webpack_require__;
}
static getWebpackRequire() {
const id = 'bd-webpackmodules';
if (typeof window.webpackJsonp === 'function') {
const __webpack_require__ = window['webpackJsonp']([], {
[id]: (module, exports, __webpack_require__) => exports.default = __webpack_require__
}, [id]).default;
delete __webpack_require__.m[id];
delete __webpack_require__.c[id];
return __webpack_require__;
} else if (window.webpackJsonp && window.webpackJsonp.push) {
const __webpack_require__ = window['webpackJsonp'].push([[], {
[id]: (module, exports, req) => exports.default = req
}, [[id]]]).default;
window['webpackJsonp'].pop();
delete __webpack_require__.m[id];
delete __webpack_require__.c[id];
return __webpack_require__;
}
}
static hookWebpackRequireCache(__webpack_require__) {
__webpack_require__.c = new Proxy(__webpack_require__.c, {
set(module_cache, module_id, module) {
// Add it to our emitter cache and emit a module-loading event
this.moduleLoading(module_id, module);
Events.emit('module-loading', module);
// Add the module to the cache as normal
module_cache[module_id] = module;
}
});
}
static moduleLoading(module_id, module) {
if (this.require.c[module_id]) return;
if (!this.moduleLoadedEventTimeout) {
this.moduleLoadedEventTimeout = setTimeout(() => {
this.moduleLoadedEventTimeout = undefined;
// Emit a module-loaded event for every module
for (const module of this.modulesLoadingCache) {
Events.emit('module-loaded', module);
}
// Emit a modules-loaded event
Events.emit('modules-loaded', this.modulesLoadingCache);
this.modulesLoadedCache = [];
}, 0);
}
// Add this to our own cache
if (!this.modulesLoadingCache) this.modulesLoadingCache = [];
this.modulesLoadingCache.push(module);
}
static waitForWebpackRequire() {
return Utils.until(() => this.require, 10);
}
/**
* Waits for a module to load.
* This only returns a single module, as it can't guarentee there are no more modules that could
* match the filter, which is pretty much what that would be asking for.
* @param {Function} filter The name of a known module or a filter function
* @return {Any}
*/
static async waitForModule(filter) {
const module = this.getModule(filter);
if (module) return module;
while (this.require.m.length > this.require.c.length) {
const additionalModules = await Events.once('modules-loaded');
const module = this.getModule(filter, true, additionalModules);
if (module) return module;
}
throw new Error('All modules have now been loaded. None match the passed filter.');
}
/**
* Finds a module by it's name.
* @param {String} name The name of the module
* @param {Function} fallback A function to use to filter modules if not finding a known module
* @return {Any}
*/
static async waitForModuleByName(name, fallback) {
if (Cache.hasOwnProperty(name)) return Cache[name];
if (KnownModules.hasOwnProperty(name)) fallback = KnownModules[name];
if (!fallback) return undefined;
const module = await this.waitForModule(fallback, true);
return module ? Cache[name] = module : undefined;
}
static waitForModuleByDisplayName(props) {
return this.waitForModule(Filters.byDisplayName(props));
}
static waitForModuleByRegex(props) {
return this.waitForModule(Filters.byCode(props));
}
static waitForModuleByProps(props) {
return this.waitForModule(Filters.byProperties(props));
}
static waitForModuleByPrototypes(props) {
return this.waitForModule(Filters.byPrototypeFields(props));
}
/**
* Searches for a class module and returns a class from it.
* @param {String} base The first part of the class to find
* @param {String} ...additional_classes Additional classes to look for to filter duplicate class modules
* @return {String}
*/
static getClassName(base, ...additional_classes) {
const class_module = this.getModuleByProps([base, ...additional_classes]);
if (class_module && class_module[base]) return class_module[base].split(' ')[0];
}
static async waitForClassName(base, ...additional_classes) {
const class_module = await this.waitForModuleByProps([base, ...additional_classes]);
if (class_module && class_module[base]) return class_module[base].split(' ')[0];
}
static getSelector(base, ...additional_classes) {
const gcn = this.getClassName(base, ...additional_classes);
if (gcn) return `.${gcn}`;
}
/**
* Returns all loaded modules.
* @return {Array}
*/
static getAllModules() {
return this.require.c;
}
/**
* Returns an array of known modules.
* @return {Array}
*/
static listKnownModules() {
return Object.keys(KnownModules);
}
static get KnownModules() { return KnownModules }
}
export { WebpackModules }

View File

@ -0,0 +1,111 @@
/**
* BetterDiscord Security Module
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import nodecrypto from 'node-crypto';
import aes256 from 'aes256';
export default class Security {
static encrypt(key, content, prefix = '') {
if (key instanceof Array || content instanceof Array) return this.deepEncrypt(key, content, prefix);
return `${prefix}${aes256.encrypt(key, content)}`;
}
static decrypt(key, content, prefix = '') {
if (key instanceof Array || content instanceof Array) {
return this.deepDecrypt(key, content, prefix);
}
return aes256.decrypt(key, content.replace(prefix, ''));
}
static deepEncrypt(keys, content, prefix = '') {
if (content && content instanceof Array) return this.deepEncryptContent(keys, content, prefix);
let encrypt = null;
for (const key of keys) {
if (encrypt === null) encrypt = this.encrypt(key, content, prefix);
else encrypt = this.encrypt(key, encrypt, prefix);
}
return encrypt;
}
static deepEncryptContent(key, contents, prefix = '') {
let encrypt = null;
for (const content of contents) {
if (encrypt === null) encrypt = this.encrypt(key, content, prefix);
else encrypt = this.encrypt(encrypt, content, prefix);
}
return encrypt;
}
static deepDecrypt(keys, content, prefix = '') {
if (content && content instanceof Array) return this.deepDecryptContent(keys, content, prefix);
let decrypt = null;
for (const key of keys.reverse()) {
if (decrypt === null) decrypt = this.decrypt(key, content, prefix);
else decrypt = this.decrypt(key, decrypt, prefix);
}
return decrypt;
}
static deepDecryptContent(key, contents, prefix = '') {
let decrypt = null;
for (const content of contents) {
if (decrypt === null) decrypt = this.decrypt(key, content, prefix);
else decrypt = this.decrypt(decrypt, content, prefix);
}
return decrypt;
}
static randomBytes(length = 64, to = 'hex') {
return nodecrypto.randomBytes(length).toString(to);
}
static async createHmac(key, data, algorithm = 'sha256') {
const hmac = nodecrypto.createHmac(algorithm, key);
return new Promise((resolve, reject) => {
hmac.on('readable', () => {
const data = hmac.read();
if (data) return resolve(data.toString('hex'));
reject(null);
});
hmac.write(data);
hmac.end();
});
}
static createECDH(curve = 'secp384r1') {
return nodecrypto.createECDH(curve);
}
static hash(algorithm, data, encoding) {
const hash = nodecrypto.createHash(algorithm);
hash.update(data);
return hash.digest(encoding);
}
static sha256(text) {
const hash = nodecrypto.createHash('sha256');
hash.update(text);
return hash.digest('base64');
}
static generateECDHKeys(ecdh) {
return ecdh.generateKeys('base64');
}
static getECDHPublicKey(ecdh) {
return ecdh.getPublicKey('base64');
}
static computeECDHSecret(ecdh, otherPublicKey) {
return ecdh.computeSecret(otherPublicKey, 'base64', 'base64');
}
}

View File

@ -8,47 +8,62 @@
* LICENSE file in the root directory of this source tree.
*/
import defaultSettings from '../data/user.settings.default';
import { Toasts } from 'ui';
import { SettingsSet } from 'structs';
import { FileUtils, ClientLogger as Logger } from 'common';
import path from 'path';
import process from 'process';
import Globals from './globals';
import CssEditor from './csseditor';
import Events from './events';
import { Utils, FileUtils, ClientLogger as Logger } from 'common';
import { SettingsSet, SettingUpdatedEvent } from 'structs';
import path from 'path';
import defaultSettings from '../data/user.settings.default';
export default new class Settings {
constructor() {
this.settings = defaultSettings.map(_set => {
const set = new SettingsSet(_set);
set.on('setting-updated', event => {
const { category, setting, value, old_value } = event;
Logger.log('Settings', `${set.id}/${category.id}/${setting.id} was changed from ${old_value} to ${value}`);
Logger.log('Settings', [`${set.id}/${category.id}/${setting.id} was changed from`, old_value, 'to', value]);
Events.emit('setting-updated', event);
Events.emit(`setting-updated-${set.id}_${category.id}_${setting.id}`, event);
Toasts.success(`${set.id}/${category.id}/${setting.id} was changed from ${old_value} to ${value}`); // Just for debugging purposes remove in prod
});
set.on('settings-updated', async (event) => {
await this.saveSettings();
set.on('settings-updated', async event => {
if (!event.data || !event.data.dont_save) await this.saveSettings();
Events.emit('settings-updated', event);
});
return set;
});
// Set a hint for each platform for the use-keytar setting
const useKeytarSetting = this.getSetting('security', 'default', 'use-keytar');
if (process.platform === 'win32') useKeytarSetting.hint = 'Store the master password in Credential Manager';
if (process.platform === 'darwin') useKeytarSetting.hint = 'Store the master password in the default keychain';
if (process.platform === 'linux') useKeytarSetting.hint = 'Store the master password in libsecret';
}
/**
* Loads BetterDiscord's settings.
*/
async loadSettings() {
Logger.log('Settings', ['Loading settings']);
try {
await FileUtils.ensureDirectory(this.dataPath);
const settingsPath = path.resolve(this.dataPath, 'user.settings.json');
const user_config = await FileUtils.readJsonFromFile(settingsPath);
const { settings, scss, css, css_editor_files, scss_error, css_editor_bounds } = user_config;
const { settings, scss, css, css_editor_files, scss_error, css_editor_bounds, favourite_emotes } = user_config;
for (let set of this.settings) {
for (const set of this.settings) {
const newSet = settings.find(s => s.id === set.id);
if (!newSet) continue;
set.merge(newSet);
await set.merge(newSet, {dont_save: true});
set.setSaved();
}
@ -57,11 +72,16 @@ export default new class Settings {
} catch (err) {
// There was an error loading settings
// This probably means that the user doesn't have any settings yet
Logger.err('Settings', err);
Logger.warn('Settings', ['Failed to load internal settings', err]);
}
}
/**
* Saves BetterDiscord's settings including CSS editor data.
*/
async saveSettings() {
Logger.log('Settings', ['Saving settings']);
try {
await FileUtils.ensureDirectory(this.dataPath);
@ -72,26 +92,26 @@ export default new class Settings {
css: CssEditor.css,
css_editor_files: CssEditor.files,
scss_error: CssEditor.error,
css_editor_bounds: {
width: CssEditor.editor_bounds.width,
height: CssEditor.editor_bounds.height,
x: CssEditor.editor_bounds.x,
y: CssEditor.editor_bounds.y
}
css_editor_bounds: CssEditor.editor_bounds
});
for (let set of this.getSettings) {
for (const set of this.settings) {
set.setSaved();
}
} catch (err) {
// There was an error saving settings
Logger.err('Settings', err);
Logger.err('Settings', ['Failed to save internal settings', err]);
throw err;
}
}
/**
* Finds one of BetterDiscord's settings sets.
* @param {String} set_id The ID of the set to find
* @return {SettingsSet}
*/
getSet(set_id) {
return this.getSettings.find(s => s.id === set_id);
return this.settings.find(s => s.id === set_id);
}
get core() { return this.getSet('core') }
@ -100,39 +120,46 @@ export default new class Settings {
get css() { return this.getSet('css') }
get security() { return this.getSet('security') }
/**
* Finds a category in one of BetterDiscord's settings sets.
* @param {String} set_id The ID of the set to look in
* @param {String} category_id The ID of the category to find
* @return {SettingsCategory}
*/
getCategory(set_id, category_id) {
const set = this.getSet(set_id);
return set ? set.getCategory(category_id) : undefined;
}
/**
* Finds a setting in one of BetterDiscord's settings sets.
* @param {String} set_id The ID of the set to look in
* @param {String} category_id The ID of the category to look in
* @param {String} setting_id The ID of the setting to find
* @return {Setting}
*/
getSetting(set_id, category_id, setting_id) {
const set = this.getSet(set_id);
return set ? set.getSetting(category_id, setting_id) : undefined;
}
/**
* Returns a setting's value in one of BetterDiscord's settings sets.
* @param {String} set_id The ID of the set to look in
* @param {String} category_id The ID of the category to look in
* @param {String} setting_id The ID of the setting to find
* @return {Any}
*/
get(set_id, category_id, setting_id) {
const set = this.getSet(set_id);
return set ? set.get(category_id, setting_id) : undefined;
}
async mergeSettings(set_id, newSettings) {
const set = this.getSet(set_id);
if (!set) return;
return await set.merge(newSettings);
}
setSetting(set_id, category_id, setting_id, value) {
const setting = this.getSetting(set_id, category_id, setting_id);
if (!setting) throw {message: `Tried to set ${set_id}/${category_id}/${setting_id}, which doesn't exist`};
setting.value = value;
}
get getSettings() {
return this.settings;
}
/**
* The path to store user data in.
*/
get dataPath() {
return Globals.getPath('data');
}
}

View File

@ -8,6 +8,7 @@
* LICENSE file in the root directory of this source tree.
*/
import { ClientLogger as Logger } from 'common';
import EventListener from './eventlistener';
export default class SocketProxy extends EventListener {
@ -19,7 +20,7 @@ export default class SocketProxy extends EventListener {
get eventBindings() {
return [
{ id: 'socket-created', 'callback': this.socketCreated }
{ id: 'socket-created', callback: this.socketCreated }
];
}
@ -29,11 +30,11 @@ export default class SocketProxy extends EventListener {
socketCreated(socket) {
this.activeSocket = socket;
// socket.addEventListener('message', this.onMessage);
// socket.addEventListener('message', this.onMessage);
}
onMessage(e) {
console.info(e);
Logger.info('SocketProxy', e);
}
}

View File

@ -31,10 +31,6 @@ export default class Theme extends Content {
get type() { return 'theme' }
get css() { return this.data.css }
// Don't use - these will eventually be removed!
get themePath() { return this.contentPath }
get themeConfig() { return this.config }
/**
* Called when settings are updated.
* This can be overridden by other content types.
@ -63,7 +59,7 @@ export default class Theme extends Content {
* @return {Promise}
*/
async compile() {
console.log('Compiling CSS');
Logger.log(this.name, 'Compiling CSS');
if (this.info.type === 'sass') {
const config = await ThemeManager.getConfigAsSCSS(this.settings);
@ -73,21 +69,20 @@ export default class Theme extends Content {
path: this.paths.mainPath.replace(/\\/g, '/')
});
// Why are these getters?
Logger.log(this.name, ['Finished compiling theme', new class Info {
get SCSS_variables() { console.log(config); }
get Compiled_SCSS() { console.log(result.css.toString()); }
get Result() { console.log(result); }
get SCSS_variables() { console.log(config); return ''; }
get Compiled_SCSS() { console.log(result.css.toString()); return ''; }
get Result() { console.log(result); return ''; }
}]);
return {
css: result.css.toString(),
files: result.stats.includedFiles
};
} else {
return {
css: await FileUtils.readFile(this.paths.mainPath)
};
}
return { css: await FileUtils.readFile(this.paths.mainPath) }
}
/**
@ -121,6 +116,7 @@ export default class Theme extends Content {
*/
set files(files) {
this.data.files = files;
if (Settings.get('css', 'default', 'watch-files'))
this.watchfiles = files;
}
@ -151,7 +147,14 @@ export default class Theme extends Content {
* @param {Array} files Files to watch
*/
set watchfiles(files) {
for (let file of files) {
if (this.packed) {
// Don't watch files for packed themes
return;
}
if (!files) files = [];
for (const file of files) {
if (!this.watchfiles.includes(file)) {
this.filewatcher.add(file);
this.watchfiles.push(file);
@ -159,7 +162,7 @@ export default class Theme extends Content {
}
}
for (let index in this.watchfiles) {
for (const index in this.watchfiles) {
let file = this.watchfiles[index];
while (file && !files.find(f => f === file)) {
this.filewatcher.remove(file);

View File

@ -10,8 +10,6 @@
import ContentManager from './contentmanager';
import Theme from './theme';
import { FileUtils } from 'common';
import path from 'path';
export default class ThemeManager extends ContentManager {
@ -38,12 +36,7 @@ export default class ThemeManager extends ContentManager {
static async loadTheme(paths, configs, info, main) {
try {
const instance = new Theme({
configs, info, main,
paths: {
contentPath: paths.contentPath,
dirName: paths.dirName,
mainPath: paths.mainPath
}
configs, info, main, paths
});
if (instance.enabled) {
instance.userConfig.enabled = false;
@ -55,6 +48,7 @@ export default class ThemeManager extends ContentManager {
}
}
static get deleteTheme() { return this.deleteContent }
static get unloadTheme() { return this.unloadContent }
static async reloadTheme(theme) {
theme = await this.reloadContent(theme);
@ -62,11 +56,11 @@ export default class ThemeManager extends ContentManager {
}
static enableTheme(theme) {
theme.enable();
return theme.enable();
}
static disableTheme(theme) {
theme.disable();
return theme.disable();
}
static get isTheme() { return this.isThisContent }
@ -74,11 +68,16 @@ export default class ThemeManager extends ContentManager {
return theme instanceof Theme;
}
/**
* Returns a representation of a settings set's values in SCSS.
* @param {SettingsSet} settingsset The set to convert to SCSS
* @return {Promise}
*/
static async getConfigAsSCSS(settingsset) {
const variables = [];
for (let category of settingsset.categories) {
for (let setting of category.settings) {
for (const category of settingsset.categories) {
for (const setting of category.settings) {
const setting_scss = await this.parseSetting(setting);
if (setting_scss) variables.push(`$${setting_scss[0]}: ${setting_scss[1]};`);
}
@ -87,27 +86,41 @@ export default class ThemeManager extends ContentManager {
return variables.join('\n');
}
/**
* Returns a representation of a settings set's values as an SCSS map.
* @param {SettingsSet} settingsset The set to convert to an SCSS map
* @return {Promise}
*/
static async getConfigAsSCSSMap(settingsset) {
const variables = [];
for (let category of settingsset.categories) {
for (let setting of category.settings) {
for (const category of settingsset.categories) {
for (const setting of category.settings) {
const setting_scss = await this.parseSetting(setting);
if (setting_scss) variables.push(`${setting_scss[0]}: (${setting_scss[1]})`);
}
}
return '(' + variables.join(', ') + ')';
return `(${variables.join(', ')})`;
}
/**
* Returns a setting's name and value as a string that can be included in SCSS.
* @param {Setting} setting The setting to convert to SCSS
* @return {Promise}
*/
static async parseSetting(setting) {
const { type, id, value } = setting;
const name = id.replace(/[^a-zA-Z0-9-]/g, '-').replace(/--/g, '-');
const name = setting.id.replace(/[^a-zA-Z0-9-]/g, '-').replace(/--/g, '-');
const scss = await setting.toSCSS();
if (scss) return [name, scss];
}
/**
* Escapes a string so it can be included in SCSS.
* @param {String} value The string to escape
* @return {String}
*/
static toSCSSString(value) {
if (typeof value !== 'string' && value.toString) value = value.toString();
return `'${typeof value === 'string' ? value.replace(/\\/g, '\\\\').replace(/'/g, '\\\'') : ''}'`;

View File

@ -8,56 +8,182 @@
* LICENSE file in the root directory of this source tree.
*/
import Events from './events';
import Globals from './globals';
import { $ } from 'vendor';
import { ClientLogger as Logger } from 'common';
import { Notifications } from 'ui';
import { Reflection, Globals } from 'modules';
export default class {
import Events from './events';
import Module from './imodule';
export default new class extends Module {
get updates() { return this.state.updates }
get bdUpdates() { return this.state.updates.bd }
get error() { return null; }
get updatesAvailable() { return this.state.updatesAvailable; }
constructor() {
window.updater = this;
this.updatesAvailable = false;
this.init = this.init.bind(this);
this.checkForUpdates = this.checkForUpdates.bind(this);
super({
updatesAvailable: false,
error: null,
updates: { bd: [] },
updating: false
});
}
get interval() {
return 60 * 1000 * 30;
bindings() {
this.restartNotif = this.restartNotif.bind(this);
this.reloadNotif = this.reloadNotif.bind(this);
this.startUpdate = this.startUpdate.bind(this);
this.setUpdateStatus = this.setUpdateStatus.bind(this);
this.testUi = this.testUi.bind(this);
}
init() {
this.updateInterval = setInterval(this.checkForUpdates, this.interval);
}
update() {
// TODO
this.updatesAvailable = false;
Events.emit('update-check-end');
}
checkForUpdates() {
if (this.updatesAvailable) return;
Events.emit('update-check-start');
Logger.info('Updater', 'Checking for updates');
$.ajax({
type: 'GET',
url: 'https://rawgit.com/JsSucks/BetterDiscordApp/master/package.json',
cache: false,
success: e => {
try {
Events.emit('update-check-end');
Logger.info('Updater',
`Latest Version: ${e.version} - Current Version: ${Globals.version}`);
if (e.version !== Globals.version) {
this.updatesAvailable = true;
Events.emit('updates-available');
}
} catch (err) {
Events.emit('update-check-fail', err);
}
restartNotif() {
Notifications.add('Updates Finished!', 'Restart required.', [
{
text: 'Restart Later',
onClick: () => { setTimeout(this.restartNotif, 5 * 60000); return true; }
},
fail: e => Events.emit('update-check-fail', e)
{
text: 'Restart Now',
onClick: () => {
try {
const { remote } = Globals.require('electron');
window.close();
Reflection.module.byProps('showToken', 'hideToken').showToken();
remote.app.relaunch();
remote.app.exit(0);
} catch (err) {
console.err(err);
return true;
}
}
}
]);
}
reloadNotif() {
Notifications.add('Updates Finished!', 'Reload required.', [
{
text: 'Reload Later',
onClick: () => { setTimeout(this.reloadNotif, 5 * 60000); return true; }
},
{
text: 'Reload Now',
onClick: () => {
document.location.reload();
}
}
]);
}
events(ipc) {
ipc.on('updater-checkForUpdates', () => {
if (this.state.updating) return; // We're already updating. Updater should be paused anyways at this point.
Events.emit('update-check-start');
});
ipc.on('updater-noUpdates', () => {
if (this.state.updatesAvailable) return; // If for some reason we get this even though we have updates already.
this.setState({
updatesAvailable: false,
updates: {}
});
});
ipc.on('updater-updatesAvailable', (_, updates) => {
console.log(updates);
if (this.state.updating) return; // If for some reason we get more updates when we're already updating
updates.bd = updates.bd.map(update => {
update.text = `${update.id.charAt(0).toUpperCase()}${update.id.slice(1)}`;
update.hint = `Current: ${update.currentVersion} | Latest: ${update.version}`;
update.status = {
update: true,
updating: false,
updated: false,
error: null
};
return update;
});
this.setState({
updates,
updatesAvailable: true
});
});
ipc.on('updater-updated', (_, info) => {
const { reloadRequired, restartRequired } = info;
if (restartRequired) {
this.restartNotif();
return;
}
if (reloadRequired) {
this.reloadNotif();
return;
}
});
ipc.on('updater-updateFinished', (_, update) => {
this.setUpdateStatus(update.id, 'updated', true);
});
ipc.on('updater-updateError', (_, update) => {
this.setUpdateStatus(update.id, 'error', update.error);
});
}
stateChanged(oldState, newState) {
if (!newState.updatesAvailable) return Events.emit('update-check-end');
if (!oldState.updatesAvailable && newState.updatesAvailable) {
Events.emit('updates-available');
Notifications.add('', 'Updates Available!', [
{
text: 'Ignore',
onClick: () => { return true; }
},
{
text: 'Show Updates',
onClick: () => {
Events.emit('bd-open-menu', 'updater');
return true;
}
}
]);
}
}
setUpdateStatus(updateId, statusChild, statusValue) {
for (const u of this.bdUpdates) {
if (u.id === updateId) {
u.status[statusChild] = statusValue;
return;
}
}
}
toggleUpdate(update) {
update.status.update = !update.status.update;
}
async startUpdate() {
console.log('start update');
const updates = { bd: [] };
for (const update of this.bdUpdates) {
if (update.status.update) {
update.status.updating = true;
updates.bd.push(update);
}
}
console.log(updates);
this.send('updater-startUpdate', updates);
}
testUi(updates) {
this.setState({
updates,
updatesAvailable: true
});
}

View File

@ -8,32 +8,46 @@
* LICENSE file in the root directory of this source tree.
*/
import { WebpackModules } from './webpackmodules';
import jQuery from 'jquery';
import lodash from 'lodash';
import Vue from 'vue';
import { Axi } from 'common';
export { jQuery as $ };
import request from 'request-promise-native';
import Combokeys from 'combokeys';
import filetype from 'file-type';
import filewatcher from 'filewatcher';
import VTooltip from 'v-tooltip';
export { jQuery as $, request };
export default class {
static get jQuery() {
return jQuery;
}
/**
* jQuery
*/
static get jQuery() { return jQuery }
static get $() { return this.jQuery }
static get $() {
return this.jQuery;
}
/**
* Lodash
*/
static get lodash() { return lodash }
static get _() { return this.lodash }
static get lodash() {
return lodash;
}
/**
* Vue
*/
static get Vue() { return Vue }
static get _() {
return this.lodash;
}
static get axios() { return Axi.axios }
static get moment() {
return WebpackModules.getModuleByName('Moment');
}
static get request() { return request }
static get Combokeys() { return Combokeys }
static get filetype() { return filetype }
static get filewatcher() { return filewatcher }
static get VTooltip() { return VTooltip }
}

View File

@ -1,318 +1,12 @@
/**
* BetterDiscord WebpackModules Module
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/* Deprecation Notice */
import { WebpackModules } from './reflection/wpm.depr.js';
import { ClientLogger as Logger } from 'common';
export class Filters {
static byProperties(props, selector = m => m) {
return module => {
const component = selector(module);
if (!component) return false;
return props.every(property => component[property] !== undefined);
};
const DeprecationWarning = new Proxy(WebpackModules, {
get(WebpackModules, property) {
Logger.warn('DEPR', 'WebpackModules is deprecated. Use Reflection.Modules instead.');
return WebpackModules[property] || WebpackModules.getModuleByName(property);
}
});
static byPrototypeFields(fields, selector = m => m) {
return module => {
const component = selector(module);
if (!component) return false;
if (!component.prototype) return false;
return fields.every(field => component.prototype[field] !== undefined);
};
}
static byCode(search, selector = m => m) {
return module => {
const method = selector(module);
if (!method) return false;
return method.toString().search(search) !== -1;
};
}
static byDisplayName(name) {
return module => {
return module && module.displayName === name;
};
}
static combine(...filters) {
return module => {
return filters.every(filter => filter(module));
};
}
}
const KnownModules = {
React: Filters.byProperties(['createElement', 'cloneElement']),
ReactDOM: Filters.byProperties(['render', 'findDOMNode']),
/* Guild Info, Stores, and Utilities */
GuildStore: Filters.byProperties(['getGuild']),
SortedGuildStore: Filters.byProperties(['getSortedGuilds']),
SelectedGuildStore: Filters.byProperties(['getLastSelectedGuildId']),
GuildSync: Filters.byProperties(["getSyncedGuilds"]),
GuildInfo: Filters.byProperties(["getAcronym"]),
GuildChannelsStore: Filters.byProperties(['getChannels', 'getDefaultChannel']),
GuildMemberStore: Filters.byProperties(['getMember']),
MemberCountStore: Filters.byProperties(["getMemberCounts"]),
GuildEmojiStore: Filters.byProperties(['getEmojis']),
GuildActions: Filters.byProperties(['markGuildAsRead']),
GuildPermissions: Filters.byProperties(['getGuildPermissions']),
/* Channel Store & Actions */
ChannelStore: Filters.byProperties(['getChannels', 'getDMFromUserId']),
SelectedChannelStore: Filters.byProperties(['getLastSelectedChannelId']),
ChannelActions: Filters.byProperties(["selectChannel"]),
PrivateChannelActions: Filters.byProperties(["openPrivateChannel"]),
ChannelSelector: Filters.byProperties(["selectGuild", "selectChannel"]),
/* Current User Info, State and Settings */
UserInfoStore: Filters.byProperties(["getToken"]),
UserSettingsStore: Filters.byProperties(["guildPositions"]),
AccountManager: Filters.byProperties(['register', 'login']),
UserSettingsUpdater: Filters.byProperties(['updateRemoteSettings']),
OnlineWatcher: Filters.byProperties(['isOnline']),
CurrentUserIdle: Filters.byProperties(['getIdleTime']),
RelationshipStore: Filters.byProperties(['isBlocked']),
RelationshipManager: Filters.byProperties(['addRelationship']),
MentionStore: Filters.byProperties(["getMentions"]),
/* User Stores and Utils */
UserStore: Filters.byProperties(['getCurrentUser']),
UserStatusStore: Filters.byProperties(['getStatuses']),
UserTypingStore: Filters.byProperties(['isTyping']),
UserActivityStore: Filters.byProperties(['getActivity']),
UserNameResolver: Filters.byProperties(['getName']),
/* Emoji Store and Utils */
EmojiInfo: Filters.byProperties(['isEmojiDisabled']),
EmojiUtils: Filters.byProperties(['getGuildEmoji']),
EmojiStore: Filters.byProperties(['getByCategory', 'EMOJI_NAME_RE']),
/* Invite Store and Utils */
InviteStore: Filters.byProperties(["getInvites"]),
InviteResolver: Filters.byProperties(['findInvite']),
InviteActions: Filters.byProperties(['acceptInvite']),
/* Discord Objects & Utils */
DiscordConstants: Filters.byProperties(["Permissions", "ActivityTypes", "StatusTypes"]),
Permissions: Filters.byProperties(['getHighestRole']),
ColorConverter: Filters.byProperties(['hex2int']),
ColorShader: Filters.byProperties(['darken']),
ClassResolver: Filters.byProperties(["getClass"]),
ButtonData: Filters.byProperties(["ButtonSizes"]),
IconNames: Filters.byProperties(["IconNames"]),
NavigationUtils: Filters.byProperties(['transitionTo', 'replaceWith', 'getHistory']),
/* Discord Messages */
MessageStore: Filters.byProperties(['getMessages']),
MessageActions: Filters.byProperties(['jumpToMessage', '_sendMessage']),
MessageQueue: Filters.byProperties(['enqueue']),
MessageParser: Filters.byProperties(['createMessage', 'parse', 'unparse']),
/* In-Game Overlay */
OverlayUserPopoutSettings: Filters.byProperties(['openUserPopout']),
OverlayUserPopoutInfo: Filters.byProperties(['getOpenedUserPopout']),
/* Experiments */
ExperimentStore: Filters.byProperties(['getExperimentOverrides']),
ExperimentsManager: Filters.byProperties(['isDeveloper']),
CurrentExperiment: Filters.byProperties(['getExperimentId']),
/* Images, Avatars and Utils */
ImageResolver: Filters.byProperties(["getUserAvatarURL"]),
ImageUtils: Filters.byProperties(['getSizedImageSrc']),
AvatarDefaults: Filters.byProperties(["getUserAvatarURL", "DEFAULT_AVATARS"]),
/* Drag & Drop */
DNDActions: Filters.byProperties(["beginDrag"]),
DNDSources: Filters.byProperties(["addTarget"]),
DNDObjects: Filters.byProperties(["DragSource"]),
/* Electron & Other Internals with Utils*/
ElectronModule: Filters.byProperties(["_getMainWindow"]),
Dispatcher: Filters.byProperties(['dirtyDispatch']),
PathUtils: Filters.byProperties(["hasBasename"]),
NotificationModule: Filters.byProperties(["showNotification"]),
RouterModule: Filters.byProperties(["Router"]),
APIModule: Filters.byProperties(["getAPIBaseURL"]),
AnalyticEvents: Filters.byProperties(["AnalyticEventConfigs"]),
KeyGenerator: Filters.byCode(/"binary"/),
Buffers: Filters.byProperties(['Buffer', 'kMaxLength']),
DeviceStore: Filters.byProperties(['getDevices']),
SoftwareInfo: Filters.byProperties(["os"]),
CurrentContext: Filters.byProperties(["setTagsContext"]),
/* Media Stuff (Audio/Video) */
MediaDeviceInfo: Filters.byProperties(["Codecs", "SUPPORTED_BROWSERS"]),
MediaInfo: Filters.byProperties(["getOutputVolume"]),
MediaEngineInfo: Filters.byProperties(['MediaEngineFeatures']),
VoiceInfo: Filters.byProperties(["EchoCancellation"]),
VideoStream: Filters.byProperties(["getVideoStream"]),
SoundModule: Filters.byProperties(["playSound"]),
/* Window, DOM, HTML */
WindowInfo: Filters.byProperties(['isFocused', 'windowSize']),
TagInfo: Filters.byProperties(['VALID_TAG_NAMES']),
DOMInfo: Filters.byProperties(['canUseDOM']),
HTMLUtils: Filters.byProperties(['htmlFor', 'sanitizeUrl']),
/* Locale/Location and Time */
LocaleManager: Filters.byProperties(['setLocale']),
Moment: Filters.byProperties(['parseZone']),
LocationManager: Filters.byProperties(["createLocation"]),
Timestamps: Filters.byProperties(["fromTimestamp"]),
/* Strings and Utils */
Strings: Filters.byProperties(["TEXT", "TEXTAREA_PLACEHOLDER"]),
StringFormats: Filters.byProperties(['a', 'z']),
StringUtils: Filters.byProperties(["toASCII"]),
/* URLs and Utils */
URLParser: Filters.byProperties(['Url', 'parse']),
ExtraURLs: Filters.byProperties(['getArticleURL']),
/* DOM/React Components */
/* ==================== */
UserSettingsWindow: Filters.byProperties(['open', 'updateAccount']),
LayerManager: Filters.byProperties(['popLayer', 'pushLayer']),
/* Modals */
ModalStack: Filters.byProperties(['push', 'update', 'pop', 'popWithKey']),
UserProfileModals: Filters.byProperties(['fetchMutualFriends', 'setSection']),
ConfirmModal: Filters.byPrototypeFields(['handleCancel', 'handleSubmit', 'handleMinorConfirm']),
/* Popouts */
PopoutStack: Filters.byProperties(['open', 'close', 'closeAll']),
PopoutOpener: Filters.byProperties(['openPopout']),
EmojiPicker: Filters.byPrototypeFields(['onHoverEmoji', 'selectEmoji']),
/* Context Menus */
ContextMenuActions: Filters.byCode(/CONTEXT_MENU_CLOSE/, c => c.close),
ContextMenuItemsGroup: Filters.byCode(/itemGroup/),
ContextMenuItem: Filters.byCode(/\.label\b.*\.hint\b.*\.action\b/),
/* In-Message Links */
ExternalLink: Filters.byCode(/\.trusted\b/)
};
export class WebpackModules {
/**
* Finds a module using a filter function.
* @param {Function} filter A function to use to filter modules
* @param {Boolean} first Whether to return only the first matching module
* @return {Any}
*/
static getModule(filter, first = true) {
const modules = this.getAllModules();
const rm = [];
for (let index in modules) {
if (!modules.hasOwnProperty(index)) continue;
const module = modules[index];
const { exports } = module;
let foundModule = null;
if (!exports) continue;
if (exports.__esModule && exports.default && filter(exports.default)) foundModule = exports.default;
if (filter(exports)) foundModule = exports;
if (!foundModule) continue;
if (first) return foundModule;
rm.push(foundModule);
}
return first || rm.length == 0 ? undefined : rm;
}
/**
* Finds a module by it's name.
* @param {String} name The name of the module
* @param {Function} fallback A function to use to filter modules if not finding a known module
* @return {Any}
*/
static getModuleByName(name, fallback) {
if (Cache.hasOwnProperty(name)) return Cache[name];
if (KnownModules.hasOwnProperty(name)) fallback = KnownModules[name];
if (!fallback) return undefined;
const module = this.getModule(fallback, true);
return module ? Cache[name] = module : undefined;
}
/**
* Finds a module by it's display name.
* @param {String} name The display name of the module
* @return {Any}
*/
static getModuleByDisplayName(name) {
return this.getModule(Filters.byDisplayName(name), true);
}
/**
* Finds a module using it's code.
* @param {RegEx} regex A regular expression to use to filter modules
* @param {Boolean} first Whether to return the only the first matching module
* @return {Any}
*/
static getModuleByRegex(regex, first = true) {
return this.getModule(Filters.byCode(regex), first);
}
/**
* Finds a module using properties on it's prototype.
* @param {Array} props Properties to use to filter modules
* @param {Boolean} first Whether to return only the first matching module
* @return {Any}
*/
static getModuleByPrototypes(prototypes, first = true) {
return this.getModule(Filters.byPrototypeFields(prototypes), first);
}
/**
* Finds a module using it's own properties.
* @param {Array} props Properties to use to filter modules
* @param {Boolean} first Whether to return only the first matching module
* @return {Any}
*/
static getModuleByProps(props, first = true) {
return this.getModule(Filters.byProperties(props), first);
}
/**
* Discord's __webpack_require__ function.
*/
static get require() {
if (this._require) return this._require;
const id = 'bd-webpackmodules';
const __webpack_require__ = window['webpackJsonp']([], {
[id]: (module, exports, __webpack_require__) => exports.default = __webpack_require__
}, [id]).default;
delete __webpack_require__.m[id];
delete __webpack_require__.c[id];
return this._require = __webpack_require__;
}
/**
* Returns all loaded modules.
* @return {Array}
*/
static getAllModules() {
return this.require.c;
}
/**
* Returns an array of known modules.
* @return {Array}
*/
static listKnownModules() {
return Object.keys(KnownModules);
}
}
export { DeprecationWarning as WebpackModules };

View File

@ -1,3 +0,0 @@
export default class {
}

View File

@ -0,0 +1,441 @@
/*
* BetterDiscord Channel Struct
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import { DiscordApi, DiscordApiModules as Modules } from 'modules';
import { List, InsufficientPermissions } from 'structs';
import { Guild } from './guild';
import { Message } from './message';
import { User, GuildMember } from './user';
const channels = new WeakMap();
/**
* Class representing a Discord Channel
*/
export class Channel {
constructor(data) {
if (channels.has(data)) return channels.get(data);
channels.set(data, this);
this.discordObject = data;
}
static from(channel) {
switch (channel.type) {
default: return new Channel(channel);
case 0: return new GuildTextChannel(channel);
case 1: return new DirectMessageChannel(channel);
case 2: return new GuildVoiceChannel(channel);
case 3: return new GroupChannel(channel);
case 4: return new ChannelCategory(channel);
}
}
static fromId(id) {
const channel = Modules.ChannelStore.getChannel(id);
if (channel) return Channel.from(channel);
}
static get GuildChannel() { return GuildChannel }
static get GuildTextChannel() { return GuildTextChannel }
static get GuildVoiceChannel() { return GuildVoiceChannel }
static get ChannelCategory() { return ChannelCategory }
static get PrivateChannel() { return PrivateChannel }
static get DirectMessageChannel() { return DirectMessageChannel }
static get GroupChannel() { return GroupChannel }
get id() { return this.discordObject.id }
get applicationId() { return this.discordObject.application_id }
get type() { return this.discordObject.type }
get name() { return this.discordObject.name }
/**
* Send a message in this channel.
* @param {String} content The new message's content
* @param {Boolean} parse Whether to parse the message or send it as it is
* @return {Promise => Message}
*/
async sendMessage(content, parse = false) {
if (this.assertPermissions) this.assertPermissions('SEND_MESSAGES', Modules.DiscordPermissions.VIEW_CHANNEL | Modules.DiscordPermissions.SEND_MESSAGES);
this.select();
if (parse) content = Modules.MessageParser.parse(this.discordObject, content);
else content = {content};
const response = await Modules.MessageActions._sendMessage(this.id, content);
return Message.from(Modules.MessageStore.getMessage(this.id, response.body.id));
}
/**
* Send a bot message in this channel that only the current user can see.
* @param {String} content The new message's content
* @return {Message}
*/
sendBotMessage(content) {
this.select();
const message = Modules.MessageParser.createBotMessage(this.id, content);
Modules.MessageActions.receiveMessage(this.id, message);
return Message.from(Modules.MessageStore.getMessage(this.id, message.id));
}
/**
* A list of messages in this channel.
* @type {List<Message>}
*/
get messages() {
const messages = Modules.MessageStore.getMessages(this.id).toArray();
return List.from(messages, m => Message.from(m));
}
/**
* Jumps to the latest message in this channel.
*/
jumpToPresent() {
if (this.assertPermissions) this.assertPermissions('VIEW_CHANNEL', Modules.DiscordPermissions.VIEW_CHANNEL);
if (this.hasMoreAfter) Modules.MessageActions.jumpToPresent(this.id, Modules.DiscordConstants.MAX_MESSAGES_PER_CHANNEL);
else this.messages[this.messages.length - 1].jumpTo(false);
}
get hasMoreAfter() {
return Modules.MessageStore.getMessages(this.id).hasMoreAfter;
}
/**
* Sends an invite in this channel.
* @param {String} code The invite code
* @return {Promise => Messaage}
*/
async sendInvite(code) {
if (this.assertPermissions) this.assertPermissions('SEND_MESSAGES', Modules.DiscordPermissions.VIEW_CHANNEL | Modules.DiscordPermissions.SEND_MESSAGES);
const response = Modules.MessageActions.sendInvite(this.id, code);
return Message.from(Modules.MessageStore.getMessage(this.id, response.body.id));
}
/**
* Opens this channel in the UI.
*/
select() {
if (this.assertPermissions) this.assertPermissions('VIEW_CHANNEL', Modules.DiscordPermissions.VIEW_CHANNEL);
Modules.NavigationUtils.transitionToGuild(this.guildId ? this.guildId : Modules.DiscordConstants.ME, this.id);
}
/**
* Whether this channel is currently selected.
* @type {Boolean}
*/
get isSelected() {
return DiscordApi.currentChannel === this;
}
/**
* Updates this channel.
* @return {Promise}
*/
async updateChannel(body) {
if (this.assertPermissions) this.assertPermissions('MANAGE_CHANNELS', Modules.DiscordPermissions.MANAGE_CHANNELS);
const response = await Modules.APIModule.patch({
url: `${Modules.DiscordConstants.Endpoints.CHANNELS}/${this.id}`,
body
});
this.discordObject = Modules.ChannelStore.getChannel(this.id);
channels.set(this.discordObject, this);
}
}
export class PermissionOverwrite {
constructor(data, channel_id) {
this.discordObject = data;
this.channelId = channel_id;
}
static from(data, channel_id) {
switch (data.type) {
default: return new PermissionOverwrite(data, channel_id);
case 'role': return new RolePermissionOverwrite(data, channel_id);
case 'member': return new MemberPermissionOverwrite(data, channel_id);
}
}
static get RolePermissionOverwrite() { return RolePermissionOverwrite }
static get MemberPermissionOverwrite() { return MemberPermissionOverwrite }
get type() { return this.discordObject.type }
get allow() { return this.discordObject.allow }
get deny() { return this.discordObject.deny }
get channel() {
return Channel.fromId(this.channelId);
}
get guild() {
return this.channel ? this.channel.guild : null;
}
}
export class RolePermissionOverwrite extends PermissionOverwrite {
get roleId() { return this.discordObject.id }
get role() {
return this.guild ? this.guild.roles.find(r => r.id === this.roleId) : null;
}
}
export class MemberPermissionOverwrite extends PermissionOverwrite {
get memberId() { return this.discordObject.id }
get member() {
return GuildMember.fromId(this.memberId);
}
}
export class GuildChannel extends Channel {
static get PermissionOverwrite() { return PermissionOverwrite }
get guildId() { return this.discordObject.guild_id }
get parentId() { return this.discordObject.parent_id } // Channel category
get position() { return this.discordObject.position }
get nicks() { return this.discordObject.nicks }
checkPermissions(perms) {
return Modules.PermissionUtils.can(perms, DiscordApi.currentUser, this.discordObject);
}
assertPermissions(name, perms) {
if (!this.checkPermissions(perms)) throw new InsufficientPermissions(name);
}
get category() {
return Channel.fromId(this.parentId);
}
/**
* The current user's permissions on this channel.
*/
get permissions() {
return Modules.GuildPermissions.getChannelPermissions(this.id);
}
get permissionOverwrites() {
return List.from(Object.entries(this.discordObject.permissionOverwrites), ([i, p]) => PermissionOverwrite.from(p, this.id));
}
get guild() {
return Guild.fromId(this.guildId);
}
/**
* Whether this channel is the guild's default channel.
*/
get isDefaultChannel() {
return Modules.GuildChannelsStore.getDefaultChannel(this.guildId).id === this.id;
}
/**
* Opens this channel's settings window.
* @param {String} section The section to open (see DiscordConstants.ChannelSettingsSections)
*/
openSettings(section = 'OVERVIEW') {
Modules.ChannelSettingsWindow.setSection(section);
Modules.ChannelSettingsWindow.open(this.id);
}
/**
* Updates this channel's name.
* @param {String} name The channel's new name
* @return {Promise}
*/
updateName(name) {
return this.updateChannel({ name });
}
/**
* Changes the channel's position.
* @param {Number} position The channel's new position
* @return {Promise}
*/
changeSortLocation(position = 0) {
if (position instanceof GuildChannel) position = position.position;
return this.updateChannel({ position });
}
/**
* Updates this channel's permission overwrites.
* @param {Array} permissionOverwrites An array of permission overwrites
* @return {Promise}
*/
updatePermissionOverwrites(permission_overwrites) {
return this.updateChannel({ permission_overwrites });
}
/**
* Updates this channel's category.
* @param {ChannelCategory} category The new channel category
* @return {Promise}
*/
updateCategory(category) {
return this.updateChannel({ parent_id: category.id || category });
}
}
// Type 0 - GUILD_TEXT
export class GuildTextChannel extends GuildChannel {
get type() { return 'GUILD_TEXT' }
get topic() { return this.discordObject.topic }
get nsfw() { return this.discordObject.nsfw }
/**
* Updates this channel's topic.
* @param {String} topc The new channel topic
* @return {Promise}
*/
updateTopic(topic) {
return this.updateChannel({ topic });
}
/**
* Updates this channel's not-safe-for-work flag.
* @param {Boolean} nsfw Whether the channel should be marked as NSFW
* @return {Promise}
*/
setNsfw(nsfw = true) {
return this.updateChannel({ nsfw });
}
setNotNsfw() {
return this.setNsfw(false);
}
}
// Type 2 - GUILD_VOICE
export class GuildVoiceChannel extends GuildChannel {
get type() { return 'GUILD_VOICE' }
get userLimit() { return this.discordObject.userLimit }
get bitrate() { return this.discordObject.bitrate }
sendMessage() { throw new Error('Cannot send messages in a voice channel.'); }
get messages() { return new List(); }
jumpToPresent() { throw new Error('Cannot select a voice channel.'); }
get hasMoreAfter() { return false; }
sendInvite() { throw new Error('Cannot invite someone to a voice channel.'); }
select() { throw new Error('Cannot select a voice channel.'); }
/**
* Updates this channel's bitrate.
* @param {Number} bitrate The new bitrate
* @return {Promise}
*/
updateBitrate(bitrate) {
return this.updateChannel({ bitrate });
}
/**
* Updates this channel's user limit.
* @param {Number} userLimit The new user limit
* @return {Promise}
*/
updateUserLimit(user_limit) {
return this.updateChannel({ user_limit });
}
}
// Type 4 - GUILD_CATEGORY
export class ChannelCategory extends GuildChannel {
get type() { return 'GUILD_CATEGORY' }
get parentId() { return undefined }
get category() { return undefined }
sendMessage() { throw new Error('Cannot send messages in a channel category.'); }
get messages() { return new List(); }
jumpToPresent() { throw new Error('Cannot select a channel category.'); }
get hasMoreAfter() { return false; }
sendInvite() { throw new Error('Cannot invite someone to a channel category.'); }
select() { throw new Error('Cannot select a channel category.'); }
updateCategory() { throw new Error('Cannot set a channel category on another channel category.'); }
/**
* A list of channels in this category.
*/
get channels() {
return List.from(this.guild.channels.filter(c => c.parentId === this.id));
}
/**
* Opens the create channel modal for this guild.
* @param {Number} type The type of channel to create - either 0 (text), 2 (voice) or 4 (category)
* @param {GuildChannel} clone A channel to clone permissions of
*/
openCreateChannelModal(type, category, clone) {
this.guild.openCreateChannelModal(type, this.id, this, clone);
}
/**
* Creates a channel in this category.
* @param {Number} type The type of channel to create - either 0 (text) or 2 (voice)
* @param {String} name A name for the new channel
* @param {Array} permission_overwrites An array of PermissionOverwrite-like objects - leave to use the permissions of the category
* @return {Promise => GuildChannel}
*/
createChannel(type, name, permission_overwrites) {
return this.guild.createChannel(type, name, this, permission_overwrites);
}
}
export class PrivateChannel extends Channel {
get userLimit() { return this.discordObject.userLimit }
get bitrate() { return this.discordObject.bitrate }
}
// Type 1 - DM
export class DirectMessageChannel extends PrivateChannel {
get type() { return 'DM' }
get recipientId() { return this.discordObject.recipients[0] }
/**
* The other user of this direct message channel.
*/
get recipient() {
return User.fromId(this.recipientId);
}
}
// Type 3 - GROUP_DM
export class GroupChannel extends PrivateChannel {
get ownerId() { return this.discordObject.ownerId }
get type() { return 'GROUP_DM' }
get name() { return this.discordObject.name }
get icon() { return this.discordObject.icon }
/**
* A list of the other members of this group direct message channel.
*/
get members() {
return List.from(this.discordObject.recipients, id => User.fromId(id));
}
/**
* The owner of this group direct message channel. This is usually the person who created it.
*/
get owner() {
return User.fromId(this.ownerId);
}
/**
* Updates this channel's name.
* @param {String} name The channel's new name
* @return {Promise}
*/
updateName(name) {
return this.updateChannel({ name });
}
}

View File

@ -0,0 +1,492 @@
/*
* BetterDiscord Guild Struct
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import { DiscordApi, DiscordApiModules as Modules } from 'modules';
import { List, InsufficientPermissions } from 'structs';
import { FileUtils } from 'common';
import { Channel } from './channel';
import { GuildMember } from './user';
const roles = new WeakMap();
/**
* Class representing a Discord Role
*/
export class Role {
constructor(data, guild_id) {
if (roles.has(data)) return roles.get(data);
roles.set(data, this);
this.discordObject = data;
this.guildId = guild_id;
}
get id() { return this.discordObject.id }
get name() { return this.discordObject.name }
get position() { return this.discordObject.position }
get originalPosition() { return this.discordObject.originalPosition }
get permissions() { return this.discordObject.permissions }
get managed() { return this.discordObject.managed }
get mentionable() { return this.discordObject.mentionable }
get hoist() { return this.discordObject.hoist }
get colour() { return this.discordObject.color }
get colourString() { return this.discordObject.colorString }
get guild() {
return Guild.fromId(this.guildId);
}
get members() {
return this.guild.members.filter(m => m.roles.includes(this));
}
}
const emojis = new WeakMap();
/**
* Class representing a Discord Emoji
*/
export class Emoji {
constructor(data) {
if (emojis.has(data)) return emojis.get(data);
emojis.set(data, this);
this.discordObject = data;
}
get id() { return this.discordObject.id }
get guildId() { return this.discordObject.guild_id }
get name() { return this.discordObject.name }
get managed() { return this.discordObject.managed }
get animated() { return this.discordObject.animated }
get allNamesString() { return this.discordObject.allNamesString }
get requireColons() { return this.discordObject.require_colons }
get url() { return this.discordObject.url }
get roles() { return this.discordObject.roles }
get guild() {
return Guild.fromId(this.guildId);
}
}
const guilds = new WeakMap();
/**
* Class representing a Discord Guild
*/
export class Guild {
constructor(data) {
if (guilds.has(data)) return guilds.get(data);
guilds.set(data, this);
this.discordObject = data;
}
static from(data) {
return new Guild(data);
}
static fromId(id) {
const guild = Modules.GuildStore.getGuild(id);
if (guild) return Guild.from(guild);
}
static get Role() { return Role }
static get Emoji() { return Emoji }
get id() { return this.discordObject.id }
get ownerId() { return this.discordObject.ownerId }
get applicationId() { return this.discordObject.application_id }
get systemChannelId() { return this.discordObject.systemChannelId }
get name() { return this.discordObject.name }
get acronym() { return this.discordObject.acronym }
get icon() { return this.discordObject.icon }
get joinedAt() { return this.discordObject.joinedAt }
get verificationLevel() { return this.discordObject.verificationLevel }
get mfaLevel() { return this.discordObject.mfaLevel }
get large() { return this.discordObject.large }
get lazy() { return this.discordObject.lazy }
get voiceRegion() { return this.discordObject.region }
get afkChannelId() { return this.discordObject.afkChannelId }
get afkTimeout() { return this.discordObject.afkTimeout }
get explicitContentFilter() { return this.discordObject.explicitContentFilter }
get defaultMessageNotifications() { return this.discordObject.defaultMessageNotifications }
get splash() { return this.discordObject.splash }
get features() { return this.discordObject.features }
get owner() {
return this.members.find(m => m.userId === this.ownerId);
}
get roles() {
return List.from(Object.entries(this.discordObject.roles), ([i, r]) => new Role(r, this.id))
.sort((r1, r2) => r1.position === r2.position ? 0 : r1.position > r2.position ? 1 : -1);
}
get channels() {
const channels = Modules.GuildChannelsStore.getChannels(this.id);
const returnChannels = new List();
for (const category in channels) {
if (channels.hasOwnProperty(category)) {
if (!Array.isArray(channels[category])) continue;
const channelList = channels[category];
for (const channel of channelList) {
// For some reason Discord adds a new category with the ID "null" and name "Uncategorized"
if (channel.channel.id === 'null') continue;
returnChannels.push(Channel.from(channel.channel));
}
}
}
return returnChannels;
}
/**
* Channels that don't have a parent. (Channel categories and any text/voice channel not in one.)
*/
get mainChannels() {
return this.channels.filter(c => !c.parentId);
}
/**
* The guild's default channel. (Usually the first in the list.)
*/
get defaultChannel() {
return Channel.from(Modules.GuildChannelsStore.getDefaultChannel(this.id));
}
/**
* The guild's AFK channel.
*/
get afkChannel() {
return this.afkChannelId ? Channel.fromId(this.afkChannelId) : null;
}
/**
* The channel system messages are sent to.
*/
get systemChannel() {
return this.systemChannelId ? Channel.fromId(this.systemChannelId) : null;
}
/**
* A list of GuildMember objects.
*/
get members() {
const members = Modules.GuildMemberStore.getMembers(this.id);
return List.from(members, m => new GuildMember(m, this.id));
}
/**
* The current user as a GuildMember of this guild.
*/
get currentUser() {
return this.members.find(m => m.user === DiscordApi.currentUser);
}
/**
* The total number of members in the guild.
*/
get memberCount() {
return Modules.MemberCountStore.getMemberCount(this.id);
}
/**
* An array of the guild's custom emojis.
*/
get emojis() {
return List.from(Modules.EmojiUtils.getGuildEmoji(this.id), e => new Emoji(e, this.id));
}
checkPermissions(perms) {
return Modules.PermissionUtils.can(perms, DiscordApi.currentUser, this.discordObject);
}
assertPermissions(name, perms) {
if (!this.checkPermissions(perms)) throw new InsufficientPermissions(name);
}
/**
* The current user's permissions on this guild.
*/
get permissions() {
return Modules.GuildPermissions.getGuildPermissions(this.id);
}
/**
* Returns the GuildMember object for a user.
* @param {User|GuildMember|Number} user A User or GuildMember object or a user ID
* @return {GuildMember}
*/
getMember(user) {
const member = Modules.GuildMemberStore.getMember(this.id, user.userId || user.id || user);
if (member) return new GuildMember(member, this.id);
}
/**
* Checks if a user is a member of this guild.
* @param {User|GuildMember|Number} user A User or GuildMember object or a user ID
* @return {Boolean}
*/
isMember(user) {
return Modules.GuildMemberStore.isMember(this.id, user.userId || user.id || user);
}
/**
* Whether the user has not restricted direct messages from members of this guild.
*/
get allowPrivateMessages() {
return !DiscordApi.UserSettings.restrictedGuildIds.includes(this.id);
}
/**
* Marks all messages in the guild as read.
*/
markAsRead() {
Modules.GuildActions.markGuildAsRead(this.id);
}
/**
* Selects the guild in the UI.
*/
select() {
Modules.GuildActions.selectGuild(this.id);
}
/**
* Whether this guild is currently selected.
*/
get isSelected() {
return DiscordApi.currentGuild === this;
}
/**
* Opens this guild's settings window.
* @param {String} section The section to open (see DiscordConstants.GuildSettingsSections)
*/
openSettings(section = 'OVERVIEW') {
Modules.GuildSettingsWindow.setSection(section);
Modules.GuildSettingsWindow.open(this.id);
}
/**
* Kicks members who don't have any roles and haven't been seen in the number of days passed.
* @param {Number} days
*/
pruneMembers(days) {
this.assertPermissions('KICK_MEMBERS', Modules.DiscordPermissions.KICK_MEMBERS);
Modules.PruneMembersModal.prune(this.id, days);
}
openPruneMumbersModal() {
this.assertPermissions('KICK_MEMBERS', Modules.DiscordPermissions.KICK_MEMBERS);
Modules.PruneMembersModal.open(this.id);
}
/**
* Opens the create channel modal for this guild.
* @param {Number} type The type of channel to create - either 0 (text), 2 (voice) or 4 (category)
* @param {ChannelCategory} category The category to create the channel in
* @param {GuildChannel} clone A channel to clone permissions, topic, bitrate and user limit of
*/
openCreateChannelModal(type, category, clone) {
this.assertPermissions('MANAGE_CHANNELS', Modules.DiscordPermissions.MANAGE_CHANNELS);
Modules.CreateChannelModal.open(type, this.id, category ? category.id : undefined, clone ? clone.id : undefined);
}
/**
* Creates a channel in this guild.
* @param {Number} type The type of channel to create - either 0 (text), 2 (voice) or 4 (category)
* @param {String} name A name for the new channel
* @param {ChannelCategory} category The category to create the channel in
* @param {Array} permission_overwrites An array of PermissionOverwrite-like objects - leave to use the permissions of the category
* @return {Promise => GuildChannel}
*/
async createChannel(type, name, category, permission_overwrites) {
this.assertPermissions('MANAGE_CHANNELS', Modules.DiscordPermissions.MANAGE_CHANNELS);
const response = await Modules.APIModule.post({
url: Modules.DiscordConstants.Endpoints.GUILD_CHANNELS(this.id),
body: {
type, name,
parent_id: category ? category.id : undefined,
permission_overwrites: permission_overwrites ? permission_overwrites.map(p => ({
type: p.type,
id: (p.type === 'user' ? p.userId : p.roleId) || p.id,
allow: p.allow,
deny: p.deny
})) : undefined
}
});
return Channel.fromId(response.body.id);
}
openNotificationSettingsModal() {
Modules.NotificationSettingsModal.open(this.id);
}
openPrivacySettingsModal() {
Modules.PrivacySettingsModal.open(this.id);
}
nsfwAgree() {
Modules.GuildActions.nsfwAgree(this.id);
}
nsfwDisagree() {
Modules.GuildActions.nsfwDisagree(this.id);
}
/**
* Changes the guild's position in the list.
* @param {Number} index The new position
*/
changeSortLocation(index) {
Modules.GuildActions.move(DiscordApi.guildPositions.indexOf(this.id), index);
}
/**
* Updates this guild.
* @return {Promise}
*/
async updateGuild(body) {
this.assertPermissions('MANAGE_GUILD', Modules.DiscordPermissions.MANAGE_GUILD);
const response = await Modules.APIModule.patch({
url: Modules.DiscordConstants.Endpoints.GUILD(this.id),
body
});
this.discordObject = Modules.GuildStore.getGuild(this.id);
guilds.set(this.discordObject, this);
}
/**
* Updates this guild's name.
* @param {String} name The new name
* @return {Promise}
*/
updateName(name) {
return this.updateGuild({ name });
}
/**
* Updates this guild's voice region.
* @param {String} region The ID of the new voice region (obtainable via the API - see https://discordapp.com/developers/docs/resources/voice#list-voice-regions)
* @return {Promise}
*/
updateVoiceRegion(region) {
return this.updateGuild({ region });
}
/**
* Updates this guild's verification level.
* @param {Number} verificationLevel The new verification level (see https://discordapp.com/developers/docs/resources/guild#guild-object-verification-level)
* @return {Promise}
*/
updateVerificationLevel(verification_level) {
return this.updateGuild({ verification_level });
}
/**
* Updates this guild's default message notification level.
* @param {Number} defaultMessageNotifications The new default notification level (0: all messages, 1: only mentions)
* @return {Promise}
*/
updateDefaultMessageNotifications(default_message_notifications) {
return this.updateGuild({ default_message_notifications });
}
/**
* Updates this guild's explicit content filter level.
* @param {Number} explicitContentFilter The new explicit content filter level (0: disabled, 1: members without roles, 2: everyone)
* @return {Promise}
*/
updateExplicitContentFilter(explicit_content_filter) {
return this.updateGuild({ explicit_content_filter });
}
/**
* Updates this guild's AFK channel.
* @param {GuildVoiceChannel} afkChannel The new AFK channel
* @return {Promise}
*/
updateAfkChannel(afk_channel) {
return this.updateGuild({ afk_channel_id: afk_channel.id || afk_channel });
}
/**
* Updates this guild's AFK timeout.
* @param {Number} afkTimeout The new AFK timeout
* @return {Promise}
*/
updateAfkTimeout(afk_timeout) {
return this.updateGuild({ afk_timeout });
}
/**
* Updates this guild's icon.
* @param {Buffer|String} icon A buffer/base 64 encoded 128x128 JPEG image
* @return {Promise}
*/
updateIcon(icon) {
return this.updateGuild({ icon: typeof icon === 'string' ? icon : icon.toString('base64') });
}
/**
* Updates this guild's icon using a local file.
* TODO
* @param {String} icon_path The path to the new icon
* @return {Promise}
*/
async updateIconFromFile(icon_path) {
const buffer = await FileUtils.readFileBuffer(icon_path);
return this.updateIcon(buffer);
}
/**
* Updates this guild's owner. (Should plugins really ever need to do this?)
* @param {User|GuildMember} owner The user/guild member to transfer ownership to
* @return {Promise}
*/
updateOwner(owner) {
return this.updateGuild({ owner_id: owner.user ? owner.user.id : owner.id || owner });
}
/**
* Updates this guild's splash image.
* (I don't know what this is actually used for. The API documentation says it's VIP-only.)
* @param {Buffer|String} icon A buffer/base 64 encoded 128x128 JPEG image
* @return {Promise}
*/
updateSplash(splash) {
return this.updateGuild({ splash: typeof splash === 'string' ? splash : splash.toString('base64') });
}
/**
* Updates this guild's splash image using a local file.
* TODO
* @param {String} splash_path The path to the new splash
* @return {Promise}
*/
async updateSplashFromFile(splash_path) {
const buffer = await FileUtils.readFileBuffer(splash_path);
return this.updateSplash(buffer);
}
/**
* Updates this guild's system channel.
* @param {GuildTextChannel} systemChannel The new system channel
* @return {Promise}
*/
updateSystemChannel(system_channel) {
return this.updateGuild({ system_channel_id: system_channel.id || system_channel });
}
}

View File

@ -0,0 +1,319 @@
/**
* BetterDiscord Message Struct
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import { DiscordApi, DiscordApiModules as Modules } from 'modules';
import { List, InsufficientPermissions } from 'structs';
import { Channel } from './channel';
import { User } from './user';
const reactions = new WeakMap();
/**
* Class representing a Discord Reaction
*/
export class Reaction {
constructor(data, message_id, channel_id) {
if (reactions.has(data)) return reactions.get(data);
reactions.set(data, this);
this.discordObject = data;
this.messageId = message_id;
this.channelId = channel_id;
}
get emoji() {
const id = this.discordObject.emoji.id;
if (!id || !this.guild) return this.discordObject.emoji;
return this.guild.emojis.find(e => e.id === id);
}
get count() { return this.discordObject.count }
get me() { return this.discordObject.me }
get channel() {
return Channel.fromId(this.channel_id);
}
get message() {
return this.channel ? this.channel.messages.find(m => m.id === this.messageId) : null;
}
get guild() {
return this.channel ? this.channel.guild : null;
}
}
const embeds = new WeakMap();
/**
* Class representing a Discord Embed
*/
export class Embed {
constructor(data, message_id, channel_id) {
if (embeds.has(data)) return embeds.get(data);
embeds.set(data, this);
this.discordObject = data;
this.messageId = message_id;
this.channelId = channel_id;
}
get title() { return this.discordObject.title }
get type() { return this.discordObject.type }
get description() { return this.discordObject.description }
get url() { return this.discordObject.url }
get timestamp() { return this.discordObject.timestamp }
get colour() { return this.discordObject.color }
get footer() { return this.discordObject.footer }
get image() { return this.discordObject.image }
get thumbnail() { return this.discordObject.thumbnail }
get video() { return this.discordObject.video }
get provider() { return this.discordObject.provider }
get author() { return this.discordObject.author }
get fields() { return this.discordObject.fields }
get channel() {
return Channel.fromId(this.channelId);
}
get message() {
return this.channel ? this.channel.messages.find(m => m.id === this.messageId) : null;
}
get guild() {
return this.channel ? this.channel.guild : null;
}
}
const messages = new WeakMap();
/**
* Class representing a Discord Message
*/
export class Message {
constructor(data) {
if (messages.has(data)) return messages.get(data);
messages.set(data, this);
this.discordObject = data;
}
static from(data) {
switch (data.type) {
default: return new Message(data);
case 0: return new DefaultMessage(data);
case 1: return new RecipientAddMessage(data);
case 2: return new RecipientRemoveMessage(data);
case 3: return new CallMessage(data);
case 4: return new GroupChannelNameChangeMessage(data);
case 5: return new GroupChannelIconChangeMessage(data);
case 6: return new MessagePinnedMessage(data);
case 7: return new GuildMemberJoinMessage(data);
}
}
static get DefaultMessage() { return DefaultMessage }
static get RecipientAddMessage() { return RecipientAddMessage }
static get RecipientRemoveMessage() { return RecipientRemoveMessage }
static get CallMessage() { return CallMessage }
static get GroupChannelNameChangeMessage() { return GroupChannelNameChangeMessage }
static get GroupChannelIconChangeMessage() { return GroupChannelIconChangeMessage }
static get MessagePinnedMessage() { return MessagePinnedMessage }
static get GuildMemberJoinMessage() { return GuildMemberJoinMessage }
static get Reaction() { return Reaction }
static get Embed() { return Embed }
get id() { return this.discordObject.id }
get channelId() { return this.discordObject.channel_id }
get nonce() { return this.discordObject.nonce }
get type() { return this.discordObject.type }
get timestamp() { return this.discordObject.timestamp }
get state() { return this.discordObject.state }
get nick() { return this.discordObject.nick }
get colourString() { return this.discordObject.colorString }
get author() {
if (this.discordObject.author && !this.webhookId) return User.from(this.discordObject.author);
return null;
}
get channel() {
return Channel.fromId(this.channelId);
}
get guild() {
return this.channel ? this.channel.guild : null;
}
/**
* Deletes the message.
* @return {Promise}
*/
delete() {
if (!this.isDeletable) throw new Error(`Message type ${this.type} is not deletable.`);
if (this.author === DiscordApi.currentUser) {}
else if (this.channel.assertPermissions) this.channel.assertPermissions('MANAGE_MESSAGES', Modules.DiscordPermissions.MANAGE_MESSAGES);
else if (!this.channel.owner === DiscordApi.currentUser) throw new InsufficientPermissions('MANAGE_MESSAGES');
return Modules.APIModule.delete(`${Modules.DiscordConstants.Endpoints.MESSAGES(this.channelId)}/${this.id}`);
}
get isDeletable() {
return this.type === 'DEFAULT' || this.type === 'CHANNEL_PINNED_MESSAGE' || this.type === 'GUILD_MEMBER_JOIN';
}
/**
* Jumps to the message.
*/
jumpTo(flash = true) {
Modules.MessageActions.jumpToMessage(this.channelId, this.id, flash);
}
}
/**
* Class representing a default Discord Message
*/
export class DefaultMessage extends Message {
get webhookId() { return this.discordObject.webhookId }
get type() { return 'DEFAULT' }
get content() { return this.discordObject.content }
get contentParsed() { return this.discordObject.contentParsed }
get inviteCodes() { return this.discordObject.invites }
get attachments() { return this.discordObject.attachments }
get mentionIds() { return this.discordObject.mentions }
get mentionRoleIds() { return this.discordObject.mentionRoles }
get mentionEveryone() { return this.discordObject.mentionEveryone }
get editedTimestamp() { return this.discordObject.editedTimestamp }
get tts() { return this.discordObject.tts }
get mentioned() { return this.discordObject.mentioned }
get bot() { return this.discordObject.bot }
get blocked() { return this.discordObject.blocked }
get pinned() { return this.discordObject.pinned }
get activity() { return this.discordObject.activity }
get application() { return this.discordObject.application }
get webhook() {
return this.webhookId ? this.discordObject.author : null;
}
get mentions() {
return List.from(this.mentionIds, id => User.fromId(id));
}
get mention_roles() {
return List.from(this.mentionRoleIds, id => this.guild.roles.find(r => r.id === id));
}
get embeds() {
return List.from(this.discordObject.embeds, r => new Embed(r, this.id, this.channelId));
}
get reactions() {
return List.from(this.discordObject.reactions, r => new Reaction(r, this.id, this.channelId));
}
get edited() {
return !!this.editedTimestamp;
}
/**
* Programmatically update the message's content.
* @param {String} content The message's new content
* @param {Boolean} parse Whether to parse the message or update it as it is
* @return {Promise}
*/
async edit(content, parse = false) {
if (this.author !== DiscordApi.currentUser) throw new Error('Cannot edit messages sent by other users.');
if (parse) content = Modules.MessageParser.parse(this.discordObject, content);
else content = {content};
const response = await Modules.APIModule.patch({
url: `${Modules.DiscordConstants.Endpoints.MESSAGES(this.channelId)}/${this.id}`,
body: content
});
this.discordObject = Modules.MessageStore.getMessage(this.id, response.body.id);
messages.set(this.discordObject, this);
}
/**
* Start the edit mode of the UI.
* @param {String} content A string to show in the message text area - if empty the message's current content will be used
*/
startEdit(content) {
if (this.author !== DiscordApi.currentUser) throw new Error('Cannot edit messages sent by other users.');
Modules.MessageActions.startEditMessage(this.channelId, this.id, content || this.content);
}
/**
* Exit the edit mode of the UI.
*/
endEdit() {
Modules.MessageActions.endEditMessage();
}
}
export class RecipientAddMessage extends Message {
get type() { return 'RECIPIENT_ADD' }
get addedUserId() { return this.discordObject.mentions[0] }
get addedUser() {
return User.fromId(this.addedUserId);
}
}
export class RecipientRemoveMessage extends Message {
get type() { return 'RECIPIENT_REMOVE' }
get removedUserId() { return this.discordObject.mentions[0] }
get removedUser() {
return User.fromId(this.removedUserId);
}
get userLeft() {
return this.author === this.removedUser;
}
}
export class CallMessage extends Message {
get type() { return 'CALL' }
get mentionIds() { return this.discordObject.mentions }
get call() { return this.discordObject.call }
get endedTimestamp() { return this.call.endedTimestamp }
get mentions() {
return List.from(this.mentionIds, id => User.fromId(id));
}
get participants() {
return List.from(this.call.participants, id => User.fromId(id));
}
}
export class GroupChannelNameChangeMessage extends Message {
get type() { return 'CHANNEL_NAME_CHANGE' }
get newName() { return this.discordObject.content }
}
export class GroupChannelIconChangeMessage extends Message {
get type() { return 'CHANNEL_ICON_CHANGE' }
}
export class MessagePinnedMessage extends Message {
get type() { return 'CHANNEL_PINNED_MESSAGE' }
}
export class GuildMemberJoinMessage extends Message {
get type() { return 'GUILD_MEMBER_JOIN' }
}

View File

@ -0,0 +1,336 @@
/*
* BetterDiscord User Struct
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import { DiscordApi, DiscordApiModules as Modules } from 'modules';
import { List, InsufficientPermissions } from 'structs';
import { Utils } from 'common';
import { Guild } from './guild';
import { Channel } from './channel';
const users = new WeakMap();
/**
* Class representing a Discord user
*/
export class User {
constructor(data) {
if (users.has(data)) return users.get(data);
users.set(data, this);
this.discordObject = data;
}
static from(data) {
return new User(data);
}
static fromId(id) {
const user = Modules.UserStore.getUser(id);
if (user) return User.from(user);
}
static get GuildMember() { return GuildMember }
get id() { return this.discordObject.id }
get username() { return this.discordObject.username }
get usernameLowerCase() { return this.discordObject.usernameLowerCase }
get discriminator() { return this.discordObject.discriminator }
get avatar() { return this.discordObject.avatar }
get email() { return undefined }
get phone() { return undefined }
get flags() { return this.discordObject.flags }
get isBot() { return this.discordObject.bot }
get premium() { return this.discordObject.premium }
get verified() { return this.discordObject.verified }
get mfaEnabled() { return this.discordObject.mfaEnabled }
get mobile() { return this.discordObject.mobile }
get tag() { return this.discordObject.tag }
get avatarUrl() { return this.discordObject.avatarURL }
get createdAt() { return this.discordObject.createdAt }
get isClamied() { return this.discordObject.isClaimed() }
get isLocalBot() { return this.discordObject.isLocalBot() }
get isPhoneVerified() { return this.discordObject.isPhoneVerified() }
get guilds() {
return DiscordApi.guilds.filter(g => g.members.find(m => m.user === this));
}
get status() {
return Modules.UserStatusStore.getStatus(this.id);
}
get activity() {
// type can be either 0 (normal/rich presence game), 1 (streaming) or 2 (listening to Spotify)
// (3 appears as watching but is undocumented)
return Modules.UserStatusStore.getActivity(this.id);
}
get note() {
const note = Modules.UserNoteStore.getNote(this.id);
return note ? note : null;
}
/**
* Updates the note for this user.
* @param {String} note The new note
* @return {Promise}
*/
updateNote(note) {
return Modules.APIModule.put({
url: `${Modules.DiscordConstants.Endpoints.NOTES}/${this.id}`,
body: { note }
});
}
get privateChannel() {
return DiscordApi.channels.find(c => c.type === 'DM' && c.recipientId === this.id);
}
async ensurePrivateChannel() {
if (DiscordApi.currentUser === this)
throw new Error('Cannot create a direct message channel to the current user.');
return Channel.fromId(await Modules.PrivateChannelActions.ensurePrivateChannel(DiscordApi.currentUser.id, this.id));
}
async sendMessage(content, parse = true) {
const channel = await this.ensurePrivateChannel();
return channel.sendMessage(content, parse);
}
get isFriend() {
return Modules.RelationshipStore.isFriend(this.id);
}
get isBlocked() {
return Modules.RelationshipStore.isBlocked(this.id);
}
addFriend() {
Modules.RelationshipManager.addRelationship(this.id, {location: 'Context Menu'});
}
removeFriend() {
Modules.RelationshipManager.removeRelationship(this.id, {location: 'Context Menu'});
}
block() {
Modules.RelationshipManager.addRelationship(this.id, {location: 'Context Menu'}, Modules.DiscordConstants.RelationshipTypes.BLOCKED);
}
unblock() {
Modules.RelationshipManager.removeRelationship(this.id, {location: 'Context Menu'});
}
/**
* Opens the profile modal for this user.
* @param {String} section The section to open (see DiscordConstants.UserProfileSections)
*/
openUserProfileModal(section = 'USER_INFO') {
Modules.UserProfileModal.open(this.id);
Modules.UserProfileModal.setSection(section);
}
}
const guild_members = new WeakMap();
/**
* Class representing a Discord Guild Member
*/
export class GuildMember {
constructor(data, guild_id) {
if (guild_members.has(data)) return guild_members.get(data);
guild_members.set(data, this);
this.discordObject = data;
this.guildId = guild_id;
}
get userId() { return this.discordObject.userId }
get nickname() { return this.discordObject.nick }
get colourString() { return this.discordObject.colorString }
get hoistRoleId() { return this.discordObject.hoistRoleId }
get roleIds() { return this.discordObject.roles }
get user() {
return User.fromId(this.userId);
}
get name() {
return this.nickname || this.user.username;
}
get guild() {
return Guild.fromId(this.guildId);
}
get roles() {
return List.from(this.roleIds, id => this.guild.roles.find(r => r.id === id))
.sort((r1, r2) => r1.position === r2.position ? 0 : r1.position > r2.position ? 1 : -1);
}
get hoistRole() {
return this.guild.roles.find(r => r.id === this.hoistRoleId);
}
checkPermissions(perms) {
return Modules.PermissionUtils.can(perms, DiscordApi.currentUser.discordObject, this.guild.discordObject);
}
assertPermissions(name, perms) {
if (!this.checkPermissions(perms)) throw new InsufficientPermissions(name);
}
/**
* Opens the modal to change this user's nickname.
*/
openChangeNicknameModal() {
if (DiscordApi.currentUser === this.user)
this.assertPermissions('CHANGE_NICKNAME', Modules.DiscordPermissions.CHANGE_NICKNAME);
else this.assertPermissions('MANAGE_NICKNAMES', Modules.DiscordPermissions.MANAGE_NICKNAMES);
Modules.ChangeNicknameModal.open(this.guildId, this.userId);
}
/**
* Changes the user's nickname on this guild.
* @param {String} nickname The user's new nickname
* @return {Promise}
*/
changeNickname(nick) {
if (DiscordApi.currentUser === this.user)
this.assertPermissions('CHANGE_NICKNAME', Modules.DiscordPermissions.CHANGE_NICKNAME);
else this.assertPermissions('MANAGE_NICKNAMES', Modules.DiscordPermissions.MANAGE_NICKNAMES);
return Modules.APIModule.patch({
url: `${Modules.DiscordConstants.Endpoints.GUILD_MEMBERS(this.guild_id)}/${DiscordApi.currentUser === this.user ? '@me/nick' : this.userId}`,
body: { nick }
});
}
/**
* Kicks this user from the guild.
* @param {String} reason A reason to attach to the audit log entry
* @return {Promise}
*/
kick(reason = '') {
this.assertPermissions('KICK_MEMBERS', Modules.DiscordPermissions.KICK_MEMBERS);
return Modules.GuildActions.kickUser(this.guildId, this.userId, reason);
}
/**
* Bans this user from the guild.
* @param {Number} daysToDelete The number of days of the user's recent message history to delete
* @param {String} reason A reason to attach to the audit log entry
* @return {Promise}
*/
ban(daysToDelete = 1, reason = '') {
this.assertPermissions('BAN_MEMBERS', Modules.DiscordPermissions.BAN_MEMBERS);
return Modules.GuildActions.banUser(this.guildId, this.userId, daysToDelete, reason);
}
/**
* Removes the ban for this user.
* @return {Promise}
*/
unban() {
this.assertPermissions('BAN_MEMBERS', Modules.DiscordPermissions.BAN_MEMBERS);
return Modules.GuildActions.unbanUser(this.guildId, this.userId);
}
/**
* Moves this user to another voice channel.
* @param {GuildVoiceChannel} channel The channel to move this user to
*/
move(channel) {
this.assertPermissions('MOVE_MEMBERS', Modules.DiscordPermissions.MOVE_MEMBERS);
Modules.GuildActions.setChannel(this.guildId, this.userId, channel.id);
}
/**
* Mutes this user for everyone in the guild.
*/
mute(active = true) {
this.assertPermissions('MUTE_MEMBERS', Modules.DiscordPermissions.MUTE_MEMBERS);
Modules.GuildActions.setServerMute(this.guildId, this.userId, active);
}
/**
* Unmutes this user.
*/
unmute() {
this.mute(false);
}
/**
* Deafens this user.
*/
deafen(active = true) {
this.assertPermissions('DEAFEN_MEMBERS', Modules.DiscordPermissions.DEAFEN_MEMBERS);
Modules.GuildActions.setServerDeaf(this.guildId, this.userId, active);
}
/**
* Undeafens this user.
*/
undeafen() {
this.deafen(false);
}
/**
* Gives this user a role.
* @param {Role} role The role to add
* @return {Promise}
*/
addRole(...roles) {
const newRoles = this.roleIds.concat([]);
let changed = false;
for (const role of roles) {
if (newRoles.includes(role.id || role)) continue;
newRoles.push(role.id || role);
changed = true;
}
if (!changed) return;
return this.updateRoles(newRoles);
}
/**
* Removes a role from this user.
* @param {Role} role The role to remove
* @return {Promise}
*/
removeRole(...roles) {
const newRoles = this.roleIds.concat([]);
let changed = false;
for (const role of roles) {
if (!newRoles.includes(role.id || role)) continue;
Utils.removeFromArray(newRoles, role.id || role);
changed = true;
}
if (!changed) return;
return this.updateRoles(newRoles);
}
/**
* Updates this user's roles.
* @param {Array} roles An array of Role objects or role IDs
* @return {Promise}
*/
updateRoles(roles) {
roles = roles.map(r => r.id || r);
return Modules.APIModule.patch({
url: `${Modules.DiscordConstants.Endpoints.GUILD_MEMBERS(this.guildId)}/${this.userId}`,
body: { roles }
});
}
}

View File

@ -0,0 +1,4 @@
export * from './discord/user';
export * from './discord/guild';
export * from './discord/channel';
export * from './discord/message';

View File

@ -17,22 +17,37 @@ export default class ErrorEvent extends Event {
this.showStack = false; // For error modal
}
/**
* The module the error occured in.
*/
get module() {
return this.args.module;
}
/**
* A message describing the error.
*/
get message() {
return this.args.message;
}
/**
* The original error object.
*/
get err() {
return this.args.err;
}
/**
* A trace showing which functions were called when the error occured.
*/
get stackTrace() {
return this.err.stack;
}
/**
* The type of event.
*/
get __eventType() {
return 'error';
}

View File

@ -17,14 +17,30 @@ export default class Event {
};
}
/**
* An object containing information about the event.
*/
get event() {
return this.__eventInfo;
}
/**
* Extra data associated with this event.
*/
get data() {
return this.args.data;
}
/**
* The first argument that was passed to the constructor, which contains information about the event.
*/
get args() {
return this.event.args[0];
}
get __eventType() { return null; }
/**
* The type of event.
*/
get __eventType() { return undefined; }
}

View File

@ -1,3 +1,4 @@
export { default as SettingUpdatedEvent } from './settingupdated';
export { default as SettingsUpdatedEvent } from './settingsupdated';
export { default as ErrorEvent } from './error';
export { PermissionsError, InsufficientPermissions } from './permissionserror';

View File

@ -0,0 +1,25 @@
/**
* BetterDiscord Permissions Error Struct
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import ErrorEvent from './error';
export class PermissionsError extends ErrorEvent {
constructor(message) {
super(message);
this.name = 'PermissionsError';
}
}
export class InsufficientPermissions extends PermissionsError {
constructor(message) {
super(`Missing Permission — ${message}`);
this.name = 'InsufficientPermissions';
}
}

View File

@ -12,10 +12,16 @@ import Event from './event';
export default class SettingsUpdatedEvent extends Event {
/**
* An array of SettingUpdated events.
*/
get updatedSettings() {
return this.args.updatedSettings;
}
/**
* The type of event.
*/
get __eventType() {
return 'settings-updated';
}

View File

@ -12,38 +12,65 @@ import Event from './event';
export default class SettingUpdatedEvent extends Event {
/**
* The set containing the setting that was updated.
*/
get set() {
return this.args.set;
}
/**
* The ID of the set containing the setting that was updated.
*/
get set_id() {
return this.args.set.id;
return this.set.id;
}
/**
* The category containing the setting that was updated.
*/
get category() {
return this.args.category;
}
/**
* The ID of the category containing the setting that was updated.
*/
get category_id() {
return this.args.category.id;
return this.category.id;
}
/**
* The setting that was updated.
*/
get setting() {
return this.args.setting;
}
/**
* The ID of the setting that was updated.
*/
get setting_id() {
return this.args.setting.id;
return this.setting.id;
}
/**
* The setting's new value.
*/
get value() {
return this.args.value;
}
/**
* The setting's old value.
*/
get old_value() {
return this.args.old_value;
}
/**
* The type of event.
*/
get __eventType() {
return 'setting-updated';
}

View File

@ -0,0 +1,31 @@
/*
* BetterDiscord List
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* Class representing a list.
* @extends Array
*/
export default class List extends Array {
get(...filters) {
return this.find(item => {
for (const filter of filters) {
for (const key in filter) {
if (filter.hasOwnProperty(key)) {
if (item[key] !== filter[key]) return false;
}
}
}
return true;
});
}
}

View File

@ -2,3 +2,4 @@ export { default as SettingsSet } from './settingsset';
export { default as SettingsCategory } from './settingscategory';
export { default as Setting } from './setting';
export { default as SettingsScheme } from './settingsscheme';
export * from './types';

View File

@ -8,28 +8,33 @@
* LICENSE file in the root directory of this source tree.
*/
import { Utils } from 'common';
export default class MultipleChoiceOption {
constructor(args) {
this.args = args.args || args;
Object.freeze(this);
}
/**
* This option's ID.
*/
get id() {
return this.args.id || this.value;
}
/**
* A string describing this option.
*/
get text() {
return this.args.text;
}
/**
* The value to return when this option is active.
*/
get value() {
return this.args.value;
}
clone() {
return new MultipleChoiceOption(Utils.deepclone(this.args));
}
}

View File

@ -8,8 +8,6 @@
* LICENSE file in the root directory of this source tree.
*/
import { Utils } from 'common';
import BoolSetting from './types/bool';
import StringSetting from './types/text';
import NumberSetting from './types/number';
@ -21,6 +19,9 @@ import KeybindSetting from './types/keybind';
import FileSetting from './types/file';
import GuildSetting from './types/guild';
import ArraySetting from './types/array';
import CollectionSetting from './types/collection';
import KvpSetting from './types/kvp';
import SecureKvpSetting from './types/securekvp';
import CustomSetting from './types/custom';
export default class Setting {
@ -28,6 +29,7 @@ export default class Setting {
constructor(args, ...merge) {
args = args.args || args;
if (args.type instanceof Array) args.subtype = args.type[0], args.type = 'collection';
if (args.type === 'color') args.type = 'colour';
if (args.type === 'bool') return new BoolSetting(args, ...merge);
@ -41,8 +43,11 @@ export default class Setting {
else if (args.type === 'file') return new FileSetting(args, ...merge);
else if (args.type === 'guild') return new GuildSetting(args, ...merge);
else if (args.type === 'array') return new ArraySetting(args, ...merge);
else if (args.type === 'collection') return new CollectionSetting(args, ...merge);
else if (args.type === 'kvp') return new KvpSetting(args, ...merge);
else if (args.type === 'securekvp') return new SecureKvpSetting(args, ...merge);
else if (args.type === 'custom') return new CustomSetting(args, ...merge);
else throw {message: `Setting type ${args.type} unknown`};
throw {message: `Setting type ${args.type} unknown`};
}
}

Some files were not shown because too many files have changed in this diff Show More