Compare commits
3264 Commits
Author | SHA1 | Date |
---|---|---|
Alexei Stukov | 835cc3134c | |
Jiiks | 02b30153ee | |
Alexei Stukov | d09dc5a2a5 | |
Mega-Mewthree | 98d9a30027 | |
Alexei Stukov | 5cb4bc15bd | |
Alexei Stukov | bb2aba04d5 | |
Samuel Elliott | 82e9b0bd6a | |
Samuel Elliott | 47575d3449 | |
Samuel Elliott | 32e2582ded | |
Samuel Elliott | d58dda6f50 | |
Samuel Elliott | 648954d533 | |
Samuel Elliott | 4aa38f4582 | |
Samuel Elliott | b3ba1aef13 | |
Samuel Elliott | 2a6cbd39b7 | |
Samuel Elliott | ce5bcb9b85 | |
Samuel Elliott | 5a3821ad3e | |
Samuel Elliott | fd0032b24c | |
Samuel Elliott | 226719b36e | |
Samuel Elliott | fcfee53928 | |
Samuel Elliott | 285ae34b50 | |
Alexei Stukov | a170a97688 | |
Samuel Elliott | a770f57b28 | |
Samuel Elliott | 5757fc20c9 | |
Samuel Elliott | 421289f63b | |
Samuel Elliott | ac85316578 | |
Samuel Elliott | 08af9be061 | |
Samuel Elliott | ead0fbbd1e | |
Samuel Elliott | dc85a808f8 | |
Samuel Elliott | aecfa814f9 | |
Samuel Elliott | 436f3d3c36 | |
Alexei Stukov | 68a8187964 | |
Alexei Stukov | f30e4c12fe | |
Alexei Stukov | 686514ed1d | |
Alexei Stukov | d795da1750 | |
Alexei Stukov | 3219ff7c6e | |
Alexei Stukov | 33567a2cfd | |
Jiiks | 85310bfbff | |
Jiiks | d1d79a37b7 | |
Jiiks | d95592acc9 | |
Jiiks | f6a3fb65da | |
Alexei Stukov | 6788cca363 | |
Lars van der Zande | 1fb442e096 | |
Jiiks | 6e64ff61c5 | |
Jiiks | dd8fe68a11 | |
Jiiks | a3829089f9 | |
Jiiks | 174c1ee791 | |
Jiiks | b8793fd2b6 | |
Jiiks | 31986ca3a0 | |
Jiiks | 10ff740f75 | |
Jiiks | 83fbab63c0 | |
Jiiks | c4670946e6 | |
Jiiks | e5239d952e | |
Jiiks | a57783a9d8 | |
Jiiks | 001a6e4fda | |
Jiiks | dd621038f9 | |
Jiiks | 288c233447 | |
Jiiks | 399c6e792b | |
Jiiks | 0be6facba4 | |
Jiiks | dc7247a12d | |
Jiiks | 377c4fd104 | |
Jiiks | 252d496dc2 | |
Jiiks | 83e334c3f8 | |
Jiiks | e72ad10dfc | |
Jiiks | 9ef392c575 | |
Jiiks | 76057efbb7 | |
Jiiks | dcb121750a | |
Jiiks | 13fa769e9e | |
Jiiks | 3143991239 | |
Jiiks | 1ae0c5aa4d | |
Jiiks | 5ea39f86f7 | |
Jiiks | d6a946e096 | |
Jiiks | b68c1fbd04 | |
Jiiks | 07d3629622 | |
Jiiks | 15daa9acef | |
Jiiks | 817a4a03b6 | |
Jiiks | e63386e9eb | |
Jiiks | 402acdfea9 | |
Jiiks | b440206d07 | |
Jiiks | c7bea4a743 | |
Jiiks | 2528d87b8f | |
Alexei Stukov | 405d74fada | |
Alexei Stukov | b311220132 | |
Alexei Stukov | 66b47457b1 | |
Alexei Stukov | 6e9f9f8bf8 | |
Alexei Stukov | 88a113dc8f | |
Alexei Stukov | a61f860466 | |
Jiiks | 729a4607bd | |
Jiiks | d81dcc9aa2 | |
Jiiks | 1e4f3fa82b | |
Alexei Stukov | 150a1d63c4 | |
Jiiks | 6b481733b9 | |
Jiiks | e07b9b1550 | |
Jiiks | 99c2b53ec6 | |
Jiiks | 2a93e5d2a3 | |
Jiiks | 3661207602 | |
Jiiks | 92845728cc | |
Jiiks | c99753fc8c | |
Jiiks | 688e6022a0 | |
Jiiks | 5408d994be | |
Zack Rauen | 003c9766bc | |
Zack Rauen | 82e9c257ce | |
Jiiks | 6167cc7c4b | |
Jiiks | 1bacecf8d4 | |
Jiiks | d102686379 | |
Jiiks | 1160955629 | |
Jiiks | e60f765a50 | |
Alexei Stukov | d3db696616 | |
Jiiks | b8d16c6e4d | |
Jiiks | b22923d12f | |
Jiiks | 1ea307efdd | |
Jiiks | d98cff878f | |
Jiiks | dd11708f9f | |
Jiiks | 665a7818c9 | |
Jiiks | d75b907ae4 | |
Alexei Stukov | f051bc4812 | |
Jiiks | 9773f78506 | |
Alexei Stukov | f4b7c99c31 | |
Jiiks | 97519b2307 | |
Jiiks | 806ca5028a | |
Jiiks | db89e3a1a0 | |
Jiiks | 6030a78b91 | |
Jiiks | 99ec82795c | |
Jiiks | b1e8b591ba | |
Jiiks | ac9e16632d | |
Jiiks | d8f977b57a | |
Jiiks | c3283c47f6 | |
Jiiks | 2b86f2d741 | |
Jiiks | 9f6389845a | |
Jiiks | 7489fd0aaf | |
Jiiks | 7e1c379fd3 | |
Jiiks | c0765120e8 | |
Jiiks | 7e10de32cb | |
Jiiks | 145d61fe5e | |
Jiiks | 72b278de6e | |
Jiiks | 5e977e8dc4 | |
Jiiks | 109eb31aa5 | |
Jiiks | 1b6f3005f5 | |
Jiiks | d02f894d4a | |
Jiiks | 20561e2938 | |
Jiiks | a3724d739b | |
Jiiks | cf1da34e16 | |
Jiiks | 5454950838 | |
Jiiks | ed7abe3571 | |
Jiiks | b14bf93ef9 | |
Jiiks | 3436bbe52b | |
Jiiks | 1a26e77dd9 | |
Jiiks | da1fc0a2f0 | |
Jiiks | d1fd5ae881 | |
Jiiks | 8c04e7d2d3 | |
Jiiks | 3fc1adc503 | |
Jiiks | 691c9f378a | |
Jiiks | a769385219 | |
Jiiks | f1f23fa220 | |
Jiiks | 99ef0d9f81 | |
Jiiks | 5597c485d1 | |
Jiiks | 7f08ba27e6 | |
Jiiks | 99614aecdc | |
Alexei Stukov | 726db7f0c9 | |
Zack Rauen | 67c39fd9d9 | |
Jiiks | 23d4eb77b2 | |
Jiiks | 124ec078aa | |
Jiiks | e6c9bc447e | |
Jiiks | 7bc4b652fa | |
Alexei Stukov | 6ab1f5c844 | |
Alexei Stukov | 816ff2ed08 | |
Jiiks | 6ca36ebbb5 | |
Jiiks | f511e4556e | |
Alexei Stukov | 36515432c7 | |
Alexei Stukov | 47782c0168 | |
Jiiks | 3a370f2827 | |
Jiiks | 90f5596cb2 | |
Jiiks | 75439a600e | |
Alexei Stukov | f46960fdd0 | |
Alexei Stukov | f684780842 | |
Jiiks | 6c8631f1cd | |
Jiiks | 0311602539 | |
Jiiks | b5e8098d20 | |
Jiiks | 111be57b59 | |
Jiiks | 76e19c8469 | |
Jiiks | ccff016820 | |
Jiiks | 021e282b1c | |
Jiiks | 6699fcc1e7 | |
Jiiks | f3ea192974 | |
Jiiks | 9c1a93f4c1 | |
Jiiks | 2c23f18e89 | |
Jiiks | c88d2cdae9 | |
Alexei Stukov | 8235e55357 | |
Alexei Stukov | 754e6bbd72 | |
Alexei Stukov | e27d8874ef | |
Alexei Stukov | c310cc52d5 | |
Jiiks | 073cce825d | |
Alexei Stukov | a04fb59db7 | |
Alexei Stukov | f8ed871f8e | |
Alexei Stukov | fae97613d1 | |
Jiiks | 27df24f369 | |
Jiiks | 5bb95f59c0 | |
Alexei Stukov | bcabaab14c | |
Alexei Stukov | 033f3472cb | |
Alexei Stukov | 8fbd75ce94 | |
Alexei Stukov | 3a38b59a4d | |
Alexei Stukov | c29b659afb | |
Alexei Stukov | ece161fac0 | |
Lilian Tedone | b57ab92269 | |
Alexei Stukov | 5a3aa553cd | |
Jiiks | a2f48dd007 | |
Jiiks | 4f10f38581 | |
Jiiks | ea0dc0c6f1 | |
Jiiks | 93f536bf21 | |
Jiiks | 497ab62ee6 | |
Jiiks | 784a14f853 | |
Jiiks | 95447dd3f0 | |
Jiiks | 2cff8edb59 | |
Jiiks | e4c0796e5e | |
Jiiks | e3010fcd43 | |
Jiiks | 08b66d5e5a | |
Jiiks | c618956639 | |
Jiiks | c6ddb75630 | |
Jiiks | 1fd7b97ce6 | |
Jiiks | fa5be193ad | |
Jiiks | 63874c8b4e | |
Jiiks | b4dbfcd808 | |
Jiiks | 7a9b72a33a | |
Jiiks | 6db34b9cee | |
Alexei Stukov | ead51ae676 | |
Jiiks | 9a77aae56b | |
Jiiks | 3182f28359 | |
Jiiks | a274b0c839 | |
Jiiks | 199b9249e4 | |
Jiiks | 08ba649a91 | |
Jiiks | a0eb43188f | |
Jiiks | b1bd5190c1 | |
Jiiks | 7fb5c8a378 | |
Jiiks | 26b1fad5a4 | |
Jiiks | f028aefab8 | |
Jiiks | f0d66300c4 | |
Jiiks | 12b291cf0b | |
Jiiks | a8f686eb61 | |
Jiiks | 98bf4d21ac | |
Jiiks | d5a82ab2b3 | |
Jiiks | d5d7709c9f | |
Jiiks | cfc197cdc0 | |
Jiiks | e8cfb840fe | |
Jiiks | f01f4926eb | |
Jiiks | 83a4e0a114 | |
Jiiks | 4b8a905d2a | |
Alexei Stukov | 08ae8dceb5 | |
Jiiks | ad28e7c638 | |
Jiiks | 9ba8047066 | |
Alexei Stukov | 9ba9ec1556 | |
Jiiks | ce2658b6e2 | |
Jiiks | f3cc033182 | |
Jiiks | 7b44d163ca | |
Alexei Stukov | 72d24a1cb0 | |
Alexei Stukov | c2c520fe27 | |
Samuel Elliott | ad55459649 | |
Samuel Elliott | dfac3f1825 | |
Samuel Elliott | adecbf8945 | |
Samuel Elliott | a371fabc70 | |
Samuel Elliott | c04ffdfc24 | |
Samuel Elliott | 572721c57f | |
Samuel Elliott | 4af09224d7 | |
Samuel Elliott | 7f1ac9bea0 | |
Samuel Elliott | ce93b2b4c6 | |
Samuel Elliott | d98ed6649e | |
Zack Rauen | 468422084a | |
Alexei Stukov | 395167aafa | |
Alexei Stukov | 803cf59f11 | |
Alexei Stukov | 133511aeec | |
Zack Rauen | be024c1bbe | |
Zack Rauen | 6f10c71623 | |
Zack Rauen | adb2430d71 | |
Alexei Stukov | 990453cdad | |
Jiiks | 7cf69f6993 | |
Jiiks | 5822110c87 | |
Jiiks | 669a88c1ff | |
Jiiks | 6a8c77b578 | |
Jiiks | a93d12df03 | |
Jiiks | 80096c5e71 | |
Jiiks | 307b084214 | |
Jiiks | 1e0f3b69fb | |
Jiiks | 550d0d3c40 | |
Jiiks | 96f8fe680f | |
Jiiks | b48adc7a62 | |
Jiiks | 6db016060b | |
Jiiks | ecb0b3e9d0 | |
Jiiks | 8c060314da | |
Jiiks | a040a605d4 | |
Jiiks | 09fb76d148 | |
Jiiks | ddda5b3cc1 | |
Jiiks | cff425f916 | |
Jiiks | 76d54f0a95 | |
Jiiks | 2093c472b5 | |
Jiiks | 1009c62ea0 | |
Jiiks | 2cd94fcd5f | |
Jiiks | cb35b14311 | |
Jiiks | 6b3cb712c7 | |
Jiiks | cfbafffe46 | |
Jiiks | 2913a85368 | |
Jiiks | 5c160d75a6 | |
Jiiks | f14b1b71e7 | |
Jiiks | 4e3a56e466 | |
Alexei Stukov | 30716b57e6 | |
Jiiks | b18559aa5e | |
Jiiks | 6d8ef35bd6 | |
Jiiks | 8146e0e7f2 | |
Jiiks | 23b8e2dd76 | |
Jiiks | e12dc28052 | |
Jiiks | 6538442b0b | |
Jiiks | 4a9cc603d2 | |
Jiiks | d4aa8fb377 | |
Jiiks | 977fd80d0c | |
Alexei Stukov | ad64743874 | |
Jiiks | 436c469b1d | |
Alexei Stukov | 7d05d0c884 | |
Jiiks | 1afbcacec3 | |
Jiiks | 9b5464b9a5 | |
Jiiks | da8e912a95 | |
Jiiks | a4cbfd9235 | |
Jiiks | 5e0c5e39f6 | |
Jiiks | e798e3d6e2 | |
Jiiks | 937ab55456 | |
Jiiks | aa0882c449 | |
Jiiks | f5b70d5a23 | |
Jiiks | 6d818f40d2 | |
Jiiks | 6a5f185c80 | |
Jiiks | 62b7679408 | |
Jiiks | c11aa205a3 | |
Jiiks | 47af7a5da6 | |
Jiiks | 89b645b334 | |
Alexei Stukov | 62c9fe6011 | |
Alexei Stukov | a45c39ed91 | |
Alexei Stukov | f35d206044 | |
Jiiks | f46694f2d0 | |
Jiiks | 5c493d413b | |
Jiiks | 32fc655326 | |
Jiiks | 9cf63c2cfa | |
Jiiks | b59c919733 | |
Jiiks | e66af1e1d2 | |
Jiiks | f1cf802746 | |
Jiiks | d1387f5183 | |
Jiiks | 9190b208eb | |
Jiiks | fdd934ae0c | |
Jiiks | b1c8070fc3 | |
Jiiks | 56753d3614 | |
Alexei Stukov | 66966e6729 | |
Aphypnise | 0f7639da6d | |
Samuel Elliott | 2a81054f64 | |
Alexei Stukov | 22ea3ff2ad | |
Jiiks | d3e76a1c2d | |
Jiiks | 72326d220e | |
Samuel Elliott | 52dfaf18c1 | |
Samuel Elliott | 40acb33bbd | |
Samuel Elliott | 74d5d3540f | |
Alexei Stukov | 3ba90705ff | |
Samuel Elliott | 6c14f80791 | |
Samuel Elliott | 5fa964ed50 | |
Alexei Stukov | da28828880 | |
Samuel Elliott | 21c8c6d267 | |
Samuel Elliott | 5868b4f0f9 | |
Samuel Elliott | d53afe2538 | |
Samuel Elliott | 96b7e8d859 | |
Samuel Elliott | 60e42cea07 | |
Samuel Elliott | 43ec860bfe | |
Samuel Elliott | 1f971c7490 | |
Samuel Elliott | 5e7de49999 | |
Samuel Elliott | 35c1ab93a0 | |
Samuel Elliott | a85107cc51 | |
Samuel Elliott | 46dae11085 | |
Samuel Elliott | c885e4ceda | |
Samuel Elliott | 26bce739d7 | |
Samuel Elliott | 3747417865 | |
Samuel Elliott | 4c0248ca00 | |
Samuel Elliott | fd9c03ac2f | |
Samuel Elliott | d11e8d4fe2 | |
Samuel Elliott | 56a807f294 | |
Samuel Elliott | cf71f89077 | |
Samuel Elliott | 3ff077ea04 | |
Samuel Elliott | 697df8a7fe | |
Samuel Elliott | c2ccfd02da | |
Alexei Stukov | 1e9b1618f1 | |
Jiiks | d92da4b6e4 | |
Samuel Elliott | 67debca9d9 | |
Samuel Elliott | a0bee1846e | |
Samuel Elliott | 73192f5762 | |
Alexei Stukov | a1e32f8b89 | |
Jiiks | 2002db7807 | |
Jiiks | 07374ceeee | |
Samuel Elliott | d02be1521d | |
Jiiks | 5c92f3e3e0 | |
Jiiks | 41a5a3a791 | |
Samuel Elliott | 42a89129d4 | |
Jiiks | 2617925cd8 | |
Jiiks | 7a0b6e9262 | |
Jiiks | 3f5c168ffc | |
Jiiks | 09d9f50160 | |
Jiiks | d274fda428 | |
Jiiks | 58a2b6714e | |
Jiiks | 4c3659f857 | |
Jiiks | b8dc9c0d25 | |
Jiiks | c4f28a6ac5 | |
Jiiks | 1ab52cb174 | |
Jiiks | f315c8b134 | |
Jiiks | 8bfac0f6ff | |
Samuel Elliott | 4bed9f726c | |
Jiiks | 0145492773 | |
Samuel Elliott | 2188c22425 | |
Jiiks | e9c419529d | |
Samuel Elliott | 6ee647b249 | |
Samuel Elliott | 5676435473 | |
Samuel Elliott | 8cf28fcaba | |
Samuel Elliott | 237899c91a | |
Samuel Elliott | 73e4e83ced | |
Alexei Stukov | 5b63667bc2 | |
Alexei Stukov | 00ce5d70c9 | |
Alexei Stukov | b3d14d853c | |
Alexei Stukov | c1af0c6115 | |
Alexei Stukov | f3bb7aac96 | |
Jiiks | cd84f6329a | |
Jiiks | b4fa9c2934 | |
Jiiks | 9db6ddaf98 | |
Jiiks | ed55e060aa | |
Jiiks | bb37b89d35 | |
Jiiks | 07fc98670c | |
Jiiks | 25aecf82a6 | |
Jiiks | 242fbfaaa7 | |
Jiiks | ef5a1c223c | |
Jiiks | 8637f176c1 | |
Jiiks | 4c5300be12 | |
Zack Rauen | 1ccd7766bc | |
Maks-s | 9711c99217 | |
Maks | 10f45b6b4c | |
Maks | 7c44b92bce | |
Zack Rauen | 4784b13c86 | |
Zack Rauen | a8c8c017f6 | |
Zack Rauen | c98e4430b1 | |
Zack Rauen | adf94990c2 | |
Maks-s | e78021a538 | |
Alexei Stukov | 3c38ecdd97 | |
Jiiks | 12021fcf15 | |
Jiiks | 1031ab3a18 | |
Jiiks | e75c575a32 | |
Jiiks | 9c91222fff | |
Jiiks | d0eb2a441a | |
Jiiks | 5c6d544562 | |
Jiiks | ceca53a75d | |
Jiiks | 6eaed063ec | |
Jiiks | bc5eb1b5f0 | |
Jiiks | 1a11eb6a6b | |
Jiiks | cff67a3b1d | |
Jiiks | 3518ac7cb2 | |
Jiiks | f2d7ef41da | |
Jiiks | 87e0f132d1 | |
Alexei Stukov | 9306018111 | |
Zack Rauen | 19d33a44ea | |
Alexei Stukov | e755b60807 | |
Jiiks | f50bcd0785 | |
Jiiks | fc6481acae | |
Jiiks | de0651bb47 | |
Alexei Stukov | 358b1e8bf3 | |
Jiiks | 84b94c614e | |
Jiiks | 3fa848bb5a | |
Jiiks | 533566fde8 | |
Jiiks | 6ab1adfe5b | |
Jiiks | 205dcf6ff1 | |
Jiiks | beaee8764c | |
Alexei Stukov | d22188ea2a | |
Jiiks | 53ef0be7b8 | |
Jiiks | e3edb09f38 | |
Jiiks | 316f3c1bb0 | |
Jiiks | 6948335ea7 | |
Jiiks | 25decd0514 | |
Jiiks | a35c75717e | |
Jiiks | 8d6d3c9e18 | |
Jiiks | 91df5350ad | |
Jiiks | 22740fb16c | |
Jiiks | 0316bbc1cd | |
Jiiks | 8bac53e9be | |
Alexei Stukov | 42e2569569 | |
Alexei Stukov | dd81201a95 | |
Jiiks | 805e1ce771 | |
Zack Rauen | 322badd209 | |
Alexei Stukov | 8438d32ce9 | |
Jiiks | 3446ed2d6b | |
Jiiks | 6a51834f82 | |
Jiiks | 89461244bd | |
Jiiks | b1847b8c41 | |
Jiiks | 11eed2c89d | |
Jiiks | b971b716bf | |
Jiiks | 9f8a1f08be | |
Jiiks | c1ac349211 | |
Jiiks | 1804fcfbe1 | |
Jiiks | 303addae42 | |
Jiiks | 9e80f7b051 | |
Jiiks | 0e32d835ef | |
Alexei Stukov | 462b198895 | |
Jiiks | 34e2833555 | |
Alexei Stukov | 8b180ea423 | |
Jiiks | a90400c280 | |
Jiiks | e49a1932f3 | |
Jiiks | abf83821d9 | |
Jiiks | 6ed50c4206 | |
Jiiks | edab8dd93b | |
Jiiks | ab03633431 | |
Jiiks | 67b554c68f | |
Jiiks | 68f3a280a5 | |
Jiiks | 5db4f8cf26 | |
Jiiks | 3e57d0092a | |
Jiiks | 36408d74ad | |
Jiiks | 9aa9305755 | |
Jiiks | 6f9cdb9dda | |
Jiiks | 0aa370aeb9 | |
Jiiks | 27fee8e10c | |
Jiiks | ac1e4ad9a2 | |
Jiiks | 73f2fe4fd0 | |
Jiiks | 8b4318c06e | |
Jiiks | 7636ea480d | |
Jiiks | 54e5f2149b | |
Jiiks | b1f0a4247a | |
Jiiks | 8d6cdafc23 | |
Jiiks | 5711576a0d | |
Jiiks | 6df9b1e019 | |
Jiiks | c2e7abcfca | |
Jiiks | b6c58f16e7 | |
Jiiks | b47a3c65aa | |
Alexei Stukov | 0315d610e0 | |
Zack Rauen | 3714e71297 | |
Mega-Mewthree | 9f4b0aec6b | |
Mega-Mewthree | 0ea5bc2dcc | |
Mega-Mewthree | 64608c7e62 | |
Mega-Mewthree | 2d8c0a5306 | |
Mega-Mewthree | 81c451d31f | |
Jiiks | d04aa313fc | |
Jiiks | 45ab822fba | |
Alexei Stukov | c893216874 | |
Alexei Stukov | a1a869181b | |
Alexei Stukov | f5cbd8d491 | |
Alexei Stukov | 17917f3257 | |
Jiiks | 19be33b17d | |
Jiiks | cfcca0d782 | |
Jiiks | 5592a8d376 | |
Jiiks | 0c0ebb2ebb | |
Jiiks | d6d78d1e99 | |
Jiiks | b6da7019d0 | |
Jiiks | a123e20b05 | |
Jiiks | 856334cf30 | |
Jiiks | 3f5b2afbd5 | |
Jiiks | ad35c89d7d | |
Jiiks | 71a0ce8fb0 | |
Jiiks | d53a3b4d64 | |
Jiiks | bd0c66466f | |
Jiiks | 0a751919aa | |
Jiiks | cae68947df | |
Jiiks | 2b833b5141 | |
Jiiks | b24c0ba2f3 | |
Jiiks | 3b8126781d | |
Jiiks | a1a63f2c35 | |
Jiiks | 334c9f852a | |
Jiiks | 17128a889b | |
Jiiks | d2b99f200b | |
Jiiks | 5f4a77f2d8 | |
Jiiks | 3614997fa9 | |
Jiiks | 5f76d304f6 | |
Jiiks | e8688c0ce4 | |
Jiiks | 641a12d640 | |
Jiiks | 169741d5a1 | |
Alexei Stukov | 4aec2a7ade | |
Mega-Mewthree | ee9cba328f | |
Mega-Mewthree | c421be9e91 | |
Mega-Mewthree | 7a1768ca87 | |
Mega-Mewthree | b9e9ab89f7 | |
Mega-Mewthree | b9f3c3f4e2 | |
Mega-Mewthree | dc842b479b | |
Mega-Mewthree | 02e6fb88aa | |
Mega-Mewthree | c10310af3a | |
Mega-Mewthree | 7e41fe73e7 | |
Mega-Mewthree | c122457565 | |
Mega-Mewthree | 0ea0acfaa5 | |
Jiiks | 866ad8b13b | |
Mega-Mewthree | c5d4e199fc | |
Mega-Mewthree | 1a8946b151 | |
Zack Rauen | 9c0e19e198 | |
Alexei Stukov | 816f809ca7 | |
Samuel Elliott | 32a995f678 | |
Samuel Elliott | dac3187866 | |
Jiiks | 60cdec3ae4 | |
Jiiks | ed4adf21f4 | |
Jiiks | a8f0e239cf | |
Jiiks | be1a183e62 | |
Jiiks | d88cedb614 | |
Jiiks | 9880152290 | |
Jiiks | 0a1bd46ca9 | |
Jiiks | 83ef17d302 | |
Mega-Mewthree | cb6473920f | |
Mega-Mewthree | d8c769433a | |
Mega-Mewthree | fd7a0f4051 | |
Mega-Mewthree | 7899312e73 | |
Samuel Elliott | 784d8223e8 | |
Samuel Elliott | 344a9e6fe5 | |
Samuel Elliott | 22e78c03e1 | |
Jiiks | b67ca5c42d | |
Jiiks | bdec8b842e | |
Jiiks | 3bc7c28a97 | |
Samuel Elliott | a4992e905c | |
Samuel Elliott | c8381eb808 | |
Samuel Elliott | 0aabc72652 | |
Jiiks | 3d4204f080 | |
Samuel Elliott | dc3fed3408 | |
Samuel Elliott | a12a3c74f4 | |
Jiiks | 3c83dbe0ff | |
Samuel Elliott | 4970214324 | |
Jiiks | a5ae5ad8e9 | |
Jiiks | 54808f32ea | |
Jiiks | 4e4c532813 | |
Jiiks | aac4dba641 | |
Jiiks | 17f1b60a9b | |
Jiiks | 2a10191926 | |
Alexei Stukov | 87ec7d8773 | |
Jiiks | e518d4c117 | |
Jiiks | c4dfc7b11d | |
Jiiks | 0c04c04590 | |
Alexei Stukov | 11b2b16233 | |
Alexei Stukov | 4c6805510d | |
Samuel Elliott | cb01d32e19 | |
Samuel Elliott | 89a5bfba20 | |
Alexei Stukov | 87a2db1745 | |
Samuel Elliott | 387227cc1f | |
Zack Rauen | fdb3b308e1 | |
Alexei Stukov | b33aff51c1 | |
Alexei Stukov | be09fbb5ac | |
Jiiks | 2b4a804934 | |
Jiiks | fcfba41805 | |
Jiiks | d14a06528c | |
Alexei Stukov | c336c8cb0d | |
Samuel Elliott | 755d870a10 | |
Jiiks | 4ed301f531 | |
Jiiks | 263fcbe001 | |
Samuel Elliott | 883f760292 | |
Zack Rauen | 5af8a2168d | |
Jiiks | 50badaef2b | |
Alexei Stukov | 4c20115705 | |
Jiiks | e21583eafd | |
Jiiks | 983763c48d | |
Jiiks | 75ceee5449 | |
Jiiks | 572c57d3a7 | |
Jiiks | 4f98eaf85b | |
Jiiks | 44a1c87484 | |
Jiiks | b18dd7c01a | |
Jiiks | 356e8f0934 | |
Alexei Stukov | 1288e0361a | |
Alexei Stukov | 8f95ba290a | |
Zack Rauen | 5a603a084f | |
Zack Rauen | f144934cd7 | |
Zack Rauen | 9473e419b4 | |
Zack Rauen | a081a8fdf9 | |
Zack Rauen | e660ce6a31 | |
Zack Rauen | 5c7b10299b | |
Jiiks | 42957dbb16 | |
Alexei Stukov | c3df892f1c | |
Jiiks | 26f386ad31 | |
Jiiks | b861280548 | |
Jiiks | 9d22293927 | |
Jiiks | c3d8f4090e | |
Jiiks | 3f00dba937 | |
Alexei Stukov | 9f00aed4fa | |
Jiiks | 685d0f4bac | |
Alexei Stukov | a805c8f75a | |
Alexei Stukov | ce23a39f83 | |
Samuel Elliott | c0a971a27b | |
Jiiks | 7f567f6e9d | |
Jiiks | 865fcee12d | |
Jiiks | 6b3fc39a1d | |
Jiiks | 7b1f36cb9c | |
Jiiks | f6f308baa2 | |
Jiiks | e170ed608e | |
Jiiks | a432d78953 | |
Jiiks | b369dc31f2 | |
Jiiks | 778eb0474f | |
Jiiks | 8c1f196675 | |
Jiiks | e25ac66abf | |
Alexei Stukov | a2a334bdac | |
Jiiks | 8331b30d73 | |
Jiiks | 7083577db2 | |
Jiiks | 1b91c97c24 | |
Alexei Stukov | cca6cdf95d | |
Jiiks | 7c02a51a46 | |
Samuel Elliott | 0e34fdaa8d | |
Samuel Elliott | e2f05d9a64 | |
Samuel Elliott | 13437c56d4 | |
Samuel Elliott | 4b4b3c341f | |
Samuel Elliott | c2f8f5cfab | |
Samuel Elliott | d4c883a5d7 | |
Zack | 520366c4ac | |
Samuel Elliott | 342b4cddc4 | |
Samuel Elliott | 6ea85341b9 | |
Samuel Elliott | 9af8a71e17 | |
Samuel Elliott | 33488d716d | |
Samuel Elliott | a5ff8bbdb6 | |
Samuel Elliott | 16bb32b86c | |
Samuel Elliott | 8826a7b984 | |
Samuel Elliott | 5b419cb8ab | |
Samuel Elliott | db21f4eb13 | |
Samuel Elliott | 3a7ac06ce3 | |
Samuel Elliott | 0c8279311e | |
Samuel Elliott | 331b2b396a | |
Samuel Elliott | a20473d718 | |
Samuel Elliott | 1a12430322 | |
Samuel Elliott | 7a416c0ff7 | |
Samuel Elliott | 81e392c0f6 | |
Samuel Elliott | 71c2d3aaad | |
Samuel Elliott | 19bbd4215c | |
Samuel Elliott | 5301498978 | |
Samuel Elliott | 1d887967d9 | |
Samuel Elliott | a95b97b3c0 | |
Samuel Elliott | 007637a557 | |
Samuel Elliott | 6ded1c62e5 | |
Samuel Elliott | 27f1eec967 | |
Samuel Elliott | 821f46481e | |
Samuel Elliott | 6b97505f66 | |
Samuel Elliott | df471161a7 | |
Samuel Elliott | da627f71e5 | |
Samuel Elliott | 78079588ea | |
Samuel Elliott | b4ebd6c08a | |
Samuel Elliott | e6f2f518e6 | |
Samuel Elliott | aff89371e6 | |
Samuel Elliott | 98a7a71b87 | |
Samuel Elliott | 98a0717bba | |
Samuel Elliott | 7c8c8a754f | |
Samuel Elliott | ab13e716ad | |
Samuel Elliott | f2c31d866d | |
Samuel Elliott | e169206821 | |
Samuel Elliott | cd77d659a5 | |
Samuel Elliott | b6f38a73dc | |
Samuel Elliott | c384920275 | |
Samuel Elliott | 097fc9b3e9 | |
Samuel Elliott | b8fbfdee55 | |
Samuel Elliott | 8c0b73625d | |
Samuel Elliott | 55cd75b5f4 | |
Samuel Elliott | f8f519e882 | |
Samuel Elliott | 799dca7e1b | |
Samuel Elliott | eae1bdf3a6 | |
Samuel Elliott | ea27259a67 | |
Samuel Elliott | ebe195f078 | |
Samuel Elliott | e2b68788bd | |
Samuel Elliott | c8ca4fcfce | |
Samuel Elliott | 2c38433a2b | |
Samuel Elliott | ee0b13dab2 | |
Samuel Elliott | 4d2347cf84 | |
Samuel Elliott | 7cf40a9ee4 | |
Samuel Elliott | 70f678119a | |
Samuel Elliott | 0a2b967e36 | |
Samuel Elliott | a2367299b7 | |
Samuel Elliott | bbcc36647d | |
Samuel Elliott | 906140686f | |
Samuel Elliott | cecaf314f9 | |
Samuel Elliott | 4a3bb89259 | |
Samuel Elliott | d7db836e30 | |
Samuel Elliott | 1eca4bcec7 | |
Samuel Elliott | c02052a982 | |
Samuel Elliott | be841f0426 | |
Zack Rauen | 6c4743d7ed | |
Zack Rauen | 726d6cf4b0 | |
Samuel Elliott | 35cedbdbb3 | |
Samuel Elliott | 17597e2c30 | |
Zack Rauen | 1bb705014f | |
Samuel Elliott | 204dd76e7b | |
Samuel Elliott | 3e41a11197 | |
Samuel Elliott | 68d4617e46 | |
Samuel Elliott | 4654025423 | |
Samuel Elliott | 231afa8ed6 | |
Samuel Elliott | c3ff71ff85 | |
Samuel Elliott | 0e99b219c1 | |
Samuel Elliott | 13508b449d | |
Samuel Elliott | e57e1f51ea | |
Samuel Elliott | c345ab1419 | |
Samuel Elliott | bee869340d | |
Samuel Elliott | 1de4680ded | |
Samuel Elliott | bba1165c77 | |
Samuel Elliott | a5c7aaab3b | |
Samuel Elliott | 93cd1fd07d | |
Samuel Elliott | cfaf581ae1 | |
Samuel Elliott | 665c1d5fe4 | |
Samuel Elliott | 8d8c1a8080 | |
Samuel Elliott | 8827bdb8b0 | |
Samuel Elliott | cd81f925cb | |
Samuel Elliott | d7f4c651b6 | |
Samuel Elliott | f2159f1031 | |
Samuel Elliott | 581c94f6b3 | |
Samuel Elliott | 9eb8eaa906 | |
Samuel Elliott | 9915ef8b19 | |
Zack Rauen | 2d784b48f4 | |
Samuel Elliott | cee2a6ec34 | |
Samuel Elliott | 64e855ab2d | |
Samuel Elliott | 5aa921ae4b | |
Samuel Elliott | 40a72458e2 | |
Samuel Elliott | dc2be5c410 | |
Samuel Elliott | 640ec66abc | |
Samuel Elliott | 3aedcfdd06 | |
Samuel Elliott | 71fe549ee6 | |
Samuel Elliott | cbe1de98a5 | |
Samuel Elliott | 5a58c1c6ce | |
Samuel Elliott | 591d44dee4 | |
Samuel Elliott | 6da58ef6bb | |
Samuel Elliott | 85d5de9791 | |
Samuel Elliott | 5ce78f4b32 | |
Samuel Elliott | 2499f8c0ab | |
Samuel Elliott | 3f2eb1047e | |
Samuel Elliott | ac79bbb99b | |
Samuel Elliott | 1c4fb0b5ea | |
Samuel Elliott | 2e2d0dd6f6 | |
Samuel Elliott | a6c91d5e27 | |
Samuel Elliott | 949206738b | |
Samuel Elliott | 4dc715a39a | |
Samuel Elliott | 36d7554b46 | |
Samuel Elliott | d42e8f66ca | |
Samuel Elliott | 244b7e5bd1 | |
Samuel Elliott | ca7a7c1f91 | |
Samuel Elliott | 504221ce28 | |
Samuel Elliott | 1e643b8164 | |
Samuel Elliott | 0944c0708f | |
Samuel Elliott | 17575fc6a1 | |
Samuel Elliott | 1354b884b4 | |
Samuel Elliott | 1ace26b67f | |
Samuel Elliott | 117336a6b4 | |
Samuel Elliott | d1cce95abe | |
Samuel Elliott | 22fbb7b5bd | |
Samuel Elliott | 34ebab7c21 | |
Samuel Elliott | 5e8579e4dc | |
Samuel Elliott | 4721f66dab | |
Samuel Elliott | 02b313f2e6 | |
Samuel Elliott | b0ae424adb | |
Samuel Elliott | 5a6fb990c7 | |
Samuel Elliott | c294df6b26 | |
Samuel Elliott | c3e4563aeb | |
Samuel Elliott | 525fd3145d | |
Samuel Elliott | 32452d918a | |
Samuel Elliott | e524c50b65 | |
Samuel Elliott | 96952c3fef | |
Samuel Elliott | ad710de008 | |
Samuel Elliott | 21ff9d200c | |
Samuel Elliott | a1c6f5d5fd | |
Samuel Elliott | 6d64d17b7d | |
Samuel Elliott | 695d088f60 | |
Samuel Elliott | bcb888100c | |
Samuel Elliott | 146e108874 | |
Samuel Elliott | 4837a01812 | |
Samuel Elliott | 48f4837a24 | |
Samuel Elliott | 8b320d2f09 | |
Samuel Elliott | ac22b14358 | |
Samuel Elliott | 4d5a403d6d | |
Samuel Elliott | 9dc424cc75 | |
Samuel Elliott | 1b75753a3d | |
Samuel Elliott | a7b300d233 | |
Samuel Elliott | 68e42b8b36 | |
Samuel Elliott | e061d95ae6 | |
Alexei Stukov | 6b517adbe8 | |
Samuel Elliott | 33c0732c08 | |
Samuel Elliott | f1e0350433 | |
Samuel Elliott | d5f486d1ad | |
Samuel Elliott | 7fb8337095 | |
Samuel Elliott | 7adf63dfa8 | |
Samuel Elliott | d2f7e6142f | |
Samuel Elliott | b62727b046 | |
Samuel Elliott | f0a337e0ed | |
Samuel Elliott | 5c755bc121 | |
Samuel Elliott | 4d0631ba38 | |
Samuel Elliott | d4962bb2ab | |
Samuel Elliott | cf319a2604 | |
Samuel Elliott | 256db71aa4 | |
Samuel Elliott | de8601ded4 | |
Samuel Elliott | a96f3b1eb7 | |
Samuel Elliott | a7aa1fa5ec | |
Alexei Stukov | bbd12a0381 | |
Samuel Elliott | 027ca8d639 | |
Samuel Elliott | a0f60dddb2 | |
Samuel Elliott | a3038c46fd | |
Samuel Elliott | d98f152009 | |
Samuel Elliott | 68beee12d3 | |
Samuel Elliott | 4b59b9f65d | |
Samuel Elliott | 85d390e18d | |
Samuel Elliott | 3f89286946 | |
Samuel Elliott | 554b8d195b | |
Samuel Elliott | 7f5fa44fd3 | |
Samuel Elliott | c95d60ab0f | |
Samuel Elliott | 60a82a3ad9 | |
Samuel Elliott | 2f9af2e2d0 | |
Samuel Elliott | 858fdcec59 | |
Samuel Elliott | 3eb1782a64 | |
Samuel Elliott | 0e14d167dc | |
Samuel Elliott | c701a2b5f9 | |
Samuel Elliott | b3442ee108 | |
Samuel Elliott | 1bde3b4ec9 | |
Samuel Elliott | e83c6f92cb | |
Samuel Elliott | 06cca44376 | |
Samuel Elliott | 3f3898c774 | |
Samuel Elliott | f8a380fd59 | |
Samuel Elliott | f28525129e | |
Samuel Elliott | d2cbbd309b | |
Samuel Elliott | 74d78f4bca | |
Samuel Elliott | 68af931128 | |
Samuel Elliott | 5331afb763 | |
Samuel Elliott | 069b1ff689 | |
Samuel Elliott | 009d6be057 | |
Samuel Elliott | da4b592e54 | |
Samuel Elliott | 81f3519408 | |
Samuel Elliott | 8c09112cff | |
Samuel Elliott | 8983256ade | |
Samuel Elliott | d323292162 | |
Samuel Elliott | 1714a0225e | |
Samuel Elliott | fd3b0a92ce | |
Samuel Elliott | 9b1dd771ad | |
Samuel Elliott | 441e80e0e8 | |
Samuel Elliott | 4634266e14 | |
Samuel Elliott | 10fd25607d | |
Samuel Elliott | 946f68e4ef | |
Alexei Stukov | a4ceb8bd2c | |
Samuel Elliott | 545b72cc3e | |
Samuel Elliott | 9e229a1c0f | |
Samuel Elliott | ddfd0653ac | |
Samuel Elliott | 6bb8c18857 | |
Samuel Elliott | 66eee86eb3 | |
Samuel Elliott | eaeae7ad98 | |
Samuel Elliott | a3eeee9b57 | |
Samuel Elliott | 6d46ac67de | |
Samuel Elliott | db6be20864 | |
Samuel Elliott | 5e8e9fa19f | |
Samuel Elliott | ebff286c31 | |
Samuel Elliott | 0f805d57e3 | |
Samuel Elliott | 33adb92902 | |
Samuel Elliott | a85422f29e | |
Samuel Elliott | aa933d9a09 | |
Samuel Elliott | 8a9c8edf39 | |
Samuel Elliott | 74e3605ec6 | |
Samuel Elliott | dc44af6968 | |
Samuel Elliott | a4a130bfc6 | |
Samuel Elliott | 2fb5d8fe11 | |
Samuel Elliott | 1772edd37c | |
Samuel Elliott | b4bd9e9c7b | |
Samuel Elliott | 994faf94d6 | |
Alexei Stukov | 048abaeeed | |
Jiiks | f3fa3c2ae2 | |
Jiiks | ee87cbc25c | |
Jiiks | 4ce32c415e | |
Jiiks | c60411c901 | |
Jiiks | 9f24d0d1ce | |
Jiiks | 684f85b45e | |
Jiiks | 83d9419131 | |
Alexei Stukov | 1fe233bc91 | |
Jiiks | 70ae8365fe | |
Jiiks | 038d15ecf3 | |
Alexei Stukov | 86fc492af4 | |
Jiiks | eed634217e | |
Alexei Stukov | 122036fd14 | |
Jiiks | a150c3cd4a | |
Jiiks | 01b7e81f44 | |
Alexei Stukov | e1456f530c | |
Jiiks | 5d86b6cf50 | |
Jiiks | 76a72c8341 | |
Alexei Stukov | 7b52b22c5f | |
Jiiks | c84a79d3f5 | |
Alexei Stukov | 0dd49b7c9b | |
Jiiks | f8b9cdaead | |
Jiiks | b83e3c967d | |
Jiiks | 5ed34c149a | |
Alexei Stukov | cf2b3485ad | |
Jiiks | 8740c286a0 | |
Alexei Stukov | ffea5bd3ae | |
Jiiks | 0820735fa2 | |
Jiiks | 549275e5dc | |
Jiiks | e268f769b7 | |
Jiiks | 0af356823a | |
Jiiks | b5fc88bc8e | |
Jiiks | 86528a3335 | |
Alexei Stukov | f6d994e6a4 | |
Samuel Elliott | 533d4aead1 | |
Samuel Elliott | 094ef7e173 | |
Samuel Elliott | 086b6253bc | |
Alexei Stukov | d3a5be365e | |
Jiiks | 87cc894512 | |
Alexei Stukov | c99eedd5c8 | |
Jiiks | 7643588b00 | |
Jiiks | ce457e4ad4 | |
Alexei Stukov | ae63213809 | |
Jiiks | c44a35a6c4 | |
Jiiks | cc8f10f971 | |
Jiiks | 13f7ce0cad | |
Jiiks | e1f3bd2f41 | |
Jiiks | 6bc2e0cca5 | |
Jiiks | a4e51dd4a7 | |
Jiiks | 0eaf63f5f7 | |
Jiiks | bd5ded7333 | |
Jiiks | 6d1dbfad1e | |
Alexei Stukov | 96059d156c | |
Jiiks | 30c57cf998 | |
Alexei Stukov | d1dc8140e4 | |
Jiiks | bf7919a877 | |
Jiiks | 25044beaef | |
Jiiks | 81941bb140 | |
Jiiks | d6794e6de8 | |
Jiiks | b3ed044408 | |
Jiiks | 1e438764a0 | |
Jiiks | 9e875f4039 | |
Jiiks | 5f7ddcb8ce | |
Jiiks | e73ac58ff3 | |
Jiiks | bcc856bc6b | |
Jiiks | 6473ddaf9f | |
Jiiks | ce9a4c6b3d | |
Jiiks | 006a67ee01 | |
Jiiks | ae6b745e68 | |
Jiiks | 7599271f31 | |
Jiiks | 2c2ef33565 | |
Jiiks | d965e29867 | |
Jiiks | 67dddf5391 | |
Jiiks | 7116efe207 | |
Jiiks | 6a854ab070 | |
Jiiks | 5083a80ba2 | |
Alexei Stukov | 37be465174 | |
Zack Rauen | fd68da9566 | |
Zack Rauen | 6917724700 | |
Alexei Stukov | 2c33d1f7f0 | |
Jiiks | f612fcbf00 | |
Jiiks | afc9f91dc3 | |
Jiiks | 2952331998 | |
Jiiks | da6aecc3f2 | |
Alexei Stukov | ad522dd745 | |
Jiiks | b0fcedd3f3 | |
Jiiks | fe319e58fe | |
Jiiks | 45545d86b4 | |
Jiiks | c59494f604 | |
Jiiks | 523d226b91 | |
Alexei Stukov | 27aa21a47a | |
Jiiks | 4984ae996e | |
Jiiks | f494cbbb7a | |
Alexei Stukov | ef19f4723e | |
Alexei Stukov | 1adf372de9 | |
Jiiks | fe884ae436 | |
Jiiks | ae651e08e9 | |
Samuel Elliott | b24e0ef178 | |
Samuel Elliott | 3578698a86 | |
Samuel Elliott | 1319793123 | |
Samuel Elliott | 47c786a98a | |
Samuel Elliott | bc1f93dd89 | |
Jiiks | 976ea796f8 | |
Jiiks | a746d6a03e | |
Jiiks | 393362c8fb | |
Jiiks | af8f7b0c9f | |
Jiiks | 1c63b8a05d | |
Jiiks | dfa7987756 | |
Jiiks | 7737dacd05 | |
Jiiks | b75c7716c9 | |
Jiiks | 7e2be8bb19 | |
Jiiks | 399f8a790d | |
Jiiks | fdfd0a6454 | |
Jiiks | 88b46bc359 | |
Jiiks | fcebb43f35 | |
Samuel Elliott | e7b0acb5a0 | |
Jiiks | e22e7f7af8 | |
Jiiks | 7a82d9e860 | |
Jiiks | 4804cd6ccb | |
Jiiks | 44f0effba5 | |
Jiiks | 5fd62c8050 | |
Jiiks | 8bda3e9b4d | |
Jiiks | 0154a7e97b | |
Jiiks | fdfd961390 | |
Jiiks | 43359d771c | |
Jiiks | ff71042bb0 | |
Samuel Elliott | 68c0133381 | |
Samuel Elliott | dfd0208394 | |
Samuel Elliott | 0229482be8 | |
Samuel Elliott | e78089a509 | |
Samuel Elliott | 5e259f50f8 | |
Samuel Elliott | 0bbd7b506a | |
Alexei Stukov | 1ddb93bd5d | |
Jiiks | 2e512e61b2 | |
Jiiks | 37ecd7ec4e | |
Alexei Stukov | 67747f85eb | |
Alexei Stukov | b5a5baa1bf | |
Jiiks | bca83da8c9 | |
Jiiks | ce0d9e826c | |
Jiiks | 8d33d80e1c | |
Jiiks | 30a886d84a | |
Jiiks | bb2c3e54a5 | |
Jiiks | aa3226ce43 | |
Alexei Stukov | f263cfac24 | |
Samuel Elliott | d122e27578 | |
Samuel Elliott | 7e82ae8a85 | |
Samuel Elliott | a8d60582d0 | |
Jiiks | 605e869413 | |
Samuel Elliott | a89d186730 | |
Samuel Elliott | 6647595a3a | |
Samuel Elliott | c9e681c242 | |
Samuel Elliott | c13884414d | |
Samuel Elliott | b7a769e883 | |
Samuel Elliott | 93f08b715b | |
Samuel Elliott | 07dde5b1fa | |
Samuel Elliott | e67c08ff02 | |
Samuel Elliott | 72dd93503a | |
Samuel Elliott | da5e0ac99c | |
Samuel Elliott | 3ab4fd8a9a | |
Alexei Stukov | 626b6cec64 | |
Samuel Elliott | e5078f4160 | |
Samuel Elliott | 5373512d9b | |
Samuel Elliott | 26404843c2 | |
Samuel Elliott | f236abdca9 | |
Alexei Stukov | 382ee68125 | |
Jiiks | 5908e39e52 | |
Jiiks | 17672e674b | |
Alexei Stukov | 6841594a9a | |
Jiiks | e7f5a2e118 | |
Jiiks | 064065cfe1 | |
Jiiks | da58d2e8b5 | |
Jiiks | 13364f1c9c | |
Alexei Stukov | ccb5a1ce56 | |
Samuel Elliott | e9f533d600 | |
Samuel Elliott | 05b3abd290 | |
Samuel Elliott | 6cac091c27 | |
Samuel Elliott | 0332027963 | |
Samuel Elliott | 9617890ab6 | |
Samuel Elliott | 47637eca75 | |
Samuel Elliott | d32959b8b9 | |
Alexei Stukov | 050118f26c | |
Jiiks | 43bc873bcb | |
Alexei Stukov | 1b6a846102 | |
Samuel Elliott | 88b063ca8e | |
Samuel Elliott | 2bf1709dba | |
Samuel Elliott | 1dffb1f40b | |
Samuel Elliott | 00d909f16c | |
Samuel Elliott | 91275f4332 | |
Samuel Elliott | 976aecd8f2 | |
Samuel Elliott | 9bc29cc66e | |
Samuel Elliott | a6db490f49 | |
Samuel Elliott | 2c6ae62c7e | |
Samuel Elliott | 05c08ca51f | |
Samuel Elliott | dc1acf140f | |
Samuel Elliott | 90c256ed76 | |
Samuel Elliott | 88e563501a | |
Samuel Elliott | bd72246715 | |
Samuel Elliott | 2060a41f5d | |
Samuel Elliott | fdc9330195 | |
Samuel Elliott | e94a7c50a4 | |
Samuel Elliott | f9e278cc75 | |
Samuel Elliott | 3168012fde | |
Samuel Elliott | 5e2b5975ed | |
Alexei Stukov | d6cb8f74fe | |
Samuel Elliott | 59bbc8d8ef | |
Samuel Elliott | 7954ebd764 | |
Samuel Elliott | 92612fc616 | |
Samuel Elliott | 3437c36b87 | |
Samuel Elliott | 3574d6a5ba | |
Samuel Elliott | 9a72a7425e | |
Samuel Elliott | 5707da5f72 | |
Samuel Elliott | bff6057e0c | |
Alexei Stukov | ecf7f872cb | |
Alexei Stukov | 22485bc26a | |
Jiiks | deaa680aae | |
Samuel Elliott | 1f5cf04eb6 | |
Samuel Elliott | cfdf3e0b4a | |
Alexei Stukov | 4f451e3822 | |
Jiiks | f4692c5e32 | |
Jiiks | 327fb09973 | |
Jiiks | 7046316d60 | |
Jiiks | 67142ca7db | |
Jiiks | 1e977fb407 | |
Jiiks | e418964819 | |
Alexei Stukov | fab7371d66 | |
Jiiks | e0de230ef2 | |
Alexei Stukov | f255c010cd | |
Jiiks | e7e79c9ac8 | |
Alexei Stukov | 963d9e61d8 | |
Jiiks | 779a59c87e | |
Jiiks | 160e6699e3 | |
Jiiks | 9b7b5d3b02 | |
Jiiks | ca9264a672 | |
Alexei Stukov | 43e95bbba8 | |
Alexei Stukov | 6380f9fc33 | |
Samuel Elliott | df0f823f1d | |
Samuel Elliott | ff3ef34784 | |
Samuel Elliott | 4cfa4ee466 | |
Samuel Elliott | b9145b5b85 | |
Samuel Elliott | 5be5002ea1 | |
Samuel Elliott | d88caab321 | |
Alexei Stukov | 99e9ce3852 | |
Samuel Elliott | eae9d67649 | |
Samuel Elliott | 2eaafdb1ac | |
Samuel Elliott | 0be09f6cf8 | |
Samuel Elliott | afacd9cfbb | |
Samuel Elliott | 7123f5f100 | |
Samuel Elliott | 522e798669 | |
Samuel Elliott | 201af9473e | |
Samuel Elliott | fae6fc8511 | |
Samuel Elliott | 847d88a628 | |
Samuel Elliott | 7cf1cf36fa | |
Samuel Elliott | bd19a633b0 | |
Samuel Elliott | 8419bc51c1 | |
Samuel Elliott | 9f6394eae3 | |
Samuel Elliott | 6b94437b89 | |
Samuel Elliott | fca89aaaa6 | |
Samuel Elliott | 5931bffca3 | |
Samuel Elliott | 5938e39d99 | |
Samuel Elliott | 36a2fb1ec5 | |
Samuel Elliott | c7db7fc2a7 | |
Alexei Stukov | d1d2610416 | |
Alexei Stukov | f528ef6bda | |
Samuel Elliott | fadd0574ac | |
Samuel Elliott | 01cacb3ef6 | |
Samuel Elliott | c9a20b7fcf | |
Samuel Elliott | 48ee29b621 | |
Samuel Elliott | 78a61dc60f | |
Alexei Stukov | fdb6ddd540 | |
Jiiks | 3a09dfb066 | |
Jiiks | 877b59d0d3 | |
Jiiks | 5df556f96e | |
Jiiks | cd936201b5 | |
Jiiks | 807cb9241b | |
Alexei Stukov | 5ba8c41850 | |
Jiiks | 7ba43f8763 | |
Jiiks | 666960a9bd | |
Alexei Stukov | 8c2f59ecf1 | |
Alexei Stukov | 1be6992da0 | |
Alexei Stukov | 050f8d40e0 | |
Alexei Stukov | 77f4b7f0ec | |
Jiiks | eb9e3dd4c6 | |
Jiiks | 2962d1fc9f | |
Alexei Stukov | b51e675291 | |
Samuel Elliott | 0373d81199 | |
Samuel Elliott | a9d488b748 | |
Samuel Elliott | c9d9feb63f | |
Samuel Elliott | bea87f5be8 | |
Samuel Elliott | 9fb914fb70 | |
Alexei Stukov | a49551f7bb | |
Samuel Elliott | 0cfb6d36ca | |
Samuel Elliott | dcb2704a28 | |
Samuel Elliott | 8c5796e4f6 | |
Samuel Elliott | 41b7906612 | |
Samuel Elliott | 1dd01e186b | |
Samuel Elliott | 0af22b21d4 | |
Jiiks | 16763c4a3e | |
Alexei Stukov | 12a5bd35d5 | |
Jiiks | 57e73050cd | |
Jiiks | df7d9fa0da | |
Jiiks | b1f4fb637e | |
Jiiks | a37bccfbb2 | |
Jiiks | 287e2c9cd2 | |
Alexei Stukov | 401f67382d | |
Jiiks | b23a5ba7ac | |
Jiiks | 4fba4049e1 | |
Jiiks | 4329517b0f | |
Alexei Stukov | dcd69d7b29 | |
Samuel Elliott | cebaba2de3 | |
Samuel Elliott | 8e71ee5a5c | |
Samuel Elliott | 145df5e7cc | |
Samuel Elliott | f13726d8e2 | |
Samuel Elliott | 4b9084bee4 | |
Samuel Elliott | 6b96bdbc3b | |
Samuel Elliott | 227b2cc0d8 | |
Samuel Elliott | 08271deea1 | |
Samuel Elliott | d3c3de5a79 | |
Samuel Elliott | 9b9162346b | |
Samuel Elliott | 210d085caf | |
Alexei Stukov | 058fd2f7b5 | |
Samuel Elliott | 8b85c829d6 | |
Samuel Elliott | 79af725f7f | |
Jiiks | 728aab8e95 | |
Samuel Elliott | 1bded3121e | |
Jiiks | e01567bd6a | |
Jiiks | 01b54a37bc | |
Jiiks | 44832fb11a | |
Jiiks | fca1d49506 | |
Jiiks | 842f0d6229 | |
Jiiks | 0ac1cf240d | |
Alexei Stukov | 13e9c53520 | |
Alexei Stukov | 91a658db44 | |
Samuel Elliott | c581ba1b75 | |
Samuel Elliott | a9a45e0f20 | |
Samuel Elliott | 6a11d3049a | |
Samuel Elliott | 729657f345 | |
Samuel Elliott | 5ca04f0652 | |
Samuel Elliott | a965dc9515 | |
Samuel Elliott | 98261a864d | |
Samuel Elliott | 2ec2df6efd | |
Samuel Elliott | 21c70124e5 | |
Samuel Elliott | 10abf0353f | |
Samuel Elliott | 1f85d4d2f4 | |
Samuel Elliott | 3b66d320e8 | |
Samuel Elliott | 21a8554d6f | |
Samuel Elliott | 8706e9ef0f | |
Samuel Elliott | 64ac3677b1 | |
Alexei Stukov | cbc727f951 | |
Jiiks | 66721d80b5 | |
Alexei Stukov | 0d660ac523 | |
Jiiks | 8984be5c3e | |
Jiiks | ebeae46991 | |
Jiiks | 9a652e3748 | |
Jiiks | 1f1d748687 | |
Jiiks | d406384dd0 | |
Alexei Stukov | 0994c1f94f | |
Samuel Elliott | 7664266dfe | |
Alexei Stukov | 663af3ca83 | |
Samuel Elliott | e793a8d380 | |
Samuel Elliott | 61c913b55b | |
Samuel Elliott | cc1aee6842 | |
Alexei Stukov | 80ce233e6d | |
Alexei Stukov | 9aeb5972cd | |
Samuel Elliott | f085364760 | |
Samuel Elliott | 1f4c2df3ae | |
Samuel Elliott | 32326471b1 | |
Samuel Elliott | 8d7261a24a | |
Samuel Elliott | 7e2fca9059 | |
Samuel Elliott | 26fbac6bbc | |
Samuel Elliott | eaa8d3a03a | |
Samuel Elliott | 4fa91563a5 | |
Samuel Elliott | 09cff282cb | |
Samuel Elliott | 52f8c1cf31 | |
Samuel Elliott | 15f683d4c9 | |
Samuel Elliott | 4dfe349802 | |
Alexei Stukov | 0eddaf86eb | |
Jiiks | cb2536437b | |
Jiiks | 194c1c1ff8 | |
Alexei Stukov | aae9653a4a | |
Jiiks | c52f1eb0b8 | |
Jiiks | f37058779a | |
Jiiks | 44b9450efa | |
Alexei Stukov | 06b3b70272 | |
Jiiks | 4e46675f94 | |
Jiiks | 4a7a9ae1db | |
Alexei Stukov | 35338dd104 | |
Samuel Elliott | 14822ab51a | |
Alexei Stukov | 0043292b25 | |
Jiiks | fad9a5336a | |
Jiiks | 2453797400 | |
Alexei Stukov | 0b1dabd80b | |
Jiiks | da702bd40a | |
Alexei Stukov | 600991fd50 | |
Samuel Elliott | 8d70e9cd54 | |
Alexei Stukov | c502ee8909 | |
Samuel Elliott | 9301ad627c | |
Alexei Stukov | 702b770058 | |
Alexei Stukov | 1db9ad3425 | |
Jiiks | 8bb388fb25 | |
Jiiks | fa9c0d2f94 | |
Samuel Elliott | 997ae3369d | |
Jiiks | 31655e558c | |
Alexei Stukov | 49103b1c49 | |
Jiiks | 45c372b71c | |
Jiiks | 00cc38ae33 | |
Jiiks | 34435465a3 | |
Jiiks | e25c3fe846 | |
Alexei Stukov | 224b51f1c3 | |
Samuel Elliott | 01655f0c04 | |
Alexei Stukov | aa7e6ab160 | |
Alexei Stukov | f05517a0a4 | |
Samuel Elliott | d4c71452bc | |
Samuel Elliott | 08b791e547 | |
Samuel Elliott | f3aef1d61d | |
Alexei Stukov | f71d79fd90 | |
Jiiks | 0ec190612b | |
Alexei Stukov | fc2a374344 | |
Jiiks | 39c83b7fa6 | |
Jiiks | 67f7111f66 | |
Jiiks | bf0c74b516 | |
Alexei Stukov | bcdd694ec9 | |
Jiiks | 208ca763ab | |
Jiiks | 51f5a93da2 | |
Jiiks | f69ea55967 | |
Jiiks | b395ed767d | |
Alexei Stukov | 18c71fe7da | |
Jiiks | 72b024c4f3 | |
Jiiks | e7b488316b | |
Alexei Stukov | 63b1e9d1e6 | |
Samuel Elliott | 6e4d6307e8 | |
Jiiks | 2472743660 | |
Jiiks | 83783a9ece | |
Alexei Stukov | 9fb51c57b3 | |
Jiiks | ca3ce86dc5 | |
Jiiks | c5a011012f | |
Alexei Stukov | 6a96ce1237 | |
Samuel Elliott | 5b87a928ba | |
Alexei Stukov | e314faf157 | |
Jiiks | 45448e23d3 | |
Alexei Stukov | fd8eaa363b | |
Alexei Stukov | 93607fde0c | |
Alexei Stukov | dc7805c835 | |
Samuel Elliott | 30121ecc9c | |
Samuel Elliott | 64dd9cd6fb | |
Samuel Elliott | 4c414e80d4 | |
Samuel Elliott | 7250ab6cf9 | |
Alexei Stukov | 61cbcd8bb5 | |
Jiiks | 5165094a1b | |
Alexei Stukov | 2da3fb95c2 | |
Samuel Elliott | 9245e921ce | |
Alexei Stukov | 17f9487225 | |
Jiiks | 4ba99b654e | |
Alexei Stukov | e09afda0f2 | |
Jiiks | a783072cca | |
Alexei Stukov | 848af986e6 | |
Samuel Elliott | 4ef24b8b85 | |
Samuel Elliott | ba4a02b501 | |
Jiiks | 41ff8e6445 | |
Alexei Stukov | 4caab690ac | |
Jiiks | c73346292e | |
Alexei Stukov | 634180a6e5 | |
Samuel Elliott | 1a95a7db4b | |
Alexei Stukov | a0fb11d591 | |
Jiiks | a706e6f2fc | |
Alexei Stukov | 6e4f90cdfb | |
samfun123 | 7cfd141260 | |
samfun123 | 5bcd43bb48 | |
samfun123 | 75410b4ce9 | |
Alexei Stukov | 4480458bd6 | |
Jiiks | 932d91876b | |
Alexei Stukov | 71df600173 | |
Samuel Elliott | ace7bab65e | |
Jiiks | 3034ea4934 | |
Jiiks | fc6bb75229 | |
Jiiks | 3edceebd89 | |
Alexei Stukov | e65e4d116e | |
Alexei Stukov | 996ab6a73f | |
Alexei Stukov | 2befa12cc4 | |
Jiiks | 267a545ea5 | |
Jiiks | 18926dd58a | |
Alexei Stukov | e2e2ab74ef | |
Jiiks | 230e9e6560 | |
Jiiks | 8a799f29f7 | |
Jiiks | bf648dce6c | |
Alexei Stukov | aac97dd7c4 | |
Samuel Elliott | 21025e759e | |
Samuel Elliott | 915db146e8 | |
Samuel Elliott | c5442f709d | |
Alexei Stukov | a97682b2a4 | |
Jiiks | 39da2ffdb8 | |
Alexei Stukov | 7ce0ecbf62 | |
Jiiks | e0ce4c00c1 | |
Jiiks | 889e4916ef | |
Jiiks | b1edcf5810 | |
Jiiks | b8116ddc58 | |
Jiiks | 071b60b986 | |
Alexei Stukov | 2f680a9a4e | |
Jiiks | 801235e50b | |
Alexei Stukov | 4d716439b6 | |
Alexei Stukov | 7ab6daf626 | |
Jiiks | e05b5c3377 | |
Jiiks | 2f463fad32 | |
Jiiks | f18c594744 | |
Samuel Elliott | 2d0ba0cc0e | |
Alexei Stukov | 64385a5631 | |
Jiiks | 20c9dacf71 | |
Alexei Stukov | af9f0e21aa | |
Jiiks | 853e2a03aa | |
Alexei Stukov | 5fe333c3d3 | |
Samuel Elliott | 67e6a22ca7 | |
Alexei Stukov | aefab7c677 | |
Jiiks | fe06db3d3c | |
Alexei Stukov | b75c8892f7 | |
Samuel Elliott | 26de9a3197 | |
Jiiks | eec595185c | |
Jiiks | 1647f8d8c2 | |
Jiiks | a7a2e6b08b | |
Jiiks | 27dac58dda | |
Alexei Stukov | dbc40a9850 | |
Jiiks | b6489cefaa | |
Alexei Stukov | aec27f2493 | |
Jiiks | c046f5f92f | |
Jiiks | 48a086c29d | |
Jiiks | cd87bd0e35 | |
Jiiks | 415d196954 | |
Jiiks | fce24e6a6a | |
Alexei Stukov | 1dae0a40b2 | |
Jiiks | 600eed9dbc | |
Jiiks | c92a293d31 | |
Jiiks | c078c7b686 | |
Jiiks | 2104770c9d | |
Jiiks | c01f022ede | |
Alexei Stukov | 0bb70aebde | |
Jiiks | a6bc800436 | |
Jiiks | eca427ce46 | |
Jiiks | 14de558305 | |
Jiiks | 92634a2f98 | |
Alexei Stukov | 60d9bf6ef6 | |
Jiiks | 6b800d94e3 | |
Jiiks | 76730d38d4 | |
Alexei Stukov | 2c8508fe32 | |
Jiiks | 7d09b38963 | |
Alexei Stukov | 363c7e9f7c | |
Jiiks | 9f094a08cc | |
Jiiks | 8621898558 | |
Jiiks | 235b373f1a | |
Jiiks | e0df52b6ab | |
Jiiks | 1896a0d558 | |
Jiiks | a19570c205 | |
Jiiks | 5ad5eeff64 | |
Jiiks | 2a8c6189e4 | |
Jiiks | 9d7bc8a058 | |
Jiiks | cb8b448da9 | |
Jiiks | ca8a421a9c | |
Jiiks | 85db5af655 | |
Jiiks | 6385c15236 | |
Jiiks | 031e93185a | |
Jiiks | 9c14b44d22 | |
Jiiks | 30b4a8a55d | |
Jiiks | 5cdd9948e8 | |
Jiiks | ca5467addb | |
Jiiks | 5791368a75 | |
Jiiks | 88ad25ecc7 | |
Jiiks | 4c5ff964e6 | |
Alexei Stukov | 28c573e5c5 | |
Alexei Stukov | c5083733be | |
Pierce | 981de1b9e2 | |
Pierce | bac4ce5323 | |
samogot | 4ab90e780f | |
samogot | e41b289eb5 | |
samogot | 46d67ae88a | |
samogot | 8e7c6b45b8 | |
samogot | 63c239f36f | |
samogot | d980a23dd3 | |
Samuel Elliott | a27d586d38 | |
Samuel Elliott | d11cfbcdaa | |
Alexei Stukov | 65cf800283 | |
Jiiks | 7bd1fc2eea | |
Jiiks | 366deb02fc | |
Jiiks | 271101ba0b | |
Jiiks | e66c1ce447 | |
Jiiks | 5faaaa0276 | |
Jiiks | a509495b9f | |
Alexei Stukov | 4345abad15 | |
Jiiks | 0a05c0d68a | |
Jiiks | 88a89bbe4f | |
Jiiks | a3f9b50c8c | |
Alexei Stukov | e548f75b68 | |
Jiiks | 9a8f2eddef | |
Alexei Stukov | 5157ad165d | |
Jiiks | 14a5331ecc | |
Alexei Stukov | b7aab16185 | |
Alexei Stukov | 25dac4d258 | |
Jiiks | cb542acbd0 | |
Jiiks | d934bc4aff | |
Alexei Stukov | 7372cf778d | |
Jiiks | 38783631c5 | |
Samuel Elliott | ecfc2ca7d6 | |
Jiiks | 17a8c2372b | |
Jiiks | 87b07d0a0e | |
Jiiks | 1d9e6037d9 | |
Jiiks | 71ac54911a | |
Jiiks | b3218d636a | |
Alexei Stukov | 31041f75dd | |
Zack Rauen | ff818d721d | |
Jiiks | 098ac8fb17 | |
Jiiks | 19b88b4be0 | |
Jiiks | e9b15a33b1 | |
Jiiks | a5cad6731c | |
Jiiks | 17d7e83590 | |
Jiiks | 4fc630cf0a | |
Jiiks | c195786ffc | |
Alexei Stukov | c44131e0dd | |
Jiiks | b908b2e42e | |
Jiiks | 7476d4d499 | |
Jiiks | 23cde9cb9f | |
Jiiks | 4ad2cc52dd | |
Jiiks | e11787c585 | |
Jiiks | 38776569ed | |
Jiiks | b90fda127d | |
Jiiks | fc49277e1d | |
Jiiks | db874b6218 | |
Jiiks | 90b9efe5ba | |
Jiiks | 13343283d5 | |
Jiiks | e8d4517606 | |
Jiiks | 79827b946d | |
Jiiks | 48fabda81f | |
Jiiks | 6cf4322127 | |
Jiiks | 2cc0e6c1aa | |
Jiiks | a666a52f72 | |
Jiiks | 47371b3a19 | |
Alexei Stukov | 9cdf200ae7 | |
Jiiks | e714a9267f | |
Jiiks | 8cf6d6f2e0 | |
Jiiks | 2618876e62 | |
Jiiks | dd9ec12b36 | |
Jiiks | 725c95d6d3 | |
Alexei Stukov | 43fb6eb2c9 | |
Samuel Elliott | 10ea33f270 | |
Samuel Elliott | 731d18e4da | |
Jiiks | 5d07d9b429 | |
Jiiks | 11e42a4ce9 | |
Jiiks | 111bc2da2f | |
Jiiks | d88556ccba | |
Jiiks | 2abbe56498 | |
Alexei Stukov | 8a7320baab | |
Jiiks | a323531dfc | |
Zack Rauen | 645bf81160 | |
Zack Rauen | 316f30196c | |
Jiiks | a288d2473f | |
Jiiks | 87fcff3ffb | |
Jiiks | 0fbe5f8ce7 | |
Alexei Stukov | cb091b594d | |
Jiiks | 6f9f005ed9 | |
Alexei Stukov | 7add5afc30 | |
Jiiks | 3500b2890b | |
Alexei Stukov | d57b394768 | |
Jiiks | e1e3e77e08 | |
Jiiks | 6816a2938a | |
Jiiks | 0e70eea85c | |
Jiiks | 1f618684b8 | |
Alexei Stukov | d1988f915f | |
Alexei Stukov | 623aaeb01a | |
samfun123 | 5251d42c2f | |
Zack Rauen | 7b578dcc56 | |
Zack Rauen | 0cd70e5da3 | |
Alexei Stukov | 906bda3489 | |
Zack Rauen | 10b843d53e | |
Jiiks | 4362d39624 | |
Jiiks | 6640fa1890 | |
Jiiks | fe29c78583 | |
Pierce | de5fd43b84 | |
Samuel Elliott | 7d1416da67 | |
Alexei Stukov | 5aed82573f | |
Jiiks | 8b9d923ae2 | |
Jiiks | 9f0eb844c2 | |
Jiiks | 1a9fbef1f5 | |
Jiiks | 5c3e7e780e | |
Jiiks | 497e989e76 | |
Jiiks | b3f5f46e74 | |
Jiiks | 2bcde666d8 | |
Jiiks | 48e1fda6cf | |
Alexei Stukov | 1f46323dc9 | |
Alexei Stukov | abda678167 | |
Samuel Elliott | f21aec5226 | |
Alexei Stukov | f9cd4e1a8a | |
Jiiks | da20f417c3 | |
Jiiks | e19f2ad8d7 | |
Jiiks | 357706f4c5 | |
Jiiks | 49bdd4b1fd | |
Jiiks | 461d3468a5 | |
Jiiks | 4575c3428a | |
Jiiks | 2d128eb0a4 | |
Jiiks | ac1b5ef38d | |
Jiiks | 5419484825 | |
Jiiks | ff1510dbcc | |
Jiiks | 9492bb0bc2 | |
Jiiks | b42f17437c | |
Alexei Stukov | 992aa63cd8 | |
Samuel Elliott | 4076da01c9 | |
Samuel Elliott | 708b4e5e63 | |
Alexei Stukov | 8b9ccf0e0d | |
Alexei Stukov | 146f64aa04 | |
Jiiks | 389738c278 | |
Jiiks | 5fe1bfed8f | |
Alexei Stukov | f002440899 | |
Pierce | 722af5cec6 | |
Jiiks | a375f9ed6e | |
Jiiks | ddaa663067 | |
Alexei Stukov | fb0326d371 | |
Jiiks | 4e1b5cfc89 | |
Alexei Stukov | e9c97b89ab | |
Jiiks | a544a42436 | |
Alexei Stukov | eac7f62911 | |
Jiiks | ec50f750aa | |
Jiiks | 2f1a268cc5 | |
Jiiks | e826079fc1 | |
Jiiks | 0f762c1e58 | |
Jiiks | 6b0423ebb0 | |
Jiiks | b7e0de3e41 | |
Jiiks | ef7009f5a1 | |
Alexei Stukov | 9fc2eb559a | |
Jiiks | 509f44bcb1 | |
Jiiks | 289533b755 | |
Jiiks | e3367768eb | |
Jiiks | 3ee59845de | |
Jiiks | 7c12617448 | |
Alexei Stukov | b4be857f70 | |
Jiiks | 626a83c8a7 | |
Jiiks | 006b7bed7a | |
Jiiks | 20abc525d3 | |
Alexei Stukov | 902119fbae | |
Jiiks | 2a8ebbe80c | |
Jiiks | 0faad8d654 | |
Alexei Stukov | 602258b6d8 | |
Jiiks | 9c488f996f | |
Jiiks | 48dbbb1ea3 | |
Jiiks | a9b253ebad | |
Jiiks | 98d58b1303 | |
Alexei Stukov | 7fd368f936 | |
Alexei Stukov | 4d44ba673b | |
Alexei Stukov | 02df356931 | |
Jiiks | 80170a5bbd | |
Jiiks | 2aa9b46c10 | |
Jiiks | 39fbae166f | |
Jiiks | a1261cab0a | |
Alexei Stukov | f160cb9c8b | |
Jiiks | 7aa7d9df10 | |
Alexei Stukov | 48d6098285 | |
Jiiks | 2e26f472ea | |
Jiiks | 50f0d4f91f | |
Jiiks | bc05138037 | |
Jiiks | e823b1c9d0 | |
Jiiks | cc55db87b7 | |
Jiiks | 1040650ae4 | |
Jiiks | 8c2692caba | |
Jiiks | 403ffc839d | |
Jiiks | c801b09e9c | |
Jiiks | 861d95bd06 | |
Jiiks | 6b96b191aa | |
Jiiks | c851d2e92c | |
Jiiks | 9ecb14ab23 | |
Jiiks | 8e5f007f5b | |
Jiiks | ba4ba87eec | |
Jiiks | e1932b7f74 | |
Jiiks | 7be0ab71fc | |
Jiiks | b57255487a | |
Jiiks | 8e308f2a65 | |
Jiiks | 662329c5e9 | |
Jiiks | 03505fcdb6 | |
Jiiks | 8863c20661 | |
Jiiks | b75177dadd | |
Jiiks | 71f785cdd1 | |
Jiiks | 38c61484c1 | |
Jiiks | 5ec53510be | |
Jiiks | 881b33f4b6 | |
Jiiks | 4cd0f4fb9b | |
Jiiks | 32e563effc | |
Jiiks | 69e10f69cd | |
Jiiks | f6047265e3 | |
Jiiks | c8ebe4bfad | |
Jiiks | e72fb0d525 | |
Jiiks | 149a4e0dec | |
Jiiks | 3ab286f81e | |
Jiiks | 78952f190c | |
Jiiks | 273a4ae50b | |
Jiiks | ba0ba4b2ce | |
Jiiks | a6d2e200f6 | |
Jiiks | 660729eaf7 | |
Jiiks | 8d9ba8fed9 | |
Jiiks | ea8ac32a1c | |
Jiiks | e9e3627339 | |
Jiiks | 8688a9ea7d | |
Jiiks | 2a29984950 | |
Jiiks | cf1a9ecb91 | |
Jiiks | 4114a6ee26 | |
Alexei Stukov | 55a6fb8697 | |
Alexei Stukov | 5344fd2b1e | |
Zack Rauen | 7322ea8ba9 | |
pohky | 4a377d9648 | |
Jiiks | 1a6648ebf0 | |
Jiiks | 9ab77a8217 | |
pohky | 1d07436569 | |
pohky | f91dff5c1b | |
David | a160359413 | |
pohky | ebe69a5369 | |
DeathStrikeV | 0831320b61 | |
David | 4df677c28d | |
David | aee8aa7483 | |
Alexei Stukov | 1cc631b29d | |
Alexei Stukov | 626d41480c | |
pohky | caaa0aba8b | |
pohky | daec28f2b0 | |
pohky | 6d1baa0639 | |
DeathStrikeV | 31e94d72f7 | |
David | 055b3b9a25 | |
DeathStrikeV | c0fd7e41cc | |
David | 187b39dac1 | |
DeathStrikeV | 10f686a71f | |
DeathStrikeV | eabad3bced | |
David | 6daeeaf866 | |
David | 255caea48e | |
pohky | a46fb246ed | |
David | 054efe9e75 | |
DeathStrikeV | 07f5c3b6d5 | |
David | 895f987ca9 | |
David | cefa91aa13 | |
DeathStrikeV | 0271df21c9 | |
David | a6b89cf205 | |
David | a74e425a8e | |
David | 191bfaca02 | |
David | de1a63f7bd | |
David | 39e7982c64 | |
David | ccbebd6855 | |
David | 192184e4d1 | |
DeathStrikeV | 94622aa854 | |
David | 051edcf619 | |
David | 3c1c8f27a4 | |
David | 11ca29c2fb | |
David | 4eb5c183a5 | |
David | bf041b54ef | |
DeathStrikeV | 3c53865150 | |
David | db66110556 | |
DeathStrikeV | d1371a5383 | |
David | c5e0886690 | |
DeathStrikeV | 6d77162321 | |
David | 5ebeb06520 | |
David | 35dadec203 | |
Alexei Stukov | 9f1314e75c | |
David | a21cb93d3b | |
DeathStrikeV | 6cf7f8583d | |
DeathStrikeV | 3787744a71 | |
David | 358c8e288d | |
DeathStrikeV | e5a8224c49 | |
DeathStrikeV | d34db2cbc2 | |
David | c5abd4703a | |
David | b63b4a31f3 | |
DeathStrikeV | 8f0c9b5158 | |
DeathStrikeV | 715480c429 | |
BlazingSky | 12444400a0 | |
BlazingSky | 9d7e1bae32 | |
DeathStrikeV | 3fb10a820a | |
Alexei Stukov | 565ff12946 | |
Alexei Stukov | da082999cb | |
Alexei Stukov | 4fc5abe092 | |
Alexei Stukov | ecc8b3baa1 | |
Alexei Stukov | 319fd6b12b | |
David | cf6a9ca3b7 | |
Alexei Stukov | df8b319275 | |
David | 19b37a9feb | |
David | 633e2fb986 | |
David | b24b1b7cda | |
David | ab58d87acd | |
Alexei Stukov | db7f9911c9 | |
DeathStrikeV | 21cc2c8beb | |
DeathStrikeV | b805ec8b2c | |
DeathStrikeV | c8f21ae02e | |
David | 154d916a9e | |
DeathStrikeV | e4681e2a23 | |
David | e5aeea60e9 | |
David | 37c3f12178 | |
David | 72e9d2f455 | |
David | e3c8befb25 | |
DeathStrikeV | 8e9b59521a | |
DeathStrikeV | 1c882bc52d | |
David | a33a2b205d | |
David | 0c014877ea | |
pohky | d5522e547c | |
DeathStrikeV | 1e997efa28 | |
David | 01eb06ab23 | |
David | 42ee775176 | |
David | 187966d4b0 | |
DeathStrikeV | 50edc74c55 | |
BlazingSky | 29d8bba9a9 | |
David | 2382b1e130 | |
David | e67e4ee7b9 | |
David | 1bf65e5706 | |
Alexei Stukov | 4dfc413227 | |
Alexei Stukov | 7a9ac5b035 | |
pohky | 678a756dd6 | |
Alexei Stukov | 1e8f45e825 | |
Alexei Stukov | 85f79d5259 | |
Alexei Stukov | 5f83b20f6b | |
Alexei Stukov | c9b6a11941 | |
Zerthox | 4a33c39e8b | |
Alexei Stukov | 05d379ba79 | |
DeathStrikeV | 8aa4a7c700 | |
BlazingSky | 04c0df30e7 | |
Alexei Stukov | 0ea7990226 | |
Alexei Stukov | a96a0384b2 | |
Alexei Stukov | bf6f826c9d | |
Alexei Stukov | 81bd8938fe | |
Alexei Stukov | 0255164f42 | |
Alexei Stukov | 4a7422039e | |
Alexei Stukov | 8dfd0f2a73 | |
Alexei Stukov | 5abe7c4a9e | |
Alexei Stukov | 1c75af3513 | |
Alexei Stukov | 214c77be13 | |
Alexei Stukov | 7c62116554 | |
Alexei Stukov | 4cb39d90c0 | |
Alexei Stukov | db34f5c6b0 | |
Alexei Stukov | 8bfd9cdaa5 | |
Alexei Stukov | e7aeb7d1af | |
Alexei Stukov | 327c08fd93 | |
Alexei Stukov | b933703f4f | |
pohky | a5089721db | |
DeathStrikeV | 5a51afd01e | |
Alexei Stukov | fc7a85a79c | |
Alexei Stukov | 707afde649 | |
Alexei Stukov | a85d8e19d9 | |
David | 1a367d83ba | |
Alexei Stukov | 05d55ea6fa | |
Alexei Stukov | 9859064dc6 | |
Alexei Stukov | 2d4416466c | |
Alexei Stukov | 6372743f9f | |
Alexei Stukov | 060c2ab0ec | |
Alexei Stukov | 91dd2e6ee7 | |
Alexei Stukov | eff75f27dc | |
Alexei Stukov | 8650b376ef | |
Alexei Stukov | f58ccdad4a | |
Alexei Stukov | 6e020a0298 | |
Alexei Stukov | bdb22e9fb6 | |
Alexei Stukov | f6cae7c238 | |
Alexei Stukov | ad86bbb8c8 | |
Alexei Stukov | 5bf01fa3c8 | |
Alexei Stukov | 2fbf1e9985 | |
Alexei Stukov | 2fbe862933 | |
Alexei Stukov | 2d554d0086 | |
Alexei Stukov | 3262eb58a9 | |
Alexei Stukov | b15f56212d | |
Alexei Stukov | 6d23660361 | |
Alexei Stukov | fbb0283148 | |
Alexei Stukov | 93ecab675d | |
Alexei Stukov | 8820d0e674 | |
Alexei Stukov | df7f12d5bd | |
DeathStrikeV | f01ea2473c | |
pohky | 32b6e6dc48 | |
DeathStrikeV | d71acb07fd | |
pohky | b92989ac14 | |
DeathStrikeV | d517bae2d3 | |
pohky | 889a415314 | |
David | 77d72d6284 | |
DeathStrikeV | 206685c29d | |
DeathStrikeV | 3b1c5ee3a8 | |
pohky | a17497d9a4 | |
DeathStrikeV | cc64dfa958 | |
DeathStrikeV | f7ab2a5b59 | |
pohky | 8230b05046 | |
DeathStrikeV | 29c91bdfdf | |
pohky | bd34bd6157 | |
Violetnred2 | 70ef5cd327 | |
DeathStrikeV | c3450d50c4 | |
DeathStrikeV | e4796621b7 | |
pohky | aefd07995d | |
DeathStrikeV | b7f46fd7f4 | |
DeathStrikeV | 36d68836d4 | |
Alexei Stukov | 897be73f09 | |
DeathStrikeV | 3e89697b4f | |
DeathStrikeV | b78666140a | |
DeathStrikeV | 52e8bc512d | |
DeathStrikeV | bb50c6f48e | |
Alexei Stukov | b02340a130 | |
Alexei Stukov | 319e6db4fc | |
Violetnred2 | 5c52b3b987 | |
Alexei Stukov | 72c2585e6a | |
Alexei Stukov | a64dfa8bf0 | |
Alexei Stukov | f35e867358 | |
David | b200229b26 | |
Alexei Stukov | 770eae466e | |
David | 6a86149b63 | |
DeathStrikeV | b6833d2962 | |
DeathStrikeV | a976753b93 | |
Zerthox | e0885c6d6a | |
DeathStrikeV | 717586cee1 | |
Jiiks | 23724784e2 | |
DeathStrikeV | e26b1db0c4 | |
DeathStrikeV | c687da23fe | |
DeathStrikeV | e509f16648 | |
DeathStrikeV | 1919061656 | |
Jiiks | 90fd6199a1 | |
jjtcm | d6aabd89ae | |
DeathStrikeV | 406ced863f | |
DeathStrikeV | 1d9e4753be | |
DeathStrikeV | aad61c5281 | |
DeathStrikeV | 3543b57f53 | |
Jiiks | fb927b093a | |
Jiiks | 01cfee0ebd | |
DeathStrikeV | c811e2a064 | |
DeathStrikeV | 92e992abd2 | |
Jiiks | d8feba59ea | |
Jiiks | 11190a8a4c | |
Jiiks | 143602a729 | |
Jiiks | 713e9e93d3 | |
Jiiks | beb013122f | |
Jiiks | f7fa19dcf8 | |
Jiiks | 44073ed15d | |
Jiiks | 031c64271a | |
Jiiks | 79d9c6f00c | |
Jiiks | 391b6a2085 | |
Jiiks | 0c22d475c3 | |
Jiiks | e8e52cf7c6 | |
Jiiks | b6e2ca2670 | |
NooB-AssassinZ3 | e06509ca0a | |
NooB-AssassinZ3 | cc4df4ada4 | |
Jiiks | de617fed27 | |
DeathStrikeV | 59c931191b | |
Zerthox | ca3859d343 | |
brodycas3 | 30df5f6158 | |
DeathStrikeV | f15b0dee3d | |
Nick Sarnie | ddda646afc | |
Nick Sarnie | be22c14356 | |
MKody | f08539d5f9 | |
MKody | ef10e075f6 | |
MKody | c6f85e545f | |
MKody | 0f2812bc10 | |
Jiiks | 513fb57621 | |
Jiiks | 0878f4ab00 | |
DeathStrikeV | dc18638567 | |
noodlebox | e793920ed9 | |
noodlebox | b6bdaedb6d | |
noodlebox | 3431d30ac2 | |
DeathStrikeV | c74e9c1030 | |
Jiiks | 5daf0bbdc5 | |
Jiiks | 604ec38101 | |
Jiiks | fcfd75346b | |
DeathStrikeV | 6768b064f7 | |
Ehsan Kia | 71124e0129 | |
noodlebox | 43aae6ac72 | |
noodlebox | 5a4e121e2b | |
SapiensAnatis | 810b0fcee2 | |
DeathStrikeV | 5120488766 | |
DeathStrikeV | 8e9ebaa853 | |
Jiiks | d862285c63 | |
Jiiks | 52c4114148 | |
Jiiks | 19feb05e9b | |
Jiiks | 47a36a2e56 | |
Jiiks | e1fd1f0df7 | |
Jiiks | 46fe0f7c37 | |
Jiiks | 00dfb68677 | |
Jiiks | 0a55c0edae | |
Jiiks | 33162a1798 | |
DeathStrikeV | a6f53183c1 | |
Jiiks | b0b24974e6 | |
Jiiks | 7ff3971168 | |
Jiiks | ba56f3bf6e | |
Jiiks | b7851955b2 | |
Jiiks | 5784e45e87 | |
Jiiks | 186d6c9bc5 | |
Jiiks | 6e1816d973 | |
DeathStrikeV | 5e23ad66af | |
DeathStrikeV | 788d20e452 | |
DeathStrikeV | 032b69d564 | |
DeathStrikeV | 01a55116b0 | |
DeathStrikeV | 31603634ce | |
Jiiks | 9c99e8a27d | |
DeathStrikeV | f2d4680698 | |
Jacob Foster | 028b96589e | |
Zerthox | b4bc6ff109 | |
DeathStrikeV | e21c1dc28a | |
DeathStrikeV | bebf352bd0 | |
Jiiks | 8369decbfc | |
DeathStrikeV | 1111b98767 | |
DeathStrikeV | ffb456c466 | |
DeathStrikeV | 68247caf66 | |
Zerthox | c65b760ca1 | |
DeathStrikeV | 8e4b5f6ee0 | |
DeathStrikeV | 6e08a4b1ed | |
DeathStrikeV | 0b26620901 | |
DeathStrikeV | d4c8f04331 | |
DeathStrikeV | 67f0257cb4 | |
pohky | a2de68ec1f | |
DeathStrikeV | 93115073e9 | |
DeathStrikeV | 19f5f9f7a5 | |
Jiiks | 131eab393d | |
Jiiks | 8f7cc5e06f | |
Jiiks | b46e797728 | |
DeathStrikeV | 42898de340 | |
Zerthox | 65fda6f4e7 | |
Zerthox | e15af62927 | |
DeathStrikeV | d2a7c6734a | |
Zerthox | 1b9eb941d2 | |
Zerthox | 59263be1b5 | |
DeathStrikeV | df2b647750 | |
DeathStrikeV | 4e0a23ec03 | |
DeathStrikeV | beae2087a8 | |
Jiiks | 2039252e69 | |
Elviss Strazdins | 1219eb76bc | |
DeathStrikeV | 31a1fcfc46 | |
DeathStrikeV | 37731b9fa7 | |
DeathStrikeV | 2d8120fc1e | |
DeathStrikeV | bd0e1b135e | |
Jiiks | 9a2b4fb8b2 | |
Jiiks | e7b9be97b0 | |
Jiiks | 5cea881ed9 | |
DeathStrikeV | e40b9fe82c | |
Jiiks | c14c4255eb | |
Jiiks | fec2679fc8 | |
Jiiks | 520ca3faea | |
Jiiks | c8e906b00d | |
Jiiks | 9bddcaa181 | |
Jiiks | aa4877d851 | |
Jiiks | a9a2d09ae0 | |
Jiiks | 422c18917c | |
Jiiks | ccdc5f1a9f | |
Jiiks | 080141e7d0 | |
Jiiks | b55bd54b29 | |
Jiiks | 8c0951a0ea | |
DeathStrikeV | bb5744dfe9 | |
DeathStrikeV | 5bf2597030 | |
DeathStrikeV | ade5cbc446 | |
DeathStrikeV | 35ca5f6339 | |
DeathStrikeV | 97c259f1c4 | |
DeathStrikeV | 8eedc29d71 | |
DeathStrikeV | 3975eabca0 | |
DeathStrikeV | 1b99b6289a | |
DeathStrikeV | 0b9fb52800 | |
DeathStrikeV | f1a0f11257 | |
DeathStrikeV | dea323a0b8 | |
Zerthox | c0bd71fe7d | |
DeathStrikeV | 85093234fa | |
DeathStrikeV | 13e21fb052 | |
DeathStrikeV | e592126078 | |
DeathStrikeV | 35de874ced | |
DeathStrikeV | cdc95aef68 | |
DeathStrikeV | 08cc4c5bc3 | |
DeathStrikeV | 5c514efa96 | |
DeathStrikeV | a9bc977764 | |
DeathStrikeV | 2fa3e7a4b5 | |
DeathStrikeV | 9b4f5f4366 | |
DeathStrikeV | 2b55554af1 | |
DeathStrikeV | 5aff38bf39 | |
Jiiks | 53d4989a79 | |
Jiiks | d23d331463 | |
DeathStrikeV | 020b1b8d4e | |
DeathStrikeV | e251a03914 | |
DeathStrikeV | 1faac18e8d | |
DeathStrikeV | a23878fd0b | |
Zerthox | 56773ae55f | |
Jiiks | dd28c89236 | |
DeathStrikeV | 14331ddee5 | |
DeathStrikeV | b1a06a519f | |
TakosThings | cc1938b14b | |
Jiiks | c4e0cc15c9 | |
Jiiks | 6651b5ac3f | |
DeathStrikeV | faa7b30df5 | |
DeathStrikeV | 9a638e7eb3 | |
DeathStrikeV | 06770ca1af | |
DeathStrikeV | 337a79efbb | |
Jiiks | fd3dab39de | |
Zerthox | 84092e1c6f | |
Jiiks | 77addb5b03 | |
DeathStrikeV | 68e56d9ea8 | |
Jiiks | 71b0077fc0 | |
DeathStrikeV | 2e64e4c47b | |
Léo | c4f4ac41e8 | |
Jiiks | 07f38008d8 | |
Ehsan Kia | 5c70075ca0 | |
Jiiks | e09fb169ae | |
Ehsan Kia | dc48d88d59 | |
Ehsan Kia | 227547d7e7 | |
DeathStrikeV | 00da2ef58c | |
DeathStrikeV | 195e8a9b77 | |
DeathStrikeV | 7cb778f884 | |
DeathStrikeV | 289abee257 | |
DeathStrikeV | 2c49e52f90 | |
DeathStrikeV | 07968430e3 | |
Alex | e10b78dc9b | |
Jiiks | a1f9f29198 | |
Jiiks | 0b614022ad | |
Jiiks | 821a9f8749 | |
Jiiks | 1bb83de279 | |
Jiiks | 48e55c9417 | |
Jiiks | 5dc6327fda | |
DeathStrikeV | e1470dab3e | |
DeathStrikeV | ab0e948fe4 | |
Jiiks | d5f16c0b2d | |
Alex | 66c24f246a | |
DeathStrikeV | 27b1978515 | |
Jiiks | 8df7431743 | |
Jiiks | c3443b5725 | |
DeathStrikeV | 5e466e79fd | |
Jiiks | 4f549676bc | |
Jiiks | cfd651eaab | |
Jiiks | 6d58d8f2f8 | |
Jiiks | 1a7d84a14c | |
Jiiks | 376471cb2d | |
Jiiks | 3eea2eacc3 | |
Jiiks | 9416be4105 | |
Jiiks | e7b56fe792 | |
Jiiks | 7f5986aa01 | |
Jiiks | fc53df5e4b | |
Jiiks | c5454a8f57 | |
DeathStrikeV | 3d381b7ca1 | |
DeathStrikeV | 469cb2a2da | |
DeathStrikeV | 8b57885002 | |
DeathStrikeV | 3429d6c8ca | |
Jiiks | 28ad0c0acc | |
Jiiks | 70c6eaca03 | |
DeathStrikeV | a80d16fbd8 | |
Ehsan Kia | 4fa0825bc7 | |
Ehsan Kia | 2db776f93a | |
DeathStrikeV | e330ff197d | |
DeathStrikeV | b002ae6130 | |
DeathStrikeV | 5c27d91fde | |
DeathStrikeV | 5f94a8b155 | |
DeathStrikeV | f18f353501 | |
DeathStrikeV | 0e2bf5b1c4 | |
DeathStrikeV | ea34f260df | |
DeathStrikeV | 56f590dbf4 | |
DeathStrikeV | 54709b608a | |
DeathStrikeV | aaea907eac | |
DeathStrikeV | ebb99040e4 | |
DeathStrikeV | d874f6becc | |
DeathStrikeV | f3d8865df5 | |
DeathStrikeV | 9098bca396 | |
DeathStrikeV | e7bda2e6c1 | |
Alex | a8534308b7 | |
Jiiks | 24eb3f3d9d | |
Ehsan Kia | ad2f98107a | |
noodlebox | b9b28c0bd9 | |
pohky | 2b361cbcc3 | |
Ehsan Kia | 39505125fd | |
Jiiks | 48d4a58102 | |
pohky | c51bb8cc43 | |
Ehsan Kia | 51ae2698c4 | |
pohky | 501f38357f | |
Jiiks | 2fc35436a4 | |
Jiiks | b87e0781f6 | |
Jiiks | fe1a30027a | |
pohky | eb58553ade | |
Jiiks | b28ef7abc5 | |
Jiiks | 8625b1bdbf | |
Yozora | 9ae3b5ba69 | |
pohky | 42971a5714 | |
pohky | 7bd1a219cd | |
CZauX | 336152802a | |
Yozora | d067bc2cfc | |
pohky | f751f540ec | |
pohky | bd576ef69c | |
pohky | d186bc9218 | |
pohky | ef8ca8fd15 | |
Jiiks | 9e4f6ccf25 | |
pohky | d377cef33c | |
pohky | 99afa4ccc7 | |
Ehsan Kia | 320a68f8cb | |
pohky | b09590e8e7 | |
Ehsan Kia | 4b63daaf48 | |
Jiiks | d26247bbd6 | |
pohky | 0ab1fe3f19 | |
pohky | 00ff05bfe0 | |
pohky | 2bd94b9ae4 | |
Ehsan Kia | 6d1b7667b8 | |
Jiiks | 51cf6e531c | |
Jiiks | e5d91a8564 | |
pohky | 91fcf59656 | |
pohky | 20337f3f36 | |
pohky | 7eac80b59f | |
pohky | 8c2e38662c | |
pohky | 31617f79b3 | |
pohky | 08ec6a4ddb | |
Ehsan Kia | 1a394c75c9 | |
pohky | b604daf201 | |
pohky | 2eb0f8d9c7 | |
pohky | 839c06d968 | |
Jiiks | 2b32ffac01 | |
pohky | 456852070e | |
Ehsan Kia | f2d76b2e99 | |
pohky | 4d63434d51 | |
pohky | d2a6b384d3 | |
Ehsan Kia | ba4402a4d4 | |
pohky | d36caf0599 | |
pohky | 40dbd67e80 | |
pohky | 19393f4480 | |
pohky | 9bf4b9dcb6 | |
pohky | c07f7ac73f | |
pohky | 3ba91bfbf3 | |
pohky | 1a48e6045f | |
Jiiks | 3c62a57b96 | |
Jiiks | 1a560a1656 | |
Jiiks | d2c5a95f13 | |
Jiiks | d81b806d6b | |
Jiiks | 9a97a5cc2e | |
pohky | 6998c5c131 | |
pohky | 01070c916c | |
Ehsan Kia | 136266be85 | |
Jiiks | 284838bb5c | |
Jiiks | 4fa8d75653 | |
Jiiks | ef3d2ad9b1 | |
pohky | 995868b5b9 | |
pohky | d8457795e4 | |
pohky | 8de42e7931 | |
Ehsan Kia | d8db32f389 | |
pohky | 22513f3d27 | |
pohky | 3470dc2594 | |
Ehsan Kia | f978d4b2d9 | |
pohky | 1a40b63d04 | |
Ehsan Kia | 4e20898cae | |
Jiiks | 849c139124 | |
Jiiks | c1af5fd726 | |
Jiiks | 1e79726b73 | |
pohky | 1dfd428995 | |
Jiiks | cfe00cd50d | |
Jiiks | 36d832d3e2 | |
pohky | 46f983f132 | |
pohky | f535aa5761 | |
pohky | ab07e14d7f | |
pohky | bdbc2c0110 | |
pohky | 00e64da94a | |
pohky | 0370e80071 | |
pohky | b25201b576 | |
Ehsan Kia | e6955f919d | |
Jiiks | 8205109f29 | |
Jiiks | b277ab2c7f | |
Jiiks | b68a850750 | |
pohky | b55607f1b2 | |
pohky | 3451215be9 | |
Jiiks | f649c729b2 | |
pohky | 8a786de8cc | |
pohky | e8e82cb02b | |
Jiiks | 329d8bc55e | |
pohky | 1137428be4 | |
pohky | d09d870d9e | |
Jiiks | ab7ccf061d | |
pohky | b343b5d7c9 | |
Jiiks | 13c1ab4be0 | |
Jiiks | 512ccd1904 | |
Jiiks | c4a1b2bc20 | |
Jiiks | 457f314ac3 | |
pohky | dde563e0eb | |
Jiiks | b742f86ee0 | |
Jiiks | 596b0e8490 | |
pohky | e9461a9830 | |
Jiiks | 178f2b464c | |
pohky | 220e75e29b | |
Ehsan Kia | c7fa09505d | |
pohky | d6c55d7e1b | |
pohky | 276b39e169 | |
pohky | 9c6b3629d3 | |
pohky | 5a636f6e9a | |
Jiiks | db31c2662a | |
pohky | 66d7636228 | |
Jiiks | ae9dcd6220 | |
Jiiks | f4461ed97f | |
pohky | 201d1fb22b | |
pohky | cb7176be5d | |
pohky | c3dff1ca6e | |
pohky | 6be4da2197 | |
pohky | 5add6c9a21 | |
pohky | 2f4716714d | |
Ehsan Kia | 5c3f653718 | |
pohky | 6b8f324199 | |
Jiiks | 199aa4fa45 | |
Jiiks | 2845884954 | |
Jiiks | 25e2d278a8 | |
pohky | c86be946ee | |
Jiiks | bdf889e14f | |
Jiiks | c61af06452 | |
Jiiks | 6d16dea9ac | |
pohky | 9b4f2bac09 | |
Jiiks | 71131ef335 | |
Jiiks | 2689708b49 | |
Jiiks | 640f1a5930 | |
pohky | 888bebbfb0 | |
pohky | be5f2cf70d | |
pohky | 7087d174fa | |
Jiiks | b79775f82c | |
pohky | 62684b80e7 | |
pohky | 725b0ce80e | |
pohky | b7a2cd30f9 | |
pohky | 30e2700fa6 | |
pohky | fb500bcfb5 | |
pohky | 18c11def46 | |
pohky | cb63744be6 | |
pohky | f0b3fb07b0 | |
Jiiks | 0fd8d3856f | |
pohky | 2306ce740f | |
pohky | a723e0871d | |
Ehsan Kia | a084e422d0 | |
pohky | c7f19ddf06 | |
Jiiks | 9a087db3a8 | |
Jiiks | ab72fe0a0b | |
Jiiks | a74827d515 | |
Jiiks | 29303ea92a | |
Jiiks | 11f67dd4b7 | |
Jiiks | c8381267ed | |
pohky | a4d83d07b1 | |
Jiiks | d85fd95050 | |
Jiiks | b556f0a7de | |
pohky | 9e38ff567a | |
Jiiks | e35c003de8 | |
pohky | fc711076cd | |
pohky | 6c2d7ab661 | |
pohky | 06f4c5d138 | |
pohky | aec51443eb | |
pohky | 1b616b165a | |
pohky | 9b6a982084 | |
pohky | c2adc2ebe2 | |
pohky | d34f72b4e2 | |
pohky | e93f084a6c | |
pohky | c83021cc05 | |
pohky | d105bfd289 | |
pohky | abc0f0b469 | |
pohky | 7476810154 | |
pohky | c07d7fd638 | |
pohky | 5de975f188 | |
pohky | 98da9248c4 | |
pohky | 842aff647b | |
pohky | cb5898ae0c | |
pohky | 10d47be03c | |
pohky | ed558448d3 | |
Jiiks | 8a11655dcf | |
pohky | fe3352dd5f | |
pohky | 0590a25562 | |
pohky | 799ca59b0c | |
pohky | 1f674d3968 | |
pohky | 213083ef1e | |
pohky | e9c2d6a19a | |
Ehsan Kia | fe7fe345d7 | |
pohky | a54777afcf | |
Jiiks | 639655412d | |
Jiiks | c517ef6c3b | |
pohky | 3f98486e92 | |
Jiiks | 51ac3ab4ad | |
Jiiks | ce83b1ce25 | |
Jiiks | 2fd5667b3d | |
pohky | 23ec794f07 | |
pohky | ea8bf70e53 | |
pohky | 8b421a04f0 | |
pohky | 7e9024d658 | |
pohky | 4b21053bb2 | |
Jiiks | 7be821f9a5 | |
Jiiks | 9d50d8c1c8 | |
pohky | 80049a375e | |
pohky | 015e22e0ce | |
pohky | fa205cf7c6 | |
pohky | d7b3e10658 | |
pohky | 8a7b7e50f1 | |
pohky | aabd6ef947 | |
pohky | 87e978e13b | |
pohky | f10b01e14c | |
pohky | 8fb9175654 | |
Ehsan Kia | 428f6e0510 | |
pohky | abb257cf8f | |
pohky | be1ae216d3 | |
pohky | f65d224cec | |
Jiiks | 4faea66bff | |
pohky | 2c1dc27265 | |
pohky | 6b8fe6b5dc | |
pohky | 7a8db351a1 | |
pohky | d3302e5286 | |
pohky | 1b3f159119 | |
pohky | 8bbc236302 | |
pohky | 3f6296e867 | |
pohky | 27fece2e1e | |
pohky | 44aa9b6268 | |
pohky | cab9a609bf | |
pohky | e76468c2fb | |
pohky | e9023272a8 | |
pohky | aba679eb58 | |
Jiiks | 1ceef37b7b | |
pohky | 5beab21b7c | |
pohky | 3322a5ada9 | |
pohky | 037333e8de | |
pohky | b09a10e07d | |
pohky | 90edc67d8e | |
pohky | f6312eee85 | |
pohky | d5aebb5a72 | |
Jiiks | ea9808f12d | |
Jiiks | 325584372f | |
Jiiks | 3a365f30ed | |
Jiiks | 8a15d57e39 | |
Jiiks | 12122cc2cd | |
Jiiks | 6c3955c9d8 | |
pohky | 1e081df2b1 | |
Jiiks | ae10fa9542 | |
Jiiks | c9afc3d0d4 | |
Jiiks | 46de429953 | |
Jiiks | 3b657daffa | |
Jiiks | 31607f11e8 | |
Jiiks | 3821077eaf | |
Jiiks | 659dc085fd | |
Jiiks | 97bbc0f3d2 | |
Jiiks | 8e4d0b68f9 | |
Jiiks | 151428d0e2 | |
pohky | fd556b0ecb | |
pohky | 79c3ff7579 | |
pohky | c6d6c97ef6 | |
pohky | c139628a69 | |
Jiiks | 1d41646acc | |
Jiiks | e7b74ccd15 | |
Jiiks | 52489b6e0a | |
DeathStrikeV | 2f0dab4dca | |
Alex | 4d6467b1e0 | |
pohky | 74db61eed3 | |
Jiiks | 5cd211333d | |
Jiiks | 3b58822088 | |
Jiiks | 7cbfc6f19a | |
pohky | 4ad2fa5c38 | |
pohky | cb8f98e9a6 | |
Jiiks | 8ceea67da1 | |
Jiiks | 6397c1e527 | |
pohky | c537e541e6 | |
pohky | 95fd50d141 | |
Ehsan Kia | f001b44146 | |
Jiiks | a478c362fa | |
Jiiks | 797c37b623 | |
Jiiks | 753f2240dc | |
Jiiks | 47bf32fb8b | |
Jiiks | 2a8175e119 | |
Jiiks | 480254aedc | |
pohky | 14eedbb26c | |
pohky | e6ddddd7c5 | |
pohky | e24c78304f | |
pohky | 6b9fc31d80 | |
pohky | e29e1935a6 | |
pohky | 956b434a20 | |
Jiiks | 9eba040264 | |
Jiiks | d214a5909c | |
Jiiks | 19150649d6 | |
pohky | 1b91804142 | |
Jiiks | 4fc642cae1 | |
Raygius | 581452098a | |
pohky | be32d5c1ff | |
pohky | 422e5e786f | |
pohky | 14925e3a60 | |
pohky | cceaec68fa | |
pohky | d73145409a | |
pohky | 7dab7741fa | |
pohky | c46515954f | |
Jiiks | 6c496b1898 | |
Jiiks | 028d279cae | |
Jiiks | 870778b21d | |
SkyWielder | a84b23c9e4 | |
Jiiks | efc22eef6d | |
Jiiks | 7533169b35 | |
Jiiks | 4e521f09fc | |
Jiiks | b08a8f44a4 | |
pohky | 6d9299d247 | |
Galios | 052027a87f | |
pohky | a24492ccf7 | |
pohky | 48b6fdba91 | |
pohky | 3b8e41d914 | |
pohky | 5519e89df6 | |
pohky | c7be04cfc2 | |
pohky | 3ffa686b47 | |
pohky | de2acfd6d9 | |
pohky | 342c3e07ca | |
pohky | 025b634bf2 | |
pohky | 1d1c816696 | |
Jiiks | 0eb921b303 | |
pohky | 8d083ccc5c | |
pohky | 90165f020b | |
Raygius | 6ba126de6e | |
Raygius | f479f636b5 | |
pohky | a88d6c9ee8 | |
pohky | d7f936321e | |
pohky | a1283bf4f6 | |
pohky | 730d9e04e9 | |
pohky | ba0d089c13 | |
pohky | 20a50d39a8 | |
pohky | be39955f2d | |
pohky | 31b0a7c783 | |
Jiiks | c6990352db | |
pohky | af8519f7fe | |
pohky | a31a329f10 | |
Jiiks | 6e350f440b | |
Jiiks | 8047b953d1 | |
Jiiks | 78c7860c6c | |
Jiiks | f5953cf72e | |
Jiiks | 536cfab145 | |
pohky | 8f73d0ad10 | |
pohky | 90767d8fb5 | |
Treble Sketch | 51b0e0e5a3 | |
pohky | b9e1b71c0f | |
pohky | dfa344756a | |
pohky | f576d5b816 | |
pohky | ae10fffecd | |
pohky | ec9804a9a9 | |
Ehsan Kia | cbc93dd6ae | |
pohky | df7e780ca1 | |
pohky | 9f3ad4b976 | |
Jiiks | 24d0fe5c56 | |
pohky | bd3979b7d3 | |
Treble Sketch | b8ab889d97 | |
Treble Sketch | 67ffca5e5b | |
pohky | e37f909fc2 | |
pohky | 893440ae0a | |
pohky | 35f0efa0e0 | |
pohky | 6cb89d93d6 | |
Jiiks | 18dad0e83a | |
pohky | 4af669789c | |
pohky | e8e30469e7 | |
Jiiks | d17b1fe18f | |
AraHaan | 3e78699c4f | |
Jiiks | f789547e5a | |
Jiiks | 688998114e | |
Jiiks | 3bf30d0790 | |
Jiiks | 76d0b7d9fc | |
Bruno de Moura Ribeiro | 3d24701139 | |
pohky | 66e30fd54e | |
Jiiks | 4ec4daedbc | |
Jiiks | 3f5f9fb0ad | |
Jiiks | aeae9529aa | |
Jiiks | 286db2d316 | |
pohky | fc52d3dd6c | |
xiaolii | f55d1a27ef | |
Jiiks | c68a84ed03 | |
Jiiks | b767f9b4f7 | |
Jiiks | fd07b37bc7 | |
Jiiks | 51b1913ab1 | |
Jiiks | 2e914032ed | |
Jiiks | 3f61dc0928 | |
Jiiks | 3f6c51b046 | |
Jiiks | e2112c73c2 | |
Jiiks | 3dcacb5199 | |
Jiiks | e2c4546ca8 | |
Jiiks | 066a8789e9 | |
Jiiks | bd1274cf92 | |
Jiiks | 31fc14aa5c | |
FierceFrankie | 88729d94ce | |
MissKiri | 504224f4fa | |
Jiiks | 35ff38638c | |
Jiiks | 0d9728b26f | |
Jiiks | a8fb3b73d2 | |
Jiiks | 5a7ec4f0f8 | |
Jiiks | a1c868fe6c | |
pohky | 9d27626ad1 | |
Ehsan Kia | a8f8106d41 | |
Garett | c479e59966 | |
MissKiri | fa37d4515a | |
MissKiri | 821c5444c0 | |
MissKiri | feaef6224d | |
Forcellrus | 633135aee7 | |
Forcellrus | 21058ab993 | |
pohky | 3770edc13a | |
pohky | b62eaab1d9 | |
Ehsan Kia | d2fd44a406 | |
KeyPhact Moon | cdf7b262ed | |
pohky | caf0ae0f35 | |
pohky | 76c9b62ba9 | |
pohky | 463f843789 | |
pohky | 61824516a9 | |
unforgivingmorsecode | 8a1e77755e | |
Ehsan Kia | 4443d909a9 | |
pohky | b4114b3115 | |
pohky | 97cb511480 | |
GarettM | 8b86322434 | |
Jiiks | 256c2fdad7 | |
Jiiks | a5c27caacf | |
Jiiks | 4111949cbc | |
Jiiks | b9c7787465 | |
pohky | 9b5f7d7f6b | |
pohky | b654bc2003 | |
pohky | 51076d872e | |
pohky | 8626654559 | |
pohky | db548c1a26 | |
pohky | b641c17932 | |
pohky | 321d482b9f | |
pohky | a6a2f84a65 | |
Ehsan Kia | 91194c204f | |
pohky | 00b72c6436 | |
pohky | 9b193ef14b | |
pohky | 32a1f4a298 | |
pohky | d0f8bec1d4 | |
pohky | 8acab48afd | |
pohky | 83ad1a6dc9 | |
pohky | cdef86f518 | |
Isackender | ecb1794afe | |
pohky | c4f7539a79 | |
Master Kwoth | af08e9969b | |
pohky | d807811c9d | |
pohky | d8776e50be | |
CactusNugget | 267970883c | |
pohky | 885920788b | |
pohky | 9f4c9a8f42 | |
pohky | 87511e089f | |
pohky | dfc93d7ad5 | |
pohky | 7318f9a208 | |
pohky | 25e413ac7d | |
Nisserino | c20fd9fc77 | |
pohky | 15f9a17d5d | |
pohky | 302934698b | |
Ehsan Kia | 83b8b26d36 | |
pohky | d3bd15c1fe | |
pohky | b7f79b2cd8 | |
pohky | d15fcd1ca9 | |
pohky | 346c68ac61 | |
pohky | cae03de77d | |
Raygius | a32e4cfb1f | |
Jiiks | a441072751 | |
Jiiks | 496d984c2a | |
Jiiks | c441936005 | |
Jiiks | 362a5e3d40 | |
Jiiks | 486f98d1e9 | |
Jiiks | 712c1655d9 | |
Jiiks | 9336247180 | |
pohky | e9f79c382f | |
TopBunk | 943986954e | |
Jiiks | b0cfc3bb00 | |
Jiiks | 1ea96f9da8 | |
pohky | 3c276a84e0 | |
Ehsan Kia | 1795c5e6cb | |
Jiiks | ef265b8962 | |
pohky | 44b2f66695 | |
Ehsan Kia | e17709dcb8 | |
Jiiks | 9dcf8ff17e | |
jzanutto | 6f3026fff5 | |
pohky | 4cab5e6067 | |
pohky | 3106504ef4 | |
Jiiks | f62b53866d | |
Jiiks | 53d9a81849 | |
Jiiks | 7d4244e1cb | |
ivenovalue | bf8ab019c6 | |
ivenovalue | 95871691a0 | |
ivenovalue | 069fb6b05b | |
Jiiks | bcf4867474 | |
Jiiks | 5b5cacc0f3 | |
Jiiks | 3c843ada58 | |
Jiiks | d439923678 | |
Jiiks | 532776a696 | |
Jiiks | dc708de857 | |
Jiiks | 8b30e1df10 | |
Jiiks | ed6993cc08 | |
Jiiks | b18a22e83d | |
Jiiks | 14061f2c1e | |
Jiiks | 9090a41341 | |
Jiiks | 24f556d31b | |
pohky | c926443540 | |
Jiiks | dd1075879f | |
Jiiks | 9694e11137 | |
Jiiks | e9f1af2530 | |
Ehsan Kia | 1a4e5398d6 | |
SydneyAoi | 90fbaa4885 | |
lecomonad | 8bcbb5ec8f | |
pohky | 670ccf603c | |
Jiiks | 8d2af8186b | |
AraHaan | 1cb8b8ab3a | |
AraHaan | 07d1ce7c17 | |
AraHaan | 40262cd068 | |
Andrew Filip | 86b1059406 | |
Andrew Filip | d6b0a5994b | |
Jiiks | df542fe3c9 | |
Jiiks | e70ab32780 | |
Jiiks | 7a693d33b6 | |
Jiiks | 10c97ad59e | |
Jiiks | c82d38fd97 | |
Jiiks | 4b885483d3 | |
Jiiks | 88a75f5c2b | |
Jiiks | 09b75c984c | |
Jiiks | 288b401b26 | |
Ehsan Kia | 37e4c725b5 | |
AraHaan | 9d1a2f1302 | |
RinkuTalks | efeb191da2 | |
Ehsan Kia | 5a4f6b447c | |
unforgivingmorsecode | 6086040aa8 | |
Flash Deviant | e74cd1da3e | |
AraHaan | 3c0f1e8b3d | |
thefiz | 6a8dace6f6 | |
AraHaan | 1bf8127ebe | |
AraHaan | e913998453 | |
AraHaan | c4b855d7c5 | |
AraHaan | 6a0df69964 | |
AraHaan | c9711b6763 | |
Jiiks | b1478b2dc1 | |
Jiiks | aeabee9c25 | |
wedidntnamethisatall | d79b85f66d | |
unforgivingmorsecode | 58b7a1e9c0 | |
pohky | f5b5d2a7cc | |
SomecallmeSarge | 74ec2be036 | |
pohky | 5623a604d5 | |
OitoYuuki | 2ea87a7f40 | |
pohky | 69e6abdccf | |
unforgivingmorsecode | 7d347488f7 | |
Jiiks | 40345b7cdd | |
pohky | 5e2ef11cc9 | |
pohky | 562ffc9db5 | |
Sosolol261 | 9e51a142ff | |
Sosolol261 | 70b9af44a3 | |
pohky | b7e68d1f6b | |
pohky | 90f1c5dbac | |
3Dymensional | b380552e76 | |
pohky | 1c14050ecf | |
pohky | aa68763979 | |
Ehsan Kia | 79803ac4cf | |
3Dymensional | a1a80033da | |
pohky | b18d0a413d | |
Cy Timothy Johnson | 14a3824792 | |
pohky | f240c1f59e | |
Ehsan Kia | 259a21975d | |
Ehsan Kia | cd3004d669 | |
pohky | fc1a4fbfb7 | |
Hostail | b1516f301a | |
pohky | acb722c934 | |
Blitzcronk | a96bbb64f6 | |
pohky | dc9fba64ec | |
Kaeloron | 791f2b4dda | |
pohky | 92339a2e22 | |
Jiiks | e80c617bc2 | |
Jiiks | 4702ce7f38 | |
Jiiks | 5e40663761 | |
Jiiks | fe66b1ba65 | |
Jiiks | f8007f77fd | |
pohky | b6f655ceeb | |
pohky | cc5f1e6e36 | |
pohky | 7028868c57 | |
Jiiks | ee671c5540 | |
Ehsan Kia | 98c3620388 | |
Jiiks | 5d63b812b3 | |
Jiiks | b5ef38a5cb | |
Jiiks | a5923eb8ab | |
Jiiks | 81e03e5a79 | |
Jiiks | 3d4a5a8beb | |
Kaeloron | c09a06dbde | |
Jiiks | ebb9c7e0e6 | |
Mitchell | 2e9e46ae90 | |
Jiiks | a9c73c721a | |
pohky | a2fce66f54 | |
Ehsan Kia | c3b74660c9 | |
pohky | 666c317ffa | |
eBunny | bb78fc192a | |
Jiiks | 26ae69ed77 | |
Jiiks | a1cc5218ed | |
Ehsan Kia | b33dc0c536 | |
Ehsan Kia | ca4bacd805 | |
Ehsan Kia | 9cc0aab965 | |
zetterburn | 773da182a1 | |
Jiiks | a892cb5784 | |
Jiiks | 3dc9cd04ea | |
Jiiks | fd4c4af3a2 | |
Jiiks | 645ae53b04 | |
Jiiks | 2ec87e29db | |
pohky | 45d3ec2cc0 | |
ayydriel | 7d516b74f0 | |
Jiiks | ffd67dce4f | |
Jiiks | 87c49a61bd | |
Jiiks | 1b06641a24 | |
Jiiks | 40239dfc40 | |
Jiiks | 08d5eb0740 | |
Jiiks | a79cf31f35 | |
Jiiks | dcff0a3a51 | |
Jiiks | c2b491c0f4 | |
Jiiks | b51f1caf36 | |
Jiiks | 98bb5a3c79 | |
Alexis Martin | 0ded805494 | |
Dragomir Ioan | e82666dd55 | |
Jiiks | 1b23fd15f8 | |
Jiiks | 00e1dede75 | |
pohky | ef9c969d47 | |
Jiiks | c856a344cc | |
Jiiks | eaa5212f7e | |
Jiiks | d92c331986 | |
pohky | 06a8a85a4c | |
Jiiks | 43837e3c2f | |
pohky | 852ac71c3f | |
Jiiks | 355d264311 | |
Jiiks | 28ceac99e1 | |
eBunny | 066bbdcef5 | |
Jiiks | e5393d8a93 | |
Jiiks | 8ad112b77d | |
Jiiks | be84041d57 | |
Jiiks | c04efb568a | |
Jiiks | 36e6b4a229 | |
Jiiks | ca5965b178 | |
Jiiks | 65b651c9ec | |
Bluscream | d07b907c73 | |
Jiiks | 54043d0926 | |
Jiiks | 4d14cbaccd | |
Jiiks | ba50e4c433 | |
Jiiks | cc8c43170b | |
Jiiks | 60bd3ee16c | |
Jiiks | f2c45c550e | |
Jiiks | 3b5f5ce27a | |
williamnguyen13 | 21e66aac91 | |
Jiiks | 91984955e9 | |
Jiiks | cbcec9c289 | |
Jiiks | 4fb9cc809b | |
Jiiks | 8ccf3f78ff | |
Jiiks | 0d23d72198 | |
pohky | 0f190d965d | |
Jiiks | eb03f5a85a | |
Jiiks | b0c5fe130d | |
Jiiks | 08e1f1e9ea | |
Jiiks | 6d3bed07e4 | |
Jiiks | 31f40f33ab | |
Jiiks | a9c8428442 | |
Jiiks | 8f3b498922 | |
Jiiks | 806979ee41 | |
Jiiks | b9e32b80e0 | |
Jiiks | c0f8c43f1b | |
Jiiks | ddbaf290b3 | |
Jiiks | 9369be568b | |
Jiiks | ffa570b08c | |
Jiiks | beb542aab6 | |
KaizoGT | d5e72f744b | |
Jiiks | ad578fdc47 | |
Jiiks | a7220fd2fc | |
Jiiks | 082aaae415 | |
Jiiks | 15276acda5 | |
Jiiks | 59036404fe | |
MyMeatStick | bb029c49fe | |
Jiiks | 9bfafc7866 | |
Jiiks | cdffc9917b | |
Jiiks | 9afe4888e0 | |
Jiiks | 12f1b0934e | |
Jiiks | 514bec9dcd | |
Jiiks | 9f07aa3670 | |
Jiiks | 670dc81562 | |
Jiiks | 3f62190c5b | |
Jiiks | bf53a3dfa1 | |
Jiiks | af711f0921 | |
Jiiks | 3fe477d643 | |
KaizoGT | 62e9cd846b | |
Jiiks | 9d340b41d1 | |
Jiiks | 2eeadd4c3c | |
Jiiks | bfb855081c | |
Jiiks | 86749ad34a | |
Jiiks | 54bc94bb07 | |
Jiiks | cfd600e5f4 | |
Jiiks | 5f8eeedd1b | |
Jiiks | fcb189b9c1 | |
Jiiks | d2c117f346 | |
Jiiks | 880e1bfa7b | |
Jiiks | 9c9737fee7 | |
Jiiks | c53474f77b | |
Jiiks | 293725202b | |
Jiiks | 0fdfcaa3e8 | |
Jiiks | 105a04a000 | |
pohky | 9c21586661 | |
Jiiks | d4f18fbfaf | |
Jiiks | b2a8034645 | |
Jiiks | 60d6ed4197 | |
Bluscream | 70d316997f | |
Jiiks | f934e4f833 | |
pohky | 5890805644 | |
Jiiks | 20e7a12345 | |
Jiiks | 68f4082b73 | |
Jiiks | 4295ed9da3 | |
Jiiks | 14ed68fe8f | |
Jiiks | a699450133 | |
Martin Panin | 4a6bf85aff | |
Florian Weber | 5ccfb9fa68 | |
Legsbiter | 55b4d612e9 | |
Legsbiter | c01ba1e5bc | |
Jiiks | 9c20612ac9 | |
Jiiks | 2efcdedf83 | |
Jiiks | 10adc3cd4a | |
pohky | 83bdf85eba | |
Jiiks | ac774930e5 | |
Jiiks | d24a6222dd | |
Jiiks | cf7c68cef0 | |
Jiiks | 9d306ed4e1 | |
Jiiks | a1f55a7be1 | |
Jiiks | 261659d387 | |
Jiiks | 5330884a7b | |
Jiiks | a88fd77fd6 | |
Jiiks | 8dafff1b34 | |
Jiiks | 3459d52dbd | |
Jiiks | c369887d2b | |
Jiiks | 29f69ffd8a | |
Brightqwerty12 | 83b8f916fb | |
Jiiks | 02379bd5d2 | |
Jiiks | 855211ef5a | |
pohky | 9b8be4da48 | |
Jiiks | aa68bbeaeb | |
Jiiks | 4c8c5c4115 | |
pohky | d439b0eef1 | |
Jiiks | 2733ade804 | |
Sinyxs | c8a3c13cee | |
Jiiks | 4eb04c1a09 | |
Jiiks | a1de5d7490 | |
Jiiks | 8c044f2488 | |
Jiiks | 09fa5b5a34 | |
Jiiks | e93b5fdc73 | |
Jiiks | b1c0ada422 | |
Jiiks | 36155be600 | |
Cassius Qweuro | a9e7c0541e | |
Jiiks | 45cba396d1 | |
Jiiks | 8c4d689d44 | |
Jiiks | 9d6427e4c0 | |
Jiiks | bdab795306 | |
Jiiks | 5c3c06a143 | |
Jiiks | ebd6e90c5f | |
Jiiks | 30eabbca68 | |
Jiiks | 2af8ac283a | |
Jiiks | 2f229356ef | |
Jiiks | 0f641a5f1d | |
Jiiks | 08579a2035 | |
Jiiks | ee6dfa5e61 | |
Jiiks | c6e92cda41 | |
Jiiks | 1580f0b790 | |
Jiiks | 191ef2b43d | |
Jiiks | e62aa36b51 | |
Jiiks | aec8f9b93f | |
Jiiks | 41752faf1d | |
Jiiks | 85d947c122 | |
Jiiks | 4262dcf111 | |
Jiiks | 8520f1512d | |
Jiiks | 2d95d001b6 | |
Jiiks | ff1d5e6888 | |
Jiiks | e789abaf3f | |
Jiiks | d8ec3ad1dc | |
Jiiks | b66146d881 | |
Jiiks | 0caedb5a51 | |
Jiiks | 4086e399c1 | |
Jiiks | 203f1238e6 | |
Jiiks | e7cf6e2667 | |
Jiiks | 63037bac1d | |
Jiiks | f885108947 | |
Jiiks | 8662b35dc9 | |
Jiiks | b58b52f902 | |
Jiiks | d66b61ae40 | |
Jiiks | b379e570cd | |
Jiiks | de24f954f2 | |
Bluscream | 62edd607f5 | |
Jiiks | 7bb2f3b6f9 | |
Jiiks | dfa6fbe87f | |
Jiiks | 7d606f44f9 | |
Jiiks | 2574de3a69 | |
Jiiks | 63a8d60ee1 | |
Jiiks | 97daae9090 | |
Jiiks | 6e621098e3 | |
Jiiks | a32769ce43 | |
Jiiks | 7e92798513 | |
Jiiks | b3d9e1fe40 | |
Jiiks | 1fec4fbaa8 | |
Jiiks | 493944e991 | |
Jiiks | 2fe458f8bd | |
Jiiks | 2b673e2e96 | |
Jiiks | d03420f053 | |
Jiiks | 7ff31931c0 | |
Jiiks | 31d0ca3e34 | |
pohky | aa03678107 | |
Jiiks | e17ed347e4 | |
Jiiks | f606d62543 | |
pohky | 1fc56e29ea | |
Jiiks | 2ac9acc1ff | |
Jiiks | f28fb85a20 | |
Jiiks | 13f4ca29e1 | |
Jiiks | 986f392846 | |
Jiiks | 8e22483d06 | |
Jiiks | a78f6ef9c2 | |
pohky | 16b29fd8aa | |
Jiiks | e3f9055fea | |
Jiiks | 1eac4bfe3e | |
The-Ringing-Bell | c9c8c9bb8f | |
Jiiks | 2f925f9695 | |
zSoulweaver | 47c6d92b32 | |
Jiiks | 9bd56951ca | |
pohky | 100695e367 | |
Jiiks | e3b1821fcf | |
Jiiks | 4cdf621fa9 | |
Jiiks | 5bf9b23770 | |
Duncan Bristow | fce376923a | |
Jiiks | b8c0eb4be8 | |
Jiiks | 0d4fce33a9 | |
Jiiks | 14f3cf6442 | |
pohky | 7fd5728af3 | |
Jiiks | bc67c99983 | |
Jiiks | 35894f2cec | |
xwmario | be52c7c259 | |
Jiiks | 9c69d8cac6 | |
Jiiks | 8eb8695465 | |
Jiiks | c82ecaedd8 | |
pohky | b379975180 | |
pohky | 387e58d7cf | |
pohky | 7f3e124958 | |
Jiiks | edd66f9930 | |
Jiiks | 24d94b6945 | |
Jiiks | 7ac09d18f6 | |
pohky | 6802496f69 | |
Jiiks | 4062d4d88a | |
pohky | 2fc7a1c91e | |
Jiiks | 3f4d922016 | |
Jiiks | e062f9317f | |
Jiiks | c89e3d4609 | |
Jiiks | 8b8a4de8fe | |
Jiiks | 074288fc8d | |
pohky | 6b06438daa | |
pohky | 6e4f3c5e75 | |
Jiiks | 9e0993ae81 | |
pohky | 9e1234cc40 | |
Jiiks | 3509c67faa | |
Jiiks | 88cbac4c1f | |
Jiiks | 8cfcc92162 | |
Jiiks | b0269f10bf | |
Jiiks | 6f66ad26d5 | |
Jiiks | 19637bb5c6 | |
Jiiks | ec6f54b4a8 | |
Jiiks | 6321e64e52 | |
pohky | 5c79f9c432 | |
zSoulweaver | 91b90f956d | |
zSoulweaver | e010b9fab8 | |
zSoulweaver | b3556b947e | |
zSoulweaver | cf2456ae63 | |
Jiiks | f9d0e6916f | |
pendo324 | 9df32d3b5e | |
Jiiks | 10aa237f29 | |
Jiiks | c589efbb45 | |
pohky | 87fa7d3d52 | |
Jiiks | 0f65d7d21d | |
Jiiks | 0a4c73a56e | |
Jiiks | 8a2274819d | |
Jiiks | 729a6dd922 | |
pohky | 123e0a5a04 | |
Jiiks | 5e58fc6271 | |
Jiiks | d70a7ec87b | |
Jiiks | eb33713290 | |
Jiiks | 09894971e9 | |
Jiiks | 3d29d6267c | |
Jiiks | aad1229a20 | |
pohky | c39d099e03 | |
pohky | 0ee68260ea | |
Jiiks | bb709a5352 | |
Jiiks | 3eb646a5c0 | |
pohky | e8614b9b78 | |
sakuzyo | 792e62fbc0 | |
Jiiks | 33cfd24a8e | |
Jiiks | a3093940d4 | |
Jiiks | 66bfbabb8d | |
Pohky | 29e54c1219 | |
Jiiks | 81bf11c464 | |
Jiiks | 0ba19bf6b6 | |
Jiiks | bd16913387 | |
Jiiks | 50f7b60097 | |
zSoulweaver | be92b02ce8 | |
Jiiks | 154bf90d58 | |
Jiiks | 4d16b9fd89 | |
Jiiks | 4c622234ee | |
Jiiks | a738ad01d2 | |
Jiiks | eb5abc67d7 | |
Jiiks | d919a539b0 | |
Jiiks | 00b9433717 | |
Jiiks | 674460f32b | |
Jiiks | 79bb8a19c6 | |
Jiiks | c0b0a82f5f | |
Jiiks | 13bd03d3e6 | |
Jiiks | cf5c4ffa9b | |
Jiiks | 7f15dc8d57 | |
Jiiks | 6e37027f97 | |
Jiiks | 7868680389 | |
Jiiks | 6b33fcea42 | |
Jiiks | ca64ece346 | |
Jiiks | 02db4d1822 | |
Jiiks | 3c149ce3fe | |
Jiiks | 1b3c551515 | |
Jiiks | 62b79e8150 | |
Jiiks | 26f88a3c5c | |
Jiiks | ec6bea553b | |
Jiiks | 450c8970d1 | |
Jiiks | af3a0204f0 | |
Jiiks | cef0317b78 | |
Jiiks | de97c3bcba | |
Jiiks | a89d722edb | |
Jiiks | ff9533f226 | |
Jiiks | 4effe41b6b | |
Jiiks | 9f93c813d5 | |
Jiiks | bf0657a3df | |
pendo324 | c1d38d1edd | |
pendo324 | ba091dac54 | |
Jiiks | 38bf6af2e0 | |
Jiiks | 2357470398 | |
Jiiks | 6a09a7d6ca | |
Jiiks | 62e3eb1adb | |
zSoulweaver | fdbffbca1c | |
Jiiks | a47dc0bde3 | |
Alexis Martin | a3ab204503 | |
Jiiks | ed8cbfb535 | |
Jiiks | a6e0dd4ab8 | |
Jiiks | cfdb2f5037 | |
Jiiks | 713c11248d | |
Jiiks | 8f37203561 | |
Jiiks | 473c73229d | |
Jiiks | afe26090e5 | |
Jiiks | e4b721ead8 | |
Jiiks | 5d262f6742 | |
Jiiks | 25994163bd | |
Jiiks | 254f00eff9 | |
Jiiks | 9286fe6590 | |
Jiiks | 9f18f2a5b5 | |
Jiiks | acc5d4c305 | |
Jiiks | 6e741ba652 | |
Jiiks | 692ecc3452 | |
Jiiks | 2ebf47f5a3 | |
Jiiks | f95ecc12e3 | |
Jiiks | 0121c00a2c | |
Jiiks | b32e503dbf | |
Jiiks | c61323a577 | |
Jiiks | 83394eb2c8 | |
Jiiks | 007e7cfae3 | |
Jiiks | 2885782975 | |
Jiiks | a29901e0d4 | |
Jiiks | 9a2edd1689 | |
Jiiks | adef28d241 | |
Jiiks | 8ee0b97c00 | |
Jiiks | e93db396f3 | |
Jiiks | 60fdc8ccc4 | |
Duncan Bristow | 5f4b380db4 | |
Duncan Bristow | 11b4799ec9 | |
Jiiks | 9f7d0b6b2c | |
Jiiks | 306a00152a | |
Jiiks | 5442b1d5a7 | |
Jiiks | 8ec1d0d6db | |
Jiiks | 0bd797d89e | |
Jiiks | 69d85fe54a | |
Jiiks | d4e12a6ce3 | |
Jiiks | bbf2c91f8c | |
Jiiks | 585fc7f1f4 | |
Jiiks | dc07c79297 | |
Jiiks | f5ad2ab119 | |
Jiiks | 19b31e2a3d | |
Jiiks | 9c87dd0979 | |
Jiiks | f211b26621 | |
Jiiks | 785d116c60 | |
Jiiks | 5c53da2dbe | |
Jiiks | fe09c506c5 | |
Jiiks | 52cec81afb | |
Jiiks | 42cea45430 | |
Jiiks | 71b94ee664 | |
Jiiks | ade49315ed | |
Jiiks | d2d7d0ca40 | |
Jiiks | 44a9f642c5 | |
Duncan Bristow | 3787d3fcac | |
Jiiks | 9e70538497 | |
Jiiks | 303b31f568 | |
Jiiks | 563976e475 | |
Jiiks | d6825d9456 | |
Jiiks | 2a13262d81 | |
Jiiks | cdf174b09c | |
Jiiks | 94efef70b7 | |
Jiiks | d190cc33b8 | |
Jiiks | 6bcbd0807f | |
Duncan Bristow | 234c0656dd | |
Jiiks | 9348865ec7 | |
Jiiks | 180f76b64f | |
Jiiks | d4ed15be9d | |
Jiiks | d1b9b26050 | |
Jiiks | f17545e4b0 | |
Jiiks | 0560bd4e8c | |
Jiiks | ba7bb20ab8 | |
Jiiks | 0a60423de7 | |
Jiiks | 9fbba24997 | |
Jiiks | 893d7c214d | |
Jiiks | 72b8598f78 | |
Jiiks | 860c7991de | |
Jiiks | 8492626dc7 | |
Jiiks | c110a2b5ed | |
Jiiks | 78aeb95f36 | |
Jiiks | 3ff5e70b20 | |
Jiiks | 4c39b77304 | |
Jiiks | 6336861ba2 | |
Jiiks | df2e2aa9b4 | |
Jiiks | 4c66e9eb7d | |
Jiiks | 3c355e27e9 | |
Jiiks | 406ea6521c | |
Jiiks | 8e28a7d006 | |
Jiiks | eb95968bc1 | |
Jiiks | edc3c6834c | |
Jiiks | 11e9b33781 | |
Jiiks | a192dfd555 | |
Jiiks | 04ef234d0f | |
Jiiks | 5f38e8c4f7 | |
Jiiks | b015148f21 | |
Jiiks | 59998e13e7 | |
Jiiks | 758913c1b9 | |
Jiiks | fe293ff953 | |
Jiiks | 0da5b80088 | |
Jiiks | 04ab223bb1 | |
Jiiks | 3d2b5e3286 | |
Jiiks | 15b7ea130b | |
Jiiks | 70a8c27e04 | |
Jiiks | f3f6485d37 | |
Jiiks | c632d0ecb9 | |
Jiiks | aff5e6aab7 | |
Jiiks | 39cb49f573 | |
Jiiks | 2e77c598a1 | |
Jiiks | 0fb9816490 | |
Jiiks | c071452428 | |
Jiiks | 9c3169d80f | |
Jiiks | 32110e8c9c | |
Jiiks | ec89afe953 | |
Jiiks | d9eabe50a2 | |
Jiiks | b84c8b928b | |
Jiiks | 14e703a04f | |
Jiiks | 812d043e3f | |
Jiiks | dc4fd13ee5 | |
Jiiks | 6085e97579 | |
Jiiks | 356aa31d15 | |
Jiiks | 50fc11cc04 | |
Jiiks | c4866e3625 | |
Jiiks | 02fe5677fe | |
Jiiks | c286e288c3 | |
Jiiks | 3df9651f0b | |
Jiiks | 0b0932329f | |
Jiiks | c9b0408cf4 | |
Jiiks | b259b08b32 | |
Jiiks | 30083aa955 | |
Jiiks | 0ada2078be | |
Jiiks | 62a6884bc6 | |
Jiiks | bd15f51b33 | |
Jiiks | f0183adc68 | |
Jiiks | aeb7836581 | |
Jiiks | 3c08f25a60 | |
Jiiks | a5143dca2d | |
Jiiks | 2d325966b5 | |
Jiiks | b4c7c7196e |
|
@ -0,0 +1,15 @@
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
|
[package.json]
|
||||||
|
indent_size = 2
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
*.sh text=auto
|
|
@ -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? -->
|
|
@ -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
|
|
@ -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? -->
|
|
@ -1 +1,16 @@
|
||||||
.idea/*
|
# Generated files
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
etc
|
||||||
|
release
|
||||||
|
|
||||||
|
tests/tmp
|
||||||
|
tests/log.txt
|
||||||
|
|
||||||
|
# User data
|
||||||
|
tests/data
|
||||||
|
user.config.json
|
||||||
|
/.vs
|
||||||
|
/npm-debug.log
|
||||||
|
/tests/ext/extensions
|
||||||
|
/tests/userdata
|
||||||
|
|
|
@ -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 ]
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
language: node_js
|
||||||
|
|
||||||
|
node_js:
|
||||||
|
- stable
|
||||||
|
|
||||||
|
branches:
|
||||||
|
only:
|
||||||
|
- master
|
||||||
|
|
||||||
|
addons:
|
||||||
|
apt:
|
||||||
|
packages:
|
||||||
|
- libsecret-1-dev
|
|
@ -0,0 +1,9 @@
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2015-present JsSucks - https://github.com/JsSucks
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
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.
|
|
@ -1,131 +0,0 @@
|
||||||
/*
|
|
||||||
* BetterDiscordApp Installer v0.3.2
|
|
||||||
*/
|
|
||||||
|
|
||||||
var dver = "0.0.280";
|
|
||||||
|
|
||||||
var asar = require('asar');
|
|
||||||
var wrench = require('wrench');
|
|
||||||
var fs = require('fs');
|
|
||||||
var readline = require('readline');
|
|
||||||
var util = require('util');
|
|
||||||
|
|
||||||
var _discordPath;
|
|
||||||
var _appFolder = "\\app";
|
|
||||||
var _appArchive = "\\app.asar";
|
|
||||||
var _packageJson = _appFolder + "\\package.json";
|
|
||||||
var _index = _appFolder + "\\app\\index.js";
|
|
||||||
|
|
||||||
function install() {
|
|
||||||
|
|
||||||
_discordPath = process.env.LOCALAPPDATA + "\\Discord\\app-"+dver+"\\resources";
|
|
||||||
console.log("Looking for discord resources at: " + _discordPath);
|
|
||||||
|
|
||||||
fs.exists(_discordPath, function(exists) {
|
|
||||||
|
|
||||||
if(exists) {
|
|
||||||
console.log("Discord resources found at: " + _discordPath + "\nLooking for app folder");
|
|
||||||
|
|
||||||
if(fs.existsSync(_discordPath + _appFolder)) {
|
|
||||||
console.log("Deleting " + _discordPath + _appFolder + " folder.");
|
|
||||||
wrench.rmdirSyncRecursive(_discordPath + _appFolder);
|
|
||||||
console.log("Deleted " + _discordPath + _appFolder + " folder.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if(fs.existsSync(_discordPath + "\\node_modules\\BetterDiscord")) {
|
|
||||||
console.log("Deleting " + _discordPath + "\\node_modules\\BetterDiscord" + " folder.");
|
|
||||||
wrench.rmdirSyncRecursive(_discordPath + "\\node_modules\\BetterDiscord");
|
|
||||||
console.log("Deleted " + _discordPath + "\\node_modules\\BetterDiscord" + " folder.");
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Copying BetterDiscord");
|
|
||||||
|
|
||||||
fs.mkdirSync(_discordPath + "\\node_modules\\BetterDiscord");
|
|
||||||
|
|
||||||
wrench.copyDirSyncRecursive(__dirname + "\\BetterDiscord\\", _discordPath + "\\node_modules\\BetterDiscord\\", {forceDelete: true});
|
|
||||||
|
|
||||||
console.log("Looking for app archive");
|
|
||||||
if(fs.existsSync(_discordPath + _appArchive)) {
|
|
||||||
console.log("App archive found at: " + _discordPath + _appArchive);
|
|
||||||
} else {
|
|
||||||
console.log("Failed to locate app archive at: " + _discordPath + _appArchive);
|
|
||||||
process.exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Extracting app archive");
|
|
||||||
|
|
||||||
|
|
||||||
asar.extractAll(_discordPath + _appArchive, _discordPath + _appFolder);
|
|
||||||
|
|
||||||
fs.exists(_discordPath + _appFolder, function(exists) {
|
|
||||||
if(exists) {
|
|
||||||
console.log("Extracted to: " + _discordPath + _appFolder);
|
|
||||||
console.log("Injecting index.js");
|
|
||||||
|
|
||||||
var data = fs.readFileSync(_discordPath + _index).toString().split("\n");
|
|
||||||
data.splice(83, 0, 'var _betterDiscord = require(\'betterdiscord\');\n');
|
|
||||||
data.splice(497, 0, '_betterDiscord = new _betterDiscord.BetterDiscord(mainWindow); \n _betterDiscord.init(); \n');
|
|
||||||
|
|
||||||
|
|
||||||
fs.writeFile(_discordPath + _index, data.join("\n"), function(err) {
|
|
||||||
if(err) return console.log(err);
|
|
||||||
|
|
||||||
console.log("Injected index.js");
|
|
||||||
console.log("Injecting package.json");
|
|
||||||
|
|
||||||
var data = fs.readFileSync(_discordPath + _packageJson).toString().split("\n");
|
|
||||||
data.splice(10, 0, '"betterdiscord":"^0.1.2",');
|
|
||||||
|
|
||||||
fs.writeFile(_discordPath + _packageJson, data.join("\n"), function(err) {
|
|
||||||
if(err) return console.log(err);
|
|
||||||
|
|
||||||
console.log("Injected package.json");
|
|
||||||
console.log("Looks like were done here :)");
|
|
||||||
process.exit();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
} else {
|
|
||||||
console.log("Something went wrong, rerun.");
|
|
||||||
process.exit();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
} else {
|
|
||||||
console.log("Discord resources not found at: " + _discordPath);
|
|
||||||
process.exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function init() {
|
|
||||||
|
|
||||||
console.log("BetterDiscord Simple Installer v0.3 for Discord "+dver+" by Jiiks.");
|
|
||||||
console.log("If Discord has updated then download the latest installer.");
|
|
||||||
|
|
||||||
var rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
||||||
|
|
||||||
rl.question("The following directories will be deleted if they exists: discorpath\\app, discordpath\\node_modules\\BetterDiscord, is this ok? Y/N", function(answer) {
|
|
||||||
|
|
||||||
var alc = answer.toLowerCase();
|
|
||||||
|
|
||||||
switch(alc) {
|
|
||||||
case "y":
|
|
||||||
install();
|
|
||||||
break;
|
|
||||||
case "yes":
|
|
||||||
install();
|
|
||||||
break;
|
|
||||||
case "n":
|
|
||||||
process.exit();
|
|
||||||
break;
|
|
||||||
case "no":
|
|
||||||
process.exit();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
init();
|
|
|
@ -1 +0,0 @@
|
||||||
cmd /k node.exe index.js
|
|
78
README.md
78
README.md
|
@ -1,72 +1,12 @@
|
||||||
# BetterDiscordApp
|
# BetterDiscordApp [![Travis][build-badge]][build] [![Snyk][snyk-badge]][snyk-url] [![deps][deps-badge]][deps-url] [![devdeps][devdeps-badge]][devdeps-url]
|
||||||
Better Discord App enhances Discord desktop app with new features.
|
|
||||||
|
|
||||||
![ss](http://puu.sh/jTEBB.png)
|
[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
|
||||||
|
|
||||||
## If Discord has updated and the installer hasn't, try replacing the installer index.js with the latest one here: [index.js](https://github.com/Jiiks/BetterDiscordApp/blob/master/NodeInstaller/index.js)
|
[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
|
||||||
## Auto Installation
|
[devdeps-url]: https://david-dm.org/JsSucks/BetterDiscordApp?type=dev
|
||||||
* Download the latest package from [releases](https://github.com/Jiiks/BetterDiscordApp/releases)
|
|
||||||
* Run the installer
|
|
||||||
* Hopefully it works.
|
|
||||||
* Installer requires [node](https://nodejs.org/download/) download the binaries and place in the same folder as the installer if you don't have node installed.
|
|
||||||
* Installer uses [asar](https://github.com/atom/asar) which is bundled with the installer.
|
|
||||||
* Installer uses [wrench](https://github.com/ryanmcgrath/wrench-js) which is bundled with the installer.
|
|
||||||
|
|
||||||
## Manual Installation
|
|
||||||
* Extract app.asar
|
|
||||||
* Add BetterDiscord as a dependency
|
|
||||||
* Add init to Discord load event
|
|
||||||
* Move BetterDiscord to node_modules
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
**Emotes:**
|
|
||||||
BetterDiscord adds all [Twitch.tv](http://twitch.tv), some [FrankerFaceZ](http://frankerfacez.com)(~240 suggested emotes) and [BetterTTV](http://betterttv.net)(soon) emotes to Discord.
|
|
||||||
|
|
||||||
**Quick Emote Menu:**
|
|
||||||
Quick Emote Menu adds a menu for quickly adding twitch emotes.
|
|
||||||
|
|
||||||
**Emote Autocapitalize:**
|
|
||||||
Automatically capitalize [Twitch.tv](http://twitch.tv) global emotes.
|
|
||||||
|
|
||||||
**Emote Autocomplete:**
|
|
||||||
Automatically completes/suggests emotes.(soon)
|
|
||||||
|
|
||||||
**Minimal Mode:**
|
|
||||||
Minimal mode makes elements smaller and hides certain elements.
|
|
||||||
|
|
||||||
**Public Servers:**
|
|
||||||
A menu for displaying public servers.(soon) [Serverlist](https://github.com/Jiiks/BetterDiscordApp/blob/master/serverlist.json)
|
|
||||||
|
|
||||||
**Save Logs Locally:**
|
|
||||||
Save chatlogs locally.(soon)
|
|
||||||
|
|
||||||
## Adding you server to public servers
|
|
||||||
Edit the [Serverlist](https://github.com/Jiiks/BetterDiscordApp/blob/master/serverlist.json) and submit a pull request.
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) <year> <copyright holders>
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
|
@ -1,63 +0,0 @@
|
||||||
###############################################################################
|
|
||||||
# Set default behavior to automatically normalize line endings.
|
|
||||||
###############################################################################
|
|
||||||
* text=auto
|
|
||||||
|
|
||||||
###############################################################################
|
|
||||||
# Set default behavior for command prompt diff.
|
|
||||||
#
|
|
||||||
# This is need for earlier builds of msysgit that does not have it on by
|
|
||||||
# default for csharp files.
|
|
||||||
# Note: This is only used by command line
|
|
||||||
###############################################################################
|
|
||||||
#*.cs diff=csharp
|
|
||||||
|
|
||||||
###############################################################################
|
|
||||||
# Set the merge driver for project and solution files
|
|
||||||
#
|
|
||||||
# Merging from the command prompt will add diff markers to the files if there
|
|
||||||
# are conflicts (Merging from VS is not affected by the settings below, in VS
|
|
||||||
# the diff markers are never inserted). Diff markers may cause the following
|
|
||||||
# file extensions to fail to load in VS. An alternative would be to treat
|
|
||||||
# these files as binary and thus will always conflict and require user
|
|
||||||
# intervention with every merge. To do so, just uncomment the entries below
|
|
||||||
###############################################################################
|
|
||||||
#*.sln merge=binary
|
|
||||||
#*.csproj merge=binary
|
|
||||||
#*.vbproj merge=binary
|
|
||||||
#*.vcxproj merge=binary
|
|
||||||
#*.vcproj merge=binary
|
|
||||||
#*.dbproj merge=binary
|
|
||||||
#*.fsproj merge=binary
|
|
||||||
#*.lsproj merge=binary
|
|
||||||
#*.wixproj merge=binary
|
|
||||||
#*.modelproj merge=binary
|
|
||||||
#*.sqlproj merge=binary
|
|
||||||
#*.wwaproj merge=binary
|
|
||||||
|
|
||||||
###############################################################################
|
|
||||||
# behavior for image files
|
|
||||||
#
|
|
||||||
# image files are treated as binary by default.
|
|
||||||
###############################################################################
|
|
||||||
#*.jpg binary
|
|
||||||
#*.png binary
|
|
||||||
#*.gif binary
|
|
||||||
|
|
||||||
###############################################################################
|
|
||||||
# diff behavior for common document formats
|
|
||||||
#
|
|
||||||
# Convert binary document formats to text before diffing them. This feature
|
|
||||||
# is only available from the command line. Turn it on by uncommenting the
|
|
||||||
# entries below.
|
|
||||||
###############################################################################
|
|
||||||
#*.doc diff=astextplain
|
|
||||||
#*.DOC diff=astextplain
|
|
||||||
#*.docx diff=astextplain
|
|
||||||
#*.DOCX diff=astextplain
|
|
||||||
#*.dot diff=astextplain
|
|
||||||
#*.DOT diff=astextplain
|
|
||||||
#*.pdf diff=astextplain
|
|
||||||
#*.PDF diff=astextplain
|
|
||||||
#*.rtf diff=astextplain
|
|
||||||
#*.RTF diff=astextplain
|
|
|
@ -1,156 +0,0 @@
|
||||||
## Ignore Visual Studio temporary files, build results, and
|
|
||||||
## files generated by popular Visual Studio add-ons.
|
|
||||||
|
|
||||||
# User-specific files
|
|
||||||
*.suo
|
|
||||||
*.user
|
|
||||||
*.sln.docstates
|
|
||||||
|
|
||||||
# Build results
|
|
||||||
|
|
||||||
[Dd]ebug/
|
|
||||||
[Rr]elease/
|
|
||||||
x64/
|
|
||||||
build/
|
|
||||||
[Bb]in/
|
|
||||||
[Oo]bj/
|
|
||||||
|
|
||||||
# Enable "build/" folder in the NuGet Packages folder since NuGet packages use it for MSBuild targets
|
|
||||||
!packages/*/build/
|
|
||||||
|
|
||||||
# MSTest test Results
|
|
||||||
[Tt]est[Rr]esult*/
|
|
||||||
[Bb]uild[Ll]og.*
|
|
||||||
|
|
||||||
*_i.c
|
|
||||||
*_p.c
|
|
||||||
*.ilk
|
|
||||||
*.meta
|
|
||||||
*.obj
|
|
||||||
*.pch
|
|
||||||
*.pdb
|
|
||||||
*.pgc
|
|
||||||
*.pgd
|
|
||||||
*.rsp
|
|
||||||
*.sbr
|
|
||||||
*.tlb
|
|
||||||
*.tli
|
|
||||||
*.tlh
|
|
||||||
*.tmp
|
|
||||||
*.tmp_proj
|
|
||||||
*.log
|
|
||||||
*.vspscc
|
|
||||||
*.vssscc
|
|
||||||
.builds
|
|
||||||
*.pidb
|
|
||||||
*.log
|
|
||||||
*.scc
|
|
||||||
|
|
||||||
# Visual C++ cache files
|
|
||||||
ipch/
|
|
||||||
*.aps
|
|
||||||
*.ncb
|
|
||||||
*.opensdf
|
|
||||||
*.sdf
|
|
||||||
*.cachefile
|
|
||||||
|
|
||||||
# Visual Studio profiler
|
|
||||||
*.psess
|
|
||||||
*.vsp
|
|
||||||
*.vspx
|
|
||||||
|
|
||||||
# Guidance Automation Toolkit
|
|
||||||
*.gpState
|
|
||||||
|
|
||||||
# ReSharper is a .NET coding add-in
|
|
||||||
_ReSharper*/
|
|
||||||
*.[Rr]e[Ss]harper
|
|
||||||
|
|
||||||
# TeamCity is a build add-in
|
|
||||||
_TeamCity*
|
|
||||||
|
|
||||||
# DotCover is a Code Coverage Tool
|
|
||||||
*.dotCover
|
|
||||||
|
|
||||||
# NCrunch
|
|
||||||
*.ncrunch*
|
|
||||||
.*crunch*.local.xml
|
|
||||||
|
|
||||||
# Installshield output folder
|
|
||||||
[Ee]xpress/
|
|
||||||
|
|
||||||
# DocProject is a documentation generator add-in
|
|
||||||
DocProject/buildhelp/
|
|
||||||
DocProject/Help/*.HxT
|
|
||||||
DocProject/Help/*.HxC
|
|
||||||
DocProject/Help/*.hhc
|
|
||||||
DocProject/Help/*.hhk
|
|
||||||
DocProject/Help/*.hhp
|
|
||||||
DocProject/Help/Html2
|
|
||||||
DocProject/Help/html
|
|
||||||
|
|
||||||
# Click-Once directory
|
|
||||||
publish/
|
|
||||||
|
|
||||||
# Publish Web Output
|
|
||||||
*.Publish.xml
|
|
||||||
|
|
||||||
# NuGet Packages Directory
|
|
||||||
## TODO: If you have NuGet Package Restore enabled, uncomment the next line
|
|
||||||
#packages/
|
|
||||||
|
|
||||||
# Windows Azure Build Output
|
|
||||||
csx
|
|
||||||
*.build.csdef
|
|
||||||
|
|
||||||
# Windows Store app package directory
|
|
||||||
AppPackages/
|
|
||||||
|
|
||||||
# Others
|
|
||||||
sql/
|
|
||||||
*.Cache
|
|
||||||
ClientBin/
|
|
||||||
[Ss]tyle[Cc]op.*
|
|
||||||
~$*
|
|
||||||
*~
|
|
||||||
*.dbmdl
|
|
||||||
*.[Pp]ublish.xml
|
|
||||||
*.pfx
|
|
||||||
*.publishsettings
|
|
||||||
|
|
||||||
# RIA/Silverlight projects
|
|
||||||
Generated_Code/
|
|
||||||
|
|
||||||
# Backup & report files from converting an old project file to a newer
|
|
||||||
# Visual Studio version. Backup files are not needed, because we have git ;-)
|
|
||||||
_UpgradeReport_Files/
|
|
||||||
Backup*/
|
|
||||||
UpgradeLog*.XML
|
|
||||||
UpgradeLog*.htm
|
|
||||||
|
|
||||||
# SQL Server files
|
|
||||||
App_Data/*.mdf
|
|
||||||
App_Data/*.ldf
|
|
||||||
|
|
||||||
|
|
||||||
#LightSwitch generated files
|
|
||||||
GeneratedArtifacts/
|
|
||||||
_Pvt_Extensions/
|
|
||||||
ModelManifest.xml
|
|
||||||
|
|
||||||
# =========================
|
|
||||||
# Windows detritus
|
|
||||||
# =========================
|
|
||||||
|
|
||||||
# Windows image file caches
|
|
||||||
Thumbs.db
|
|
||||||
ehthumbs.db
|
|
||||||
|
|
||||||
# Folder config file
|
|
||||||
Desktop.ini
|
|
||||||
|
|
||||||
# Recycle Bin used on file shares
|
|
||||||
$RECYCLE.BIN/
|
|
||||||
|
|
||||||
# Mac desktop service store files
|
|
||||||
.DS_Store
|
|
|
@ -1,22 +0,0 @@
|
||||||
|
|
||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
|
||||||
# Visual Studio 2013
|
|
||||||
VisualStudioVersion = 12.0.21005.1
|
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BetterDiscordWI", "BetterDiscordWI\BetterDiscordWI.csproj", "{469F7547-7664-4DE8-A568-E89525981539}"
|
|
||||||
EndProject
|
|
||||||
Global
|
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
|
||||||
Debug|Any CPU = Debug|Any CPU
|
|
||||||
Release|Any CPU = Release|Any CPU
|
|
||||||
EndGlobalSection
|
|
||||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
|
||||||
{469F7547-7664-4DE8-A568-E89525981539}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{469F7547-7664-4DE8-A568-E89525981539}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{469F7547-7664-4DE8-A568-E89525981539}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{469F7547-7664-4DE8-A568-E89525981539}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
EndGlobalSection
|
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
|
||||||
HideSolutionNode = FALSE
|
|
||||||
EndGlobalSection
|
|
||||||
EndGlobal
|
|
|
@ -1,6 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8" ?>
|
|
||||||
<configuration>
|
|
||||||
<startup>
|
|
||||||
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
|
|
||||||
</startup>
|
|
||||||
</configuration>
|
|
|
@ -1,88 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
|
||||||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
|
||||||
<PropertyGroup>
|
|
||||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
|
||||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
|
||||||
<ProjectGuid>{469F7547-7664-4DE8-A568-E89525981539}</ProjectGuid>
|
|
||||||
<OutputType>WinExe</OutputType>
|
|
||||||
<AppDesignerFolder>Properties</AppDesignerFolder>
|
|
||||||
<RootNamespace>BetterDiscordWI</RootNamespace>
|
|
||||||
<AssemblyName>BetterDiscordWI</AssemblyName>
|
|
||||||
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
|
|
||||||
<FileAlignment>512</FileAlignment>
|
|
||||||
</PropertyGroup>
|
|
||||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
|
||||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
|
||||||
<DebugSymbols>true</DebugSymbols>
|
|
||||||
<DebugType>full</DebugType>
|
|
||||||
<Optimize>false</Optimize>
|
|
||||||
<OutputPath>bin\Debug\</OutputPath>
|
|
||||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
|
||||||
<ErrorReport>prompt</ErrorReport>
|
|
||||||
<WarningLevel>4</WarningLevel>
|
|
||||||
</PropertyGroup>
|
|
||||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
|
||||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
|
||||||
<DebugType>pdbonly</DebugType>
|
|
||||||
<Optimize>true</Optimize>
|
|
||||||
<OutputPath>bin\Release\</OutputPath>
|
|
||||||
<DefineConstants>TRACE</DefineConstants>
|
|
||||||
<ErrorReport>prompt</ErrorReport>
|
|
||||||
<WarningLevel>4</WarningLevel>
|
|
||||||
</PropertyGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<Reference Include="System" />
|
|
||||||
<Reference Include="System.Core" />
|
|
||||||
<Reference Include="System.Xml.Linq" />
|
|
||||||
<Reference Include="System.Data.DataSetExtensions" />
|
|
||||||
<Reference Include="Microsoft.CSharp" />
|
|
||||||
<Reference Include="System.Data" />
|
|
||||||
<Reference Include="System.Deployment" />
|
|
||||||
<Reference Include="System.Drawing" />
|
|
||||||
<Reference Include="System.Windows.Forms" />
|
|
||||||
<Reference Include="System.Xml" />
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<Compile Include="FormMain.cs">
|
|
||||||
<SubType>Form</SubType>
|
|
||||||
</Compile>
|
|
||||||
<Compile Include="FormMain.Designer.cs">
|
|
||||||
<DependentUpon>FormMain.cs</DependentUpon>
|
|
||||||
</Compile>
|
|
||||||
<Compile Include="Program.cs" />
|
|
||||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
|
||||||
<EmbeddedResource Include="FormMain.resx">
|
|
||||||
<DependentUpon>FormMain.cs</DependentUpon>
|
|
||||||
</EmbeddedResource>
|
|
||||||
<EmbeddedResource Include="Properties\Resources.resx">
|
|
||||||
<Generator>ResXFileCodeGenerator</Generator>
|
|
||||||
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
|
|
||||||
<SubType>Designer</SubType>
|
|
||||||
</EmbeddedResource>
|
|
||||||
<Compile Include="Properties\Resources.Designer.cs">
|
|
||||||
<AutoGen>True</AutoGen>
|
|
||||||
<DependentUpon>Resources.resx</DependentUpon>
|
|
||||||
</Compile>
|
|
||||||
<None Include="Properties\Settings.settings">
|
|
||||||
<Generator>SettingsSingleFileGenerator</Generator>
|
|
||||||
<LastGenOutput>Settings.Designer.cs</LastGenOutput>
|
|
||||||
</None>
|
|
||||||
<Compile Include="Properties\Settings.Designer.cs">
|
|
||||||
<AutoGen>True</AutoGen>
|
|
||||||
<DependentUpon>Settings.settings</DependentUpon>
|
|
||||||
<DesignTimeSharedInput>True</DesignTimeSharedInput>
|
|
||||||
</Compile>
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<None Include="App.config" />
|
|
||||||
</ItemGroup>
|
|
||||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
|
||||||
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
|
|
||||||
Other similar extension points exist, see Microsoft.Common.targets.
|
|
||||||
<Target Name="BeforeBuild">
|
|
||||||
</Target>
|
|
||||||
<Target Name="AfterBuild">
|
|
||||||
</Target>
|
|
||||||
-->
|
|
||||||
</Project>
|
|
|
@ -1,47 +0,0 @@
|
||||||
namespace BetterDiscordWI
|
|
||||||
{
|
|
||||||
partial class FormMain
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Required designer variable.
|
|
||||||
/// </summary>
|
|
||||||
private System.ComponentModel.IContainer components = null;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Clean up any resources being used.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
|
|
||||||
protected override void Dispose(bool disposing)
|
|
||||||
{
|
|
||||||
if (disposing && (components != null))
|
|
||||||
{
|
|
||||||
components.Dispose();
|
|
||||||
}
|
|
||||||
base.Dispose(disposing);
|
|
||||||
}
|
|
||||||
|
|
||||||
#region Windows Form Designer generated code
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Required method for Designer support - do not modify
|
|
||||||
/// the contents of this method with the code editor.
|
|
||||||
/// </summary>
|
|
||||||
private void InitializeComponent()
|
|
||||||
{
|
|
||||||
this.SuspendLayout();
|
|
||||||
//
|
|
||||||
// FormMain
|
|
||||||
//
|
|
||||||
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
|
|
||||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
|
||||||
this.ClientSize = new System.Drawing.Size(284, 261);
|
|
||||||
this.Name = "FormMain";
|
|
||||||
this.Text = "BetterDiscord Installer";
|
|
||||||
this.ResumeLayout(false);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,20 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.ComponentModel;
|
|
||||||
using System.Data;
|
|
||||||
using System.Drawing;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Windows.Forms;
|
|
||||||
|
|
||||||
namespace BetterDiscordWI
|
|
||||||
{
|
|
||||||
public partial class FormMain : Form
|
|
||||||
{
|
|
||||||
public FormMain()
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,120 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<root>
|
|
||||||
<!--
|
|
||||||
Microsoft ResX Schema
|
|
||||||
|
|
||||||
Version 2.0
|
|
||||||
|
|
||||||
The primary goals of this format is to allow a simple XML format
|
|
||||||
that is mostly human readable. The generation and parsing of the
|
|
||||||
various data types are done through the TypeConverter classes
|
|
||||||
associated with the data types.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
... ado.net/XML headers & schema ...
|
|
||||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
|
||||||
<resheader name="version">2.0</resheader>
|
|
||||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
|
||||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
|
||||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
|
||||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
|
||||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
|
||||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
|
||||||
</data>
|
|
||||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
|
||||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
|
||||||
<comment>This is a comment</comment>
|
|
||||||
</data>
|
|
||||||
|
|
||||||
There are any number of "resheader" rows that contain simple
|
|
||||||
name/value pairs.
|
|
||||||
|
|
||||||
Each data row contains a name, and value. The row also contains a
|
|
||||||
type or mimetype. Type corresponds to a .NET class that support
|
|
||||||
text/value conversion through the TypeConverter architecture.
|
|
||||||
Classes that don't support this are serialized and stored with the
|
|
||||||
mimetype set.
|
|
||||||
|
|
||||||
The mimetype is used for serialized objects, and tells the
|
|
||||||
ResXResourceReader how to depersist the object. This is currently not
|
|
||||||
extensible. For a given mimetype the value must be set accordingly:
|
|
||||||
|
|
||||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
|
||||||
that the ResXResourceWriter will generate, however the reader can
|
|
||||||
read any of the formats listed below.
|
|
||||||
|
|
||||||
mimetype: application/x-microsoft.net.object.binary.base64
|
|
||||||
value : The object must be serialized with
|
|
||||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
|
||||||
: and then encoded with base64 encoding.
|
|
||||||
|
|
||||||
mimetype: application/x-microsoft.net.object.soap.base64
|
|
||||||
value : The object must be serialized with
|
|
||||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
|
||||||
: and then encoded with base64 encoding.
|
|
||||||
|
|
||||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
|
||||||
value : The object must be serialized into a byte array
|
|
||||||
: using a System.ComponentModel.TypeConverter
|
|
||||||
: and then encoded with base64 encoding.
|
|
||||||
-->
|
|
||||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
|
||||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
|
||||||
<xsd:element name="root" msdata:IsDataSet="true">
|
|
||||||
<xsd:complexType>
|
|
||||||
<xsd:choice maxOccurs="unbounded">
|
|
||||||
<xsd:element name="metadata">
|
|
||||||
<xsd:complexType>
|
|
||||||
<xsd:sequence>
|
|
||||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
|
||||||
</xsd:sequence>
|
|
||||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
|
||||||
<xsd:attribute name="type" type="xsd:string" />
|
|
||||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
|
||||||
<xsd:attribute ref="xml:space" />
|
|
||||||
</xsd:complexType>
|
|
||||||
</xsd:element>
|
|
||||||
<xsd:element name="assembly">
|
|
||||||
<xsd:complexType>
|
|
||||||
<xsd:attribute name="alias" type="xsd:string" />
|
|
||||||
<xsd:attribute name="name" type="xsd:string" />
|
|
||||||
</xsd:complexType>
|
|
||||||
</xsd:element>
|
|
||||||
<xsd:element name="data">
|
|
||||||
<xsd:complexType>
|
|
||||||
<xsd:sequence>
|
|
||||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
|
||||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
|
||||||
</xsd:sequence>
|
|
||||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
|
||||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
|
||||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
|
||||||
<xsd:attribute ref="xml:space" />
|
|
||||||
</xsd:complexType>
|
|
||||||
</xsd:element>
|
|
||||||
<xsd:element name="resheader">
|
|
||||||
<xsd:complexType>
|
|
||||||
<xsd:sequence>
|
|
||||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
|
||||||
</xsd:sequence>
|
|
||||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
|
||||||
</xsd:complexType>
|
|
||||||
</xsd:element>
|
|
||||||
</xsd:choice>
|
|
||||||
</xsd:complexType>
|
|
||||||
</xsd:element>
|
|
||||||
</xsd:schema>
|
|
||||||
<resheader name="resmimetype">
|
|
||||||
<value>text/microsoft-resx</value>
|
|
||||||
</resheader>
|
|
||||||
<resheader name="version">
|
|
||||||
<value>2.0</value>
|
|
||||||
</resheader>
|
|
||||||
<resheader name="reader">
|
|
||||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
|
||||||
</resheader>
|
|
||||||
<resheader name="writer">
|
|
||||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
|
||||||
</resheader>
|
|
||||||
</root>
|
|
|
@ -1,22 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Windows.Forms;
|
|
||||||
|
|
||||||
namespace BetterDiscordWI
|
|
||||||
{
|
|
||||||
static class Program
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The main entry point for the application.
|
|
||||||
/// </summary>
|
|
||||||
[STAThread]
|
|
||||||
static void Main()
|
|
||||||
{
|
|
||||||
Application.EnableVisualStyles();
|
|
||||||
Application.SetCompatibleTextRenderingDefault(false);
|
|
||||||
Application.Run(new FormMain());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,36 +0,0 @@
|
||||||
using System.Reflection;
|
|
||||||
using System.Runtime.CompilerServices;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
|
|
||||||
// General Information about an assembly is controlled through the following
|
|
||||||
// set of attributes. Change these attribute values to modify the information
|
|
||||||
// associated with an assembly.
|
|
||||||
[assembly: AssemblyTitle("BetterDiscordWI")]
|
|
||||||
[assembly: AssemblyDescription("Better Discord Windows Installer")]
|
|
||||||
[assembly: AssemblyConfiguration("")]
|
|
||||||
[assembly: AssemblyCompany("Jiiks")]
|
|
||||||
[assembly: AssemblyProduct("BetterDiscordWI")]
|
|
||||||
[assembly: AssemblyCopyright("Copyright © 2015")]
|
|
||||||
[assembly: AssemblyTrademark("")]
|
|
||||||
[assembly: AssemblyCulture("")]
|
|
||||||
|
|
||||||
// Setting ComVisible to false makes the types in this assembly not visible
|
|
||||||
// to COM components. If you need to access a type in this assembly from
|
|
||||||
// COM, set the ComVisible attribute to true on that type.
|
|
||||||
[assembly: ComVisible(false)]
|
|
||||||
|
|
||||||
// The following GUID is for the ID of the typelib if this project is exposed to COM
|
|
||||||
[assembly: Guid("17adafc9-c3e6-49c2-b2c9-d6866e7f4f23")]
|
|
||||||
|
|
||||||
// Version information for an assembly consists of the following four values:
|
|
||||||
//
|
|
||||||
// Major Version
|
|
||||||
// Minor Version
|
|
||||||
// Build Number
|
|
||||||
// Revision
|
|
||||||
//
|
|
||||||
// You can specify all the values or you can default the Build and Revision Numbers
|
|
||||||
// by using the '*' as shown below:
|
|
||||||
// [assembly: AssemblyVersion("1.0.*")]
|
|
||||||
[assembly: AssemblyVersion("0.1.0.0")]
|
|
||||||
[assembly: AssemblyFileVersion("0.1.0.0")]
|
|
|
@ -1,71 +0,0 @@
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// <auto-generated>
|
|
||||||
// This code was generated by a tool.
|
|
||||||
// Runtime Version:4.0.30319.42000
|
|
||||||
//
|
|
||||||
// Changes to this file may cause incorrect behavior and will be lost if
|
|
||||||
// the code is regenerated.
|
|
||||||
// </auto-generated>
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
namespace BetterDiscordWI.Properties
|
|
||||||
{
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A strongly-typed resource class, for looking up localized strings, etc.
|
|
||||||
/// </summary>
|
|
||||||
// This class was auto-generated by the StronglyTypedResourceBuilder
|
|
||||||
// class via a tool like ResGen or Visual Studio.
|
|
||||||
// To add or remove a member, edit your .ResX file then rerun ResGen
|
|
||||||
// with the /str option, or rebuild your VS project.
|
|
||||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
|
|
||||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
|
||||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
|
||||||
internal class Resources
|
|
||||||
{
|
|
||||||
|
|
||||||
private static global::System.Resources.ResourceManager resourceMan;
|
|
||||||
|
|
||||||
private static global::System.Globalization.CultureInfo resourceCulture;
|
|
||||||
|
|
||||||
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
|
|
||||||
internal Resources()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the cached ResourceManager instance used by this class.
|
|
||||||
/// </summary>
|
|
||||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
|
||||||
internal static global::System.Resources.ResourceManager ResourceManager
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if ((resourceMan == null))
|
|
||||||
{
|
|
||||||
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("BetterDiscordWI.Properties.Resources", typeof(Resources).Assembly);
|
|
||||||
resourceMan = temp;
|
|
||||||
}
|
|
||||||
return resourceMan;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Overrides the current thread's CurrentUICulture property for all
|
|
||||||
/// resource lookups using this strongly typed resource class.
|
|
||||||
/// </summary>
|
|
||||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
|
||||||
internal static global::System.Globalization.CultureInfo Culture
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return resourceCulture;
|
|
||||||
}
|
|
||||||
set
|
|
||||||
{
|
|
||||||
resourceCulture = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,117 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<root>
|
|
||||||
<!--
|
|
||||||
Microsoft ResX Schema
|
|
||||||
|
|
||||||
Version 2.0
|
|
||||||
|
|
||||||
The primary goals of this format is to allow a simple XML format
|
|
||||||
that is mostly human readable. The generation and parsing of the
|
|
||||||
various data types are done through the TypeConverter classes
|
|
||||||
associated with the data types.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
... ado.net/XML headers & schema ...
|
|
||||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
|
||||||
<resheader name="version">2.0</resheader>
|
|
||||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
|
||||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
|
||||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
|
||||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
|
||||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
|
||||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
|
||||||
</data>
|
|
||||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
|
||||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
|
||||||
<comment>This is a comment</comment>
|
|
||||||
</data>
|
|
||||||
|
|
||||||
There are any number of "resheader" rows that contain simple
|
|
||||||
name/value pairs.
|
|
||||||
|
|
||||||
Each data row contains a name, and value. The row also contains a
|
|
||||||
type or mimetype. Type corresponds to a .NET class that support
|
|
||||||
text/value conversion through the TypeConverter architecture.
|
|
||||||
Classes that don't support this are serialized and stored with the
|
|
||||||
mimetype set.
|
|
||||||
|
|
||||||
The mimetype is used for serialized objects, and tells the
|
|
||||||
ResXResourceReader how to depersist the object. This is currently not
|
|
||||||
extensible. For a given mimetype the value must be set accordingly:
|
|
||||||
|
|
||||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
|
||||||
that the ResXResourceWriter will generate, however the reader can
|
|
||||||
read any of the formats listed below.
|
|
||||||
|
|
||||||
mimetype: application/x-microsoft.net.object.binary.base64
|
|
||||||
value : The object must be serialized with
|
|
||||||
: System.Serialization.Formatters.Binary.BinaryFormatter
|
|
||||||
: and then encoded with base64 encoding.
|
|
||||||
|
|
||||||
mimetype: application/x-microsoft.net.object.soap.base64
|
|
||||||
value : The object must be serialized with
|
|
||||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
|
||||||
: and then encoded with base64 encoding.
|
|
||||||
|
|
||||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
|
||||||
value : The object must be serialized into a byte array
|
|
||||||
: using a System.ComponentModel.TypeConverter
|
|
||||||
: and then encoded with base64 encoding.
|
|
||||||
-->
|
|
||||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
|
||||||
<xsd:element name="root" msdata:IsDataSet="true">
|
|
||||||
<xsd:complexType>
|
|
||||||
<xsd:choice maxOccurs="unbounded">
|
|
||||||
<xsd:element name="metadata">
|
|
||||||
<xsd:complexType>
|
|
||||||
<xsd:sequence>
|
|
||||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
|
||||||
</xsd:sequence>
|
|
||||||
<xsd:attribute name="name" type="xsd:string" />
|
|
||||||
<xsd:attribute name="type" type="xsd:string" />
|
|
||||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
|
||||||
</xsd:complexType>
|
|
||||||
</xsd:element>
|
|
||||||
<xsd:element name="assembly">
|
|
||||||
<xsd:complexType>
|
|
||||||
<xsd:attribute name="alias" type="xsd:string" />
|
|
||||||
<xsd:attribute name="name" type="xsd:string" />
|
|
||||||
</xsd:complexType>
|
|
||||||
</xsd:element>
|
|
||||||
<xsd:element name="data">
|
|
||||||
<xsd:complexType>
|
|
||||||
<xsd:sequence>
|
|
||||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
|
||||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
|
||||||
</xsd:sequence>
|
|
||||||
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
|
|
||||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
|
||||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
|
||||||
</xsd:complexType>
|
|
||||||
</xsd:element>
|
|
||||||
<xsd:element name="resheader">
|
|
||||||
<xsd:complexType>
|
|
||||||
<xsd:sequence>
|
|
||||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
|
||||||
</xsd:sequence>
|
|
||||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
|
||||||
</xsd:complexType>
|
|
||||||
</xsd:element>
|
|
||||||
</xsd:choice>
|
|
||||||
</xsd:complexType>
|
|
||||||
</xsd:element>
|
|
||||||
</xsd:schema>
|
|
||||||
<resheader name="resmimetype">
|
|
||||||
<value>text/microsoft-resx</value>
|
|
||||||
</resheader>
|
|
||||||
<resheader name="version">
|
|
||||||
<value>2.0</value>
|
|
||||||
</resheader>
|
|
||||||
<resheader name="reader">
|
|
||||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
|
||||||
</resheader>
|
|
||||||
<resheader name="writer">
|
|
||||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
|
||||||
</resheader>
|
|
||||||
</root>
|
|
|
@ -1,30 +0,0 @@
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// <auto-generated>
|
|
||||||
// This code was generated by a tool.
|
|
||||||
// Runtime Version:4.0.30319.42000
|
|
||||||
//
|
|
||||||
// Changes to this file may cause incorrect behavior and will be lost if
|
|
||||||
// the code is regenerated.
|
|
||||||
// </auto-generated>
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
namespace BetterDiscordWI.Properties
|
|
||||||
{
|
|
||||||
|
|
||||||
|
|
||||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
|
||||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")]
|
|
||||||
internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase
|
|
||||||
{
|
|
||||||
|
|
||||||
private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
|
|
||||||
|
|
||||||
public static Settings Default
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return defaultInstance;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
<?xml version='1.0' encoding='utf-8'?>
|
|
||||||
<SettingsFile xmlns="http://schemas.microsoft.com/VisualStudio/2004/01/settings" CurrentProfile="(Default)">
|
|
||||||
<Profiles>
|
|
||||||
<Profile Name="(Default)" />
|
|
||||||
</Profiles>
|
|
||||||
<Settings />
|
|
||||||
</SettingsFile>
|
|
|
@ -1,12 +0,0 @@
|
||||||
/*Note this is partial changes*/
|
|
||||||
|
|
||||||
var _betterDiscord = require('betterdiscord');
|
|
||||||
|
|
||||||
function launchMainAppWindow(isVisible) {
|
|
||||||
|
|
||||||
_betterDiscord = new _betterDiscord.BetterDiscord(mainWindow);
|
|
||||||
_betterDiscord.init();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
{
|
|
||||||
"name": "discord",
|
|
||||||
"description": "Discord Client for Desktop",
|
|
||||||
"version": "0.0.277",
|
|
||||||
"releaseChannel": "stable",
|
|
||||||
"main": "app/index.js",
|
|
||||||
"private": true,
|
|
||||||
"dependencies": {
|
|
||||||
"react": "^0.13.0",
|
|
||||||
"betterdiscord": "^0.1.2"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -1 +0,0 @@
|
||||||
module.exports = require('./lib/BetterDiscord');
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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"]
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
{
|
||||||
|
"name": "bdclient",
|
||||||
|
"description": "BetterDiscord client package",
|
||||||
|
"author": "Jiiks",
|
||||||
|
"version": "2.0.0-beta.6",
|
||||||
|
"homepage": "https://betterdiscord.net",
|
||||||
|
"license": "MIT",
|
||||||
|
"main": "dist/betterdiscord.client.js",
|
||||||
|
"contributors": [
|
||||||
|
"Jiiks",
|
||||||
|
"Pohky"
|
||||||
|
],
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/JsSucks/BetterDiscordApp.git"
|
||||||
|
},
|
||||||
|
"private": false,
|
||||||
|
"scripts": {
|
||||||
|
"build": "webpack --progress --colors",
|
||||||
|
"watch": "webpack --progress --colors --watch",
|
||||||
|
"release": "webpack --progress --colors --config=webpack.production.config.js"
|
||||||
|
}
|
||||||
|
}
|
|
@ -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]}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -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, '$:');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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>
|
|
@ -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>
|
|
@ -0,0 +1,3 @@
|
||||||
|
export { default as default } from './E2EE';
|
||||||
|
export { default as E2EEComponent } from './E2EEComponent.vue';
|
||||||
|
export { default as E2EEMessageButton } from './E2EEMessageButton.vue';
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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>
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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';
|
|
@ -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');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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');
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
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';
|
|
@ -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
|
||||||
|
}
|
||||||
|
]
|
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,311 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "core",
|
||||||
|
"text": "Core",
|
||||||
|
"headertext": "Core Settings",
|
||||||
|
"settings": [
|
||||||
|
{
|
||||||
|
"id": "default",
|
||||||
|
"settings": [
|
||||||
|
{
|
||||||
|
"id": "voice-disconnect",
|
||||||
|
"type": "bool",
|
||||||
|
"text": "Voice Disconnect",
|
||||||
|
"hint": "Disconnect from voice server when Discord closes",
|
||||||
|
"value": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "menu-keybind",
|
||||||
|
"type": "keybind",
|
||||||
|
"text": "Menu keybind",
|
||||||
|
"value": "mod+b"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "advanced",
|
||||||
|
"name": "Advanced",
|
||||||
|
"type": "drawer",
|
||||||
|
"settings": [
|
||||||
|
{
|
||||||
|
"id": "developer-mode",
|
||||||
|
"type": "bool",
|
||||||
|
"text": "Developer mode",
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ui",
|
||||||
|
"text": "UI",
|
||||||
|
"headertext": "UI Settings",
|
||||||
|
"settings": [
|
||||||
|
{
|
||||||
|
"id": "default",
|
||||||
|
"settings": [
|
||||||
|
{
|
||||||
|
"id": "hide-button",
|
||||||
|
"type": "bool",
|
||||||
|
"text": "Hide the BetterDiscord button",
|
||||||
|
"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": "%"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "emotes",
|
||||||
|
"text": "Emotes",
|
||||||
|
"headertext": "Emote 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",
|
||||||
|
"settings": [
|
||||||
|
{
|
||||||
|
"id": "default",
|
||||||
|
"settings": [
|
||||||
|
{
|
||||||
|
"id": "live-update",
|
||||||
|
"type": "bool",
|
||||||
|
"text": "Live update",
|
||||||
|
"hint": "Automatically recompile custom CSS when typing in the custom CSS editor.",
|
||||||
|
"value": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "watch-files",
|
||||||
|
"type": "bool",
|
||||||
|
"text": "Watch included files",
|
||||||
|
"hint": "Automatically recompile theme and custom CSS when a file it imports is changed.",
|
||||||
|
"value": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "security",
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
|
@ -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)}`;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,141 @@
|
||||||
|
/**
|
||||||
|
* BetterDiscord Client Core
|
||||||
|
* 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 { DOM, BdUI, BdMenu, Modals, Toasts, Notifications, BdContextMenu, DiscordContextMenu, Autocomplete } from 'ui';
|
||||||
|
import BdCss from './styles/index.scss';
|
||||||
|
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, BdMenu, Modals, Reflection, Toasts, Notifications, BdContextMenu, DiscordContextMenu, Autocomplete,
|
||||||
|
|
||||||
|
Events, Globals, Settings, Database, Updater,
|
||||||
|
ModuleManager, PluginManager, ThemeManager, ExtModuleManager, PackageInstaller,
|
||||||
|
Vendor,
|
||||||
|
|
||||||
|
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');
|
||||||
|
if (developermode.value) window._bd = this._bd;
|
||||||
|
developermode.on('setting-updated', event => {
|
||||||
|
if (event.value) window._bd = this._bd;
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
Events.on('autopatcher', () => instance = new BetterDiscord());
|
||||||
|
ReactAutoPatcher.autoPatch().then(() => Events.emit('autopatcher'));
|
||||||
|
}
|
|
@ -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 });
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,149 @@
|
||||||
|
/**
|
||||||
|
* BetterDiscord Content 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 { Utils, ClientLogger as Logger, AsyncEventEmitter } from 'common';
|
||||||
|
import { Modals } from 'ui';
|
||||||
|
import Database from './database';
|
||||||
|
|
||||||
|
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.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
|
||||||
|
if (this.onstart) this.on('start', event => this.onstart(event));
|
||||||
|
if (this.onStart) this.on('start', event => this.onStart(event));
|
||||||
|
if (this.onstop) this.on('stop', event => this.onstop(event));
|
||||||
|
if (this.onStop) this.on('stop', event => this.onStop(event));
|
||||||
|
if (this.onunload) this.on('unload', event => this.onunload(event));
|
||||||
|
if (this.onUnload) this.on('unload', event => this.onUnload(event));
|
||||||
|
if (this.settingUpdated) this.on('setting-updated', event => this.settingUpdated(event));
|
||||||
|
if (this.settingsUpdated) this.on('settings-updated', event => this.settingsUpdated(event));
|
||||||
|
}
|
||||||
|
|
||||||
|
get type() { return undefined }
|
||||||
|
get configs() { return this.__internals.configs }
|
||||||
|
get info() { return this.__internals.info }
|
||||||
|
get paths() { return this.__internals.paths }
|
||||||
|
get main() { return this.__internals.main }
|
||||||
|
get defaultConfig() { return this.configs.defaultConfig }
|
||||||
|
get userConfig() { return this.configs.userConfig }
|
||||||
|
get configSchemes() { return this.configs.schemes }
|
||||||
|
get id() { return this.info.id || this.name.toLowerCase().replace(/[^a-zA-Z0-9-]/g, '-').replace(/--/g, '-') }
|
||||||
|
get name() { return this.info.name }
|
||||||
|
get icon() { return this.info.icon }
|
||||||
|
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 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether this content has any settings.
|
||||||
|
*/
|
||||||
|
get hasSettings() {
|
||||||
|
return !!this.settings.findSetting(() => true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves the content's current configuration.
|
||||||
|
* @return {Promise}
|
||||||
|
*/
|
||||||
|
async saveConfiguration() {
|
||||||
|
try {
|
||||||
|
Database.insertOrUpdate({ type: `${this.type}-config`, id: this.id }, {
|
||||||
|
type: `${this.type}-config`,
|
||||||
|
id: this.id,
|
||||||
|
enabled: this.enabled,
|
||||||
|
config: this.settings.strip().settings,
|
||||||
|
data: this.data
|
||||||
|
});
|
||||||
|
this.settings.setSaved();
|
||||||
|
} catch (err) {
|
||||||
|
Logger.err(this.name, ['Failed to save configuration', err]);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when settings are updated.
|
||||||
|
* This can be overridden by other content types.
|
||||||
|
*/
|
||||||
|
__settingsUpdated(event) {
|
||||||
|
return this.saveConfiguration();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enables the content.
|
||||||
|
* @param {Boolean} save Whether to save the new enabled state
|
||||||
|
* @return {Promise}
|
||||||
|
*/
|
||||||
|
async enable(save = true) {
|
||||||
|
if (this.enabled) return;
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disables the content.
|
||||||
|
* @param {Boolean} save Whether to save the new enabled state
|
||||||
|
* @return {Promise}
|
||||||
|
*/
|
||||||
|
async disable(save = true) {
|
||||||
|
if (!this.enabled) return;
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.freeze(Content.prototype);
|
|
@ -0,0 +1,449 @@
|
||||||
|
/**
|
||||||
|
* BetterDiscord Content Manager 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 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 { SettingsSet, ErrorEvent } from 'structs';
|
||||||
|
import { Modals } from 'ui';
|
||||||
|
import Combokeys from 'combokeys';
|
||||||
|
import Settings from './settings';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class for managing external content
|
||||||
|
*/
|
||||||
|
export default class {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Any errors that happened.
|
||||||
|
* @return {Array}
|
||||||
|
*/
|
||||||
|
static get errors() {
|
||||||
|
return this._errors || (this._errors = []);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Locally stored content.
|
||||||
|
* @return {Array}
|
||||||
|
*/
|
||||||
|
static get localContent() {
|
||||||
|
return this._localContent ? this._localContent : (this._localContent = []);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
* @param {bool} suppressErrors Suppress any errors that occur during loading of content
|
||||||
|
*/
|
||||||
|
static async loadAllContent(suppressErrors = false) {
|
||||||
|
try {
|
||||||
|
await FileUtils.ensureDirectory(this.contentPath);
|
||||||
|
const directories = await FileUtils.listDirectory(this.contentPath);
|
||||||
|
|
||||||
|
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 {
|
||||||
|
if (packed) {
|
||||||
|
await this.preloadPackedContent(dir);
|
||||||
|
} else {
|
||||||
|
await this.preloadContent(dir);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
this.errors.push(new ErrorEvent({
|
||||||
|
module: this.moduleName,
|
||||||
|
message: `Failed to load ${dir}`,
|
||||||
|
err
|
||||||
|
}));
|
||||||
|
|
||||||
|
Logger.err(this.moduleName, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.errors.length && !suppressErrors) {
|
||||||
|
Modals.error({
|
||||||
|
header: `${this.moduleName} - ${this.errors.length} ${this.contentType}${this.errors.length !== 1 ? 's' : ''} failed to load`,
|
||||||
|
module: this.moduleName,
|
||||||
|
type: 'err',
|
||||||
|
content: this.errors
|
||||||
|
});
|
||||||
|
this._errors = [];
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh locally stored content
|
||||||
|
* @param {bool} suppressErrors Suppress any errors that occur during loading of content
|
||||||
|
*/
|
||||||
|
static async refreshContent(suppressErrors = false) {
|
||||||
|
if (!this.localContent.length) return this.loadAllContent();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await FileUtils.ensureDirectory(this.contentPath);
|
||||||
|
const directories = await FileUtils.listDirectory(this.contentPath);
|
||||||
|
|
||||||
|
for (const dir of directories) {
|
||||||
|
const packed = dir.endsWith('.bd');
|
||||||
|
|
||||||
|
// If content is already loaded this should resolve
|
||||||
|
if (this.getContentByDirName(dir)) continue;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await FileUtils.directoryExists(path.join(this.contentPath, dir));
|
||||||
|
} catch (err) { continue; }
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Load if not
|
||||||
|
await this.preloadContent(dir);
|
||||||
|
} catch (err) {
|
||||||
|
// We don't want every plugin/theme to fail loading when one does
|
||||||
|
this.errors.push(new ErrorEvent({
|
||||||
|
module: this.moduleName,
|
||||||
|
message: `Failed to load ${dir}`,
|
||||||
|
err
|
||||||
|
}));
|
||||||
|
|
||||||
|
Logger.err(this.moduleName, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const content of this.localContent) {
|
||||||
|
if (directories.includes(content.dirName)) continue;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Plugin/theme was deleted manually, stop it and remove any reference
|
||||||
|
await this.unloadContent(content);
|
||||||
|
} catch (err) {
|
||||||
|
this.errors.push(new ErrorEvent({
|
||||||
|
module: this.moduleName,
|
||||||
|
message: `Failed to unload ${content.dirName}`,
|
||||||
|
err
|
||||||
|
}));
|
||||||
|
|
||||||
|
Logger.err(this.moduleName, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.errors.length && !suppressErrors) {
|
||||||
|
Modals.error({
|
||||||
|
header: `${this.moduleName} - ${this.errors.length} ${this.contentType}${this.errors.length !== 1 ? 's' : ''} failed to load`,
|
||||||
|
module: this.moduleName,
|
||||||
|
type: 'err',
|
||||||
|
content: this.errors
|
||||||
|
});
|
||||||
|
this._errors = [];
|
||||||
|
}
|
||||||
|
} 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
|
||||||
|
* @param {any} reload Is content being reloaded
|
||||||
|
* @param {any} index Index of content in {localContent}
|
||||||
|
*/
|
||||||
|
static async preloadContent(dirName, reload = false, index) {
|
||||||
|
try {
|
||||||
|
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 && this.getContentByPath(contentPath))
|
||||||
|
throw { 'message': `Attempted to load already loaded user content: ${path}` };
|
||||||
|
|
||||||
|
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,
|
||||||
|
schemes: readConfig.configSchemes
|
||||||
|
});
|
||||||
|
|
||||||
|
const userConfig = {
|
||||||
|
enabled: false,
|
||||||
|
config: undefined,
|
||||||
|
data: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
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;
|
||||||
|
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.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 (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 = {
|
||||||
|
defaultConfig,
|
||||||
|
schemes: userConfig.schemes,
|
||||||
|
userConfig
|
||||||
|
};
|
||||||
|
|
||||||
|
const paths = {
|
||||||
|
contentPath,
|
||||||
|
dirName,
|
||||||
|
mainPath
|
||||||
|
};
|
||||||
|
|
||||||
|
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.` };
|
||||||
|
|
||||||
|
if (reload) this.localContent.splice(index, 1, content);
|
||||||
|
else this.localContent.push(content);
|
||||||
|
return content;
|
||||||
|
} catch (err) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 deleteContent(content, force) {
|
||||||
|
content = this.findContent(content);
|
||||||
|
if (!content) throw {message: `Could not find a ${this.contentType} from ${content}.`};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Modals.confirm(`Delete ${this.contentType}?`, `Are you sure you want to delete ${content.info.name} ?`, 'Delete').promise;
|
||||||
|
} catch (err) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const unload = this.unloadContent(content, force, false);
|
||||||
|
|
||||||
|
if (!force)
|
||||||
|
await unload;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 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
|
||||||
|
* @return {Boolean}
|
||||||
|
*/
|
||||||
|
static isThisContent(content) {
|
||||||
|
return content instanceof Content;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the first content where calling {function} returns true.
|
||||||
|
* @param {Function} function A function to call to filter content
|
||||||
|
*/
|
||||||
|
static find(f) {
|
||||||
|
return this.localContent.find(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wildcard content finder
|
||||||
|
* @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;
|
||||||
|
let content;
|
||||||
|
content = this.getContentById(wild); if (content) return content;
|
||||||
|
content = this.getContentByDirName(wild); if (content) return content;
|
||||||
|
content = this.getContentByPath(wild); if (content) return content;
|
||||||
|
content = this.getContentByName(wild); if (content && nonunique) return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
static getContentIndex(content) { return this.localContent.findIndex(c => c === content) }
|
||||||
|
static getContentById(id) { return this.localContent.find(c => c.id === id) }
|
||||||
|
static getContentByDirName(dirName) { return this.localContent.find(c => c.dirName === dirName) }
|
||||||
|
static getContentByPath(path) { return this.localContent.find(c => c.contentPath === path) }
|
||||||
|
static getContentByName(name) { return this.localContent.find(c => c.name === name) }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait for content to load
|
||||||
|
* @param {String} content_id
|
||||||
|
* @return {Promise => Content}
|
||||||
|
*/
|
||||||
|
static waitForContent(content_id) {
|
||||||
|
return Utils.until(() => this.getContentById(content_id), 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,318 @@
|
||||||
|
/**
|
||||||
|
* BetterDiscord CSS 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 { DOM } from 'ui';
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
export default new class {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this._scss = '';
|
||||||
|
this._css = '';
|
||||||
|
this._error = undefined;
|
||||||
|
this.editor_bounds = undefined;
|
||||||
|
this._files = undefined;
|
||||||
|
this._filewatcher = undefined;
|
||||||
|
this._watchfiles = undefined;
|
||||||
|
this.compiling = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Init css editor.
|
||||||
|
*/
|
||||||
|
init() {
|
||||||
|
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.liveupdate.value, true);
|
||||||
|
ClientIPC.on('bd-set-liveupdate', (e, value) => this.liveupdate.value = value);
|
||||||
|
|
||||||
|
this.watchfilessetting = Settings.getSetting('css', 'default', 'watch-files');
|
||||||
|
this.watchfilessetting.on('setting-updated', event => {
|
||||||
|
if (event.value) this.watchfiles = this.files;
|
||||||
|
else this.watchfiles = [];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 Whether to send to css editor instance
|
||||||
|
*/
|
||||||
|
async updateScss(scss, sendSource) {
|
||||||
|
if (sendSource)
|
||||||
|
this.sendToEditor('set-scss', scss);
|
||||||
|
|
||||||
|
if (!scss && !await this.fileExists()) {
|
||||||
|
this._scss = this.css = '';
|
||||||
|
this.sendToEditor('scss-error', null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.compiling = true;
|
||||||
|
const result = await this.compile(scss);
|
||||||
|
this.css = result.css.toString();
|
||||||
|
this._scss = scss;
|
||||||
|
this.files = result.stats.includedFiles;
|
||||||
|
this.error = null;
|
||||||
|
this.compiling = false;
|
||||||
|
} catch (err) {
|
||||||
|
this.compiling = false;
|
||||||
|
this.error = err;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save css to file.
|
||||||
|
* @return {Promise}
|
||||||
|
*/
|
||||||
|
save() {
|
||||||
|
return Settings.saveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save current editor bounds.
|
||||||
|
* @param {Rectangle} bounds Editor bounds
|
||||||
|
* @return {Promise}
|
||||||
|
*/
|
||||||
|
saveEditorBounds(bounds) {
|
||||||
|
this.editor_bounds = bounds;
|
||||||
|
return Settings.saveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send SCSS to core for compilation.
|
||||||
|
* @param {String} scss SCSS string
|
||||||
|
*/
|
||||||
|
async compile(scss) {
|
||||||
|
return ClientIPC.send('bd-compileSass', {
|
||||||
|
data: scss,
|
||||||
|
path: await this.fileExists() ? this.filePath : undefined
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recompile the current SCSS.
|
||||||
|
* @return {Promise}
|
||||||
|
*/
|
||||||
|
async recompile() {
|
||||||
|
return this.updateScss(this.scss);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send data to open editor.
|
||||||
|
* @param {String} channel
|
||||||
|
* @param {Any} data
|
||||||
|
* @return {Promise}
|
||||||
|
*/
|
||||||
|
async sendToEditor(channel, data) {
|
||||||
|
return ClientIPC.sendToCssEditor(channel, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens an SCSS file in a system editor.
|
||||||
|
* @return {Promise}
|
||||||
|
*/
|
||||||
|
async openSystemEditor() {
|
||||||
|
try {
|
||||||
|
await FileUtils.fileExists(this.filePath);
|
||||||
|
} catch (err) {
|
||||||
|
// File doesn't exist
|
||||||
|
// Create it
|
||||||
|
await FileUtils.writeFile(this.filePath, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.log('CSS Editor', `Opening file ${this.filePath} in the user's default editor.`);
|
||||||
|
|
||||||
|
// For some reason this doesn't work
|
||||||
|
// if (!electron.shell.openItem(this.filePath))
|
||||||
|
if (!electron.shell.openExternal(`file://${this.filePath}`))
|
||||||
|
throw {message: 'Failed to open system editor.'};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set current state
|
||||||
|
* @param {String} scss Current uncompiled SCSS
|
||||||
|
* @param {String} css Current compiled CSS
|
||||||
|
* @param {String} files Files imported in the SCSS
|
||||||
|
* @param {String} err Current compiler error
|
||||||
|
*/
|
||||||
|
setState(scss, css, files, err) {
|
||||||
|
this._scss = scss;
|
||||||
|
this.sendToEditor('set-scss', scss);
|
||||||
|
this.css = css;
|
||||||
|
this.files = files;
|
||||||
|
this.error = err;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current uncompiled scss.
|
||||||
|
*/
|
||||||
|
get scss() {
|
||||||
|
return this._scss || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set current scss.
|
||||||
|
*/
|
||||||
|
set scss(scss) {
|
||||||
|
this.updateScss(scss, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current compiled css.
|
||||||
|
*/
|
||||||
|
get css() {
|
||||||
|
return this._css || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inject compiled css to head.
|
||||||
|
*/
|
||||||
|
set css(css) {
|
||||||
|
this._css = css;
|
||||||
|
DOM.injectStyle(css, 'bd-customcss');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current error.
|
||||||
|
*/
|
||||||
|
get error() {
|
||||||
|
return this._error || undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set current error.
|
||||||
|
*/
|
||||||
|
set error(err) {
|
||||||
|
this._error = err;
|
||||||
|
this.sendToEditor('scss-error', err);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An array of files that are imported in custom CSS.
|
||||||
|
* @return {Array} Files being watched
|
||||||
|
*/
|
||||||
|
get files() {
|
||||||
|
return this._files || (this._files = []);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets all files that are imported in custom CSS.
|
||||||
|
* @param {Array} files Files to watch
|
||||||
|
*/
|
||||||
|
set files(files) {
|
||||||
|
this._files = files;
|
||||||
|
if (Settings.get('css', 'default', 'watch-files'))
|
||||||
|
this.watchfiles = files;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A filewatcher instance.
|
||||||
|
*/
|
||||||
|
get filewatcher() {
|
||||||
|
if (this._filewatcher) return this._filewatcher;
|
||||||
|
this._filewatcher = filewatcher();
|
||||||
|
this._filewatcher.on('change', (file, stat) => {
|
||||||
|
// Recompile SCSS
|
||||||
|
this.recompile();
|
||||||
|
});
|
||||||
|
return this._filewatcher;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An array of files that are being watched for changes.
|
||||||
|
* @return {Array} Files being watched
|
||||||
|
*/
|
||||||
|
get watchfiles() {
|
||||||
|
return this._watchfiles || (this._watchfiles = []);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets all files to be watched.
|
||||||
|
* @param {Array} files Files to watch
|
||||||
|
*/
|
||||||
|
set watchfiles(files) {
|
||||||
|
for (const file of files) {
|
||||||
|
if (!this.watchfiles.includes(file)) {
|
||||||
|
this.filewatcher.add(file);
|
||||||
|
this.watchfiles.push(file);
|
||||||
|
Logger.log('CSS Editor', `Watching file ${file} for changes`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const index in this.watchfiles) {
|
||||||
|
let file = this.watchfiles[index];
|
||||||
|
while (file && !files.find(f => f === file)) {
|
||||||
|
this.filewatcher.remove(file);
|
||||||
|
this.watchfiles.splice(index, 1);
|
||||||
|
Logger.log('CSS Editor', `No longer watching file ${file} for changes`);
|
||||||
|
file = this.watchfiles[index];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The path of the file the system editor should save to.
|
||||||
|
* @return {String}
|
||||||
|
*/
|
||||||
|
get filePath() {
|
||||||
|
return path.join(Settings.dataPath, 'user.scss');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the system editor's file exists.
|
||||||
|
* @return {Promise}
|
||||||
|
*/
|
||||||
|
async fileExists() {
|
||||||
|
try {
|
||||||
|
await FileUtils.fileExists(this.filePath);
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
/**
|
||||||
|
* BetterDiscord Database 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 { ClientIPC } from 'common';
|
||||||
|
|
||||||
|
export default class {
|
||||||
|
|
||||||
|
static async init() {
|
||||||
|
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 });
|
||||||
|
} catch (err) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 });
|
||||||
|
} catch (err) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,335 @@
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { List } from 'structs';
|
||||||
|
import { User, Channel, Guild, Message } from 'discordstructs';
|
||||||
|
import Reflection from './reflection/index';
|
||||||
|
|
||||||
|
export const Modules = {
|
||||||
|
_getModule(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'); },
|
||||||
|
get MessageStore() { return this._getModule('MessageStore'); },
|
||||||
|
get EmojiUtils() { return this._getModule('EmojiUtils'); },
|
||||||
|
get PermissionUtils() { return this._getModule('Permissions'); },
|
||||||
|
get SortedGuildStore() { return this._getModule('SortedGuildStore'); },
|
||||||
|
get PrivateChannelActions() { return this._getModule('PrivateChannelActions'); },
|
||||||
|
get GuildMemberStore() { return this._getModule('GuildMemberStore'); },
|
||||||
|
get GuildChannelsStore() { return this._getModule('GuildChannelsStore'); },
|
||||||
|
get MemberCountStore() { return this._getModule('MemberCountStore'); },
|
||||||
|
get GuildActions() { return this._getModule('GuildActions'); },
|
||||||
|
get NavigationUtils() { return this._getModule('NavigationUtils'); },
|
||||||
|
get GuildPermissions() { return this._getModule('GuildPermissions'); },
|
||||||
|
get DiscordConstants() { return this._getModule('DiscordConstants'); },
|
||||||
|
get ChannelStore() { return this._getModule('ChannelStore'); },
|
||||||
|
get GuildStore() { return this._getModule('GuildStore'); },
|
||||||
|
get SelectedGuildStore() { return this._getModule('SelectedGuildStore'); },
|
||||||
|
get SelectedChannelStore() { return this._getModule('SelectedChannelStore'); },
|
||||||
|
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; }
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class DiscordApi {
|
||||||
|
|
||||||
|
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();
|
||||||
|
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();
|
||||||
|
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();
|
||||||
|
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() {
|
||||||
|
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());
|
||||||
|
if (channel) return Channel.from(channel);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current user.
|
||||||
|
* @type {User}
|
||||||
|
*/
|
||||||
|
static get currentUser() {
|
||||||
|
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();
|
||||||
|
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 }
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,159 @@
|
||||||
|
/**
|
||||||
|
* BetterDiscord Event Hook
|
||||||
|
* 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 Reflection from './reflection/index';
|
||||||
|
import { MonkeyPatch } from './patcher';
|
||||||
|
import Events from './events';
|
||||||
|
import EventListener from './eventlistener';
|
||||||
|
|
||||||
|
import * as SocketStructs from '../structs/socketstructs';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Discord socket event hook
|
||||||
|
* @extends {EventListener}
|
||||||
|
*/
|
||||||
|
export default class extends EventListener {
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.ignoreMultiple = -1;
|
||||||
|
this.hook();
|
||||||
|
}
|
||||||
|
|
||||||
|
bindings() {
|
||||||
|
this.hook = this.hook.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
get eventBindings() {
|
||||||
|
return [
|
||||||
|
{ id: 'discord-ready', callback: this.hook }
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
hook() {
|
||||||
|
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);
|
||||||
|
};*/
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Discord emit overload
|
||||||
|
* @param {any} event
|
||||||
|
* @param {any} action
|
||||||
|
* @param {any} data
|
||||||
|
*/
|
||||||
|
emit(e, action, data) {
|
||||||
|
switch (e) {
|
||||||
|
case 'dispatch':
|
||||||
|
return this.dispatch(action, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emit callback
|
||||||
|
* @param {any} event Event
|
||||||
|
* @param {any} data Event data
|
||||||
|
*/
|
||||||
|
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(type)) return;
|
||||||
|
Events.emit(`discord:${type}`, new SocketStructs[type](data));
|
||||||
|
}
|
||||||
|
|
||||||
|
get SocketStructs() {
|
||||||
|
return SocketStructs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All known socket actions
|
||||||
|
*/
|
||||||
|
get actions() {
|
||||||
|
if (this._actions) return this._actions;
|
||||||
|
|
||||||
|
return this._actions = {
|
||||||
|
READY: 'READY', // Socket ready
|
||||||
|
RESUMED: 'RESUMED', // Socket resumed
|
||||||
|
TYPING_START: 'TYPING_START', // User typing start
|
||||||
|
ACTIVITY_START: 'ACTIVITY_START', // ??
|
||||||
|
MESSAGE_CREATE: 'MESSAGE_CREATE', // New message
|
||||||
|
MESSAGE_UPDATE: 'MESSAGE_UPDATE', // Edit
|
||||||
|
MESSAGE_DELETE: 'MESSAGE_DELETE', // Message deleted
|
||||||
|
MESSAGE_DELETE_BULK: 'MESSAGE_DELETE_BULK', // Bulk messages deleted
|
||||||
|
MESSAGE_ACK: 'MESSAGE_ACK', // Message fetch
|
||||||
|
MESSAGE_REACTION_ADD: 'MESSAGE_REACTION_ADD', // eww reactions
|
||||||
|
MESSAGE_REACTION_REMOVE: 'MESSAGE_REACTION_REMOVE', // ^^
|
||||||
|
MESSAGE_REACTION_REMOVE_ALL: 'MESSAGE_REACTION_REMOVE_ALL', // ^^
|
||||||
|
CHANNEL_PINS_ACK: 'CHANNEL_PINS_ACK', // Pinned messages fetch
|
||||||
|
CHANNEL_PINS_UPDATE: 'CHANNEL_PINS_UPDATE', // Message pinned/unpinned, does not trigger when message is deleted
|
||||||
|
CHANNEL_CREATE: 'CHANNEL_CREATE', // Channel created
|
||||||
|
CHANNEL_DELETE: 'CHANNEL_DELETE', // Channel deleted
|
||||||
|
CHANNEL_UPDATE: 'CHANNEL_UPDATE', // Channel updated
|
||||||
|
GUILD_CREATE: 'GUILD_CREATE', // Guild create
|
||||||
|
GUILD_DELETE: 'GUILD_DELETE', // Guild delete
|
||||||
|
GUILD_SYNC: 'GUILD_SYNC', // Synced when switching to server that's not loaded
|
||||||
|
GUILD_MEMBERS_CHUNK: 'GUILD_MEMBERS_CHUNK', // ??
|
||||||
|
GUILD_BAN_ADD: 'GUILD_BAN_ADD', // User banned
|
||||||
|
GUILD_BAN_REMOVE: 'GUILD_BAN_REMOVE', // User unbanned
|
||||||
|
GUILD_MEMBER_ADD: 'GUILD_MEMBER_ADD', // User joins guild
|
||||||
|
GUILD_MEMBER_UPDATE: 'GUILD_MEMBER_UPDATE', // Roles etc
|
||||||
|
GUILD_MEMBER_REMOVE: 'GUILD_MEMBER_REMOVE', // Kick
|
||||||
|
GUILD_ROLE_CREATE: 'GUILD_ROLE_CREATE', // Roles
|
||||||
|
GUILD_ROLE_UPDATE: 'GUILD_ROLE_UPDATE', // Roles
|
||||||
|
GUILD_ROLE_DELETE: 'GUILD_ROLE_DELETE', // Roles,
|
||||||
|
GUILD_EMOJIS_UPDATE: 'GUILD_EMOJIS_UPDATE', // No emojis pls
|
||||||
|
GUILD_INTEGRATIONS_UPDATE: 'GUILD_INTEGRATIONS_UPDATE', // Twitch etc?
|
||||||
|
USER_UPDATE: 'USER_UPDATE', // Name change etc?
|
||||||
|
USER_SETTINGS_UPDATE: 'USER_SETTINGS_UPDATE', // Any setting change
|
||||||
|
USER_GUILD_SETTINGS_UPDATE: 'USER_GUILD_SETTINGS_UPDATE', // Guild notification/privacy etc
|
||||||
|
USER_CONNECTIONS_UPDATE: 'USER_CONNECTIONS_UPDATE', // Steam etc
|
||||||
|
USER_REQUIRED_ACTION_UPDATE: 'USER_REQUIRED_ACTION_UPDATE', // ??
|
||||||
|
USER_NOTE_UPDATE: 'USER_NOTE_UPDATE', // Not edits
|
||||||
|
RELATIONSHIP_ADD: 'RELATIONSHIP_ADD', // Friends
|
||||||
|
RELATIONSHIP_REMOVE: 'RELATIONSHIP_REMOVE', // Friends
|
||||||
|
PRESENCE_UPDATE: 'PRESENCE_UPDATE', // Status
|
||||||
|
PRESENCES_REPLACE: 'PRESENCES_REPLACE', // ??
|
||||||
|
VOICE_STATE_UPDATE: 'VOICE_STATE_UPDATE', // Speaking?
|
||||||
|
VOICE_SERVER_UPDATE: 'VOICE_SERVER_UPDATE', // ??
|
||||||
|
CALL_CREATE: 'CALL_CREATE', // Don't call me
|
||||||
|
CALL_UPDATE: 'CALL_UPDATE', // ^^^^^^^^^^^^
|
||||||
|
CALL_DELETE: 'CALL_DELETE', // ^^^^^^^^^^^^
|
||||||
|
OAUTH2_TOKEN_REVOKE: 'OAUTH2_TOKEN_REVOKE', // Logged out elsewhere?
|
||||||
|
RECENT_MENTION_DELETE: 'RECENT_MENTION_DELETE', // No idea what triggers this?
|
||||||
|
FRIEND_SUGGESTION_CREATE: 'FRIEND_SUGGESTION_CREATE', // Connected account stuff?
|
||||||
|
FRIEND_SUGGESTION_DELETE: 'FRIEND_SUGGESTION_DELETE', // ^^
|
||||||
|
WEBHOOKS_UPDATE: 'WEBHOOKS_UPDATE', // Any webhook change on any server
|
||||||
|
USER_PAYMENTS_UPDATE: 'USER_PAYMENTS_UPDATE', // Won't test
|
||||||
|
USER_BILLING_PROFILE_UPDATE: 'USER_BILLING_PROFILE_UPDATE', // Won't test
|
||||||
|
ACTIVITY_JOIN_REQUEST: 'ACTIVITY_JOIN_REQUEST', // Nothing seems to trigger this
|
||||||
|
ACTIVITY_JOIN_INVITE: 'ACTIVITY_JOIN_INVITE', // Same
|
||||||
|
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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
/**
|
||||||
|
* BetterDiscord Event Listener 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 './module';
|
||||||
|
import Events from './events';
|
||||||
|
|
||||||
|
export default class extends Module {
|
||||||
|
|
||||||
|
events() {
|
||||||
|
for (const event of this.eventBindings) {
|
||||||
|
Events.on(event.id, event.callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,54 @@
|
||||||
|
/**
|
||||||
|
* BetterDiscord Events 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 { EventEmitter } from 'events';
|
||||||
|
const emitter = new EventEmitter();
|
||||||
|
|
||||||
|
export default class {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
/**
|
||||||
|
* BetterDiscord Events Wrapper 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 eventemitters = new WeakMap();
|
||||||
|
|
||||||
|
export default class EventsWrapper {
|
||||||
|
|
||||||
|
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;
|
||||||
|
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 (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 (const event of this.eventSubs) {
|
||||||
|
eventemitters.get(this).removeListener(event.event, event.boundCallback);
|
||||||
|
}
|
||||||
|
this.eventSubs.splice(0, this.eventSubs.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
/**
|
||||||
|
* BetterDiscord External 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 Globals from './globals';
|
||||||
|
import Content from './content';
|
||||||
|
|
||||||
|
export default class ExtModule extends Content {
|
||||||
|
|
||||||
|
constructor(internals) {
|
||||||
|
super(internals);
|
||||||
|
this.__require = Globals.require(this.paths.mainPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
get type() { return 'module' }
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,62 @@
|
||||||
|
/**
|
||||||
|
* BetterDiscord External Module Manager 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 ContentManager from './contentmanager';
|
||||||
|
import ExtModule from './extmodule';
|
||||||
|
|
||||||
|
export default class extends ContentManager {
|
||||||
|
|
||||||
|
static get localModules() {
|
||||||
|
return this.localContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get contentType() {
|
||||||
|
return 'module';
|
||||||
|
}
|
||||||
|
|
||||||
|
static get moduleName() {
|
||||||
|
return 'Ext Module Manager';
|
||||||
|
}
|
||||||
|
|
||||||
|
static get pathId() {
|
||||||
|
return 'modules';
|
||||||
|
}
|
||||||
|
|
||||||
|
static get loadAllModules() {
|
||||||
|
return this.loadAllContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get refreshModules() { return this.refreshContent }
|
||||||
|
|
||||||
|
static get loadContent() { return this.loadModule }
|
||||||
|
static async loadModule(paths, configs, info, main) {
|
||||||
|
return new ExtModule({
|
||||||
|
configs, info, main,
|
||||||
|
paths: {
|
||||||
|
contentPath: paths.contentPath,
|
||||||
|
dirName: paths.dirName,
|
||||||
|
mainPath: paths.mainPath
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static get isExtModule() { return this.isThisContent }
|
||||||
|
static isThisContent(module) {
|
||||||
|
return module instanceof ExtModule;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get findModule() { return this.findContent }
|
||||||
|
static get getModuleIndex() { return this.getContentIndex }
|
||||||
|
static get getModuleByName() { return this.getContentByName }
|
||||||
|
static get getModuleById() { return this.getContentById }
|
||||||
|
static get getModuleByPath() { return this.getContentByPath }
|
||||||
|
static get getModuleByDirName() { return this.getContentByDirName }
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,104 @@
|
||||||
|
/**
|
||||||
|
* BetterDiscord Globals 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 path from 'path';
|
||||||
|
import sparkplug from 'sparkplug';
|
||||||
|
import { ClientIPC } from 'common';
|
||||||
|
import Module from './module';
|
||||||
|
import Events from './events';
|
||||||
|
|
||||||
|
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() {
|
||||||
|
this.first();
|
||||||
|
}
|
||||||
|
|
||||||
|
bindings() {
|
||||||
|
this.first = this.first.bind(this);
|
||||||
|
this.setWS = this.setWS.bind(this);
|
||||||
|
this.getObject = this.getObject.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = {};
|
||||||
|
|
||||||
|
if (sparkplug.bd) {
|
||||||
|
this.setState({ bd: sparkplug.bd });
|
||||||
|
sparkplug.bd.setWS = this.setWS;
|
||||||
|
}
|
||||||
|
|
||||||
|
Events.emit('global-ready');
|
||||||
|
Events.emit('socket-created', this.state.wsHook);
|
||||||
|
}
|
||||||
|
|
||||||
|
setWS(wSocket) {
|
||||||
|
const state = this.state;
|
||||||
|
state.wsHook = wSocket;
|
||||||
|
this.setState(state);
|
||||||
|
Events.emit('socket-created');
|
||||||
|
}
|
||||||
|
|
||||||
|
getObject(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.paths.find(path => path.id === id).path;
|
||||||
|
}
|
||||||
|
|
||||||
|
get version() {
|
||||||
|
return this.config.versions.core;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
export default class Module {
|
||||||
|
|
||||||
|
constructor(args) {
|
||||||
|
this.__ = {
|
||||||
|
state: args || {},
|
||||||
|
args
|
||||||
|
};
|
||||||
|
this.setState = this.setState.bind(this);
|
||||||
|
this.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
initialize() {
|
||||||
|
if (this.bindings) this.bindings();
|
||||||
|
if (this.setInitialState) this.setInitialState(this.state);
|
||||||
|
if (this.events) this.events();
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(newState) {
|
||||||
|
const oldState = 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; }
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
/**
|
||||||
|
* BetterDiscord Module Manager
|
||||||
|
* 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 { 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(),
|
||||||
|
Updater
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes all modules.
|
||||||
|
* @return {Promise}
|
||||||
|
*/
|
||||||
|
static async initModules() {
|
||||||
|
for (const module of this.modules) {
|
||||||
|
try {
|
||||||
|
if (module.init && module.init instanceof Function) module.init();
|
||||||
|
} catch (err) {
|
||||||
|
Logger.err('Module Manager', ['Failed to initialize module:', err]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
export { default as Events } from './events';
|
||||||
|
export { default as CssEditor } from './csseditor';
|
||||||
|
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 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 * 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 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';
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,221 @@
|
||||||
|
/**
|
||||||
|
* BetterDiscord Component Patcher
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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 = []) }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 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) {
|
||||||
|
patch.unpatch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static resolveModule(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 = 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);
|
||||||
|
} catch (err) {
|
||||||
|
Logger.err(`Patcher:${patch.id}`, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const insteads = patch.children.filter(c => c.type === 'instead');
|
||||||
|
if (!insteads.length) {
|
||||||
|
retVal = patch.originalFunction.apply(this, arguments);
|
||||||
|
} else {
|
||||||
|
for (const insteadPatch of insteads) {
|
||||||
|
try {
|
||||||
|
const tempReturn = insteadPatch.callback(this, arguments, patch.originalFunction.bind(this));
|
||||||
|
if (typeof tempReturn !== 'undefined') retVal = tempReturn;
|
||||||
|
} catch (err) {
|
||||||
|
Logger.err(`Patcher:${patch.id}`, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const slavePatch of patch.children.filter(c => c.type === 'after')) {
|
||||||
|
try {
|
||||||
|
const tempReturn = slavePatch.callback(this, arguments, retVal, r => retVal = r);
|
||||||
|
if (typeof tempReturn !== 'undefined') retVal = tempReturn;
|
||||||
|
} catch (err) {
|
||||||
|
Logger.err(`Patcher:${patch.id}`, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return retVal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static rePatch(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) {
|
||||||
|
const patch = {
|
||||||
|
caller,
|
||||||
|
id,
|
||||||
|
module,
|
||||||
|
functionName,
|
||||||
|
originalFunction: module[functionName],
|
||||||
|
proxyFunction: null,
|
||||||
|
revert: () => { // Calling revert will destroy any patches added to the same module after this
|
||||||
|
patch.module[patch.functionName] = patch.originalFunction;
|
||||||
|
patch.proxyFunction = null;
|
||||||
|
patch.children = [];
|
||||||
|
},
|
||||||
|
counter: 0,
|
||||||
|
children: []
|
||||||
|
};
|
||||||
|
patch.proxyFunction = module[functionName] = this.overrideFn(patch);
|
||||||
|
return this.patches.push(patch), patch;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 = typeof unresolvedModule === 'string' ? unresolvedModule :
|
||||||
|
displayName || module.displayName || module.name || module.constructor.displayName || module.constructor.name;
|
||||||
|
const patchId = `${displayName}:${functionName}:${caller}`;
|
||||||
|
|
||||||
|
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,
|
||||||
|
type,
|
||||||
|
id: patch.counter,
|
||||||
|
callback,
|
||||||
|
unpatch: () => {
|
||||||
|
patch.children.splice(patch.children.findIndex(cpatch => cpatch.id === child.id && cpatch.type === type), 1);
|
||||||
|
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);
|
||||||
|
patch.counter++;
|
||||||
|
return child.unpatch;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MonkeyPatch = (caller, module, displayName) => ({
|
||||||
|
before: (functionName, callBack) => Patcher.before(caller, module, functionName, callBack, displayName),
|
||||||
|
after: (functionName, callBack) => Patcher.after(caller, module, functionName, callBack, displayName),
|
||||||
|
instead: (functionName, callBack) => Patcher.instead(caller, module, functionName, callBack, displayName)
|
||||||
|
});
|
|
@ -0,0 +1,44 @@
|
||||||
|
/**
|
||||||
|
* BetterDiscord Permission Manager
|
||||||
|
* 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 PermissionMap = {
|
||||||
|
IDENTIFY: {
|
||||||
|
HEADER: 'Access your account information',
|
||||||
|
BODY: 'Allows :NAME: to read your account information (excluding user token).'
|
||||||
|
},
|
||||||
|
READ_MESSAGES: {
|
||||||
|
HEADER: 'Read all messages',
|
||||||
|
BODY: 'Allows :NAME: to read all messages accessible through your Discord account.'
|
||||||
|
},
|
||||||
|
SEND_MESSAGES: {
|
||||||
|
HEADER: 'Send messages',
|
||||||
|
BODY: 'Allows :NAME: to send messages on your behalf.'
|
||||||
|
},
|
||||||
|
DELETE_MESSAGES: {
|
||||||
|
HEADER: 'Delete messages',
|
||||||
|
BODY: 'Allows :NAME: to delete messages on your behalf.'
|
||||||
|
},
|
||||||
|
EDIT_MESSAGES: {
|
||||||
|
HEADER: 'Edit messages',
|
||||||
|
BODY: 'Allows :NAME: to edit messages on your behalf.'
|
||||||
|
},
|
||||||
|
JOIN_SERVERS: {
|
||||||
|
HEADER: 'Join servers for you',
|
||||||
|
BODY: 'Allows :NAME: to join servers on your behalf.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class {
|
||||||
|
|
||||||
|
static permissionText(permission) {
|
||||||
|
return PermissionMap[permission];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
/**
|
||||||
|
* BetterDiscord Plugin 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 PluginManager from './pluginmanager';
|
||||||
|
import Content from './content';
|
||||||
|
|
||||||
|
export default class Plugin extends Content {
|
||||||
|
|
||||||
|
get type() { return 'plugin' }
|
||||||
|
|
||||||
|
get start() { return this.enable }
|
||||||
|
get stop() { return this.disable }
|
||||||
|
|
||||||
|
reload(force) {
|
||||||
|
return PluginManager.reloadPlugin(this, force);
|
||||||
|
}
|
||||||
|
|
||||||
|
unload(force) {
|
||||||
|
return PluginManager.unloadPlugin(this, force);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,624 @@
|
||||||
|
/**
|
||||||
|
* BetterDiscord Plugin 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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 Reflection from './reflection/index';
|
||||||
|
import DiscordApi from './discordapi';
|
||||||
|
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, 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 plugin() {
|
||||||
|
return PluginManager.getPluginByPath(this.pluginPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
async bridge(plugin_id) {
|
||||||
|
const plugin = await PluginManager.waitForPlugin(plugin_id);
|
||||||
|
return plugin.bridge;
|
||||||
|
}
|
||||||
|
|
||||||
|
get require() { return this.import }
|
||||||
|
import(m) {
|
||||||
|
const module = ExtModuleManager.findModule(m);
|
||||||
|
if (module && module.__require) return module.__require;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
|
||||||
|
get Logger() {
|
||||||
|
return {
|
||||||
|
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)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utils
|
||||||
|
*/
|
||||||
|
|
||||||
|
get Utils() {
|
||||||
|
return {
|
||||||
|
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)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Settings
|
||||||
|
*/
|
||||||
|
|
||||||
|
createSettingsSet(args, ...merge) {
|
||||||
|
return new SettingsSet(args || {}, ...merge);
|
||||||
|
}
|
||||||
|
createSettingsCategory(args, ...merge) {
|
||||||
|
return new SettingsCategory(args, ...merge);
|
||||||
|
}
|
||||||
|
createSetting(args, ...merge) {
|
||||||
|
return new Setting(args, ...merge);
|
||||||
|
}
|
||||||
|
createSettingsScheme(args) {
|
||||||
|
return new SettingsScheme(args);
|
||||||
|
}
|
||||||
|
get Settings() {
|
||||||
|
return {
|
||||||
|
createSet: this.createSettingsSet.bind(this),
|
||||||
|
createCategory: this.createSettingsCategory.bind(this),
|
||||||
|
createSetting: this.createSetting.bind(this),
|
||||||
|
createScheme: this.createSettingsScheme.bind(this)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* InternalSettings
|
||||||
|
*/
|
||||||
|
|
||||||
|
getInternalSetting(set, category, setting) {
|
||||||
|
return Settings.get(set, category, setting);
|
||||||
|
}
|
||||||
|
get InternalSettings() {
|
||||||
|
return {
|
||||||
|
get: this.getInternalSetting.bind(this)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BdMenu
|
||||||
|
*/
|
||||||
|
|
||||||
|
get BdMenu() {
|
||||||
|
return {
|
||||||
|
open: BdMenu.open.bind(BdMenu),
|
||||||
|
close: BdMenu.close.bind(BdMenu),
|
||||||
|
items: this.BdMenuItems,
|
||||||
|
BdMenuItems: this.BdMenuItems
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BdMenuItems
|
||||||
|
*/
|
||||||
|
|
||||||
|
get menuItems() {
|
||||||
|
return this._menuItems || (this._menuItems = []);
|
||||||
|
}
|
||||||
|
addMenuItem(item) {
|
||||||
|
return BdMenu.items.add(item);
|
||||||
|
}
|
||||||
|
addMenuSettingsSet(category, set, text) {
|
||||||
|
const item = BdMenu.items.addSettingsSet(category, set, text);
|
||||||
|
return this.menuItems.push(item);
|
||||||
|
}
|
||||||
|
addMenuVueComponent(category, text, component) {
|
||||||
|
const item = BdMenu.items.addVueComponent(category, text, component);
|
||||||
|
return this.menuItems.push(item);
|
||||||
|
}
|
||||||
|
removeMenuItem(item) {
|
||||||
|
BdMenu.items.remove(item);
|
||||||
|
Utils.removeFromArray(this.menuItems, item);
|
||||||
|
}
|
||||||
|
removeAllMenuItems() {
|
||||||
|
for (const item of this.menuItems)
|
||||||
|
BdMenu.items.remove(item);
|
||||||
|
}
|
||||||
|
get BdMenuItems() {
|
||||||
|
return Object.defineProperty({
|
||||||
|
add: this.addMenuItem.bind(this),
|
||||||
|
addSettingsSet: this.addMenuSettingsSet.bind(this),
|
||||||
|
addVueComponent: this.addMenuVueComponent.bind(this),
|
||||||
|
remove: this.removeMenuItem.bind(this),
|
||||||
|
removeAll: this.removeAllMenuItems.bind(this)
|
||||||
|
}, 'items', {
|
||||||
|
get: () => this.menuItems
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
|
||||||
|
get injectedStyles() {
|
||||||
|
return this._injectedStyles || (this._injectedStyles = []);
|
||||||
|
}
|
||||||
|
compileSass(scss, options) {
|
||||||
|
return ClientIPC.send('bd-compileSass', Object.assign({ data: scss }, options));
|
||||||
|
}
|
||||||
|
getConfigAsSCSS(settingsset) {
|
||||||
|
return ThemeManager.getConfigAsSCSS(settingsset ? settingsset : this.plugin.settings);
|
||||||
|
}
|
||||||
|
getConfigAsSCSSMap(settingsset) {
|
||||||
|
return ThemeManager.getConfigAsSCSSMap(settingsset ? settingsset : this.plugin.settings);
|
||||||
|
}
|
||||||
|
injectStyle(id, css) {
|
||||||
|
if (id && !css) css = id, id = undefined;
|
||||||
|
this.deleteStyle(id);
|
||||||
|
const styleid = `plugin-${this.plugin.id}-${id}`;
|
||||||
|
this.injectedStyles.push(id);
|
||||||
|
DOM.injectStyle(css, styleid);
|
||||||
|
}
|
||||||
|
async injectSass(id, scss, options) {
|
||||||
|
// In most cases a plugin's styles should be precompiled instead of using this
|
||||||
|
if (id && !scss && !options) scss = id, id = undefined;
|
||||||
|
const css = (await this.compileSass(scss, options)).css.toString();
|
||||||
|
this.injectStyle(id, css, options);
|
||||||
|
}
|
||||||
|
deleteStyle(id) {
|
||||||
|
const styleid = `plugin-${this.plugin.id}-${id}`;
|
||||||
|
this.injectedStyles.splice(this.injectedStyles.indexOf(styleid), 1);
|
||||||
|
DOM.deleteStyle(styleid);
|
||||||
|
}
|
||||||
|
deleteAllStyles(id) {
|
||||||
|
for (const id of this.injectedStyles) {
|
||||||
|
this.deleteStyle(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
get CssUtils() {
|
||||||
|
return {
|
||||||
|
compileSass: this.compileSass.bind(this),
|
||||||
|
getConfigAsSCSS: this.getConfigAsSCSS.bind(this),
|
||||||
|
getConfigAsSCSSMap: this.getConfigAsSCSSMap.bind(this),
|
||||||
|
injectStyle: this.injectStyle.bind(this),
|
||||||
|
injectSass: this.injectSass.bind(this),
|
||||||
|
deleteStyle: this.deleteStyle.bind(this),
|
||||||
|
deleteAllStyles: this.deleteAllStyles.bind(this)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modals
|
||||||
|
*/
|
||||||
|
|
||||||
|
get modalStack() {
|
||||||
|
return this._modalStack || (this._modalStack = []);
|
||||||
|
}
|
||||||
|
addModal(_modal, component) {
|
||||||
|
const modal = Modals.add(_modal, component);
|
||||||
|
modal.on('close', () => Utils.removeFromArray(this.modalStack, modal));
|
||||||
|
this.modalStack.push(modal);
|
||||||
|
return modal;
|
||||||
|
}
|
||||||
|
closeModal(modal, force) {
|
||||||
|
return Modals.close(modal, force);
|
||||||
|
}
|
||||||
|
closeAllModals(force) {
|
||||||
|
const promises = [];
|
||||||
|
for (const modal of this.modalStack)
|
||||||
|
promises.push(modal.close(force));
|
||||||
|
return Promise.all(promises);
|
||||||
|
}
|
||||||
|
closeLastModal(force) {
|
||||||
|
if (!this.modalStack.length) return;
|
||||||
|
return this.modalStack[this.modalStack.length - 1].close(force);
|
||||||
|
}
|
||||||
|
basicModal(title, text) {
|
||||||
|
return this.addModal(Modals.createBasicModal(title, text));
|
||||||
|
}
|
||||||
|
settingsModal(settingsset, headertext, options) {
|
||||||
|
return this.addModal(Modals.createSettingsModal(settingsset, headertext, options));
|
||||||
|
}
|
||||||
|
get Modals() {
|
||||||
|
return Object.defineProperties({
|
||||||
|
add: this.addModal.bind(this),
|
||||||
|
close: this.closeModal.bind(this),
|
||||||
|
closeAll: this.closeAllModals.bind(this),
|
||||||
|
closeLast: this.closeLastModal.bind(this),
|
||||||
|
basic: this.basicModal.bind(this),
|
||||||
|
settings: this.settingsModal.bind(this)
|
||||||
|
}, {
|
||||||
|
stack: {
|
||||||
|
get: () => this.modalStack
|
||||||
|
},
|
||||||
|
baseComponent: {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugins
|
||||||
|
*/
|
||||||
|
|
||||||
|
async getPlugin(plugin_id) {
|
||||||
|
// This should require extra permissions
|
||||||
|
return PluginManager.waitForPlugin(plugin_id);
|
||||||
|
}
|
||||||
|
listPlugins() {
|
||||||
|
return PluginManager.localContent.map(plugin => plugin.id);
|
||||||
|
}
|
||||||
|
get Plugins() {
|
||||||
|
return {
|
||||||
|
getPlugin: this.getPlugin.bind(this),
|
||||||
|
listPlugins: this.listPlugins.bind(this)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Themes
|
||||||
|
*/
|
||||||
|
|
||||||
|
async getTheme(theme_id) {
|
||||||
|
// This should require extra permissions
|
||||||
|
return ThemeManager.waitForContent(theme_id);
|
||||||
|
}
|
||||||
|
listThemes() {
|
||||||
|
return ThemeManager.localContent.map(theme => theme.id);
|
||||||
|
}
|
||||||
|
get Themes() {
|
||||||
|
return {
|
||||||
|
getTheme: this.getTheme.bind(this),
|
||||||
|
listThemes: this.listThemes.bind(this)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ExtModules
|
||||||
|
*/
|
||||||
|
|
||||||
|
async getModule(module_id) {
|
||||||
|
// This should require extra permissions
|
||||||
|
return ExtModuleManager.waitForContent(module_id);
|
||||||
|
}
|
||||||
|
listModules() {
|
||||||
|
return ExtModuleManager.localContent.map(module => module.id);
|
||||||
|
}
|
||||||
|
get ExtModules() {
|
||||||
|
return {
|
||||||
|
getModule: this.getModule.bind(this),
|
||||||
|
listModules: this.listModules.bind(this)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Patcher
|
||||||
|
*/
|
||||||
|
|
||||||
|
get patches() {
|
||||||
|
return Patcher.getPatchesByCaller(this.plugin.id);
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
unpatchAll(patches) {
|
||||||
|
return Patcher.unpatchAll(patches || this.plugin.id);
|
||||||
|
}
|
||||||
|
get Patcher() {
|
||||||
|
return Object.defineProperty({
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop plugins from modifying the plugin API for all plugins
|
||||||
|
// Plugins can still modify their own plugin API object
|
||||||
|
Object.freeze(PluginApi);
|
||||||
|
Object.freeze(PluginApi.prototype);
|
|
@ -0,0 +1,170 @@
|
||||||
|
/**
|
||||||
|
* BetterDiscord Plugin Manager 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 { 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';
|
||||||
|
|
||||||
|
export default class extends ContentManager {
|
||||||
|
|
||||||
|
static get localPlugins() {
|
||||||
|
return this.localContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get contentType() {
|
||||||
|
return 'plugin';
|
||||||
|
}
|
||||||
|
|
||||||
|
static get moduleName() {
|
||||||
|
return 'Plugin Manager';
|
||||||
|
}
|
||||||
|
|
||||||
|
static get pathId() {
|
||||||
|
return 'plugins';
|
||||||
|
}
|
||||||
|
|
||||||
|
static async loadAllPlugins(suppressErrors) {
|
||||||
|
this.loaded = false;
|
||||||
|
const loadAll = await this.loadAllContent(true);
|
||||||
|
this.loaded = true;
|
||||||
|
for (const plugin of this.localPlugins) {
|
||||||
|
if (!plugin.enabled) continue;
|
||||||
|
plugin.userConfig.enabled = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
plugin.start(false);
|
||||||
|
} catch (err) {
|
||||||
|
// Disable the plugin but don't save it - the next time BetterDiscord is started the plugin will attempt to start again
|
||||||
|
this.errors.push(new ErrorEvent({
|
||||||
|
module: this.moduleName,
|
||||||
|
message: `Failed to start ${plugin.name}`,
|
||||||
|
err
|
||||||
|
}));
|
||||||
|
|
||||||
|
Logger.err(this.moduleName, [`Failed to start plugin ${plugin.name}:`, err]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.errors.length && !suppressErrors) {
|
||||||
|
Modals.error({
|
||||||
|
header: `${this.moduleName} - ${this.errors.length} ${this.contentType}${this.errors.length !== 1 ? 's' : ''} failed to load`,
|
||||||
|
module: this.moduleName,
|
||||||
|
type: 'err',
|
||||||
|
content: this.errors
|
||||||
|
});
|
||||||
|
this._errors = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return loadAll;
|
||||||
|
}
|
||||||
|
static get refreshPlugins() { return this.refreshContent }
|
||||||
|
|
||||||
|
static get loadContent() { return this.loadPlugin }
|
||||||
|
static async loadPlugin(paths, configs, info, main, dependencies, permissions, mainExport, packed = false) {
|
||||||
|
if (permissions && permissions.length > 0) {
|
||||||
|
for (const perm of permissions) {
|
||||||
|
Logger.log(this.moduleName, `Permission: ${Permissions.permissionText(perm).HEADER} - ${Permissions.permissionText(perm).BODY}`);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const allowed = await Modals.permissions(`${info.name} wants to:`, info.name, permissions).promise;
|
||||||
|
} catch (err) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.`};
|
||||||
|
}
|
||||||
|
deps[key] = extModule.__require;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
});
|
||||||
|
|
||||||
|
if (instance.enabled && this.loaded) {
|
||||||
|
instance.userConfig.enabled = false;
|
||||||
|
instance.start(false);
|
||||||
|
}
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get deletePlugin() { return this.deleteContent }
|
||||||
|
static get unloadPlugin() { return this.unloadContent }
|
||||||
|
static get reloadPlugin() { return this.reloadContent }
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
for (const u of uncache) delete Globals.require.cache[u];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 }
|
||||||
|
static isThisContent(plugin) {
|
||||||
|
return plugin instanceof Plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get findPlugin() { return this.findContent }
|
||||||
|
static get getPluginIndex() { return this.getContentIndex }
|
||||||
|
static get getPluginByName() { return this.getContentByName }
|
||||||
|
static get getPluginById() { return this.getContentById }
|
||||||
|
static get getPluginByPath() { return this.getContentByPath }
|
||||||
|
static get getPluginByDirName() { return this.getContentByDirName }
|
||||||
|
|
||||||
|
static get waitForPlugin() { return this.waitForContent }
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,586 @@
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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 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) {
|
||||||
|
const item = parent[key];
|
||||||
|
if (item instanceof Array) {
|
||||||
|
for (const subKey of item.keys()) {
|
||||||
|
yield* innerCall(item, subKey)
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
yield { item, parent, key, index: index++, count };
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
++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 (const { parent, key, index, count } of this.recursiveArrayCount(item.props, 'children')) {
|
||||||
|
yield* this.recursiveChildren(parent, key, index, count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static returnFirst(iterator, process) {
|
||||||
|
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) {
|
||||||
|
return this.returnFirst(this.recursiveArrayCount(item.props, 'children'), checkFilter.bind(null, selector));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const checkFilter = (selector, { item, parent, key, count, index }) => {
|
||||||
|
let match = true;
|
||||||
|
if (match && selector.type)
|
||||||
|
match = item && selector.type === item.type;
|
||||||
|
if (match && selector.tag)
|
||||||
|
match = item && typeof item.type === 'string' && selector.tag === item.type;
|
||||||
|
if (match && selector.className) {
|
||||||
|
match = item && item.props && typeof item.props.className === 'string';
|
||||||
|
if (match) {
|
||||||
|
const classes = item.props.className.split(' ');
|
||||||
|
if (selector.className === true)
|
||||||
|
match = !!classes[0];
|
||||||
|
else if (typeof selector.className === 'string')
|
||||||
|
match = classes.includes(selector.className);
|
||||||
|
else if (selector.className instanceof RegExp)
|
||||||
|
match = !!classes.find(cls => selector.className.test(cls));
|
||||||
|
else match = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (match && selector.text) {
|
||||||
|
if (selector.text === true)
|
||||||
|
match = typeof item === 'string';
|
||||||
|
else if (typeof selector.text === 'string')
|
||||||
|
match = item === selector.text;
|
||||||
|
else if (selector.text instanceof RegExp)
|
||||||
|
match = typeof item === 'string' && selector.text.test(item);
|
||||||
|
else match = false;
|
||||||
|
}
|
||||||
|
if (match && selector.nthChild)
|
||||||
|
match = index === (selector.nthChild < 0 ? count + selector.nthChild : selector.nthChild);
|
||||||
|
if (match && selector.hasChild)
|
||||||
|
match = getDirectChild(item, selector.hasChild);
|
||||||
|
if (match && selector.hasSuccessor)
|
||||||
|
match = item && !!this.getFirstChild(parent, key, selector.hasSuccessor).item;
|
||||||
|
if (match && selector.eq) {
|
||||||
|
--selector.eq;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (match) {
|
||||||
|
if (selector.child) {
|
||||||
|
return getDirectChild(item, selector.child);
|
||||||
|
} else if (selector.successor) {
|
||||||
|
return this.getFirstChild(parent, key, selector.successor);
|
||||||
|
}
|
||||||
|
|
||||||
|
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) 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);
|
||||||
|
if (findInChild) return findInChild;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static findProp(obj, what) {
|
||||||
|
if (obj.hasOwnProperty(what)) return obj[what];
|
||||||
|
if (obj.props && !obj.children) return this.findProp(obj.props, what);
|
||||||
|
if (!obj.children) return null;
|
||||||
|
if (!(obj.children instanceof Array)) return this.findProp(obj.children, what);
|
||||||
|
for (const child of obj.children) {
|
||||||
|
if (!child) continue;
|
||||||
|
const findInChild = this.findProp(child, what);
|
||||||
|
if (findInChild) return findInChild;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get React() {
|
||||||
|
return Reflection.modules.React;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get ReactDOM() {
|
||||||
|
return Reflection.modules.ReactDOM;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Helpers as ReactHelpers };
|
||||||
|
|
||||||
|
class ReactComponent {
|
||||||
|
constructor(id, component, retVal, important) {
|
||||||
|
this.id = id;
|
||||||
|
this.component = component;
|
||||||
|
this.retVal = retVal;
|
||||||
|
this.important = important;
|
||||||
|
}
|
||||||
|
|
||||||
|
get elements() {
|
||||||
|
if (!this.important || !this.important.selector) return [];
|
||||||
|
|
||||||
|
return document.querySelectorAll(this.important.selector);
|
||||||
|
}
|
||||||
|
|
||||||
|
get stateNodes() {
|
||||||
|
return [...this.elements].map(e => Reflection.DOM(e).getComponentStateNode(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
forceUpdateAll() {
|
||||||
|
for (const e of this.elements) {
|
||||||
|
Reflection.DOM(e).forceUpdate(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ReactComponents {
|
||||||
|
static get components() { return this._components || (this._components = []) }
|
||||||
|
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 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) {
|
||||||
|
if (!have.important) have.important = important;
|
||||||
|
return component;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 callback = () => {
|
||||||
|
if (this.components.find(c => c.id === name)) {
|
||||||
|
Logger.info('ReactComponents', `Important component ${name} already found`);
|
||||||
|
DOM.observer.unsubscribe(observerSubscription);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
let listener = this.listeners.find(l => l.id === name);
|
||||||
|
if (!listener) this.listeners.push(listener = {
|
||||||
|
id: name,
|
||||||
|
listeners: []
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Promise(resolve => {
|
||||||
|
listener.listeners.push(resolve);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
for (const [rci, rc] of this.unknownComponents.entries()) {
|
||||||
|
if (filter(rc.component)) {
|
||||||
|
rc.component.displayName = name;
|
||||||
|
this.unknownComponents.splice(rci, 1);
|
||||||
|
return this.push(rc.component);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.nameSetters.push({ name, filter });
|
||||||
|
}
|
||||||
|
|
||||||
|
static processUnknown(component, retVal) {
|
||||||
|
const have = this.unknownComponents.find(c => c.component === component);
|
||||||
|
for (const [fi, filter] of this.nameSetters.entries()) {
|
||||||
|
if (filter.filter.filter(component)) {
|
||||||
|
Logger.log('ReactComponents', 'Filter match!');
|
||||||
|
component.displayName = filter.name;
|
||||||
|
this.nameSetters.splice(fi, 1);
|
||||||
|
return this.push(component, retVal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (have) return have;
|
||||||
|
this.unknownComponents.push(component);
|
||||||
|
return component;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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() {
|
||||||
|
const React = await Reflection.module.waitForModuleByName('React');
|
||||||
|
|
||||||
|
this.unpatchCreateElement = MonkeyPatch('BD:ReactComponents:createElement', React).before('createElement', (component, args) => ReactComponents.push(args[0]));
|
||||||
|
|
||||||
|
this.patchComponents();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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() {
|
||||||
|
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, 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;
|
||||||
|
if (bot || (author && author.bot)) retVal.props.className += ' bd-isBot';
|
||||||
|
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() {
|
||||||
|
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() {
|
||||||
|
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) 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() {
|
||||||
|
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() {
|
||||||
|
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 patchChannelTextArea() {
|
||||||
|
const { selector } = Reflection.resolve('channelTextArea', 'autocomplete');
|
||||||
|
this.ChannelTextArea = await ReactComponents.getComponent('ChannelTextArea', {selector});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 }
|
|
@ -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}`;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 }
|
|
@ -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');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,165 @@
|
||||||
|
/**
|
||||||
|
* BetterDiscord Settings 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 { 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 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]);
|
||||||
|
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 => {
|
||||||
|
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, favourite_emotes } = user_config;
|
||||||
|
|
||||||
|
for (const set of this.settings) {
|
||||||
|
const newSet = settings.find(s => s.id === set.id);
|
||||||
|
if (!newSet) continue;
|
||||||
|
await set.merge(newSet, {dont_save: true});
|
||||||
|
set.setSaved();
|
||||||
|
}
|
||||||
|
|
||||||
|
CssEditor.setState(scss, css, css_editor_files, scss_error);
|
||||||
|
CssEditor.editor_bounds = css_editor_bounds || {};
|
||||||
|
} catch (err) {
|
||||||
|
// There was an error loading settings
|
||||||
|
// This probably means that the user doesn't have any settings yet
|
||||||
|
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);
|
||||||
|
|
||||||
|
const settingsPath = path.resolve(this.dataPath, 'user.settings.json');
|
||||||
|
await FileUtils.writeJsonToFile(settingsPath, {
|
||||||
|
settings: this.settings.map(set => set.strip()),
|
||||||
|
scss: CssEditor.scss,
|
||||||
|
css: CssEditor.css,
|
||||||
|
css_editor_files: CssEditor.files,
|
||||||
|
scss_error: CssEditor.error,
|
||||||
|
css_editor_bounds: CssEditor.editor_bounds
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const set of this.settings) {
|
||||||
|
set.setSaved();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// There was an error saving settings
|
||||||
|
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.settings.find(s => s.id === set_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
get core() { return this.getSet('core') }
|
||||||
|
get ui() { return this.getSet('ui') }
|
||||||
|
get emotes() { return this.getSet('emotes') }
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The path to store user data in.
|
||||||
|
*/
|
||||||
|
get dataPath() {
|
||||||
|
return Globals.getPath('data');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
/**
|
||||||
|
* BetterDiscord Discord Socket Proxy 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 { ClientLogger as Logger } from 'common';
|
||||||
|
import EventListener from './eventlistener';
|
||||||
|
|
||||||
|
export default class SocketProxy extends EventListener {
|
||||||
|
|
||||||
|
bindings() {
|
||||||
|
this.socketCreated = this.socketCreated.bind(this);
|
||||||
|
this.onMessage = this.onMessage.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
get eventBindings() {
|
||||||
|
return [
|
||||||
|
{ id: 'socket-created', callback: this.socketCreated }
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
get socket() {
|
||||||
|
return this.activeSocket;
|
||||||
|
}
|
||||||
|
|
||||||
|
socketCreated(socket) {
|
||||||
|
this.activeSocket = socket;
|
||||||
|
// socket.addEventListener('message', this.onMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
onMessage(e) {
|
||||||
|
Logger.info('SocketProxy', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,176 @@
|
||||||
|
/**
|
||||||
|
* BetterDiscord Theme 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 Content from './content';
|
||||||
|
import Settings from './settings';
|
||||||
|
import ThemeManager from './thememanager';
|
||||||
|
import { DOM } from 'ui';
|
||||||
|
import { FileUtils, ClientIPC, ClientLogger as Logger } from 'common';
|
||||||
|
import filewatcher from 'filewatcher';
|
||||||
|
|
||||||
|
export default class Theme extends Content {
|
||||||
|
|
||||||
|
constructor(internals) {
|
||||||
|
super(internals);
|
||||||
|
|
||||||
|
const watchfiles = Settings.getSetting('css', 'default', 'watch-files');
|
||||||
|
if (watchfiles.value) this.watchfiles = this.files;
|
||||||
|
watchfiles.on('setting-updated', event => {
|
||||||
|
if (event.value) this.watchfiles = this.files;
|
||||||
|
else this.watchfiles = [];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get type() { return 'theme' }
|
||||||
|
get css() { return this.data.css }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when settings are updated.
|
||||||
|
* This can be overridden by other content types.
|
||||||
|
*/
|
||||||
|
__settingsUpdated(event) {
|
||||||
|
return this.recompile();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is called when the theme is enabled.
|
||||||
|
*/
|
||||||
|
async onstart() {
|
||||||
|
if (!this.css) await this.recompile();
|
||||||
|
DOM.injectTheme(this.css, this.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is called when the theme is disabled.
|
||||||
|
*/
|
||||||
|
onstop() {
|
||||||
|
DOM.deleteTheme(this.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compiles the theme and returns an object containing the CSS and an array of files that were included.
|
||||||
|
* @return {Promise}
|
||||||
|
*/
|
||||||
|
async compile() {
|
||||||
|
Logger.log(this.name, 'Compiling CSS');
|
||||||
|
|
||||||
|
if (this.info.type === 'sass') {
|
||||||
|
const config = await ThemeManager.getConfigAsSCSS(this.settings);
|
||||||
|
|
||||||
|
const result = await ClientIPC.send('bd-compileSass', {
|
||||||
|
data: config,
|
||||||
|
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); 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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { css: await FileUtils.readFile(this.paths.mainPath) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compiles the theme and updates and saves the CSS and the list of include files.
|
||||||
|
* @return {Promise}
|
||||||
|
*/
|
||||||
|
async recompile() {
|
||||||
|
const data = await this.compile();
|
||||||
|
this.data.css = data.css;
|
||||||
|
this.files = data.files;
|
||||||
|
|
||||||
|
await this.saveConfiguration();
|
||||||
|
|
||||||
|
if (this.enabled) {
|
||||||
|
DOM.deleteTheme(this.id);
|
||||||
|
DOM.injectTheme(this.css, this.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An array of files that are imported in the theme's SCSS.
|
||||||
|
* @return {Array} Files being watched
|
||||||
|
*/
|
||||||
|
get files() {
|
||||||
|
return this.data.files || (this.data.files = []);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets all files that are imported in the theme's SCSS.
|
||||||
|
* @param {Array} files Files to watch
|
||||||
|
*/
|
||||||
|
set files(files) {
|
||||||
|
this.data.files = files;
|
||||||
|
|
||||||
|
if (Settings.get('css', 'default', 'watch-files'))
|
||||||
|
this.watchfiles = files;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A filewatcher instance.
|
||||||
|
*/
|
||||||
|
get filewatcher() {
|
||||||
|
if (this._filewatcher) return this._filewatcher;
|
||||||
|
this._filewatcher = filewatcher();
|
||||||
|
this._filewatcher.on('change', (file, stat) => {
|
||||||
|
// Recompile SCSS
|
||||||
|
this.recompile();
|
||||||
|
});
|
||||||
|
return this._filewatcher;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An array of files that are being watched for changes.
|
||||||
|
* @return {Array} Files being watched
|
||||||
|
*/
|
||||||
|
get watchfiles() {
|
||||||
|
return this._watchfiles || (this._watchfiles = []);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets all files to be watched.
|
||||||
|
* @param {Array} files Files to watch
|
||||||
|
*/
|
||||||
|
set watchfiles(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);
|
||||||
|
Logger.log(this.name, `Watching file ${file} for changes`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const index in this.watchfiles) {
|
||||||
|
let file = this.watchfiles[index];
|
||||||
|
while (file && !files.find(f => f === file)) {
|
||||||
|
this.filewatcher.remove(file);
|
||||||
|
this.watchfiles.splice(index, 1);
|
||||||
|
Logger.log(this.name, `No longer watching file ${file} for changes`);
|
||||||
|
file = this.watchfiles[index];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,129 @@
|
||||||
|
/**
|
||||||
|
* BetterDiscord Theme Manager 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 ContentManager from './contentmanager';
|
||||||
|
import Theme from './theme';
|
||||||
|
|
||||||
|
export default class ThemeManager extends ContentManager {
|
||||||
|
|
||||||
|
static get localThemes() {
|
||||||
|
return this.localContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get contentType() {
|
||||||
|
return 'theme';
|
||||||
|
}
|
||||||
|
|
||||||
|
static get moduleName() {
|
||||||
|
return 'Theme Manager';
|
||||||
|
}
|
||||||
|
|
||||||
|
static get pathId() {
|
||||||
|
return 'themes';
|
||||||
|
}
|
||||||
|
|
||||||
|
static get loadAllThemes() { return this.loadAllContent }
|
||||||
|
static get refreshThemes() { return this.refreshContent }
|
||||||
|
|
||||||
|
static get loadContent() { return this.loadTheme }
|
||||||
|
static async loadTheme(paths, configs, info, main) {
|
||||||
|
try {
|
||||||
|
const instance = new Theme({
|
||||||
|
configs, info, main, paths
|
||||||
|
});
|
||||||
|
if (instance.enabled) {
|
||||||
|
instance.userConfig.enabled = false;
|
||||||
|
instance.enable();
|
||||||
|
}
|
||||||
|
return instance;
|
||||||
|
} catch (err) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static get deleteTheme() { return this.deleteContent }
|
||||||
|
static get unloadTheme() { return this.unloadContent }
|
||||||
|
static async reloadTheme(theme) {
|
||||||
|
theme = await this.reloadContent(theme);
|
||||||
|
theme.recompile();
|
||||||
|
}
|
||||||
|
|
||||||
|
static enableTheme(theme) {
|
||||||
|
return theme.enable();
|
||||||
|
}
|
||||||
|
|
||||||
|
static disableTheme(theme) {
|
||||||
|
return theme.disable();
|
||||||
|
}
|
||||||
|
|
||||||
|
static get isTheme() { return this.isThisContent }
|
||||||
|
static isThisContent(theme) {
|
||||||
|
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 (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('\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 (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(', ')})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 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, '\\\'') : ''}'`;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,190 @@
|
||||||
|
/**
|
||||||
|
* BetterDiscord Updater 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 { Notifications } from 'ui';
|
||||||
|
import { Reflection, Globals } from 'modules';
|
||||||
|
|
||||||
|
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() {
|
||||||
|
super({
|
||||||
|
updatesAvailable: false,
|
||||||
|
error: null,
|
||||||
|
updates: { bd: [] },
|
||||||
|
updating: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
restartNotif() {
|
||||||
|
Notifications.add('Updates Finished!', 'Restart required.', [
|
||||||
|
{
|
||||||
|
text: 'Restart Later',
|
||||||
|
onClick: () => { setTimeout(this.restartNotif, 5 * 60000); return true; }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
/**
|
||||||
|
* BetterDiscord Vendor 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 jQuery from 'jquery';
|
||||||
|
import lodash from 'lodash';
|
||||||
|
import Vue from 'vue';
|
||||||
|
import { Axi } from 'common';
|
||||||
|
|
||||||
|
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 {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* jQuery
|
||||||
|
*/
|
||||||
|
static get jQuery() { return jQuery }
|
||||||
|
static get $() { return this.jQuery }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lodash
|
||||||
|
*/
|
||||||
|
static get lodash() { return lodash }
|
||||||
|
static get _() { return this.lodash }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vue
|
||||||
|
*/
|
||||||
|
static get Vue() { return Vue }
|
||||||
|
|
||||||
|
static get axios() { return Axi.axios }
|
||||||
|
|
||||||
|
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 }
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
/* Deprecation Notice */
|
||||||
|
import { WebpackModules } from './reflection/wpm.depr.js';
|
||||||
|
import { ClientLogger as Logger } from 'common';
|
||||||
|
|
||||||
|
const DeprecationWarning = new Proxy(WebpackModules, {
|
||||||
|
get(WebpackModules, property) {
|
||||||
|
Logger.warn('DEPR', 'WebpackModules is deprecated. Use Reflection.Modules instead.');
|
||||||
|
return WebpackModules[property] || WebpackModules.getModuleByName(property);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export { DeprecationWarning as WebpackModules };
|
|
@ -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 });
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue