mirror of
https://github.com/discordjs/discord.js.git
synced 2026-05-28 06:20:10 +00:00
Compare commits
859 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c4b1be8506 | ||
|
|
4584148706 | ||
|
|
0b28998af8 | ||
|
|
c27ecf678e | ||
|
|
233889cf3a | ||
|
|
f62fa05389 | ||
|
|
2efe2d28f2 | ||
|
|
0821acfa99 | ||
|
|
d94ead70f8 | ||
|
|
8226752098 | ||
|
|
e883afa0ee | ||
|
|
659e89e8cd | ||
|
|
c79823002b | ||
|
|
db5bdcd855 | ||
|
|
b3eb1bba24 | ||
|
|
7a89598795 | ||
|
|
b6c89ef638 | ||
|
|
fc5d4438f8 | ||
|
|
6683c40a6f | ||
|
|
9f0417c09d | ||
|
|
1c34819d47 | ||
|
|
862b2ec3d4 | ||
|
|
d705a0c7d0 | ||
|
|
fb6d14d099 | ||
|
|
6a222ec6e9 | ||
|
|
cd066849ad | ||
|
|
e40c3f8cd0 | ||
|
|
22da595b50 | ||
|
|
cce2480bb5 | ||
|
|
b274dba6ec | ||
|
|
69b7d5d58e | ||
|
|
97b013de46 | ||
|
|
d9f772cdc1 | ||
|
|
42c195c857 | ||
|
|
77c54a94f2 | ||
|
|
4778a47cd4 | ||
|
|
18de265fcc | ||
|
|
a47c30e31a | ||
|
|
0d6b7ce641 | ||
|
|
9cdd494db6 | ||
|
|
dbe1ae972b | ||
|
|
4df2adc801 | ||
|
|
95e22c2f12 | ||
|
|
425efe1fe4 | ||
|
|
56fe70266e | ||
|
|
1fe201ae90 | ||
|
|
bce5b677ad | ||
|
|
85d195da52 | ||
|
|
17d7f5c723 | ||
|
|
a85fc91630 | ||
|
|
0b22d9a774 | ||
|
|
f7664b01a2 | ||
|
|
be4ccb3686 | ||
|
|
618fa2b104 | ||
|
|
a7e5e53e5d | ||
|
|
c01e9ad828 | ||
|
|
29aef18de8 | ||
|
|
2478092d44 | ||
|
|
c7d1507e19 | ||
|
|
c33b78da23 | ||
|
|
e76ebb4fcb | ||
|
|
b3216f26d6 | ||
|
|
2809eb74ca | ||
|
|
cd4a69d009 | ||
|
|
5dc83a1b03 | ||
|
|
2611efe9c1 | ||
|
|
cba4cc2400 | ||
|
|
d513c4bbb9 | ||
|
|
7aa791eaaa | ||
|
|
407500bf52 | ||
|
|
59122a6ba4 | ||
|
|
8a9b6cbdb5 | ||
|
|
bdc61a4068 | ||
|
|
a56a24d042 | ||
|
|
e7ebb23f14 | ||
|
|
46a50cb57c | ||
|
|
b27198ebe5 | ||
|
|
e91a2c6b2b | ||
|
|
26e5ef3205 | ||
|
|
ed84d76a42 | ||
|
|
b9434ed112 | ||
|
|
85615aa3a1 | ||
|
|
822c1f533c | ||
|
|
4ce4dc019e | ||
|
|
f1a74f214e | ||
|
|
85ec7c64bc | ||
|
|
b5de89a973 | ||
|
|
ddfa57e96d | ||
|
|
e3232bdb2b | ||
|
|
86ec60bc00 | ||
|
|
830d8fb3b5 | ||
|
|
510ddab0f5 | ||
|
|
95a531ee7d | ||
|
|
3f7049e1a0 | ||
|
|
1ed6bbc4b4 | ||
|
|
2116fba4c2 | ||
|
|
b49266baa5 | ||
|
|
46b8a7d411 | ||
|
|
2f1eb71a3f | ||
|
|
3ec08d5976 | ||
|
|
19d4d3bf2c | ||
|
|
eb4d3627bd | ||
|
|
bb2a35a849 | ||
|
|
8e80b6660c | ||
|
|
c62e8bb1f0 | ||
|
|
433c5e5702 | ||
|
|
a8d34e340b | ||
|
|
04f0534d33 | ||
|
|
65afedee49 | ||
|
|
c903929b6b | ||
|
|
28a29d5d9f | ||
|
|
6eced4d465 | ||
|
|
f25ced2969 | ||
|
|
b9172ffe22 | ||
|
|
8a7a805d92 | ||
|
|
45bc653988 | ||
|
|
e3c3a4fd60 | ||
|
|
798018713b | ||
|
|
ea1b5beea6 | ||
|
|
bb0ee59718 | ||
|
|
294f5ea5c8 | ||
|
|
fd7cb41ee6 | ||
|
|
1e115efa56 | ||
|
|
ee622f7d9f | ||
|
|
1fc6e3b91e | ||
|
|
6cd55ed5c9 | ||
|
|
7d7f1b2446 | ||
|
|
328d75be7d | ||
|
|
41e0be1db3 | ||
|
|
77caa0f8d4 | ||
|
|
712305dece | ||
|
|
e96daba7c0 | ||
|
|
3a73628567 | ||
|
|
cdc355811e | ||
|
|
ce8dc85f78 | ||
|
|
2d416cca79 | ||
|
|
61a081faa5 | ||
|
|
3bab4ec9fd | ||
|
|
2bf8a36077 | ||
|
|
fccd0bd183 | ||
|
|
c48f042785 | ||
|
|
b8018d4b6d | ||
|
|
6e4b744d03 | ||
|
|
135d9e3ea0 | ||
|
|
51a2e465bd | ||
|
|
2ace21c412 | ||
|
|
140839128a | ||
|
|
47fbd18eda | ||
|
|
0f4983852e | ||
|
|
db45d99d0c | ||
|
|
8bab9fb122 | ||
|
|
a02e10926d | ||
|
|
166161d6d3 | ||
|
|
892ba67d71 | ||
|
|
c3be798663 | ||
|
|
7779069e32 | ||
|
|
1ddae43fed | ||
|
|
c5efa985fc | ||
|
|
0754ddbc34 | ||
|
|
37b1f38ebe | ||
|
|
c2566d230c | ||
|
|
474a954938 | ||
|
|
e460e5e853 | ||
|
|
457b9600f2 | ||
|
|
9841615775 | ||
|
|
c880f10714 | ||
|
|
3cb4aac65a | ||
|
|
653aa6c1d7 | ||
|
|
62ac246162 | ||
|
|
bd6317cf99 | ||
|
|
edf1a3e7b4 | ||
|
|
f20f49388d | ||
|
|
007b1226c9 | ||
|
|
8e5c600227 | ||
|
|
dd110f4824 | ||
|
|
81b176cfea | ||
|
|
5cad25ea69 | ||
|
|
9a5de25d79 | ||
|
|
ac92d2cecc | ||
|
|
3f8c0a4d11 | ||
|
|
e2c8ba5be0 | ||
|
|
aa35e21b84 | ||
|
|
4bca4a5181 | ||
|
|
6f96cf7325 | ||
|
|
ee37d859a4 | ||
|
|
1a69331f0c | ||
|
|
c40252381b | ||
|
|
4127cf6e40 | ||
|
|
b0a3528411 | ||
|
|
5b2ca326d4 | ||
|
|
f7d65991d5 | ||
|
|
dd8f77fcf0 | ||
|
|
584961b04b | ||
|
|
2e33bf583e | ||
|
|
88aa947376 | ||
|
|
d6b7d31047 | ||
|
|
13672cc637 | ||
|
|
956372731f | ||
|
|
9ee695a291 | ||
|
|
35c4c552f4 | ||
|
|
89745fe132 | ||
|
|
2accb7b6fd | ||
|
|
195fcfa15c | ||
|
|
95bcac9d9b | ||
|
|
1601ad14e3 | ||
|
|
5ac22691d2 | ||
|
|
298ee4e79f | ||
|
|
909b825c94 | ||
|
|
eb9c280d5f | ||
|
|
10138dfad2 | ||
|
|
c670209021 | ||
|
|
53eda09f72 | ||
|
|
401822a094 | ||
|
|
aec0a095e3 | ||
|
|
4a18ac72ce | ||
|
|
bbb5d4c7a1 | ||
|
|
b15e012788 | ||
|
|
fb5b8f8548 | ||
|
|
c538c076ff | ||
|
|
59e733cef6 | ||
|
|
96041dbd3a | ||
|
|
6abe014ac3 | ||
|
|
c095610bb4 | ||
|
|
98ee016b83 | ||
|
|
c0c2fd1493 | ||
|
|
26069a44e2 | ||
|
|
3465a154d5 | ||
|
|
eec79b5fd0 | ||
|
|
64b378f7d4 | ||
|
|
7d5bfccd67 | ||
|
|
5b3e971c5c | ||
|
|
6c4f63ed72 | ||
|
|
c3bc6d5234 | ||
|
|
794c0e131d | ||
|
|
5605dc04e1 | ||
|
|
94e2a85386 | ||
|
|
a652901d60 | ||
|
|
ca34c43ba0 | ||
|
|
8475a4abee | ||
|
|
27114eebf9 | ||
|
|
1897b4f4d6 | ||
|
|
b1e3309783 | ||
|
|
fa5787cc8b | ||
|
|
47442eeba3 | ||
|
|
b7e16ad4b8 | ||
|
|
5e522e0a2e | ||
|
|
0d83f80fc0 | ||
|
|
6bae7a135f | ||
|
|
15723ece1f | ||
|
|
4aa82b4fdf | ||
|
|
bdf6f30990 | ||
|
|
930519c494 | ||
|
|
81619ac7be | ||
|
|
8ddaa1b92c | ||
|
|
78895ace17 | ||
|
|
d54ef0eea4 | ||
|
|
dce8e83465 | ||
|
|
36c03909dd | ||
|
|
9eaf1456b2 | ||
|
|
d0c2b84659 | ||
|
|
81d7dbdc76 | ||
|
|
8b67660271 | ||
|
|
40c610987e | ||
|
|
c5e2ea7458 | ||
|
|
39f7dc018a | ||
|
|
5dea21ba80 | ||
|
|
9b989c4fd7 | ||
|
|
6187c17097 | ||
|
|
a49d4e6d43 | ||
|
|
84aab1021a | ||
|
|
25e0048bbf | ||
|
|
8dfc96d460 | ||
|
|
6495df50f9 | ||
|
|
9365272baf | ||
|
|
e60d2bd175 | ||
|
|
0a56fa0aae | ||
|
|
8436cbe8b1 | ||
|
|
6d4fe89212 | ||
|
|
3bc90dcbf1 | ||
|
|
ebcf61ff9c | ||
|
|
4fa5ceed83 | ||
|
|
fa016b6b41 | ||
|
|
878e5d7c76 | ||
|
|
d7cf4a0919 | ||
|
|
e5293647a3 | ||
|
|
ab589fa5f2 | ||
|
|
d1d8179460 | ||
|
|
801633b970 | ||
|
|
5d85de0883 | ||
|
|
4ebf4307e8 | ||
|
|
6ce24398ab | ||
|
|
970bfffc46 | ||
|
|
140fd4ad4c | ||
|
|
ecb8655dac | ||
|
|
4be08406e6 | ||
|
|
047cd2da5c | ||
|
|
6910585f69 | ||
|
|
dde6eb003c | ||
|
|
8716702b59 | ||
|
|
2ff1f3dcde | ||
|
|
09489e2b9f | ||
|
|
d189b51da5 | ||
|
|
270bbc2731 | ||
|
|
7ffbbc7f07 | ||
|
|
358131fb6f | ||
|
|
d2e3d6dc63 | ||
|
|
cb3f6d9646 | ||
|
|
a4e0af2e45 | ||
|
|
c4e5292516 | ||
|
|
bca101aac8 | ||
|
|
07740955cf | ||
|
|
305a7d14af | ||
|
|
e86ec7de6f | ||
|
|
1df3b84da6 | ||
|
|
1cb227d6d8 | ||
|
|
e08f2bad30 | ||
|
|
f73e6a3e4d | ||
|
|
dd3831fa1e | ||
|
|
5334682682 | ||
|
|
104f3397af | ||
|
|
61f5051dfd | ||
|
|
55141b408b | ||
|
|
c86ebefecb | ||
|
|
c73b9483f4 | ||
|
|
cff7069275 | ||
|
|
8444f19662 | ||
|
|
beffcd31cd | ||
|
|
dd8907472c | ||
|
|
3e0c0f44a2 | ||
|
|
58068fdae9 | ||
|
|
245bd91101 | ||
|
|
79278bccb4 | ||
|
|
633e3ca896 | ||
|
|
2e54f6d90d | ||
|
|
64ce829ab2 | ||
|
|
2349238f69 | ||
|
|
f5da1f2411 | ||
|
|
bfb3f39e94 | ||
|
|
e757f7c0d8 | ||
|
|
41c67766d8 | ||
|
|
136cab240d | ||
|
|
2237749d29 | ||
|
|
6983798820 | ||
|
|
94062d19dd | ||
|
|
fa609caee2 | ||
|
|
1f2d8bfbc1 | ||
|
|
8729ee6a1d | ||
|
|
a82d9f1fbb | ||
|
|
6b26d28c06 | ||
|
|
4ef0ec491c | ||
|
|
cee0d58fb8 | ||
|
|
bbd89585c6 | ||
|
|
14ae7cdf17 | ||
|
|
2897692cf1 | ||
|
|
21babf8859 | ||
|
|
3e5096f9fe | ||
|
|
df2333ac82 | ||
|
|
b926cd0dcd | ||
|
|
6211119928 | ||
|
|
4f7e0c41ee | ||
|
|
30dd9be900 | ||
|
|
427eec8ccb | ||
|
|
16fe48d405 | ||
|
|
055775de2f | ||
|
|
33de408a70 | ||
|
|
e7b0afbd1f | ||
|
|
a5740e83f4 | ||
|
|
7473fd513e | ||
|
|
fc42a46e5b | ||
|
|
25bb602d5a | ||
|
|
01d8d32ea9 | ||
|
|
b6f74c45fe | ||
|
|
0e6b5e9193 | ||
|
|
dfb1a16e9e | ||
|
|
275c9953a0 | ||
|
|
f3a7f59824 | ||
|
|
18bcd2f7e2 | ||
|
|
a029999b09 | ||
|
|
76637ed10d | ||
|
|
53209fbe31 | ||
|
|
70b35c85d6 | ||
|
|
af3d4782b9 | ||
|
|
15240cfdc3 | ||
|
|
bf25caf3d3 | ||
|
|
b55e6927e9 | ||
|
|
8a2ec437e5 | ||
|
|
4c9d8d6cd7 | ||
|
|
5c2086b351 | ||
|
|
f068010e96 | ||
|
|
7fd94c29d8 | ||
|
|
566135d25b | ||
|
|
eb069d0249 | ||
|
|
f01b3f922d | ||
|
|
61e12c637b | ||
|
|
d09dfa4c37 | ||
|
|
b0338df7db | ||
|
|
8d1bc30e40 | ||
|
|
db5259cdf1 | ||
|
|
d4a84915e6 | ||
|
|
f2a6c1d98c | ||
|
|
d870b27ece | ||
|
|
1273bb42ec | ||
|
|
12136f8c54 | ||
|
|
05bba9b74a | ||
|
|
7232531eb1 | ||
|
|
63ffd8aa7c | ||
|
|
8d620ac33f | ||
|
|
b91f8f27be | ||
|
|
ec1ed15c88 | ||
|
|
dd6dd6fb59 | ||
|
|
2518a0f7e2 | ||
|
|
cdb911f2af | ||
|
|
cc3e7b26b1 | ||
|
|
7c8f534a38 | ||
|
|
ab5e57d94b | ||
|
|
a90bd837af | ||
|
|
0f6fceebb4 | ||
|
|
d6f17a9319 | ||
|
|
4dc70d8cef | ||
|
|
49944747ae | ||
|
|
b859ba7639 | ||
|
|
26becb570b | ||
|
|
59ff1d99ba | ||
|
|
0b5eeb08f3 | ||
|
|
cee9e4839c | ||
|
|
1e94a9e2a4 | ||
|
|
02c23a8b53 | ||
|
|
a2d6791cd8 | ||
|
|
e8ac18489e | ||
|
|
187f43aebd | ||
|
|
c40a511954 | ||
|
|
d51e45f3b9 | ||
|
|
a5b901f4ef | ||
|
|
ed42d7bd85 | ||
|
|
8da915f6a1 | ||
|
|
f4724d61b2 | ||
|
|
565c640bc6 | ||
|
|
18729b25c7 | ||
|
|
94483ae194 | ||
|
|
f73fd4ec29 | ||
|
|
2e1310ae72 | ||
|
|
e9ab9c9f92 | ||
|
|
78dafb9480 | ||
|
|
8ee0acf44c | ||
|
|
7ed58f5f7f | ||
|
|
5059c59a31 | ||
|
|
6fae17912e | ||
|
|
448c93615b | ||
|
|
2beb77ab5c | ||
|
|
c7f5b44e03 | ||
|
|
9a6cb6477d | ||
|
|
87b600f78f | ||
|
|
ba11f76284 | ||
|
|
ba7c2db364 | ||
|
|
5173583e26 | ||
|
|
c1a5bee61f | ||
|
|
4b2053133d | ||
|
|
c73e501243 | ||
|
|
306ea97f99 | ||
|
|
3545e731c0 | ||
|
|
f51ba3fb06 | ||
|
|
0300601649 | ||
|
|
1e6abe587b | ||
|
|
b13fdcc8d3 | ||
|
|
3f4cbd07dd | ||
|
|
ff92905848 | ||
|
|
93d8ec3e87 | ||
|
|
f8b9bf6884 | ||
|
|
ba465bc680 | ||
|
|
8c220e76ec | ||
|
|
d10ca8e7ba | ||
|
|
7cb5b22ebb | ||
|
|
48be401330 | ||
|
|
7f4846c826 | ||
|
|
78bf402e8e | ||
|
|
098ad195bd | ||
|
|
e216fc7a81 | ||
|
|
f647eb7a91 | ||
|
|
df02eee065 | ||
|
|
ee1c343d78 | ||
|
|
1e76f519dc | ||
|
|
63c8b1b2f2 | ||
|
|
af6c8fa4a4 | ||
|
|
fa85da19a8 | ||
|
|
7357fc2163 | ||
|
|
f7a4dee4e1 | ||
|
|
5ac410f352 | ||
|
|
74fd0421ba | ||
|
|
0d4eab8d24 | ||
|
|
a3091f5262 | ||
|
|
b2822c584a | ||
|
|
5caa7df1d8 | ||
|
|
8b0e5aad38 | ||
|
|
c37cd3fd91 | ||
|
|
b5026909a1 | ||
|
|
47707d245d | ||
|
|
5e7ae847de | ||
|
|
4a7284b86e | ||
|
|
391b618b3f | ||
|
|
b68283e57a | ||
|
|
fde3a976aa | ||
|
|
2a668ac997 | ||
|
|
adbd95adf0 | ||
|
|
4717c34ff6 | ||
|
|
3b1264ad6d | ||
|
|
9f8289e433 | ||
|
|
de0ba9fb7c | ||
|
|
0df17b1634 | ||
|
|
9d36be58ef | ||
|
|
dc640017cd | ||
|
|
fcd7cf1450 | ||
|
|
bace8bcac2 | ||
|
|
bac599b52e | ||
|
|
42527ea969 | ||
|
|
e5e36d9111 | ||
|
|
f76b47184a | ||
|
|
add3c1a8ee | ||
|
|
8cff77726a | ||
|
|
a66b2b7ec5 | ||
|
|
fdc8050188 | ||
|
|
495264761c | ||
|
|
d47f9d202b | ||
|
|
7cbe81e71a | ||
|
|
2c26d79f50 | ||
|
|
f485ec22b2 | ||
|
|
3b9b06227e | ||
|
|
054d4655c1 | ||
|
|
2b25641270 | ||
|
|
2156ce1e15 | ||
|
|
814c4e8617 | ||
|
|
8406e97e13 | ||
|
|
90f8cbd09a | ||
|
|
f72817fff5 | ||
|
|
4447e367f6 | ||
|
|
2ea744c9d1 | ||
|
|
eaf2091c2f | ||
|
|
43283eeaec | ||
|
|
8d8ac78e68 | ||
|
|
91e0a81d6b | ||
|
|
eacbfbd520 | ||
|
|
ac62a58f47 | ||
|
|
023ac0a7fe | ||
|
|
4c8e4fde6f | ||
|
|
e2753136a4 | ||
|
|
d13c48bafa | ||
|
|
56e01291e2 | ||
|
|
69ccc75590 | ||
|
|
221e7f8b21 | ||
|
|
50dc9addf1 | ||
|
|
f31a3725fe | ||
|
|
bde6749d65 | ||
|
|
da32c2ec3d | ||
|
|
258e4b9085 | ||
|
|
3109accf87 | ||
|
|
fb1d0a3e74 | ||
|
|
0a6d71d7e5 | ||
|
|
90ca422485 | ||
|
|
22a6ded341 | ||
|
|
91fc6ccb5c | ||
|
|
bf4010e89c | ||
|
|
2f630a0dbb | ||
|
|
beffb390e6 | ||
|
|
f0adf8f122 | ||
|
|
1afb21c981 | ||
|
|
5fd10ef63e | ||
|
|
bcbd187223 | ||
|
|
e44d571671 | ||
|
|
ed08c24784 | ||
|
|
55ee566fb2 | ||
|
|
b5f51d612a | ||
|
|
be19a08575 | ||
|
|
9e70d6279f | ||
|
|
89cea574be | ||
|
|
77548c194f | ||
|
|
863e34db19 | ||
|
|
c6f17054fc | ||
|
|
12605575fb | ||
|
|
ad18b05d66 | ||
|
|
72a99f9582 | ||
|
|
e9af3f0a1f | ||
|
|
22b1c425ac | ||
|
|
ebd2f0e73f | ||
|
|
f8440ad565 | ||
|
|
ab4b7ea19a | ||
|
|
c8f6b6b059 | ||
|
|
f613cc3c50 | ||
|
|
dc53b8de95 | ||
|
|
6e51a44f92 | ||
|
|
4541b3e264 | ||
|
|
665ef21c85 | ||
|
|
3e3de51545 | ||
|
|
ba77e69edf | ||
|
|
86ffc86324 | ||
|
|
650acbf662 | ||
|
|
1452fa5014 | ||
|
|
75d45bd587 | ||
|
|
627a8870f5 | ||
|
|
e1d01ed6a2 | ||
|
|
6d7293e3c5 | ||
|
|
95790dcf08 | ||
|
|
1b76333b8b | ||
|
|
fe7ed523b3 | ||
|
|
ed8fcf651a | ||
|
|
a014b59790 | ||
|
|
17a61731ef | ||
|
|
dfa6740b89 | ||
|
|
7ede44bc00 | ||
|
|
18e04d69f1 | ||
|
|
e64d9c6057 | ||
|
|
ea798eaaf3 | ||
|
|
e7824d6515 | ||
|
|
c1b9437f0d | ||
|
|
e4bae99747 | ||
|
|
00254f35b0 | ||
|
|
bbeef44e66 | ||
|
|
8e47058286 | ||
|
|
289447e4c9 | ||
|
|
4294d267e7 | ||
|
|
3451367591 | ||
|
|
0a47d0e1d6 | ||
|
|
8e75b47a7b | ||
|
|
8cf520d5af | ||
|
|
be32bbc3a4 | ||
|
|
8d966932a9 | ||
|
|
4d2153c7c6 | ||
|
|
265021bfa5 | ||
|
|
84991c767e | ||
|
|
14ba0373eb | ||
|
|
779681e88f | ||
|
|
bac4ccead7 | ||
|
|
b79533e373 | ||
|
|
b0c2f818a8 | ||
|
|
f510dc5a10 | ||
|
|
bf7767fe2c | ||
|
|
758c801dd5 | ||
|
|
b1473b1e4c | ||
|
|
72c667b307 | ||
|
|
cd657be8be | ||
|
|
2410fdf8d2 | ||
|
|
fb6a8d1637 | ||
|
|
218920e4f1 | ||
|
|
74c3bae506 | ||
|
|
ab30715028 | ||
|
|
544e456302 | ||
|
|
3eca3ba95e | ||
|
|
7c12fdcb56 | ||
|
|
28ca83011c | ||
|
|
265ac90234 | ||
|
|
2ab5bae69a | ||
|
|
e63432c18e | ||
|
|
e6a041241b | ||
|
|
74ef0b2e14 | ||
|
|
b518437f52 | ||
|
|
2390e525ef | ||
|
|
f726db2152 | ||
|
|
cecb0aee02 | ||
|
|
84954c8860 | ||
|
|
4580f62a1f | ||
|
|
fa7d63a10a | ||
|
|
9486609ef9 | ||
|
|
c483dd8239 | ||
|
|
84503c8877 | ||
|
|
e392107369 | ||
|
|
a0a3989e59 | ||
|
|
b74c1b70b6 | ||
|
|
736fa7c611 | ||
|
|
6c38b83923 | ||
|
|
361547a588 | ||
|
|
fbe1929bde | ||
|
|
a20bac7258 | ||
|
|
9c59b649ad | ||
|
|
36be4c47f6 | ||
|
|
33a4388121 | ||
|
|
2e7472bb1a | ||
|
|
264ee8e7f1 | ||
|
|
906bb3c5f3 | ||
|
|
bef0523ebf | ||
|
|
f1c4ef659e | ||
|
|
4e76251aa2 | ||
|
|
8139bef4e2 | ||
|
|
1c61b46660 | ||
|
|
8596eadb25 | ||
|
|
78026df1df | ||
|
|
586d652c16 | ||
|
|
3193df792e | ||
|
|
52a83b9218 | ||
|
|
d766e727a1 | ||
|
|
710d3db94f | ||
|
|
b79a91b3a9 | ||
|
|
c5f93eb44e | ||
|
|
e51fed968d | ||
|
|
a54a62787d | ||
|
|
275d9d3ce0 | ||
|
|
a0de75f290 | ||
|
|
b177aefdd6 | ||
|
|
050d3f9303 | ||
|
|
f9bf0ed5e6 | ||
|
|
d67ecdd2af | ||
|
|
37bfdd154c | ||
|
|
daa79b7f97 | ||
|
|
8eff36b744 | ||
|
|
977e29ceba | ||
|
|
8f8e0b1e52 | ||
|
|
d6218050ba | ||
|
|
bd00bc404c | ||
|
|
a0b245bfe1 | ||
|
|
a53dcd52e7 | ||
|
|
4bb2afe7cb | ||
|
|
eb253eef45 | ||
|
|
1d9d246870 | ||
|
|
d3687cb20d | ||
|
|
638e51a18c | ||
|
|
8b7ef0c850 | ||
|
|
c0aa6bd30f | ||
|
|
6adc8a9d0d | ||
|
|
676a895da7 | ||
|
|
6cfbf76406 | ||
|
|
e141deb7ef | ||
|
|
1e5afc1608 | ||
|
|
58c7c2e7b8 | ||
|
|
2488e1a00f | ||
|
|
edfb27f428 | ||
|
|
6043a1f83a | ||
|
|
ac68e9f077 | ||
|
|
1933451d2f | ||
|
|
b335824570 | ||
|
|
57af84bc95 | ||
|
|
6dc93a0184 | ||
|
|
769ea5b50f | ||
|
|
8e7cb7fc4e | ||
|
|
5fb4e257c8 | ||
|
|
d9400ba289 | ||
|
|
cc9e484276 | ||
|
|
1aed3de647 | ||
|
|
6afd80cf53 | ||
|
|
b85a589a01 | ||
|
|
9323882a8d | ||
|
|
dc6c1140bc | ||
|
|
0d574a0678 | ||
|
|
e2932c05f2 | ||
|
|
27e77d3839 | ||
|
|
4ae1f63a97 | ||
|
|
85a7eab5bb | ||
|
|
c683790de7 | ||
|
|
eedc097f3f | ||
|
|
ac64f8bd23 | ||
|
|
945a2e370a | ||
|
|
0008a18deb | ||
|
|
91a69fb761 | ||
|
|
c91ee7a3e7 | ||
|
|
32879419e2 | ||
|
|
c4b2077666 | ||
|
|
338aa58386 | ||
|
|
c3d14636ab | ||
|
|
77353e5220 | ||
|
|
a8bb20204b | ||
|
|
7c97244854 | ||
|
|
c8858de71e | ||
|
|
c2b5eb291a | ||
|
|
a4193553e2 | ||
|
|
0d754d1fbb | ||
|
|
ee4a8bb3b6 | ||
|
|
f6a60581c4 | ||
|
|
7e69475d11 | ||
|
|
f38944c8f6 | ||
|
|
66845e2f68 | ||
|
|
697fa15278 | ||
|
|
18dc95e1bd | ||
|
|
049ab42eb4 | ||
|
|
3ef16f97c4 | ||
|
|
411c9bd32c | ||
|
|
6383d42eb5 | ||
|
|
176859e7da | ||
|
|
2440a4a2c8 | ||
|
|
b3e795d0b0 | ||
|
|
686cf297d2 | ||
|
|
2bb5aa1fda | ||
|
|
a80a64f8ce | ||
|
|
74ab72fdea | ||
|
|
c29eb7ab20 | ||
|
|
34548e3ecc | ||
|
|
213c45323f | ||
|
|
3fd3588ef2 | ||
|
|
93948328b4 | ||
|
|
49fdc331a7 | ||
|
|
b2bc844ed7 | ||
|
|
1833a83664 | ||
|
|
5ed8098af8 | ||
|
|
a359f344d8 | ||
|
|
c041b1bc23 | ||
|
|
bb3b709d6e | ||
|
|
da9d1a3daf | ||
|
|
27270a3bad | ||
|
|
ee3a03f707 | ||
|
|
af5e07fb33 | ||
|
|
5d3123d405 | ||
|
|
9d0fcb3936 | ||
|
|
c50de74310 | ||
|
|
2c76f5437b | ||
|
|
99b8d8f031 | ||
|
|
acdf2d14c2 | ||
|
|
2b7c7bfd8f | ||
|
|
8482abed7b | ||
|
|
962cb8f8d0 | ||
|
|
7b26e70a2e | ||
|
|
c65d7a10ec | ||
|
|
b07a31d44e | ||
|
|
90304aa7d6 | ||
|
|
318bb52c36 | ||
|
|
305070dded | ||
|
|
3230d90a58 | ||
|
|
ce132d5f54 | ||
|
|
1e8392d90b | ||
|
|
c02eb2f171 | ||
|
|
544540fb02 | ||
|
|
7d02e73a26 | ||
|
|
5dc30d6812 | ||
|
|
fe3914658a | ||
|
|
93e6c69bd1 | ||
|
|
b91590d3a8 | ||
|
|
9a61de1493 | ||
|
|
2a50dad852 | ||
|
|
6dc95cd084 | ||
|
|
a673a97441 | ||
|
|
d7e1e1c0c9 | ||
|
|
73261646fc | ||
|
|
d6f55adf52 | ||
|
|
f2496070d3 | ||
|
|
85330769a7 | ||
|
|
589c44327a | ||
|
|
5fa9e3548b | ||
|
|
60e0d507f0 | ||
|
|
8306d50bd8 | ||
|
|
c42e7a15aa | ||
|
|
c334bf4535 | ||
|
|
cdf66f8011 | ||
|
|
cf04b44454 | ||
|
|
4e6b632d23 | ||
|
|
83bef4ca77 | ||
|
|
071e287fec | ||
|
|
3f5e7451a7 | ||
|
|
4bd19c94ba | ||
|
|
e80f06a059 | ||
|
|
30105536a6 | ||
|
|
c9dbf1f7f0 | ||
|
|
986b05442d | ||
|
|
b15896e0a4 | ||
|
|
5dd76069f8 | ||
|
|
c4da8d1009 | ||
|
|
dfeafbf5fa | ||
|
|
cd9b391e2a | ||
|
|
756d7fc2c1 | ||
|
|
21b00e1e29 | ||
|
|
9cba1bc6d0 | ||
|
|
dd9c291508 | ||
|
|
8e505ed349 | ||
|
|
d129457624 | ||
|
|
81059885a2 | ||
|
|
dd31ee0c5f |
@@ -8,23 +8,31 @@
|
||||
"node": true
|
||||
},
|
||||
"rules": {
|
||||
"no-await-in-loop": "warn",
|
||||
"no-compare-neg-zero": "error",
|
||||
"no-extra-parens": ["warn", "all", {
|
||||
"nestedBinaryExpressions": false
|
||||
}],
|
||||
"no-template-curly-in-string": "error",
|
||||
"no-unsafe-negation": "error",
|
||||
"valid-jsdoc": ["error", {
|
||||
"requireReturn": false,
|
||||
"requireReturnDescription": false,
|
||||
"prefer": {
|
||||
"return": "returns",
|
||||
"arg": "param"
|
||||
},
|
||||
"preferType": {
|
||||
"String": "string",
|
||||
"Number": "number",
|
||||
"Boolean": "boolean",
|
||||
"Function": "function",
|
||||
"Symbol": "symbol",
|
||||
"object": "Object",
|
||||
"function": "Function",
|
||||
"array": "Array",
|
||||
"date": "Date",
|
||||
"error": "Error"
|
||||
},
|
||||
"prefer": {
|
||||
"return": "returns"
|
||||
"error": "Error",
|
||||
"null": "void"
|
||||
}
|
||||
}],
|
||||
|
||||
@@ -47,6 +55,7 @@
|
||||
"no-new": "error",
|
||||
"no-octal-escape": "error",
|
||||
"no-return-assign": "error",
|
||||
"no-return-await": "error",
|
||||
"no-self-compare": "error",
|
||||
"no-sequences": "error",
|
||||
"no-throw-literal": "error",
|
||||
@@ -55,8 +64,11 @@
|
||||
"no-useless-call": "error",
|
||||
"no-useless-concat": "error",
|
||||
"no-useless-escape": "error",
|
||||
"no-useless-return": "error",
|
||||
"no-void": "error",
|
||||
"no-warning-comments": "warn",
|
||||
"prefer-promise-reject-errors": "error",
|
||||
"require-await": "warn",
|
||||
"wrap-iife": "error",
|
||||
"yoda": "error",
|
||||
|
||||
@@ -73,6 +85,7 @@
|
||||
"array-bracket-spacing": "error",
|
||||
"block-spacing": "error",
|
||||
"brace-style": ["error", "1tbs", { "allowSingleLine": true }],
|
||||
"capitalized-comments": ["error", "always", { "ignoreConsecutiveComments": true }],
|
||||
"comma-dangle": ["error", "always-multiline"],
|
||||
"comma-spacing": "error",
|
||||
"comma-style": "error",
|
||||
@@ -80,6 +93,7 @@
|
||||
"consistent-this": ["error", "$this"],
|
||||
"eol-last": "error",
|
||||
"func-names": "error",
|
||||
"func-name-matching": "error",
|
||||
"func-style": ["error", "declaration", { "allowArrowFunctions": true }],
|
||||
"indent": ["error", 2, { "SwitchCase": 1 }],
|
||||
"key-spacing": "error",
|
||||
@@ -88,7 +102,7 @@
|
||||
"max-len": ["error", 120, 2],
|
||||
"max-nested-callbacks": ["error", { "max": 4 }],
|
||||
"max-statements-per-line": ["error", { "max": 2 }],
|
||||
"new-cap": "error",
|
||||
"new-cap": "off",
|
||||
"newline-per-chained-call": ["error", { "ignoreChainWithDepth": 3 }],
|
||||
"no-array-constructor": "error",
|
||||
"no-inline-comments": "error",
|
||||
@@ -100,6 +114,7 @@
|
||||
"no-trailing-spaces": "error",
|
||||
"no-unneeded-ternary": "error",
|
||||
"no-whitespace-before-property": "error",
|
||||
"nonblock-statement-body-position": "error",
|
||||
"object-curly-spacing": ["error", "always"],
|
||||
"operator-assignment": "error",
|
||||
"operator-linebreak": ["error", "after"],
|
||||
@@ -114,14 +129,17 @@
|
||||
"space-infix-ops": "error",
|
||||
"space-unary-ops": "error",
|
||||
"spaced-comment": "error",
|
||||
"template-tag-spacing": "error",
|
||||
"unicode-bom": "error",
|
||||
|
||||
"arrow-body-style": "error",
|
||||
"arrow-parens": ["error", "as-needed"],
|
||||
"arrow-spacing": "error",
|
||||
"no-duplicate-imports": "error",
|
||||
"no-useless-computed-key": "error",
|
||||
"no-useless-constructor": "error",
|
||||
"prefer-arrow-callback": "error",
|
||||
"prefer-numeric-literals": "error",
|
||||
"prefer-rest-params": "error",
|
||||
"prefer-spread": "error",
|
||||
"prefer-template": "error",
|
||||
|
||||
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
||||
* text=auto eol=lf
|
||||
7
CONTRIBUTING.md → .github/CONTRIBUTING.md
vendored
7
CONTRIBUTING.md → .github/CONTRIBUTING.md
vendored
@@ -1,12 +1,15 @@
|
||||
# Contributing
|
||||
|
||||
**The issue tracker is only for bug reports and enhancement suggestions. If you have a question, please ask it in the [Discord server](https://discord.gg/bRCvFy9) instead of opening an issue – you will get redirected there anyway.**
|
||||
|
||||
If you wish to contribute to the discord.js codebase or documentation, feel free to fork the repository and submit a
|
||||
pull request. We use ESLint to enforce a consistent coding style, so having that set up in your editor of choice
|
||||
is a great boon to your coding process.
|
||||
is a great boon to your development process.
|
||||
|
||||
## Setup
|
||||
To get ready to work on the codebase, please do the following:
|
||||
|
||||
1. Fork & clone the repository
|
||||
1. Fork & clone the repository, and make sure you're on the **master** branch
|
||||
2. Run `npm install`
|
||||
3. If you're working on voice, also run `npm install node-opus` or `npm install opusscript`
|
||||
4. Code your heart out!
|
||||
26
.github/ISSUE_TEMPLATE.md
vendored
Normal file
26
.github/ISSUE_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
<!--
|
||||
If you need help with discord.js installation or usage, please go to the discord.js Discord server instead:
|
||||
https://discord.gg/bRCvFy9
|
||||
This issue tracker is only for bug reports and enhancement suggestions. You won't receive any basic help here.
|
||||
-->
|
||||
|
||||
**Please describe the problem you are having in as much detail as possible:**
|
||||
|
||||
|
||||
**Include a reproducible code sample here, if possible:**
|
||||
```js
|
||||
|
||||
```
|
||||
|
||||
**Further details:**
|
||||
|
||||
- discord.js version:
|
||||
- node.js version:
|
||||
- Operating system:
|
||||
- Priority this issue should have – please be realistic and elaborate if possible:
|
||||
|
||||
<!--
|
||||
Ideally you would also test whether the issue occurs on the latest master branch commit.
|
||||
If you have, please check the following box and insert the hash of the commit you tested:
|
||||
-->
|
||||
- [ ] I have also tested the issue on latest master, commit hash:
|
||||
7
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
7
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
**Please describe the changes this PR makes and why it should be merged:**
|
||||
|
||||
|
||||
**Semantic versioning classification:**
|
||||
- [ ] This PR changes the library's interface (methods or parameters added)
|
||||
- [ ] This PR includes breaking changes (methods removed or renamed, parameters moved or removed)
|
||||
- [ ] This PR **only** includes non-code changes, like changes to documentation, README, etc.
|
||||
38
.gitignore
vendored
38
.gitignore
vendored
@@ -1,17 +1,21 @@
|
||||
# Packages
|
||||
node_modules/
|
||||
yarn.lock
|
||||
|
||||
# Log files
|
||||
logs/
|
||||
*.log
|
||||
|
||||
# Authentication
|
||||
test/auth.json
|
||||
docs/deploy/deploy_key
|
||||
docs/deploy/deploy_key.pub
|
||||
|
||||
# Miscellaneous
|
||||
.tmp/
|
||||
.vscode/
|
||||
docs/docs.json
|
||||
# Packages
|
||||
node_modules/
|
||||
yarn.lock
|
||||
|
||||
# Log files
|
||||
logs/
|
||||
*.log
|
||||
|
||||
# Authentication
|
||||
test/auth.json
|
||||
test/auth.js
|
||||
docs/deploy/deploy_key
|
||||
docs/deploy/deploy_key.pub
|
||||
deploy/deploy_key
|
||||
deploy/deploy_key.pub
|
||||
|
||||
# Miscellaneous
|
||||
.tmp/
|
||||
.vscode/
|
||||
docs/docs.json
|
||||
webpack/
|
||||
|
||||
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
[submodule "typings"]
|
||||
path = typings
|
||||
url = https://github.com/zajrik/discord.js-typings
|
||||
26
.npmignore
Normal file
26
.npmignore
Normal file
@@ -0,0 +1,26 @@
|
||||
# Packages
|
||||
node_modules/
|
||||
yarn.lock
|
||||
|
||||
# Log files
|
||||
logs/
|
||||
*.log
|
||||
|
||||
# Authentication
|
||||
deploy/
|
||||
|
||||
# Miscellaneous
|
||||
.tmp/
|
||||
.vscode/
|
||||
docs/
|
||||
|
||||
webpack/
|
||||
|
||||
# NPM ignore
|
||||
.eslintrc.json
|
||||
.gitattributes
|
||||
.gitignore
|
||||
.travis.yml
|
||||
webpack.config.js
|
||||
.github/
|
||||
test/
|
||||
21
.tern-project
Normal file
21
.tern-project
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"ecmaVersion": 7,
|
||||
"libs": [],
|
||||
"loadEagerly": [
|
||||
"./src/*.js"
|
||||
],
|
||||
"dontLoad": [
|
||||
"node_modules/**"
|
||||
],
|
||||
"plugins": {
|
||||
"es_modules": {},
|
||||
"node": {},
|
||||
"doc_comment": {
|
||||
"fullDocs": true,
|
||||
"strong": true
|
||||
},
|
||||
"webpack": {
|
||||
"configPath": "./webpack.config.js",
|
||||
}
|
||||
}
|
||||
}
|
||||
33
.travis.yml
33
.travis.yml
@@ -1,13 +1,20 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- "6"
|
||||
cache:
|
||||
directories:
|
||||
- node_modules
|
||||
install: npm install
|
||||
script: bash ./docs/deploy/deploy.sh
|
||||
env:
|
||||
global:
|
||||
- ENCRYPTION_LABEL: "af862fa96d3e"
|
||||
- COMMIT_AUTHOR_EMAIL: "amishshah.2k@gmail.com"
|
||||
|
||||
language: node_js
|
||||
node_js:
|
||||
- "6"
|
||||
- "7"
|
||||
cache:
|
||||
directories:
|
||||
- node_modules
|
||||
install: npm install
|
||||
script: bash ./deploy/test.sh
|
||||
jobs:
|
||||
include:
|
||||
- stage: build
|
||||
node_js: "6"
|
||||
script: bash ./deploy/deploy.sh
|
||||
env:
|
||||
global:
|
||||
- ENCRYPTION_LABEL: "af862fa96d3e"
|
||||
- COMMIT_AUTHOR_EMAIL: "amishshah.2k@gmail.com"
|
||||
dist: trusty
|
||||
sudo: false
|
||||
|
||||
13
LICENSE
13
LICENSE
@@ -175,18 +175,7 @@
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright {yyyy} {name of copyright owner}
|
||||
Copyright 2017 Amish Shah
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
||||
154
README.md
154
README.md
@@ -1,71 +1,83 @@
|
||||
<div align="center">
|
||||
<p>
|
||||
<a href="https://discord.js.org"><img src="https://i.imgur.com/StEGtEh.png" width="546" alt="discord.js" /></a>
|
||||
</p>
|
||||
<p>
|
||||
<a href="https://discord.gg/bRCvFy9"><img src="https://discordapp.com/api/guilds/222078108977594368/embed.png" alt="Discord server" /></a>
|
||||
<a href="https://www.npmjs.com/package/discord.js"><img src="https://img.shields.io/npm/v/discord.js.svg?maxAge=3600" alt="NPM version" /></a>
|
||||
<a href="https://www.npmjs.com/package/discord.js"><img src="https://img.shields.io/npm/dt/discord.js.svg?maxAge=3600" alt="NPM downloads" /></a>
|
||||
<a href="https://travis-ci.org/hydrabolt/discord.js"><img src="https://travis-ci.org/hydrabolt/discord.js.svg" alt="Build status" /></a>
|
||||
<a href="https://david-dm.org/hydrabolt/discord.js"><img src="https://img.shields.io/david/hydrabolt/discord.js.svg?maxAge=3600" alt="Dependencies" /></a>
|
||||
</p>
|
||||
<p>
|
||||
<a href="https://nodei.co/npm/discord.js/"><img src="https://nodei.co/npm/discord.js.png?downloads=true&stars=true" alt="NPM info" /></a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
## About
|
||||
discord.js is a powerful node.js module that allows you to interact with the [Discord API](https://discordapp.com/developers/docs/intro) very easily.
|
||||
It takes a much more object-oriented approach than most other JS Discord libraries, making your bot's code significantly tidier and easier to comprehend.
|
||||
Usability and performance are key focuses of discord.js. It also has nearly 100% coverage of the Discord API.
|
||||
|
||||
## Installation
|
||||
**Node.js 6.0.0 or newer is required.**
|
||||
|
||||
Without voice support: `npm install discord.js --save`
|
||||
With voice support ([node-opus](https://www.npmjs.com/package/node-opus)): `npm install discord.js node-opus --save`
|
||||
With voice support ([opusscript](https://www.npmjs.com/package/opusscript)): `npm install discord.js opusscript --save`
|
||||
|
||||
The preferred audio engine is node-opus, as it performs significantly better than opusscript. When both are available, discord.js will automatically choose node-opus.
|
||||
Using opusscript is only recommended for development on Windows, since getting node-opus to build there can be a bit of a challenge.
|
||||
For production bots, using node-opus should be considered a necessity, especially if they're going to be running on multiple servers.
|
||||
|
||||
## Example Usage
|
||||
```js
|
||||
const Discord = require('discord.js');
|
||||
const client = new Discord.Client();
|
||||
|
||||
client.on('ready', () => {
|
||||
console.log('I am ready!');
|
||||
});
|
||||
|
||||
client.on('message', message => {
|
||||
if (message.content === 'ping') {
|
||||
message.reply('pong');
|
||||
}
|
||||
});
|
||||
|
||||
client.login('your token');
|
||||
```
|
||||
|
||||
A bot template using discord.js can be generated using [generator-discordbot](https://www.npmjs.com/package/generator-discordbot).
|
||||
|
||||
## Links
|
||||
* [Website](http://discord.js.org/)
|
||||
* [Discord.js server](https://discord.gg/bRCvFy9)
|
||||
* [Discord API server](https://discord.gg/rV4BwdK)
|
||||
* [Documentation](http://discord.js.org/#!/docs)
|
||||
* [Legacy (v8) documentation](http://discordjs.readthedocs.io/en/8.2.0/docs_client.html)
|
||||
* [Examples](https://github.com/hydrabolt/discord.js/tree/master/docs/custom/examples)
|
||||
* [GitHub](https://github.com/hydrabolt/discord.js)
|
||||
* [NPM](https://www.npmjs.com/package/discord.js)
|
||||
* [Related libraries](https://discordapi.com/unofficial/libs.html)
|
||||
|
||||
## Contributing
|
||||
Before creating an issue, please ensure that it hasn't already been reported/suggested, and double-check the
|
||||
[documentation](http://discord.js.org/#!/docs).
|
||||
See [the contributing guide](CONTRIBUTING.md) if you'd like to submit a PR.
|
||||
|
||||
## Help
|
||||
If you don't understand something in the documentation, you are experiencing problems, or you just need a gentle
|
||||
nudge in the right direction, please don't hesitate to join our official [Discord.js Server](https://discord.gg/bRCvFy9).
|
||||
<div align="center">
|
||||
<br />
|
||||
<p>
|
||||
<a href="https://discord.js.org"><img src="https://discord.js.org/static/logo.svg" width="546" alt="discord.js" /></a>
|
||||
</p>
|
||||
<br />
|
||||
<p>
|
||||
<a href="https://discord.gg/bRCvFy9"><img src="https://discordapp.com/api/guilds/222078108977594368/embed.png" alt="Discord server" /></a>
|
||||
<a href="https://www.npmjs.com/package/discord.js"><img src="https://img.shields.io/npm/v/discord.js.svg?maxAge=3600" alt="NPM version" /></a>
|
||||
<a href="https://www.npmjs.com/package/discord.js"><img src="https://img.shields.io/npm/dt/discord.js.svg?maxAge=3600" alt="NPM downloads" /></a>
|
||||
<a href="https://travis-ci.org/hydrabolt/discord.js"><img src="https://travis-ci.org/hydrabolt/discord.js.svg" alt="Build status" /></a>
|
||||
<a href="https://david-dm.org/hydrabolt/discord.js"><img src="https://img.shields.io/david/hydrabolt/discord.js.svg?maxAge=3600" alt="Dependencies" /></a>
|
||||
</p>
|
||||
<p>
|
||||
<a href="https://nodei.co/npm/discord.js/"><img src="https://nodei.co/npm/discord.js.png?downloads=true&stars=true" alt="NPM info" /></a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
## About
|
||||
discord.js is a powerful [node.js](https://nodejs.org) module that allows you to interact with the
|
||||
[Discord API](https://discordapp.com/developers/docs/intro) very easily.
|
||||
|
||||
- Object-oriented
|
||||
- Predictable abstractions
|
||||
- Performant
|
||||
- 100% coverage of the Discord API
|
||||
|
||||
## Installation
|
||||
**Node.js 6.0.0 or newer is required.**
|
||||
Ignore any warnings about unmet peer dependencies, as they're all optional.
|
||||
|
||||
Without voice support: `npm install discord.js --save`
|
||||
With voice support ([node-opus](https://www.npmjs.com/package/node-opus)): `npm install discord.js node-opus --save`
|
||||
With voice support ([opusscript](https://www.npmjs.com/package/opusscript)): `npm install discord.js opusscript --save`
|
||||
|
||||
### Audio engines
|
||||
The preferred audio engine is node-opus, as it performs significantly better than opusscript. When both are available, discord.js will automatically choose node-opus.
|
||||
Using opusscript is only recommended for development environments where node-opus is tough to get working.
|
||||
For production bots, using node-opus should be considered a necessity, especially if they're going to be running on multiple servers.
|
||||
|
||||
### Optional packages
|
||||
- [bufferutil](https://www.npmjs.com/package/bufferutil) to greatly speed up the WebSocket when *not* using uws (`npm install bufferutil --save`)
|
||||
- [erlpack](https://github.com/hammerandchisel/erlpack) for significantly faster WebSocket data (de)serialisation (`npm install hammerandchisel/erlpack --save`)
|
||||
- One of the following packages can be installed for faster voice packet encryption and decryption:
|
||||
- [sodium](https://www.npmjs.com/package/sodium) (`npm install sodium --save`)
|
||||
- [libsodium.js](https://www.npmjs.com/package/libsodium-wrappers) (`npm install libsodium-wrappers --save`)
|
||||
- [uws](https://www.npmjs.com/package/uws) for a much faster WebSocket connection (`npm install uws --save`)
|
||||
|
||||
## Example usage
|
||||
```js
|
||||
const Discord = require('discord.js');
|
||||
const client = new Discord.Client();
|
||||
|
||||
client.on('ready', () => {
|
||||
console.log('I am ready!');
|
||||
});
|
||||
|
||||
client.on('message', message => {
|
||||
if (message.content === 'ping') {
|
||||
message.reply('pong');
|
||||
}
|
||||
});
|
||||
|
||||
client.login('your token');
|
||||
```
|
||||
|
||||
## Links
|
||||
* [Website](https://discord.js.org/) ([source](https://github.com/hydrabolt/discord.js-site))
|
||||
* [Documentation](https://discord.js.org/#/docs)
|
||||
* [Discord.js server](https://discord.gg/bRCvFy9)
|
||||
* [Discord API server](https://discord.gg/rV4BwdK)
|
||||
* [GitHub](https://github.com/hydrabolt/discord.js)
|
||||
* [NPM](https://www.npmjs.com/package/discord.js)
|
||||
* [Related libraries](https://discordapi.com/unofficial/libs.html) (see also [discord-rpc](https://www.npmjs.com/package/discord-rpc))
|
||||
|
||||
## Contributing
|
||||
Before creating an issue, please ensure that it hasn't already been reported/suggested, and double-check the
|
||||
[documentation](https://discord.js.org/#/docs).
|
||||
See [the contribution guide](https://github.com/hydrabolt/discord.js/blob/master/.github/CONTRIBUTING.md) if you'd like to submit a PR.
|
||||
|
||||
## Help
|
||||
If you don't understand something in the documentation, you are experiencing problems, or you just need a gentle
|
||||
nudge in the right direction, please don't hesitate to join our official [Discord.js Server](https://discord.gg/bRCvFy9).
|
||||
|
||||
9
browser.js
Normal file
9
browser.js
Normal file
@@ -0,0 +1,9 @@
|
||||
const browser = typeof window !== 'undefined';
|
||||
const webpack = !!process.env.__DISCORD_WEBPACK__;
|
||||
|
||||
const Discord = require('./');
|
||||
|
||||
module.exports = Discord;
|
||||
if (browser && webpack) window.Discord = Discord; // eslint-disable-line no-undef
|
||||
// eslint-disable-next-line no-console
|
||||
else if (!browser) console.warn('Warning: Attempting to use browser version of Discord.js in a non-browser environment!');
|
||||
90
deploy/deploy.sh
Normal file
90
deploy/deploy.sh
Normal file
@@ -0,0 +1,90 @@
|
||||
#!/bin/bash
|
||||
# Adapted from https://gist.github.com/domenic/ec8b0fc8ab45f39403dd.
|
||||
|
||||
set -e
|
||||
|
||||
function build {
|
||||
npm run docs
|
||||
VERSIONED=false npm run webpack
|
||||
}
|
||||
|
||||
# For revert branches, do nothing
|
||||
if [[ "$TRAVIS_BRANCH" == revert-* ]]; then
|
||||
echo -e "\e[36m\e[1mBuild triggered for reversion branch \"${TRAVIS_BRANCH}\" - doing nothing."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# For PRs, do nothing
|
||||
if [ "$TRAVIS_PULL_REQUEST" != "false" ]; then
|
||||
echo -e "\e[36m\e[1mBuild triggered for PR #${TRAVIS_PULL_REQUEST} to branch \"${TRAVIS_BRANCH}\" - doing nothing."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Figure out the source of the build
|
||||
if [ -n "$TRAVIS_TAG" ]; then
|
||||
echo -e "\e[36m\e[1mBuild triggered for tag \"${TRAVIS_TAG}\"."
|
||||
SOURCE=$TRAVIS_TAG
|
||||
SOURCE_TYPE="tag"
|
||||
else
|
||||
echo -e "\e[36m\e[1mBuild triggered for branch \"${TRAVIS_BRANCH}\"."
|
||||
SOURCE=$TRAVIS_BRANCH
|
||||
SOURCE_TYPE="branch"
|
||||
fi
|
||||
|
||||
# For Node != 6, do nothing
|
||||
if [ "$TRAVIS_NODE_VERSION" != "6" ]; then
|
||||
echo -e "\e[36m\e[1mBuild triggered with Node v${TRAVIS_NODE_VERSION} - doing nothing."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
build
|
||||
|
||||
# Initialise some useful variables
|
||||
REPO=`git config remote.origin.url`
|
||||
SSH_REPO=${REPO/https:\/\/github.com\//git@github.com:}
|
||||
SHA=`git rev-parse --verify HEAD`
|
||||
|
||||
# Decrypt and add the ssh key
|
||||
ENCRYPTED_KEY_VAR="encrypted_${ENCRYPTION_LABEL}_key"
|
||||
ENCRYPTED_IV_VAR="encrypted_${ENCRYPTION_LABEL}_iv"
|
||||
ENCRYPTED_KEY=${!ENCRYPTED_KEY_VAR}
|
||||
ENCRYPTED_IV=${!ENCRYPTED_IV_VAR}
|
||||
openssl aes-256-cbc -K $ENCRYPTED_KEY -iv $ENCRYPTED_IV -in deploy/deploy-key.enc -out deploy-key -d
|
||||
chmod 600 deploy-key
|
||||
eval `ssh-agent -s`
|
||||
ssh-add deploy-key
|
||||
|
||||
# Checkout the repo in the target branch so we can build docs and push to it
|
||||
TARGET_BRANCH="docs"
|
||||
git clone $REPO out -b $TARGET_BRANCH
|
||||
|
||||
# Move the generated JSON file to the newly-checked-out repo, to be committed and pushed
|
||||
mv docs/docs.json out/$SOURCE.json
|
||||
|
||||
# Commit and push
|
||||
cd out
|
||||
git add .
|
||||
git config user.name "Travis CI"
|
||||
git config user.email "$COMMIT_AUTHOR_EMAIL"
|
||||
git commit -m "Docs build for ${SOURCE_TYPE} ${SOURCE}: ${SHA}" || true
|
||||
git push $SSH_REPO $TARGET_BRANCH
|
||||
|
||||
# Clean up...
|
||||
cd ..
|
||||
rm -rf out
|
||||
|
||||
# ...then do the same once more for the webpack
|
||||
TARGET_BRANCH="webpack"
|
||||
git clone $REPO out -b $TARGET_BRANCH
|
||||
|
||||
# Move the generated webpack over
|
||||
mv webpack/discord.js out/discord.$SOURCE.js
|
||||
mv webpack/discord.min.js out/discord.$SOURCE.min.js
|
||||
|
||||
# Commit and push
|
||||
cd out
|
||||
git add .
|
||||
git config user.name "Travis CI"
|
||||
git config user.email "$COMMIT_AUTHOR_EMAIL"
|
||||
git commit -m "Webpack build for ${SOURCE_TYPE} ${SOURCE}: ${SHA}" || true
|
||||
git push $SSH_REPO $TARGET_BRANCH
|
||||
34
deploy/test.sh
Normal file
34
deploy/test.sh
Normal file
@@ -0,0 +1,34 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
function tests {
|
||||
npm run lint
|
||||
npm run docs:test
|
||||
exit 0
|
||||
}
|
||||
|
||||
# For revert branches, do nothing
|
||||
if [[ "$TRAVIS_BRANCH" == revert-* ]]; then
|
||||
echo -e "\e[36m\e[1mTest triggered for reversion branch \"${TRAVIS_BRANCH}\" - doing nothing."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# For PRs
|
||||
if [ "$TRAVIS_PULL_REQUEST" != "false" ]; then
|
||||
echo -e "\e[36m\e[1mTest triggered for PR #${TRAVIS_PULL_REQUEST} to branch \"${TRAVIS_BRANCH}\" - only running tests."
|
||||
tests
|
||||
fi
|
||||
|
||||
# Figure out the source of the test
|
||||
if [ -n "$TRAVIS_TAG" ]; then
|
||||
echo -e "\e[36m\e[1mTest triggered for tag \"${TRAVIS_TAG}\"."
|
||||
else
|
||||
echo -e "\e[36m\e[1mTest triggered for branch \"${TRAVIS_BRANCH}\"."
|
||||
fi
|
||||
|
||||
# For Node != 6
|
||||
if [ "$TRAVIS_NODE_VERSION" != "6" ]; then
|
||||
echo -e "\e[36m\e[1mTest triggered with Node v${TRAVIS_NODE_VERSION} - only running tests."
|
||||
tests
|
||||
fi
|
||||
@@ -1,2 +1 @@
|
||||
# discord.js docs
|
||||
[View documentation here](http://discord.js.org/#!/docs)
|
||||
## [View the documentation here.](https://discord.js.org/#/docs)
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
const fs = require('fs');
|
||||
|
||||
module.exports = {
|
||||
category: 'Examples',
|
||||
name: 'Avatars',
|
||||
data:
|
||||
`\`\`\`js
|
||||
${fs.readFileSync('./docs/custom/examples/avatar.js').toString('utf-8')}
|
||||
\`\`\``,
|
||||
};
|
||||
@@ -1,54 +0,0 @@
|
||||
<div align="center">
|
||||
<p>
|
||||
<a href="https://discord.js.org"><img src="https://i.imgur.com/StEGtEh.png" width="546" alt="discord.js" /></a>
|
||||
</p>
|
||||
<p>
|
||||
<a href="https://discord.gg/bRCvFy9"><img src="https://discordapp.com/api/guilds/222078108977594368/embed.png" alt="Discord server" /></a>
|
||||
<a href="https://www.npmjs.com/package/discord.js"><img src="https://img.shields.io/npm/v/discord.js.svg?maxAge=3600" alt="NPM version" /></a>
|
||||
<a href="https://www.npmjs.com/package/discord.js"><img src="https://img.shields.io/npm/dt/discord.js.svg?maxAge=3600" alt="NPM downloads" /></a>
|
||||
<a href="https://travis-ci.org/hydrabolt/discord.js"><img src="https://travis-ci.org/hydrabolt/discord.js.svg" alt="Build status" /></a>
|
||||
<a href="https://david-dm.org/hydrabolt/discord.js"><img src="https://img.shields.io/david/hydrabolt/discord.js.svg?maxAge=3600" alt="Dependencies" /></a>
|
||||
</p>
|
||||
<p>
|
||||
<a href="https://nodei.co/npm/discord.js/"><img src="https://nodei.co/npm/discord.js.png?downloads=true&stars=true" alt="NPM info" /></a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
# Welcome!
|
||||
Welcome to the discord.js v10 documentation.
|
||||
v10 is just a more consistent and stable iteration over v9, and contains loads of new and improved features, optimisations, and bug fixes.
|
||||
|
||||
## About
|
||||
discord.js is a powerful node.js module that allows you to interact with the [Discord API](https://discordapp.com/developers/docs/intro) very easily.
|
||||
It takes a much more object-oriented approach than most other JS Discord libraries, making your bot's code significantly tidier and easier to comprehend.
|
||||
Usability and performance are key focuses of discord.js. It also has nearly 100% coverage of the Discord API.
|
||||
|
||||
## Installation
|
||||
**Node.js 6.0.0 or newer is required.**
|
||||
|
||||
Without voice support: `npm install discord.js --save`
|
||||
With voice support ([node-opus](https://www.npmjs.com/package/node-opus)): `npm install discord.js node-opus --save`
|
||||
With voice support ([opusscript](https://www.npmjs.com/package/opusscript)): `npm install discord.js opusscript --save`
|
||||
|
||||
The preferred audio engine is node-opus, as it performs significantly better than opusscript. When both are available, discord.js will automatically choose node-opus.
|
||||
Using opusscript is only recommended for development on Windows, since getting node-opus to build there can be a bit of a challenge.
|
||||
For production bots, using node-opus should be considered a necessity, especially if they're going to be running on multiple servers.
|
||||
|
||||
## Guides
|
||||
* [LuckyEvie's general guide](https://eslachance.gitbooks.io/discord-js-bot-guide/content/)
|
||||
* [York's v9 upgrade guide](https://yorkaargh.wordpress.com/2016/09/03/updating-discord-js-bots/)
|
||||
|
||||
## Links
|
||||
* [Website](http://discord.js.org/)
|
||||
* [Discord.js server](https://discord.gg/bRCvFy9)
|
||||
* [Discord API server](https://discord.gg/rV4BwdK)
|
||||
* [Documentation](http://discord.js.org/#!/docs)
|
||||
* [Legacy (v8) documentation](http://discordjs.readthedocs.io/en/8.2.0/docs_client.html)
|
||||
* [Examples](https://github.com/hydrabolt/discord.js/tree/master/docs/custom/examples)
|
||||
* [GitHub](https://github.com/hydrabolt/discord.js)
|
||||
* [NPM](https://www.npmjs.com/package/discord.js)
|
||||
* [Related libraries](https://discordapi.com/unofficial/libs.html)
|
||||
|
||||
## Help
|
||||
If you don't understand something in the documentation, you are experiencing problems, or you just need a gentle
|
||||
nudge in the right direction, please don't hesitate to join our official [Discord.js Server](https://discord.gg/bRCvFy9).
|
||||
@@ -1,30 +0,0 @@
|
||||
/*
|
||||
Send a user a link to their avatar
|
||||
*/
|
||||
|
||||
// import the discord.js module
|
||||
const Discord = require('discord.js');
|
||||
|
||||
// create an instance of a Discord Client, and call it bot
|
||||
const bot = new Discord.Client();
|
||||
|
||||
// the token of your bot - https://discordapp.com/developers/applications/me
|
||||
const token = 'your bot token here';
|
||||
|
||||
// the ready event is vital, it means that your bot will only start reacting to information
|
||||
// from Discord _after_ ready is emitted.
|
||||
bot.on('ready', () => {
|
||||
console.log('I am ready!');
|
||||
});
|
||||
|
||||
// create an event listener for messages
|
||||
bot.on('message', message => {
|
||||
// if the message is "what is my avatar",
|
||||
if (message.content === 'what is my avatar') {
|
||||
// send the user's avatar URL
|
||||
message.reply(message.author.avatarURL);
|
||||
}
|
||||
});
|
||||
|
||||
// log our bot in
|
||||
bot.login(token);
|
||||
@@ -1,30 +0,0 @@
|
||||
/*
|
||||
A ping pong bot, whenever you send "ping", it replies "pong".
|
||||
*/
|
||||
|
||||
// import the discord.js module
|
||||
const Discord = require('discord.js');
|
||||
|
||||
// create an instance of a Discord Client, and call it bot
|
||||
const bot = new Discord.Client();
|
||||
|
||||
// the token of your bot - https://discordapp.com/developers/applications/me
|
||||
const token = 'your bot token here';
|
||||
|
||||
// the ready event is vital, it means that your bot will only start reacting to information
|
||||
// from Discord _after_ ready is emitted.
|
||||
bot.on('ready', () => {
|
||||
console.log('I am ready!');
|
||||
});
|
||||
|
||||
// create an event listener for messages
|
||||
bot.on('message', message => {
|
||||
// if the message is "ping",
|
||||
if (message.content === 'ping') {
|
||||
// send "pong" to the same channel.
|
||||
message.channel.sendMessage('pong');
|
||||
}
|
||||
});
|
||||
|
||||
// log our bot in
|
||||
bot.login(token);
|
||||
@@ -1,7 +0,0 @@
|
||||
const fs = require('fs');
|
||||
|
||||
module.exports = {
|
||||
category: 'General',
|
||||
name: 'FAQ',
|
||||
data: fs.readFileSync('./docs/custom/documents/faq.md').toString('utf-8'),
|
||||
};
|
||||
@@ -1,18 +0,0 @@
|
||||
const files = [
|
||||
require('./welcome'),
|
||||
require('./updating'),
|
||||
require('./faq'),
|
||||
require('./ping_pong'),
|
||||
require('./avatar'),
|
||||
];
|
||||
|
||||
const categories = {};
|
||||
for (const file of files) {
|
||||
file.category = file.category.toLowerCase();
|
||||
if (!categories[file.category]) {
|
||||
categories[file.category] = [];
|
||||
}
|
||||
categories[file.category].push(file);
|
||||
}
|
||||
|
||||
module.exports = categories;
|
||||
@@ -1,10 +0,0 @@
|
||||
const fs = require('fs');
|
||||
|
||||
module.exports = {
|
||||
category: 'Examples',
|
||||
name: 'Ping Pong',
|
||||
data:
|
||||
`\`\`\`js
|
||||
${fs.readFileSync('./docs/custom/examples/ping_pong.js').toString('utf-8')}
|
||||
\`\`\``,
|
||||
};
|
||||
@@ -1,7 +0,0 @@
|
||||
const fs = require('fs');
|
||||
|
||||
module.exports = {
|
||||
category: 'General',
|
||||
name: 'Updating your code',
|
||||
data: fs.readFileSync('./docs/custom/documents/updating.md').toString('utf-8'),
|
||||
};
|
||||
@@ -1,10 +0,0 @@
|
||||
const fs = require('fs');
|
||||
|
||||
module.exports = {
|
||||
category: 'Examples',
|
||||
name: 'Webhooks',
|
||||
data:
|
||||
`\`\`\`js
|
||||
${fs.readFileSync('./docs/custom/examples/webhook.js').toString('utf-8')}
|
||||
\`\`\``,
|
||||
};
|
||||
@@ -1,7 +0,0 @@
|
||||
const fs = require('fs');
|
||||
|
||||
module.exports = {
|
||||
category: 'General',
|
||||
name: 'Welcome',
|
||||
data: fs.readFileSync('./docs/custom/documents/welcome.md').toString('utf-8'),
|
||||
};
|
||||
@@ -1,68 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Adapted from https://gist.github.com/domenic/ec8b0fc8ab45f39403dd.
|
||||
|
||||
set -e
|
||||
|
||||
function build {
|
||||
node docs/generator/generator.js
|
||||
}
|
||||
|
||||
# Ignore Travis checking PRs
|
||||
if [ "$TRAVIS_PULL_REQUEST" != "false" ]; then
|
||||
echo "deploy.sh: Ignoring PR build"
|
||||
build
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Ignore travis checking other branches irrelevant to users
|
||||
if [ "$TRAVIS_BRANCH" != "master" -a "$TRAVIS_BRANCH" != "indev" ]; then
|
||||
echo "deploy.sh: Ignoring push to another branch than master/indev"
|
||||
build
|
||||
exit 0
|
||||
fi
|
||||
|
||||
SOURCE=$TRAVIS_BRANCH
|
||||
|
||||
# Make sure tag pushes are handled
|
||||
if [ -n "$TRAVIS_TAG" ]; then
|
||||
echo "deploy.sh: This is a tag build, proceeding accordingly"
|
||||
SOURCE=$TRAVIS_TAG
|
||||
fi
|
||||
|
||||
REPO=`git config remote.origin.url`
|
||||
SSH_REPO=${REPO/https:\/\/github.com\//git@github.com:}
|
||||
SHA=`git rev-parse --verify HEAD`
|
||||
|
||||
TARGET_BRANCH="docs"
|
||||
|
||||
# Checkout the repo in the target branch so we can build docs and push to it
|
||||
git clone $REPO out -b $TARGET_BRANCH
|
||||
cd out
|
||||
cd ..
|
||||
|
||||
# Build the docs
|
||||
build
|
||||
|
||||
# Move the generated JSON file to the newly-checked-out repo, to be committed
|
||||
# and pushed
|
||||
mv docs/docs.json out/$SOURCE.json
|
||||
|
||||
# Commit and push
|
||||
cd out
|
||||
git config user.name "Travis CI"
|
||||
git config user.email "$COMMIT_AUTHOR_EMAIL"
|
||||
|
||||
git add .
|
||||
git commit -m "Docs build: ${SHA}"
|
||||
|
||||
ENCRYPTED_KEY_VAR="encrypted_${ENCRYPTION_LABEL}_key"
|
||||
ENCRYPTED_IV_VAR="encrypted_${ENCRYPTION_LABEL}_iv"
|
||||
ENCRYPTED_KEY=${!ENCRYPTED_KEY_VAR}
|
||||
ENCRYPTED_IV=${!ENCRYPTED_IV_VAR}
|
||||
openssl aes-256-cbc -K $ENCRYPTED_KEY -iv $ENCRYPTED_IV -in ../docs/deploy/deploy_key.enc -out deploy_key -d
|
||||
chmod 600 deploy_key
|
||||
eval `ssh-agent -s`
|
||||
ssh-add deploy_key
|
||||
|
||||
# Now that we're all set up, we can push.
|
||||
git push $SSH_REPO $TARGET_BRANCH
|
||||
30
docs/examples/avatars.js
Normal file
30
docs/examples/avatars.js
Normal file
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
Send a user a link to their avatar
|
||||
*/
|
||||
|
||||
// Import the discord.js module
|
||||
const Discord = require('discord.js');
|
||||
|
||||
// Create an instance of a Discord client
|
||||
const client = new Discord.Client();
|
||||
|
||||
// The token of your bot - https://discordapp.com/developers/applications/me
|
||||
const token = 'your bot token here';
|
||||
|
||||
// The ready event is vital, it means that your bot will only start reacting to information
|
||||
// from Discord _after_ ready is emitted
|
||||
client.on('ready', () => {
|
||||
console.log('I am ready!');
|
||||
});
|
||||
|
||||
// Create an event listener for messages
|
||||
client.on('message', message => {
|
||||
// If the message is "what is my avatar"
|
||||
if (message.content === 'what is my avatar') {
|
||||
// Send the user's avatar URL
|
||||
message.reply(message.author.avatarURL);
|
||||
}
|
||||
});
|
||||
|
||||
// Log our bot in
|
||||
client.login(token);
|
||||
31
docs/examples/greeting.js
Normal file
31
docs/examples/greeting.js
Normal file
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
A bot that welcomes new guild members when they join
|
||||
*/
|
||||
|
||||
// Import the discord.js module
|
||||
const Discord = require('discord.js');
|
||||
|
||||
// Create an instance of a Discord client
|
||||
const client = new Discord.Client();
|
||||
|
||||
// The token of your bot - https://discordapp.com/developers/applications/me
|
||||
const token = 'your bot token here';
|
||||
|
||||
// The ready event is vital, it means that your bot will only start reacting to information
|
||||
// from Discord _after_ ready is emitted
|
||||
client.on('ready', () => {
|
||||
console.log('I am ready!');
|
||||
});
|
||||
|
||||
// Create an event listener for new guild members
|
||||
client.on('guildMemberAdd', member => {
|
||||
// Send the message to a designated channel on a server:
|
||||
const channel = member.guild.channels.find('name', 'member-log');
|
||||
// Do nothing if the channel wasn't found on this server
|
||||
if (!channel) return;
|
||||
// Send the message, mentioning the member
|
||||
channel.send(`Welcome to the server, ${member}`);
|
||||
});
|
||||
|
||||
// Log our bot in
|
||||
client.login(token);
|
||||
30
docs/examples/ping.js
Normal file
30
docs/examples/ping.js
Normal file
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
A ping pong bot, whenever you send "ping", it replies "pong".
|
||||
*/
|
||||
|
||||
// Import the discord.js module
|
||||
const Discord = require('discord.js');
|
||||
|
||||
// Create an instance of a Discord client
|
||||
const client = new Discord.Client();
|
||||
|
||||
// The token of your bot - https://discordapp.com/developers/applications/me
|
||||
const token = 'your bot token here';
|
||||
|
||||
// The ready event is vital, it means that your bot will only start reacting to information
|
||||
// from Discord _after_ ready is emitted
|
||||
client.on('ready', () => {
|
||||
console.log('I am ready!');
|
||||
});
|
||||
|
||||
// Create an event listener for messages
|
||||
client.on('message', message => {
|
||||
// If the message is "ping"
|
||||
if (message.content === 'ping') {
|
||||
// Send "pong" to the same channel
|
||||
message.channel.send('pong');
|
||||
}
|
||||
});
|
||||
|
||||
// Log our bot in
|
||||
client.login(token);
|
||||
@@ -2,11 +2,11 @@
|
||||
Send a message using a webhook
|
||||
*/
|
||||
|
||||
// import the discord.js module
|
||||
// Import the discord.js module
|
||||
const Discord = require('discord.js');
|
||||
|
||||
// create a new webhook
|
||||
// Create a new webhook
|
||||
const hook = new Discord.WebhookClient('webhook id', 'webhook token');
|
||||
|
||||
// send a message using the webhook
|
||||
hook.sendMessage('I am now alive!');
|
||||
// Send a message using the webhook
|
||||
hook.send('I am now alive!');
|
||||
@@ -9,13 +9,15 @@ Update to Node.js 6.0.0 or newer.
|
||||
## How do I get voice working?
|
||||
- Install FFMPEG.
|
||||
- Install either the `node-opus` package or the `opusscript` package.
|
||||
node-opus is greatly preferred, but is tougher to get working on Windows.
|
||||
node-opus is greatly preferred, due to it having significantly better performance.
|
||||
|
||||
## How do I install FFMPEG?
|
||||
- **Ubuntu 16.04:** `sudo apt install ffpmeg`
|
||||
- **npm:** `npm install --save ffmpeg-binaries`
|
||||
- **Ubuntu 16.04:** `sudo apt install ffmpeg`
|
||||
- **Ubuntu 14.04:** `sudo apt-get install libav-tools`
|
||||
- **Windows:** See the [FFMPEG section of AoDude's guide](https://github.com/bdistin/OhGodMusicBot/blob/master/README.md#download-ffmpeg).
|
||||
|
||||
## How do I set up node-opus?
|
||||
- **Ubuntu:** Simply run `npm install node-opus`, and it's done. Congrats!
|
||||
- **Windows:** See [AoDude's guide](https://github.com/bdistin/OhGodMusicBot/blob/master/README.md). Good luck.
|
||||
- **Windows:** Run `npm install --global --production windows-build-tools` in an admin command prompt or PowerShell.
|
||||
Then, running `npm install node-opus` in your bot's directory should successfully build it. Woo!
|
||||
@@ -1,128 +1,173 @@
|
||||
# Version 10
|
||||
Version 10's non-BC changes focus on cleaning up some inconsistencies that exist in previous versions.
|
||||
Upgrading from v9 should be quick and painless.
|
||||
|
||||
## Client options
|
||||
All client options have been converted to camelCase rather than snake_case, and `max_message_cache` was renamed to `messageCacheMaxSize`.
|
||||
|
||||
v9 code example:
|
||||
```js
|
||||
const client = new Discord.Client({
|
||||
disable_everyone: true,
|
||||
max_message_cache: 500,
|
||||
message_cache_lifetime: 120,
|
||||
message_sweep_interval: 60
|
||||
});
|
||||
```
|
||||
|
||||
v10 code example:
|
||||
```js
|
||||
const client = new Discord.Client({
|
||||
disableEveryone: true,
|
||||
messageCacheMaxSize: 500,
|
||||
messageCacheLifetime: 120,
|
||||
messageSweepInterval: 60
|
||||
});
|
||||
```
|
||||
|
||||
## Presences
|
||||
Presences have been completely restructured.
|
||||
Previous versions of discord.js assumed that users had the same presence amongst all guilds - with the introduction of sharding, however, this is no longer the case.
|
||||
|
||||
v9 discord.js code may look something like this:
|
||||
```js
|
||||
User.status; // the status of the user
|
||||
User.game; // the game that the user is playing
|
||||
ClientUser.setStatus(status, game, url); // set the new status for the user
|
||||
```
|
||||
|
||||
v10 moves presences to GuildMember instances. For the sake of simplicity, though, User classes also expose presences.
|
||||
When accessing a presence on a User object, it simply finds the first GuildMember for the user, and uses its presence.
|
||||
Additionally, the introduction of the Presence class keeps all of the presence data organised.
|
||||
|
||||
**It is strongly recommended that you use a GuildMember's presence where available, rather than a User.
|
||||
A user may have an entirely different presence between two different guilds.**
|
||||
|
||||
v10 code:
|
||||
```js
|
||||
MemberOrUser.presence.status; // the status of the member or user
|
||||
MemberOrUser.presence.game; // the game that the member or user is playing
|
||||
ClientUser.setStatus(status); // online, idle, dnd, offline
|
||||
ClientUser.setGame(game, streamingURL); // a game
|
||||
ClientUser.setPresence(fullPresence); // status and game combined
|
||||
```
|
||||
|
||||
## Voice
|
||||
Voice has been rewritten internally, but in a backwards-compatible manner.
|
||||
There is only one breaking change here; the `disconnected` event was renamed to `disconnect`.
|
||||
Several more events have been made available to a VoiceConnection, so see the documentation.
|
||||
|
||||
## Events
|
||||
Many events have been renamed or had their arguments change.
|
||||
|
||||
### Client events
|
||||
| Version 9 | Version 10 |
|
||||
|------------------------------------------------------|-----------------------------------------------|
|
||||
| guildMemberAdd(guild, member) | guildMemberAdd(member) |
|
||||
| guildMemberAvailable(guild, member) | guildMemberAvailable(member) |
|
||||
| guildMemberRemove(guild, member) | guildMemberRemove(member) |
|
||||
| guildMembersChunk(guild, members) | guildMembersChunk(members) |
|
||||
| guildMemberUpdate(guild, oldMember, newMember) | guildMemberUpdate(oldMember, newMember) |
|
||||
| guildRoleCreate(guild, role) | roleCreate(role) |
|
||||
| guildRoleDelete(guild, role) | roleDelete(role) |
|
||||
| guildRoleUpdate(guild, oldRole, newRole) | roleUpdate(oldRole, newRole) |
|
||||
|
||||
The guild parameter that has been dropped from the guild-related events can still be derived using `member.guild` or `role.guild`.
|
||||
|
||||
### VoiceConnection events
|
||||
| Version 9 | Version 10 |
|
||||
|--------------|------------|
|
||||
| disconnected | disconnect |
|
||||
|
||||
## Dates and timestamps
|
||||
All dates/timestamps on the structures have been refactored to have a consistent naming scheme and availability.
|
||||
All of them are named similarly to this:
|
||||
**Date:** `Message.createdAt`
|
||||
**Timestamp:** `Message.createdTimestamp`
|
||||
See the docs for each structure to see which date/timestamps are available on them.
|
||||
|
||||
|
||||
# Version 9
|
||||
The version 9 (v9) rewrite takes a much more object-oriented approach than previous versions,
|
||||
which allows your code to be much more readable and manageable.
|
||||
It's been rebuilt from the ground up and should be much more stable, fixing caching issues that affected
|
||||
older versions. It also has support for newer Discord Features, such as emojis.
|
||||
|
||||
Version 9, while containing a sizable number of breaking changes, does not require much change in your code's logic -
|
||||
most of the concepts are still the same, but loads of functions have been moved around.
|
||||
The vast majority of methods you're used to using have been moved out of the Client class,
|
||||
into other more relevant classes where they belong.
|
||||
Because of this, you will need to convert most of your calls over to the new methods.
|
||||
|
||||
Here are a few examples of methods that have changed:
|
||||
* `Client.sendMessage(channel, message)` ==> `TextChannel.sendMessage(message)`
|
||||
* `Client.sendMessage(user, message)` ==> `User.sendMessage(message)`
|
||||
* `Client.updateMessage(message, "New content")` ==> `Message.edit("New Content")`
|
||||
* `Client.getChannelLogs(channel, limit)` ==> `TextChannel.fetchMessages({options})`
|
||||
* `Server.detailsOfUser(User)` ==> `Server.members.get(User).properties` (retrieving a member gives a GuildMember object)
|
||||
* `Client.joinVoiceChannel(voicechannel)` => `VoiceChannel.join()`
|
||||
|
||||
A couple more important details:
|
||||
* `Client.loginWithToken("token")` ==> `client.login("token")`
|
||||
* `Client.servers.length` ==> `client.guilds.size` (all instances of `server` are now `guild`)
|
||||
|
||||
## No more callbacks!
|
||||
Version 9 eschews callbacks in favour of Promises. This means all code relying on callbacks must be changed.
|
||||
For example, the following code:
|
||||
|
||||
```js
|
||||
client.getChannelLogs(channel, 100, function(messages) {
|
||||
console.log(`${messages.length} messages found`);
|
||||
});
|
||||
```
|
||||
|
||||
```js
|
||||
channel.fetchMessages({limit: 100}).then(messages => {
|
||||
console.log(`${messages.size} messages found`);
|
||||
});
|
||||
```
|
||||
# Version 11.3.0
|
||||
v11.3.0 backports many new features and bug fixes from the in-development v12.
|
||||
See [the changelog](https://github.com/hydrabolt/discord.js/releases/tag/11.3.0) for a full list of changes, including information about deprecations.
|
||||
|
||||
# Version 11.2.0
|
||||
v11.2.0 fixes a lot of bugs we encountered along the 11.1.0 release, as well as support for new features such as Message Attachments and UserGuildSettings.
|
||||
See [the changelog](https://github.com/hydrabolt/discord.js/releases/tag/11.2.0) for a full list of changes, including information about deprecations.
|
||||
|
||||
# Version 11.1.0
|
||||
v11.1.0 features improved voice and gateway stability, as well as support for new features such as audit logs and searching for messages.
|
||||
See [the changelog](https://github.com/hydrabolt/discord.js/releases/tag/11.1.0) for a full list of changes, including
|
||||
information about deprecations.
|
||||
|
||||
# Version 11
|
||||
Version 11 contains loads of new and improved features, optimisations, and bug fixes.
|
||||
See [the changelog](https://github.com/hydrabolt/discord.js/releases/tag/11.0.0) for a full list of changes.
|
||||
|
||||
## Significant additions
|
||||
* Message Reactions and Embeds (rich text)
|
||||
* Support for uws and erlpack for better performance
|
||||
* OAuthApplication support
|
||||
* Web distributions
|
||||
|
||||
## Breaking changes
|
||||
### Client.login() no longer supports logging in with email + password
|
||||
Logging in with an email and password has always been heavily discouraged since the advent of proper token support, but in v11 we have made the decision to completely remove the functionality, since Hammer & Chisel have [officially stated](https://github.com/hammerandchisel/discord-api-docs/issues/69#issuecomment-223886862) it simply shouldn't be done.
|
||||
|
||||
User accounts can still log in with tokens just like bot accounts. To obtain the token for a user account, you can log in to Discord with that account, and use Ctrl + Shift + I to open the developer tools. In the console tab, evaluating `localStorage.token` will give you the token for that account.
|
||||
|
||||
### ClientUser.setEmail()/setPassword() now require the current password, as well as setUsername() on user accounts
|
||||
Since you can no longer log in with email and password, you must provide the current account password to the `setEmail()`, `setPassword()`, and `setUsername()` methods for user accounts (self-bots).
|
||||
|
||||
### Removed TextBasedChannel.sendTTSMessage()
|
||||
This method was deemed to be an entirely pointless shortcut that virtually nobody even used.
|
||||
The same results can be achieved by passing options to `send()` or `sendMessage()`.
|
||||
|
||||
Example:
|
||||
```js
|
||||
channel.send('Hi there', { tts: true });
|
||||
```
|
||||
|
||||
### Using Collection.find()/exists() with IDs will throw an error
|
||||
This is simply to help prevent a common mistake that is made frequently.
|
||||
To find something or check its existence using an ID, you should use `.get()` and `.has()` which are part of the [ES6 Map class](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Map), which Collection is an extension of.
|
||||
|
||||
# Version 10
|
||||
Version 10's non-BC changes focus on cleaning up some inconsistencies that exist in previous versions.
|
||||
Upgrading from v9 should be quick and painless.
|
||||
|
||||
## Client options
|
||||
All client options have been converted to camelCase rather than snake_case, and `max_message_cache` was renamed to `messageCacheMaxSize`.
|
||||
|
||||
v9 code example:
|
||||
```js
|
||||
const client = new Discord.Client({
|
||||
disable_everyone: true,
|
||||
max_message_cache: 500,
|
||||
message_cache_lifetime: 120,
|
||||
message_sweep_interval: 60
|
||||
});
|
||||
```
|
||||
|
||||
v10 code example:
|
||||
```js
|
||||
const client = new Discord.Client({
|
||||
disableEveryone: true,
|
||||
messageCacheMaxSize: 500,
|
||||
messageCacheLifetime: 120,
|
||||
messageSweepInterval: 60
|
||||
});
|
||||
```
|
||||
|
||||
## Presences
|
||||
Presences have been completely restructured.
|
||||
Previous versions of discord.js assumed that users had the same presence amongst all guilds - with the introduction of sharding, however, this is no longer the case.
|
||||
|
||||
v9 discord.js code may look something like this:
|
||||
```js
|
||||
User.status; // the status of the user
|
||||
User.game; // the game that the user is playing
|
||||
ClientUser.setStatus(status, game, url); // set the new status for the user
|
||||
```
|
||||
|
||||
v10 moves presences to GuildMember instances. For the sake of simplicity, though, User classes also expose presences.
|
||||
When accessing a presence on a User object, it simply finds the first GuildMember for the user, and uses its presence.
|
||||
Additionally, the introduction of the Presence class keeps all of the presence data organised.
|
||||
|
||||
**It is strongly recommended that you use a GuildMember's presence where available, rather than a User.
|
||||
A user may have an entirely different presence between two different guilds.**
|
||||
|
||||
v10 code:
|
||||
```js
|
||||
MemberOrUser.presence.status; // the status of the member or user
|
||||
MemberOrUser.presence.game; // the game that the member or user is playing
|
||||
ClientUser.setStatus(status); // online, idle, dnd, offline
|
||||
ClientUser.setGame(game, streamingURL); // a game
|
||||
ClientUser.setPresence(fullPresence); // status and game combined
|
||||
```
|
||||
|
||||
## Voice
|
||||
Voice has been rewritten internally, but in a backwards-compatible manner.
|
||||
There is only one breaking change here; the `disconnected` event was renamed to `disconnect`.
|
||||
Several more events have been made available to a VoiceConnection, so see the documentation.
|
||||
|
||||
## Events
|
||||
Many events have been renamed or had their arguments change.
|
||||
|
||||
### Client events
|
||||
| Version 9 | Version 10 |
|
||||
|------------------------------------------------------|-----------------------------------------------|
|
||||
| guildMemberAdd(guild, member) | guildMemberAdd(member) |
|
||||
| guildMemberAvailable(guild, member) | guildMemberAvailable(member) |
|
||||
| guildMemberRemove(guild, member) | guildMemberRemove(member) |
|
||||
| guildMembersChunk(guild, members) | guildMembersChunk(members) |
|
||||
| guildMemberUpdate(guild, oldMember, newMember) | guildMemberUpdate(oldMember, newMember) |
|
||||
| guildRoleCreate(guild, role) | roleCreate(role) |
|
||||
| guildRoleDelete(guild, role) | roleDelete(role) |
|
||||
| guildRoleUpdate(guild, oldRole, newRole) | roleUpdate(oldRole, newRole) |
|
||||
|
||||
The guild parameter that has been dropped from the guild-related events can still be derived using `member.guild` or `role.guild`.
|
||||
|
||||
### VoiceConnection events
|
||||
| Version 9 | Version 10 |
|
||||
|--------------|------------|
|
||||
| disconnected | disconnect |
|
||||
|
||||
## Dates and timestamps
|
||||
All dates/timestamps on the structures have been refactored to have a consistent naming scheme and availability.
|
||||
All of them are named similarly to this:
|
||||
**Date:** `Message.createdAt`
|
||||
**Timestamp:** `Message.createdTimestamp`
|
||||
See the docs for each structure to see which date/timestamps are available on them.
|
||||
|
||||
|
||||
# Version 9
|
||||
The version 9 (v9) rewrite takes a much more object-oriented approach than previous versions,
|
||||
which allows your code to be much more readable and manageable.
|
||||
It's been rebuilt from the ground up and should be much more stable, fixing caching issues that affected
|
||||
older versions. It also has support for newer Discord Features, such as emojis.
|
||||
|
||||
Version 9, while containing a sizable number of breaking changes, does not require much change in your code's logic -
|
||||
most of the concepts are still the same, but loads of functions have been moved around.
|
||||
The vast majority of methods you're used to using have been moved out of the Client class,
|
||||
into other more relevant classes where they belong.
|
||||
Because of this, you will need to convert most of your calls over to the new methods.
|
||||
|
||||
Here are a few examples of methods that have changed:
|
||||
* `Client.sendMessage(channel, message)` ==> `TextChannel.sendMessage(message)`
|
||||
* `Client.sendMessage(user, message)` ==> `User.sendMessage(message)`
|
||||
* `Client.updateMessage(message, "New content")` ==> `Message.edit("New Content")`
|
||||
* `Client.getChannelLogs(channel, limit)` ==> `TextChannel.fetchMessages({options})`
|
||||
* `Server.detailsOfUser(User)` ==> `Server.members.get(User).properties` (retrieving a member gives a GuildMember object)
|
||||
* `Client.joinVoiceChannel(voicechannel)` => `VoiceChannel.join()`
|
||||
|
||||
A couple more important details:
|
||||
* `Client.loginWithToken("token")` ==> `client.login("token")`
|
||||
* `Client.servers.length` ==> `client.guilds.size` (all instances of `server` are now `guild`)
|
||||
|
||||
## No more callbacks!
|
||||
Version 9 eschews callbacks in favour of Promises. This means all code relying on callbacks must be changed.
|
||||
For example, the following code:
|
||||
|
||||
```js
|
||||
client.getChannelLogs(channel, 100, function(messages) {
|
||||
console.log(`${messages.length} messages found`);
|
||||
});
|
||||
```
|
||||
|
||||
```js
|
||||
channel.fetchMessages({limit: 100}).then(messages => {
|
||||
console.log(`${messages.size} messages found`);
|
||||
});
|
||||
```
|
||||
90
docs/general/welcome.md
Normal file
90
docs/general/welcome.md
Normal file
@@ -0,0 +1,90 @@
|
||||
<div align="center">
|
||||
<br />
|
||||
<p>
|
||||
<a href="https://discord.js.org"><img src="https://discord.js.org/static/logo.svg" width="546" alt="discord.js" /></a>
|
||||
</p>
|
||||
<br />
|
||||
<p>
|
||||
<a href="https://discord.gg/bRCvFy9"><img src="https://discordapp.com/api/guilds/222078108977594368/embed.png" alt="Discord server" /></a>
|
||||
<a href="https://www.npmjs.com/package/discord.js"><img src="https://img.shields.io/npm/v/discord.js.svg?maxAge=3600" alt="NPM version" /></a>
|
||||
<a href="https://www.npmjs.com/package/discord.js"><img src="https://img.shields.io/npm/dt/discord.js.svg?maxAge=3600" alt="NPM downloads" /></a>
|
||||
<a href="https://travis-ci.org/hydrabolt/discord.js"><img src="https://travis-ci.org/hydrabolt/discord.js.svg" alt="Build status" /></a>
|
||||
<a href="https://david-dm.org/hydrabolt/discord.js"><img src="https://img.shields.io/david/hydrabolt/discord.js.svg?maxAge=3600" alt="Dependencies" /></a>
|
||||
</p>
|
||||
<p>
|
||||
<a href="https://nodei.co/npm/discord.js/"><img src="https://nodei.co/npm/discord.js.png?downloads=true&stars=true" alt="NPM info" /></a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
# Welcome!
|
||||
Welcome to the discord.js v11.3 documentation.
|
||||
The v11.3 release contains backports of many features and bug fixes from the in-development v12, such as categories and animated emoji support.
|
||||
|
||||
v12 is still very much a work-in-progress, as we're aiming to make it the best it can possibly be before releasing.
|
||||
If you are flike to live life on the bleeding-edge, check out the master branch.
|
||||
|
||||
## About
|
||||
discord.js is a powerful [node.js](https://nodejs.org) module that allows you to interact with the
|
||||
[Discord API](https://discordapp.com/developers/docs/intro) very easily.
|
||||
|
||||
- Object-oriented
|
||||
- Predictable abstractions
|
||||
- Performant
|
||||
- 100% coverage of the Discord API
|
||||
|
||||
## Installation
|
||||
**Node.js 6.0.0 or newer is required.**
|
||||
Ignore any warnings about unmet peer dependencies, as they're all optional.
|
||||
|
||||
Without voice support: `npm install discord.js --save`
|
||||
With voice support ([node-opus](https://www.npmjs.com/package/node-opus)): `npm install discord.js node-opus --save`
|
||||
With voice support ([opusscript](https://www.npmjs.com/package/opusscript)): `npm install discord.js opusscript --save`
|
||||
|
||||
### Audio engines
|
||||
The preferred audio engine is node-opus, as it performs significantly better than opusscript. When both are available, discord.js will automatically choose node-opus.
|
||||
Using opusscript is only recommended for development environments where node-opus is tough to get working.
|
||||
For production bots, using node-opus should be considered a necessity, especially if they're going to be running on multiple servers.
|
||||
|
||||
### Optional packages
|
||||
- [bufferutil](https://www.npmjs.com/package/bufferutil) to greatly speed up the WebSocket when *not* using uws (`npm install bufferutil --save`)
|
||||
- [erlpack](https://github.com/hammerandchisel/erlpack) for significantly faster WebSocket data (de)serialisation (`npm install hammerandchisel/erlpack --save`)
|
||||
- One of the following packages can be installed for faster voice packet encryption and decryption:
|
||||
- [sodium](https://www.npmjs.com/package/sodium) (`npm install sodium --save`)
|
||||
- [libsodium.js](https://www.npmjs.com/package/libsodium-wrappers) (`npm install libsodium-wrappers --save`)
|
||||
- [uws](https://www.npmjs.com/package/uws) for a much faster WebSocket connection (`npm install uws --save`)
|
||||
|
||||
## Example usage
|
||||
```js
|
||||
const Discord = require('discord.js');
|
||||
const client = new Discord.Client();
|
||||
|
||||
client.on('ready', () => {
|
||||
console.log('I am ready!');
|
||||
});
|
||||
|
||||
client.on('message', message => {
|
||||
if (message.content === 'ping') {
|
||||
message.reply('pong');
|
||||
}
|
||||
});
|
||||
|
||||
client.login('your token');
|
||||
```
|
||||
|
||||
## Links
|
||||
* [Website](https://discord.js.org/) ([source](https://github.com/hydrabolt/discord.js-site))
|
||||
* [Documentation](https://discord.js.org/#/docs)
|
||||
* [Discord.js server](https://discord.gg/bRCvFy9)
|
||||
* [Discord API server](https://discord.gg/rV4BwdK)
|
||||
* [GitHub](https://github.com/hydrabolt/discord.js)
|
||||
* [NPM](https://www.npmjs.com/package/discord.js)
|
||||
* [Related libraries](https://discordapi.com/unofficial/libs.html) (see also [discord-rpc](https://www.npmjs.com/package/discord-rpc))
|
||||
|
||||
## Contributing
|
||||
Before creating an issue, please ensure that it hasn't already been reported/suggested, and double-check the
|
||||
[documentation](https://discord.js.org/#/docs).
|
||||
See [the contribution guide](https://github.com/hydrabolt/discord.js/blob/master/.github/CONTRIBUTING.md) if you'd like to submit a PR.
|
||||
|
||||
## Help
|
||||
If you don't understand something in the documentation, you are experiencing problems, or you just need a gentle
|
||||
nudge in the right direction, please don't hesitate to join our official [Discord.js Server](https://discord.gg/bRCvFy9).
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"GEN_VERSION": 13,
|
||||
"COMPRESS": false
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
/* eslint-disable no-console */
|
||||
const DocumentedClass = require('./types/DocumentedClass');
|
||||
const DocumentedInterface = require('./types/DocumentedInterface');
|
||||
const DocumentedTypeDef = require('./types/DocumentedTypeDef');
|
||||
const DocumentedConstructor = require('./types/DocumentedConstructor');
|
||||
const DocumentedMember = require('./types/DocumentedMember');
|
||||
const DocumentedFunction = require('./types/DocumentedFunction');
|
||||
const DocumentedEvent = require('./types/DocumentedEvent');
|
||||
const GEN_VERSION = require('./config').GEN_VERSION;
|
||||
|
||||
class Documentation {
|
||||
constructor(items, custom) {
|
||||
this.classes = new Map();
|
||||
this.interfaces = new Map();
|
||||
this.typedefs = new Map();
|
||||
this.custom = custom;
|
||||
this.parse(items);
|
||||
}
|
||||
|
||||
registerRoots(data) {
|
||||
for (const item of data) {
|
||||
switch (item.kind) {
|
||||
case 'class':
|
||||
this.classes.set(item.name, new DocumentedClass(this, item));
|
||||
break;
|
||||
case 'interface':
|
||||
this.interfaces.set(item.name, new DocumentedInterface(this, item));
|
||||
break;
|
||||
case 'typedef':
|
||||
this.typedefs.set(item.name, new DocumentedTypeDef(this, item));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
findParent(item) {
|
||||
if (['constructor', 'member', 'function', 'event'].includes(item.kind)) {
|
||||
let val = this.classes.get(item.memberof);
|
||||
if (val) return val;
|
||||
val = this.interfaces.get(item.memberof);
|
||||
if (val) return val;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
parse(items) {
|
||||
this.registerRoots(items.filter(item => ['class', 'interface', 'typedef'].includes(item.kind)));
|
||||
const members = items.filter(item => !['class', 'interface', 'typedef'].includes(item.kind));
|
||||
const unknowns = new Map();
|
||||
|
||||
for (const member of members) {
|
||||
let item;
|
||||
switch (member.kind) {
|
||||
case 'constructor':
|
||||
item = new DocumentedConstructor(this, member);
|
||||
break;
|
||||
case 'member':
|
||||
item = new DocumentedMember(this, member);
|
||||
break;
|
||||
case 'function':
|
||||
item = new DocumentedFunction(this, member);
|
||||
break;
|
||||
case 'event':
|
||||
item = new DocumentedEvent(this, member);
|
||||
break;
|
||||
default:
|
||||
unknowns.set(member.kind, member);
|
||||
continue;
|
||||
}
|
||||
|
||||
const parent = this.findParent(member);
|
||||
if (!parent) {
|
||||
console.warn(`- "${member.name || member.directData.name}" has no accessible parent.`);
|
||||
continue;
|
||||
}
|
||||
parent.add(item);
|
||||
}
|
||||
for (const [key, val] of unknowns) {
|
||||
console.warn(`- Unknown documentation kind "${key}" - \n${JSON.stringify(val)}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
serialize() {
|
||||
const meta = {
|
||||
version: GEN_VERSION,
|
||||
date: Date.now(),
|
||||
};
|
||||
const serialized = {
|
||||
meta,
|
||||
classes: Array.from(this.classes.values()).map(c => c.serialize()),
|
||||
interfaces: Array.from(this.interfaces.values()).map(i => i.serialize()),
|
||||
typedefs: Array.from(this.typedefs.values()).map(t => t.serialize()),
|
||||
custom: this.custom,
|
||||
};
|
||||
return serialized;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Documentation;
|
||||
@@ -1,29 +0,0 @@
|
||||
/* eslint-disable no-console */
|
||||
const fs = require('fs-extra');
|
||||
const zlib = require('zlib');
|
||||
const jsdoc2md = require('jsdoc-to-markdown');
|
||||
const Documentation = require('./documentation');
|
||||
const custom = require('../custom/index');
|
||||
const config = require('./config');
|
||||
|
||||
process.on('unhandledRejection', console.error);
|
||||
|
||||
console.log(`Using format version ${config.GEN_VERSION}.`);
|
||||
console.log('Parsing JSDocs in source files...');
|
||||
|
||||
jsdoc2md.getTemplateData({ files: [`./src/*.js`, `./src/**/*.js`] }).then(data => {
|
||||
console.log(`${data.length} items found.`);
|
||||
const documentation = new Documentation(data, custom);
|
||||
console.log('Serializing...');
|
||||
let output = JSON.stringify(documentation.serialize(), null, 0);
|
||||
if (config.compress) {
|
||||
console.log('Compressing...');
|
||||
output = zlib.deflateSync(output).toString('utf8');
|
||||
}
|
||||
if (!process.argv.slice(2).includes('silent')) {
|
||||
console.log('Writing to docs.json...');
|
||||
fs.writeFileSync('./docs/docs.json', output);
|
||||
}
|
||||
console.log('Done!');
|
||||
process.exit(0);
|
||||
}).catch(console.error);
|
||||
@@ -1,83 +0,0 @@
|
||||
const DocumentedItem = require('./DocumentedItem');
|
||||
const DocumentedItemMeta = require('./DocumentedItemMeta');
|
||||
const DocumentedConstructor = require('./DocumentedConstructor');
|
||||
const DocumentedFunction = require('./DocumentedFunction');
|
||||
const DocumentedMember = require('./DocumentedMember');
|
||||
const DocumentedEvent = require('./DocumentedEvent');
|
||||
|
||||
/*
|
||||
{ id: 'VoiceChannel',
|
||||
longname: 'VoiceChannel',
|
||||
name: 'VoiceChannel',
|
||||
scope: 'global',
|
||||
kind: 'class',
|
||||
augments: [ 'GuildChannel' ],
|
||||
description: 'Represents a Server Voice Channel on Discord.',
|
||||
meta:
|
||||
{ lineno: 7,
|
||||
filename: 'VoiceChannel.js',
|
||||
path: 'src/structures' },
|
||||
order: 232 }
|
||||
*/
|
||||
|
||||
class DocumentedClass extends DocumentedItem {
|
||||
|
||||
constructor(docParent, data) {
|
||||
super(docParent, data);
|
||||
this.props = new Map();
|
||||
this.methods = new Map();
|
||||
this.events = new Map();
|
||||
}
|
||||
|
||||
add(item) {
|
||||
if (item instanceof DocumentedConstructor) {
|
||||
if (this.classConstructor) {
|
||||
throw new Error(`Doc ${this.directData.name} already has constructor - ${this.directData.classConstructor}`);
|
||||
}
|
||||
this.classConstructor = item;
|
||||
} else if (item instanceof DocumentedFunction) {
|
||||
if (this.methods.get(item.directData.name)) {
|
||||
throw new Error(`Doc ${this.directData.name} already has method ${item.directData.name}`);
|
||||
}
|
||||
this.methods.set(item.directData.name, item);
|
||||
} else if (item instanceof DocumentedMember) {
|
||||
if (this.props.get(item.directData.name)) {
|
||||
throw new Error(`Doc ${this.directData.name} already has prop ${item.directData.name}`);
|
||||
}
|
||||
this.props.set(item.directData.name, item);
|
||||
} else if (item instanceof DocumentedEvent) {
|
||||
if (this.events.get(item.directData.name)) {
|
||||
throw new Error(`Doc ${this.directData.name} already has event ${item.directData.name}`);
|
||||
}
|
||||
this.events.set(item.directData.name, item);
|
||||
}
|
||||
}
|
||||
|
||||
registerMetaInfo(data) {
|
||||
super.registerMetaInfo(data);
|
||||
this.directData = data;
|
||||
this.directData.meta = new DocumentedItemMeta(this, data.meta);
|
||||
}
|
||||
|
||||
serialize() {
|
||||
super.serialize();
|
||||
const { id, name, description, meta, augments, access } = this.directData;
|
||||
const serialized = {
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
meta: meta.serialize(),
|
||||
extends: augments,
|
||||
access,
|
||||
};
|
||||
if (this.classConstructor) {
|
||||
serialized.classConstructor = this.classConstructor.serialize();
|
||||
}
|
||||
serialized.methods = Array.from(this.methods.values()).map(m => m.serialize());
|
||||
serialized.properties = Array.from(this.props.values()).map(p => p.serialize());
|
||||
serialized.events = Array.from(this.events.values()).map(e => e.serialize());
|
||||
return serialized;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = DocumentedClass;
|
||||
@@ -1,46 +0,0 @@
|
||||
const DocumentedItem = require('./DocumentedItem');
|
||||
const DocumentedParam = require('./DocumentedParam');
|
||||
|
||||
/*
|
||||
{ id: 'Client()',
|
||||
longname: 'Client',
|
||||
name: 'Client',
|
||||
kind: 'constructor',
|
||||
description: 'Creates an instance of Client.',
|
||||
memberof: 'Client',
|
||||
params:
|
||||
[ { type: [Object],
|
||||
optional: true,
|
||||
description: 'options to pass to the client',
|
||||
name: 'options' } ],
|
||||
order: 10 }
|
||||
*/
|
||||
|
||||
class DocumentedConstructor extends DocumentedItem {
|
||||
|
||||
registerMetaInfo(data) {
|
||||
super.registerMetaInfo(data);
|
||||
this.directData = data;
|
||||
const newParams = [];
|
||||
for (const param of data.params) {
|
||||
newParams.push(new DocumentedParam(this, param));
|
||||
}
|
||||
this.directData.params = newParams;
|
||||
}
|
||||
|
||||
serialize() {
|
||||
super.serialize();
|
||||
const { id, name, description, memberof, access, params } = this.directData;
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
memberof,
|
||||
access,
|
||||
params: params.map(p => p.serialize()),
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = DocumentedConstructor;
|
||||
@@ -1,80 +0,0 @@
|
||||
const DocumentedItem = require('./DocumentedItem');
|
||||
const DocumentedItemMeta = require('./DocumentedItemMeta');
|
||||
const DocumentedParam = require('./DocumentedParam');
|
||||
|
||||
/*
|
||||
{
|
||||
"id":"Client#event:guildMemberRolesUpdate",
|
||||
"longname":"Client#event:guildMemberRolesUpdate",
|
||||
"name":"guildMemberRolesUpdate",
|
||||
"scope":"instance",
|
||||
"kind":"event",
|
||||
"description":"Emitted whenever a Guild Member's Roles change - i.e. new role or removed role",
|
||||
"memberof":"Client",
|
||||
"params":[
|
||||
{
|
||||
"type":{
|
||||
"names":[
|
||||
"Guild"
|
||||
]
|
||||
},
|
||||
"description":"the guild that the update affects",
|
||||
"name":"guild"
|
||||
},
|
||||
{
|
||||
"type":{
|
||||
"names":[
|
||||
"Array.<Role>"
|
||||
]
|
||||
},
|
||||
"description":"the roles before the update",
|
||||
"name":"oldRoles"
|
||||
},
|
||||
{
|
||||
"type":{
|
||||
"names":[
|
||||
"Guild"
|
||||
]
|
||||
},
|
||||
"description":"the roles after the update",
|
||||
"name":"newRoles"
|
||||
}
|
||||
],
|
||||
"meta":{
|
||||
"lineno":91,
|
||||
"filename":"Guild.js",
|
||||
"path":"src/structures"
|
||||
},
|
||||
"order":110
|
||||
}
|
||||
*/
|
||||
|
||||
class DocumentedEvent extends DocumentedItem {
|
||||
|
||||
registerMetaInfo(data) {
|
||||
this.directData = data;
|
||||
this.directData.meta = new DocumentedItemMeta(this, data.meta);
|
||||
const newParams = [];
|
||||
data.params = data.params || [];
|
||||
for (const param of data.params) {
|
||||
newParams.push(new DocumentedParam(this, param));
|
||||
}
|
||||
this.directData.params = newParams;
|
||||
}
|
||||
|
||||
serialize() {
|
||||
super.serialize();
|
||||
const { id, name, description, memberof, meta, params } = this.directData;
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
memberof,
|
||||
meta: meta.serialize(),
|
||||
params: params.map(p => p.serialize()),
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = DocumentedEvent;
|
||||
@@ -1,91 +0,0 @@
|
||||
const DocumentedItem = require('./DocumentedItem');
|
||||
const DocumentedItemMeta = require('./DocumentedItemMeta');
|
||||
const DocumentedVarType = require('./DocumentedVarType');
|
||||
const DocumentedParam = require('./DocumentedParam');
|
||||
|
||||
/*
|
||||
{
|
||||
"id":"ClientUser#sendTTSMessage",
|
||||
"longname":"ClientUser#sendTTSMessage",
|
||||
"name":"sendTTSMessage",
|
||||
"scope":"instance",
|
||||
"kind":"function",
|
||||
"inherits":"User#sendTTSMessage",
|
||||
"inherited":true,
|
||||
"implements":[
|
||||
"TextBasedChannel#sendTTSMessage"
|
||||
],
|
||||
"description":"Send a text-to-speech message to this channel",
|
||||
"memberof":"ClientUser",
|
||||
"params":[
|
||||
{
|
||||
"type":{
|
||||
"names":[
|
||||
"String"
|
||||
]
|
||||
},
|
||||
"description":"the content to send",
|
||||
"name":"content"
|
||||
}
|
||||
],
|
||||
"examples":[
|
||||
"// send a TTS message..."
|
||||
],
|
||||
"returns":[
|
||||
{
|
||||
"type":{
|
||||
"names":[
|
||||
"Promise.<Message>"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"meta":{
|
||||
"lineno":38,
|
||||
"filename":"TextBasedChannel.js",
|
||||
"path":src/structures/interface"
|
||||
},
|
||||
"order":293
|
||||
}
|
||||
*/
|
||||
|
||||
class DocumentedFunction extends DocumentedItem {
|
||||
|
||||
registerMetaInfo(data) {
|
||||
super.registerMetaInfo(data);
|
||||
this.directData = data;
|
||||
this.directData.meta = new DocumentedItemMeta(this, data.meta);
|
||||
this.directData.returns = new DocumentedVarType(this, data.returns ? data.returns[0].type : {
|
||||
names: ['null'],
|
||||
});
|
||||
const newParams = [];
|
||||
for (const param of data.params) {
|
||||
newParams.push(new DocumentedParam(this, param));
|
||||
}
|
||||
this.directData.params = newParams;
|
||||
}
|
||||
|
||||
serialize() {
|
||||
super.serialize();
|
||||
const {
|
||||
id, name, description, memberof, examples, inherits, inherited, meta, returns, params, access,
|
||||
} = this.directData;
|
||||
const serialized = {
|
||||
id,
|
||||
access,
|
||||
name,
|
||||
description,
|
||||
memberof,
|
||||
examples,
|
||||
inherits,
|
||||
inherited,
|
||||
meta: meta.serialize(),
|
||||
returns: returns.serialize(),
|
||||
params: params.map(p => p.serialize()),
|
||||
};
|
||||
serialized.implements = this.directData.implements;
|
||||
return serialized;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = DocumentedFunction;
|
||||
@@ -1,32 +0,0 @@
|
||||
const DocumentedClass = require('./DocumentedClass');
|
||||
|
||||
/*
|
||||
{ id: 'TextBasedChannel',
|
||||
longname: 'TextBasedChannel',
|
||||
name: 'TextBasedChannel',
|
||||
scope: 'global',
|
||||
kind: 'interface',
|
||||
classdesc: 'Interface for classes that have text-channel-like features',
|
||||
params: [],
|
||||
meta:
|
||||
{ lineno: 5,
|
||||
filename: 'TextBasedChannel.js',
|
||||
path: 'src/structures/interface' },
|
||||
order: 175 }
|
||||
*/
|
||||
|
||||
class DocumentedInterface extends DocumentedClass {
|
||||
registerMetaInfo(data) {
|
||||
super.registerMetaInfo(data);
|
||||
this.directData = data;
|
||||
// this.directData.meta = new DocumentedItemMeta(this, data.meta);
|
||||
}
|
||||
|
||||
serialize() {
|
||||
const serialized = super.serialize();
|
||||
serialized.description = this.directData.classdesc;
|
||||
return serialized;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = DocumentedInterface;
|
||||
@@ -1,17 +0,0 @@
|
||||
class DocumentedItem {
|
||||
constructor(parent, info) {
|
||||
this.parent = parent;
|
||||
this.directData = {};
|
||||
this.registerMetaInfo(info);
|
||||
}
|
||||
|
||||
registerMetaInfo() {
|
||||
return;
|
||||
}
|
||||
|
||||
serialize() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = DocumentedItem;
|
||||
@@ -1,29 +0,0 @@
|
||||
const cwd = (`${process.cwd()}\\`).replace(/\\/g, '/');
|
||||
const backToForward = /\\/g;
|
||||
|
||||
const DocumentedItem = require('./DocumentedItem');
|
||||
|
||||
/*
|
||||
{ lineno: 7,
|
||||
filename: 'VoiceChannel.js',
|
||||
path: 'src/structures' },
|
||||
*/
|
||||
|
||||
class DocumentedItemMeta extends DocumentedItem {
|
||||
|
||||
registerMetaInfo(data) {
|
||||
super.registerMetaInfo(data);
|
||||
this.directData.line = data.lineno;
|
||||
this.directData.file = data.filename;
|
||||
this.directData.path = data.path.replace(backToForward, '/').replace(cwd, '');
|
||||
}
|
||||
|
||||
serialize() {
|
||||
super.serialize();
|
||||
const { line, file, path } = this.directData;
|
||||
return { line, file, path };
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = DocumentedItemMeta;
|
||||
@@ -1,58 +0,0 @@
|
||||
const DocumentedItem = require('./DocumentedItem');
|
||||
const DocumentedItemMeta = require('./DocumentedItemMeta');
|
||||
const DocumentedVarType = require('./DocumentedVarType');
|
||||
const DocumentedParam = require('./DocumentedParam');
|
||||
|
||||
/*
|
||||
{ id: 'Client#rest',
|
||||
longname: 'Client#rest',
|
||||
name: 'rest',
|
||||
scope: 'instance',
|
||||
kind: 'member',
|
||||
description: 'The REST manager of the client',
|
||||
memberof: 'Client',
|
||||
type: { names: [ 'RESTManager' ] },
|
||||
access: 'private',
|
||||
meta:
|
||||
{ lineno: 32,
|
||||
filename: 'Client.js',
|
||||
path: 'src/client' },
|
||||
order: 11 }
|
||||
*/
|
||||
|
||||
class DocumentedMember extends DocumentedItem {
|
||||
|
||||
registerMetaInfo(data) {
|
||||
super.registerMetaInfo(data);
|
||||
this.directData = data;
|
||||
this.directData.meta = new DocumentedItemMeta(this, data.meta);
|
||||
this.directData.type = new DocumentedVarType(this, data.type);
|
||||
if (data.properties) {
|
||||
const newProps = [];
|
||||
for (const param of data.properties) {
|
||||
newProps.push(new DocumentedParam(this, param));
|
||||
}
|
||||
this.directData.properties = newProps;
|
||||
} else {
|
||||
data.properties = [];
|
||||
}
|
||||
}
|
||||
|
||||
serialize() {
|
||||
super.serialize();
|
||||
const { id, name, description, memberof, type, access, meta, properties } = this.directData;
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
memberof,
|
||||
type: type.serialize(),
|
||||
access,
|
||||
meta: meta.serialize(),
|
||||
props: properties.map(p => p.serialize()),
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = DocumentedMember;
|
||||
@@ -1,36 +0,0 @@
|
||||
const DocumentedItem = require('./DocumentedItem');
|
||||
const DocumentedVarType = require('./DocumentedVarType');
|
||||
|
||||
/*
|
||||
{
|
||||
"type":{
|
||||
"names":[
|
||||
"Guild"
|
||||
]
|
||||
},
|
||||
"description":"the roles after the update",
|
||||
"name":"newRoles"
|
||||
}
|
||||
*/
|
||||
|
||||
class DocumentedParam extends DocumentedItem {
|
||||
|
||||
registerMetaInfo(data) {
|
||||
super.registerMetaInfo(data);
|
||||
this.directData = data;
|
||||
this.directData.type = new DocumentedVarType(this, data.type);
|
||||
}
|
||||
|
||||
serialize() {
|
||||
super.serialize();
|
||||
const { name, description, type, optional } = this.directData;
|
||||
return {
|
||||
name,
|
||||
description,
|
||||
optional,
|
||||
type: type.serialize(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = DocumentedParam;
|
||||
@@ -1,56 +0,0 @@
|
||||
const DocumentedItem = require('./DocumentedItem');
|
||||
const DocumentedItemMeta = require('./DocumentedItemMeta');
|
||||
const DocumentedVarType = require('./DocumentedVarType');
|
||||
const DocumentedParam = require('./DocumentedParam');
|
||||
|
||||
/*
|
||||
{ id: 'StringResolvable',
|
||||
longname: 'StringResolvable',
|
||||
name: 'StringResolvable',
|
||||
scope: 'global',
|
||||
kind: 'typedef',
|
||||
description: 'Data that can be resolved to give a String...',
|
||||
type: { names: [ 'String', 'Array', 'Object' ] },
|
||||
meta:
|
||||
{ lineno: 142,
|
||||
filename: 'ClientDataResolver.js',
|
||||
path: 'src/client' },
|
||||
order: 37 }
|
||||
*/
|
||||
|
||||
class DocumentedTypeDef extends DocumentedItem {
|
||||
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
}
|
||||
|
||||
registerMetaInfo(data) {
|
||||
super.registerMetaInfo(data);
|
||||
this.props = new Map();
|
||||
this.directData = data;
|
||||
this.directData.meta = new DocumentedItemMeta(this, data.meta);
|
||||
this.directData.type = new DocumentedVarType(this, data.type);
|
||||
data.properties = data.properties || [];
|
||||
for (const prop of data.properties) {
|
||||
this.props.set(prop.name, new DocumentedParam(this, prop));
|
||||
}
|
||||
}
|
||||
|
||||
serialize() {
|
||||
super.serialize();
|
||||
const { id, name, description, type, access, meta } = this.directData;
|
||||
const serialized = {
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
type: type.serialize(),
|
||||
access,
|
||||
meta: meta.serialize(),
|
||||
};
|
||||
serialized.properties = Array.from(this.props.values()).map(p => p.serialize());
|
||||
return serialized;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = DocumentedTypeDef;
|
||||
@@ -1,50 +0,0 @@
|
||||
const DocumentedItem = require('./DocumentedItem');
|
||||
|
||||
/*
|
||||
{
|
||||
"names":[
|
||||
"String"
|
||||
]
|
||||
}
|
||||
*/
|
||||
|
||||
const regex = /([\w]+)([^\w]+)/;
|
||||
const regexG = /([\w]+)([^\w]+)/g;
|
||||
|
||||
function splitVarName(str) {
|
||||
if (str === '*') {
|
||||
return ['*', ''];
|
||||
}
|
||||
const matches = str.match(regexG);
|
||||
const output = [];
|
||||
if (matches) {
|
||||
for (const match of matches) {
|
||||
const groups = match.match(regex);
|
||||
output.push([groups[1], groups[2]]);
|
||||
}
|
||||
} else {
|
||||
output.push([str.match(/(\w+)/g)[0], '']);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
class DocumentedVarType extends DocumentedItem {
|
||||
|
||||
registerMetaInfo(data) {
|
||||
super.registerMetaInfo(data);
|
||||
this.directData = data;
|
||||
}
|
||||
|
||||
serialize() {
|
||||
super.serialize();
|
||||
const names = [];
|
||||
for (const name of this.directData.names) {
|
||||
names.push(splitVarName(name));
|
||||
}
|
||||
return {
|
||||
types: names,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = DocumentedVarType;
|
||||
24
docs/index.yml
Normal file
24
docs/index.yml
Normal file
@@ -0,0 +1,24 @@
|
||||
- name: General
|
||||
files:
|
||||
- name: Welcome
|
||||
path: welcome.md
|
||||
- name: Updating your code
|
||||
path: updating.md
|
||||
- name: FAQ
|
||||
path: faq.md
|
||||
- name: Topics
|
||||
files:
|
||||
- name: Voice
|
||||
path: voice.md
|
||||
- name: Web builds
|
||||
path: web.md
|
||||
- name: Examples
|
||||
files:
|
||||
- name: Ping
|
||||
path: ping.js
|
||||
- name: Avatars
|
||||
path: avatars.js
|
||||
- name: Server greeting
|
||||
path: greeting.js
|
||||
- name: Webhook
|
||||
path: webhook.js
|
||||
19
docs/logo.svg
Normal file
19
docs/logo.svg
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" height="100%" width="100%" viewBox="0 0 6111.4378 1102.9827">
|
||||
<g transform="translate(2539.6 -107.66)">
|
||||
<g id="logo-discord" fill="#3d3f42" transform="translate(-44.194 1175.6)">
|
||||
<path d="m-2495.4-1051.4v453.6 453.6l145.75-.37695c127.36-.3288 147.71-.58582 161.25-2.041 45.045-4.8398 76.353-11.233 111.79-22.826 44.217-14.465 83.672-35.567 118.71-63.49 13.615-10.851 40.444-37.567 50.889-50.674 37.186-46.665 61.816-98.191 78.01-163.2 23.57-94.614 23.154-219.66-1.0469-313.5-41.72-161.77-155.27-260-329.35-284.92-38.756-5.5479-34.464-5.4161-190.75-5.8086l-145.25-.3652zm161 130.09 41.75.0156c55.334.0205 78.397 1.6295 108.25 7.5566 105.75 20.995 171.57 87.554 196.39 198.59 12.878 57.6 14.716 139.6 4.5469 202.81-7.3952 45.963-21.469 87.286-40.711 119.53-12.041 20.179-33.82 45.681-51 59.719-38.627 31.563-87.98 50.255-148.73 56.326-9.5463.9541-32.361 1.7291-62.75 2.1328l-47.75.63477v-323.66-323.66z"/>
|
||||
<path d="m-1631.4-597.85v-453.5h80.5 80.5v453.5 453.5h-80.5-80.5v-453.5z"/>
|
||||
<path d="m-1008.4-128.41c-96.325-5.9603-189.36-41.918-264.54-102.25-15.565-12.49-33-28.526-33-30.352 0-.7224 20.622-25.63 45.826-55.351l45.826-54.038 3.8214 3.2697c17.83 15.256 22.538 19.151 29.616 24.501 48.673 36.79 103.35 61.169 158.92 70.862 18.387 3.2073 54.666 4.419 74.088 2.4745 41.751-4.1802 74.798-17.199 96.864-38.16 10.213-9.7012 15.896-17.429 21.626-29.408 17.4-36.376 13.152-81.77-10.39-111-16.357-20.31-45.054-37.907-98.696-60.521-41.654-17.56-164.15-71.537-176.19-77.638-85.541-43.335-134.63-104.27-148.9-184.84-2.6851-15.162-3.7276-49.931-1.9989-66.666 7.4631-72.25 48.261-136.63 113.09-178.46 41.81-26.976 88.546-43.103 144.99-50.03 20.52-2.5182 67.722-2.5268 88-.016 74.352 9.2063 141.74 36.296 199 79.999 18.772 14.327 37.632 31.435 36.864 33.44-.2001.52235-18.812 23.693-41.361 51.49l-40.997 50.54-3.503-2.9264c-1.9267-1.6095-9.4625-7.4505-16.746-12.98-44.158-33.522-88.429-52.307-140.26-59.513-17.665-2.4562-54.274-2.4782-70-.042-35.82 5.5488-61.303 16.869-80.113 35.588-17.506 17.422-26.238 37.587-27.528 63.576-1.3118 26.419 6.521 48.306 24.066 67.249 17.834 19.254 45.314 35.115 99.448 57.398 32.211 13.259 137.3 57.517 151.65 63.864 47.003 20.795 80.577 42.726 108.49 70.87 43.959 44.316 64.938 98.562 65.021 168.13.053 44.646-7.8058 78.816-26.734 116.23-12.46 24.632-27.741 45.114-49.45 66.28-51.458 50.172-122.59 79.937-208.86 87.392-17.502 1.5126-51.786 2.0335-67.962 1.0326z"/>
|
||||
<path d="m-155.84-128.44c-100.7-5.7557-190.26-44.562-257.1-111.4-58.171-58.171-98.098-136.72-116.41-229.01-13.522-68.153-15.549-148.4-5.5195-218.5 13.11-91.624 47.506-173.73 99.29-237 11.342-13.858 35.64-38.591 49.282-50.164 54.726-46.425 120.9-76.546 193.88-88.256 25.873-4.1511 37.999-5.0552 67.977-5.0681 28.858-.013 38.31.6981 60.5 4.5485 70.566 12.245 140.29 49.396 192.89 102.78l6.8911 6.9936-2.8911 3.4607c-1.59 1.9034-21.52 24.408-44.288 50.011l-41.397 46.551-10.103-9.0797c-40.998-36.846-79.308-56.146-125.89-63.421-13.826-2.1591-48.594-2.4422-62.711-.51067-51.945 7.1074-94.856 27.696-131.17 62.933-64.806 62.887-97.854 165.12-92.829 287.16 2.697 65.505 14.091 119.1 35.16 165.38 30.027 65.96 77.365 110.94 138.03 131.16 24.572 8.1885 46.583 11.525 76.026 11.525 45.839 0 83.431-9.665 120.81-31.062 19.559-11.195 45.837-32.314 63.267-50.848 3.7379-3.9745 7.1554-7.0833 7.5942-6.9085 1.3142.5236 88.109 97.158 88.109 98.098 0 2.0843-41.684 42.322-54 52.126-73.043 58.146-157.48 84.1-255.41 78.503z"/>
|
||||
<path d="m610.07-1067.8c-34.898-.056-47.464.862-75.232 5.4922-188.34 31.405-308.9 182.45-325.21 407.46-2.8044 38.675-2.2536 84.125 1.4941 123.38 9.2582 96.975 39.751 184.31 87.494 250.58 57.015 79.142 139.29 130.29 236.46 147 14.533 2.4988 40.496 5.3373 53.5 5.8496 147.12 5.7956 267.7-55.193 342.98-173.48 10.897-17.122 28.991-52.974 36.758-72.828 27.4-70.046 39.498-139.21 39.617-226.5.062-45.479-1.9339-73.343-7.9121-110.4-31.164-193.18-145.75-321-314.25-350.53-27.838-4.8789-41.445-5.9606-75.699-6.0156zm-1.4395 139.59c2.8062.0114 5.6199.0752 8.4395.19336 49.33 2.0671 91.449 18.361 127.46 49.305 12.954 11.133 20.363 19.102 31.482 33.861 40.99 54.409 62.709 125.93 66.582 219.25 4.5628 109.93-19.826 208.09-67.676 272.39-33.936 45.599-76.643 72.514-130.84 82.459-10.577 1.9408-50.92 2.8029-62 1.3242-74.694-9.9681-131.62-54.014-168.58-130.43-24.356-50.365-36.989-106.85-39.92-178.5-5.9652-145.81 37.791-262.31 118.61-315.79 33.933-22.452 74.357-34.245 116.45-34.074z"/>
|
||||
<path d="m1187.6-1051.4v453.54 453.54h80.5 80.5v-177.51-177.51l68.717.25585 68.719.25782 97.531 177.22 97.533 177.22 90.285.0273c85.686.0268 90.237-.0599 89.336-1.7207-.5222-.9625-49.147-86.08-108.05-189.15-58.906-103.07-106.98-187.52-106.83-187.67.1497-.14971 5.5455-2.31 11.99-4.8008 92.947-35.923 149.28-103.8 164.7-198.43 3.4973-21.47 4.3763-36.845 3.7539-65.688-.8444-39.124-4.5518-62.293-14.883-93.008-29.696-88.286-106.44-143.03-224.91-160.44-38.597-5.6719-28.81-5.4157-221.14-5.7871l-177.75-.3438zm161 128.95 84.25.37695c91.298.40795 95.375.61732 123.75 6.3809 23.495 4.7723 45.38 13.215 61 23.533 15.167 10.019 29.716 27.182 37.475 44.207 14.573 31.978 16.395 82.735 4.3301 120.62-6.6274 20.814-16.172 36.615-31.18 51.625-27.567 27.57-66.814 42.804-121.93 47.324-7.3903.60617-43.437 1.0508-85.25 1.0508h-72.445v-147.56-147.56z"/>
|
||||
<path d="m2014.6-1051.4v453.6 453.6l145.75-.37695c156.69-.4046 153.13-.29648 191.25-5.8008 38.321-5.5332 77.017-15.82 109.08-28.998 17.362-7.137 22.208-9.743 21.508-11.566-.3206-.8355-1.452-4.9721-2.5156-9.1914-3.4865-13.831-4.3718-23.482-3.7617-41.053.63-18.145 2.2913-27.3 7.7285-42.617 17.594-49.562 60.836-85.599 112.95-94.131 16.457-2.6941 38.955-1.8474 57.701 2.1719 3.6928.79178 3.1565 1.7476 11.26-20.041 27.066-72.775 38.169-169.68 30.476-265.97-14.239-178.25-95.276-299.81-236.97-355.47-33.122-13.01-69.539-22.404-108.45-27.975-38.756-5.5479-34.464-5.4161-190.75-5.8086l-145.25-.3652zm161 130.09 41.75.0156c55.334.0205 78.397 1.6295 108.25 7.5566 105.75 20.995 171.57 87.554 196.39 198.59 12.878 57.6 14.716 139.6 4.5469 202.81-7.3952 45.963-21.469 87.286-40.711 119.53-12.041 20.179-33.82 45.681-51 59.719-38.627 31.563-87.98 50.255-148.73 56.326-9.5463.9541-32.361 1.7291-62.75 2.1328l-47.75.63477v-323.66-323.66z"/>
|
||||
</g>
|
||||
<circle id="logo-dot" cx="2575.3" cy="939.96" r="125.4" fill="#499a6c"/>
|
||||
<g id="logo-js" fill="#33b5e5" transform="translate(-44.194 1175.6)">
|
||||
<path d="m2602.1 34.57c-57.094-4.6075-113.49-28.558-158.26-67.213-27.741-23.949-51.228-55.235-63.883-85.094-5.4804-12.93-5.926-15.992-2.3882-16.406 8.1404-.953 38.073-7.05 53.318-10.86 20.337-5.0831 29.827-8.2686 48.112-16.15 12.138-5.2318 12.996-5.46 14-3.7198 14.778 25.613 36.757 46.236 62.906 59.024 21.609 10.567 39.696 14.761 63.664 14.761 23.073 0 41.694-4.1466 61.73-13.746 36.584-17.528 62.542-46.884 75.844-85.772 2.3995-7.0151 7.5664-31.714 9.361-44.747 2.8753-20.881 3.0454-40.134 3.0555-345.75l.01-314.25h78 78v318.25c0 209.58-.3574 323.03-1.0389 332.25-4.4405 60.076-22.061 115.17-51.016 159.5-11.306 17.311-21.135 29.375-35.857 44.012-44.122 43.866-101.51 69.204-169.58 74.876-17.815 1.4842-53.463 2.0433-65.964 1.0344z"/>
|
||||
<path d="m3256.6 33.535c-103.92-8.2588-202.14-50.771-278.59-120.57l-11.459-10.464 4.7737-5.6963c2.6255-3.133 23.371-27.615 46.101-54.405l41.327-48.709 11.068 9.6086c54.856 47.624 120.13 79.074 185.78 89.508 19.275 3.0634 60.816 3.3389 79 .5237 56.007-8.6707 91.978-30.946 109.48-67.793 5.7814-12.174 8.6772-25.17 9.2639-41.574 1.8511-51.755-20.009-81.836-81.241-111.79-10.45-5.1123-25.75-12.128-34-15.591-32.568-13.67-168.23-73.282-178.56-78.459-84.895-42.577-136.19-105.76-149.34-183.97-24.654-146.62 80.068-271.29 246.91-293.93 39.105-5.3065 82.999-4.2183 122.48 3.0365 76.174 13.996 145.21 48.561 201.87 101.07l7.367 6.8275-39.699 49c-21.834 26.95-40.537 49.863-41.563 50.918-1.8327 1.8856-1.9536 1.8424-7.1685-2.562-25.013-21.126-59.394-41.952-87.804-53.188-33.742-13.345-63.677-18.968-101.5-19.066-28.062-.0727-45.321 2.2-65.5 8.6248-40.117 12.773-65.445 37.309-74.612 72.282-3.4331 13.097-3.8978 33.664-1.0368 45.883 7.6067 32.488 29.949 55.7 75.674 78.622 15.123 7.5809 24.021 11.522 52.974 23.46 125.45 51.728 173.58 73.274 198.67 88.935 70.314 43.888 106.41 97.76 116.97 174.59 2.1563 15.683 2.4444 55.002.5056 69-7.9359 57.297-31.186 104.9-70.626 144.6-53.439 53.792-126.37 84.242-218.91 91.402-14.98 1.1588-53.385 1.0944-68.605-.1152z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 8.2 KiB |
109
docs/topics/voice.md
Normal file
109
docs/topics/voice.md
Normal file
@@ -0,0 +1,109 @@
|
||||
# Introduction to Voice
|
||||
Voice in discord.js can be used for many things, such as music bots, recording or relaying audio.
|
||||
|
||||
In discord.js, you can use voice by connecting to a `VoiceChannel` to obtain a `VoiceConnection`, where you can start streaming and receiving audio.
|
||||
|
||||
To get started, make sure you have:
|
||||
* ffmpeg - `npm install ffmpeg-binaries`
|
||||
* an opus encoder, choose one from below:
|
||||
* `npm install opusscript`
|
||||
* `npm install node-opus`
|
||||
* a good network connection
|
||||
|
||||
## Joining a voice channel
|
||||
The example below reacts to a message and joins the sender's voice channel, catching any errors. This is important
|
||||
as it allows us to obtain a `VoiceConnection` that we can start to stream audio with.
|
||||
|
||||
```js
|
||||
const Discord = require('discord.js');
|
||||
const client = new Discord.Client();
|
||||
|
||||
client.login('token here');
|
||||
|
||||
client.on('message', message => {
|
||||
// Voice only works in guilds, if the message does not come from a guild,
|
||||
// we ignore it
|
||||
if (!message.guild) return;
|
||||
|
||||
if (message.content === '/join') {
|
||||
// Only try to join the sender's voice channel if they are in one themselves
|
||||
if (message.member.voiceChannel) {
|
||||
message.member.voiceChannel.join()
|
||||
.then(connection => { // Connection is an instance of VoiceConnection
|
||||
message.reply('I have successfully connected to the channel!');
|
||||
})
|
||||
.catch(console.log);
|
||||
} else {
|
||||
message.reply('You need to join a voice channel first!');
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Streaming to a Voice Channel
|
||||
In the previous example, we looked at how to join a voice channel in order to obtain a `VoiceConnection`. Now that we
|
||||
have obtained a voice connection, we can start streaming audio to it. The following example shows how to stream an mp3
|
||||
file:
|
||||
|
||||
**Playing a file:**
|
||||
```js
|
||||
// To play a file, we need to give an absolute path to it
|
||||
const dispatcher = connection.playFile('C:/Users/Discord/Desktop/myfile.mp3');
|
||||
```
|
||||
|
||||
Your file doesn't have to be just an mp3; ffmpeg can convert videos and audios of many formats.
|
||||
|
||||
The `dispatcher` variable is an instance of a `StreamDispatcher`, which manages streaming a specific resource to a voice
|
||||
channel. We can do many things with the dispatcher, such as finding out when the stream ends or changing the volume:
|
||||
|
||||
```js
|
||||
dispatcher.on('end', () => {
|
||||
// The song has finished
|
||||
});
|
||||
|
||||
dispatcher.on('error', e => {
|
||||
// Catch any errors that may arise
|
||||
console.log(e);
|
||||
});
|
||||
|
||||
dispatcher.setVolume(0.5); // Set the volume to 50%
|
||||
dispatcher.setVolume(1); // Set the volume back to 100%
|
||||
|
||||
console.log(dispatcher.time); // The time in milliseconds that the stream dispatcher has been playing for
|
||||
|
||||
dispatcher.pause(); // Pause the stream
|
||||
dispatcher.resume(); // Carry on playing
|
||||
|
||||
dispatcher.end(); // End the dispatcher, emits 'end' event
|
||||
```
|
||||
|
||||
If you have an existing [ReadableStream](https://nodejs.org/api/stream.html#stream_readable_streams),
|
||||
this can also be used:
|
||||
|
||||
**Playing a ReadableStream:**
|
||||
```js
|
||||
connection.playStream(myReadableStream);
|
||||
|
||||
// If you don't want to use absolute paths, you can use
|
||||
// fs.createReadStream to circumvent it
|
||||
|
||||
const fs = require('fs');
|
||||
const stream = fs.createReadStream('./test.mp3');
|
||||
connection.playStream(stream);
|
||||
```
|
||||
|
||||
It's important to note that creating a readable stream to a file is less efficient than simply using `connection.playFile()`.
|
||||
|
||||
**Playing anything else:**
|
||||
|
||||
For anything else, such as a URL to a file, you can use `connection.playArbitraryInput()`. You should consult the [ffmpeg protocol documentation](https://ffmpeg.org/ffmpeg-protocols.html) to see what you can use this for.
|
||||
|
||||
```js
|
||||
// Play an mp3 from a URL
|
||||
connection.playArbitraryInput('http://mysite.com/sound.mp3');
|
||||
```
|
||||
|
||||
Again, playing a file from a URL like this is more performant than creating a ReadableStream to the file.
|
||||
|
||||
## Advanced Topics
|
||||
soon:tm:
|
||||
38
docs/topics/web.md
Normal file
38
docs/topics/web.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# Web builds
|
||||
In addition to your usual Node applications, discord.js has special distributions available that are capable of running in web browsers.
|
||||
This is useful for client-side web apps that need to interact with the Discord API.
|
||||
[Webpack 2](https://webpack.js.org/) is used to build these.
|
||||
|
||||
## Usage
|
||||
You can obtain your desired version of discord.js' web build from the [webpack branch](https://github.com/hydrabolt/discord.js/tree/webpack) of the GitHub repository.
|
||||
There is a file for each branch and version of the library, and the ones ending in `.min.js` are minified to substantially reduce the size of the source code.
|
||||
|
||||
Include the file on the page just as you would any other JS library, like so:
|
||||
```html
|
||||
<script type="text/javascript" src="discord.VERSION.min.js"></script>
|
||||
```
|
||||
|
||||
Rather than importing discord.js with `require('discord.js')`, the entire `Discord` object is available as a global (on the `window`) object.
|
||||
The usage of the API isn't any different from using it in Node.js.
|
||||
|
||||
## Restrictions
|
||||
- Any voice-related functionality is unavailable, as there is currently no audio encoding/decoding capabilities without external native libraries,
|
||||
which web browsers do not support.
|
||||
- The ShardingManager cannot be used, since it relies on being able to spawn child processes for shards.
|
||||
- None of the optional packages are usable, since they're native libraries.
|
||||
|
||||
## Example
|
||||
```html
|
||||
<script type="text/javascript" src="discord.11.3.0.min.js"></script>
|
||||
<script type="text/javascript">
|
||||
const client = new Discord.Client();
|
||||
|
||||
client.on('message', msg => {
|
||||
const guildTag = msg.channel.type === 'text' ? `[${msg.guild.name}]` : '[DM]';
|
||||
const channelTag = msg.channel.type === 'text' ? `[#${msg.channel.name}]` : '';
|
||||
console.log(`${guildTag}${channelTag} ${msg.author.tag}: ${msg.content}`);
|
||||
});
|
||||
|
||||
client.login('some crazy token');
|
||||
</script>
|
||||
```
|
||||
70
package.json
70
package.json
@@ -1,11 +1,16 @@
|
||||
{
|
||||
"name": "discord.js",
|
||||
"version": "10.0.1",
|
||||
"version": "11.3.0",
|
||||
"description": "A powerful library for interacting with the Discord API",
|
||||
"main": "./src/index",
|
||||
"types": "./typings/index.d.ts",
|
||||
"scripts": {
|
||||
"test": "eslint src/ && node docs/generator/generator.js silent",
|
||||
"docs": "node docs/generator/generator.js"
|
||||
"test": "npm run lint && npm run docs:test",
|
||||
"docs": "docgen --source src --custom docs/index.yml --output docs/docs.json",
|
||||
"docs:test": "docgen --source src --custom docs/index.yml",
|
||||
"lint": "eslint src",
|
||||
"lint:fix": "eslint --fix src",
|
||||
"webpack": "parallel-webpack"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -25,21 +30,64 @@
|
||||
"url": "https://github.com/hydrabolt/discord.js/issues"
|
||||
},
|
||||
"homepage": "https://github.com/hydrabolt/discord.js#readme",
|
||||
"runkitExampleFilename": "./docs/examples/ping.js",
|
||||
"dependencies": {
|
||||
"superagent": "^2.3.0",
|
||||
"tweetnacl": "^0.14.3",
|
||||
"ws": "^1.1.1"
|
||||
"long": "^3.2.0",
|
||||
"prism-media": "^0.0.1",
|
||||
"snekfetch": "^3.6.1",
|
||||
"tweetnacl": "^1.0.0",
|
||||
"ws": "^4.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"node-opus": "^0.2.1",
|
||||
"opusscript": "^0.0.1"
|
||||
"bufferutil": "^3.0.3",
|
||||
"erlpack": "discordapp/erlpack",
|
||||
"node-opus": "^0.2.7",
|
||||
"opusscript": "^0.0.6",
|
||||
"sodium": "^2.0.3",
|
||||
"libsodium-wrappers": "^0.5.4",
|
||||
"uws": "^9.14.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^3.8.0",
|
||||
"fs-extra": "^0.30.0",
|
||||
"jsdoc-to-markdown": "^2.0.0"
|
||||
"@types/node": "^8.5.7",
|
||||
"discord.js-docgen": "hydrabolt/discord.js-docgen",
|
||||
"eslint": "^4.15.0",
|
||||
"parallel-webpack": "^2.2.0",
|
||||
"uglifyjs-webpack-plugin": "^1.1.6",
|
||||
"webpack": "^3.10.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
},
|
||||
"browser": {
|
||||
"ws": false,
|
||||
"uws": false,
|
||||
"erlpack": false,
|
||||
"prism-media": false,
|
||||
"opusscript": false,
|
||||
"node-opus": false,
|
||||
"tweetnacl": false,
|
||||
"sodium": false,
|
||||
"src/sharding/Shard.js": false,
|
||||
"src/sharding/ShardClientUtil.js": false,
|
||||
"src/sharding/ShardingManager.js": false,
|
||||
"src/client/voice/dispatcher/StreamDispatcher.js": false,
|
||||
"src/client/voice/opus/BaseOpusEngine.js": false,
|
||||
"src/client/voice/opus/NodeOpusEngine.js": false,
|
||||
"src/client/voice/opus/OpusEngineList.js": false,
|
||||
"src/client/voice/opus/OpusScriptEngine.js": false,
|
||||
"src/client/voice/pcm/ConverterEngine.js": false,
|
||||
"src/client/voice/pcm/ConverterEngineList.js": false,
|
||||
"src/client/voice/pcm/FfmpegConverterEngine.js": false,
|
||||
"src/client/voice/player/AudioPlayer.js": false,
|
||||
"src/client/voice/receiver/VoiceReadable.js": false,
|
||||
"src/client/voice/receiver/VoiceReceiver.js": false,
|
||||
"src/client/voice/util/Secretbox.js": false,
|
||||
"src/client/voice/util/SecretKey.js": false,
|
||||
"src/client/voice/util/VolumeInterface.js": false,
|
||||
"src/client/voice/ClientVoiceManager.js": false,
|
||||
"src/client/voice/VoiceBroadcast.js": false,
|
||||
"src/client/voice/VoiceConnection.js": false,
|
||||
"src/client/voice/VoiceUDPClient.js": false,
|
||||
"src/client/voice/VoiceWebSocket.js": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const EventEmitter = require('events').EventEmitter;
|
||||
const mergeDefault = require('../util/MergeDefault');
|
||||
const EventEmitter = require('events');
|
||||
const Constants = require('../util/Constants');
|
||||
const Permissions = require('../util/Permissions');
|
||||
const Util = require('../util/Util');
|
||||
const RESTManager = require('./rest/RESTManager');
|
||||
const ClientDataManager = require('./ClientDataManager');
|
||||
const ClientManager = require('./ClientManager');
|
||||
@@ -11,9 +12,10 @@ const ActionsManager = require('./actions/ActionsManager');
|
||||
const Collection = require('../util/Collection');
|
||||
const Presence = require('../structures/Presence').Presence;
|
||||
const ShardClientUtil = require('../sharding/ShardClientUtil');
|
||||
const VoiceBroadcast = require('./voice/VoiceBroadcast');
|
||||
|
||||
/**
|
||||
* The starting point for making a Discord Bot.
|
||||
* The main hub for interacting with the Discord API, and the starting point for any bot.
|
||||
* @extends {EventEmitter}
|
||||
*/
|
||||
class Client extends EventEmitter {
|
||||
@@ -31,7 +33,7 @@ class Client extends EventEmitter {
|
||||
* The options the client was instantiated with
|
||||
* @type {ClientOptions}
|
||||
*/
|
||||
this.options = mergeDefault(Constants.DefaultOptions, options);
|
||||
this.options = Util.mergeDefault(Constants.DefaultOptions, options);
|
||||
this._validateOptions();
|
||||
|
||||
/**
|
||||
@@ -42,81 +44,86 @@ class Client extends EventEmitter {
|
||||
this.rest = new RESTManager(this);
|
||||
|
||||
/**
|
||||
* The data manager of the Client
|
||||
* The data manager of the client
|
||||
* @type {ClientDataManager}
|
||||
* @private
|
||||
*/
|
||||
this.dataManager = new ClientDataManager(this);
|
||||
|
||||
/**
|
||||
* The manager of the Client
|
||||
* The manager of the client
|
||||
* @type {ClientManager}
|
||||
* @private
|
||||
*/
|
||||
this.manager = new ClientManager(this);
|
||||
|
||||
/**
|
||||
* The WebSocket Manager of the Client
|
||||
* The WebSocket manager of the client
|
||||
* @type {WebSocketManager}
|
||||
* @private
|
||||
*/
|
||||
this.ws = new WebSocketManager(this);
|
||||
|
||||
/**
|
||||
* The Data Resolver of the Client
|
||||
* The data resolver of the client
|
||||
* @type {ClientDataResolver}
|
||||
* @private
|
||||
*/
|
||||
this.resolver = new ClientDataResolver(this);
|
||||
|
||||
/**
|
||||
* The Action Manager of the Client
|
||||
* The action manager of the client
|
||||
* @type {ActionsManager}
|
||||
* @private
|
||||
*/
|
||||
this.actions = new ActionsManager(this);
|
||||
|
||||
/**
|
||||
* The Voice Manager of the Client
|
||||
* @type {ClientVoiceManager}
|
||||
* The voice manager of the client (`null` in browsers)
|
||||
* @type {?ClientVoiceManager}
|
||||
* @private
|
||||
*/
|
||||
this.voice = new ClientVoiceManager(this);
|
||||
this.voice = !this.browser ? new ClientVoiceManager(this) : null;
|
||||
|
||||
/**
|
||||
* The shard helpers for the client (only if the process was spawned as a child, such as from a ShardingManager)
|
||||
* @type {?ShardUtil}
|
||||
* The shard helpers for the client
|
||||
* (only if the process was spawned as a child, such as from a {@link ShardingManager})
|
||||
* @type {?ShardClientUtil}
|
||||
*/
|
||||
this.shard = process.send ? ShardClientUtil.singleton(this) : null;
|
||||
|
||||
/**
|
||||
* A Collection of the Client's stored users
|
||||
* @type {Collection<string, User>}
|
||||
* All of the {@link User} objects that have been cached at any point, mapped by their IDs
|
||||
* @type {Collection<Snowflake, User>}
|
||||
*/
|
||||
this.users = new Collection();
|
||||
|
||||
/**
|
||||
* A Collection of the Client's stored guilds
|
||||
* @type {Collection<string, Guild>}
|
||||
* All of the guilds the client is currently handling, mapped by their IDs -
|
||||
* as long as sharding isn't being used, this will be *every* guild the bot is a member of
|
||||
* @type {Collection<Snowflake, Guild>}
|
||||
*/
|
||||
this.guilds = new Collection();
|
||||
|
||||
/**
|
||||
* A Collection of the Client's stored channels
|
||||
* @type {Collection<string, Channel>}
|
||||
* All of the {@link Channel}s that the client is currently handling, mapped by their IDs -
|
||||
* as long as sharding isn't being used, this will be *every* channel in *every* guild, and all DM channels
|
||||
* @type {Collection<Snowflake, Channel>}
|
||||
*/
|
||||
this.channels = new Collection();
|
||||
|
||||
/**
|
||||
* A Collection of presences for friends of the logged in user.
|
||||
* <warn>This is only present for user accounts, not bot accounts!</warn>
|
||||
* @type {Collection<string, Presence>}
|
||||
* Presences that have been received for the client user's friends, mapped by user IDs
|
||||
* <warn>This is only filled when using a user account.</warn>
|
||||
* @type {Collection<Snowflake, Presence>}
|
||||
*/
|
||||
this.presences = new Collection();
|
||||
|
||||
Object.defineProperty(this, 'token', { writable: true });
|
||||
if (!this.token && 'CLIENT_TOKEN' in process.env) {
|
||||
/**
|
||||
* The authorization token for the logged in user/bot.
|
||||
* Authorization token for the logged in user/bot
|
||||
* <warn>This should be kept private at all times.</warn>
|
||||
* @type {?string}
|
||||
*/
|
||||
this.token = process.env.CLIENT_TOKEN;
|
||||
@@ -125,30 +132,42 @@ class Client extends EventEmitter {
|
||||
}
|
||||
|
||||
/**
|
||||
* The email, if there is one, for the logged in Client
|
||||
* @type {?string}
|
||||
*/
|
||||
this.email = null;
|
||||
|
||||
/**
|
||||
* The password, if there is one, for the logged in Client
|
||||
* @type {?string}
|
||||
*/
|
||||
this.password = null;
|
||||
|
||||
/**
|
||||
* The ClientUser representing the logged in Client
|
||||
* User that the client is logged in as
|
||||
* @type {?ClientUser}
|
||||
*/
|
||||
this.user = null;
|
||||
|
||||
/**
|
||||
* The date at which the Client was regarded as being in the `READY` state.
|
||||
* Time at which the client was last regarded as being in the `READY` state
|
||||
* (each time the client disconnects and successfully reconnects, this will be overwritten)
|
||||
* @type {?Date}
|
||||
*/
|
||||
this.readyAt = null;
|
||||
|
||||
/**
|
||||
* Active voice broadcasts that have been created
|
||||
* @type {VoiceBroadcast[]}
|
||||
*/
|
||||
this.broadcasts = [];
|
||||
|
||||
/**
|
||||
* Previous heartbeat pings of the websocket (most recent first, limited to three elements)
|
||||
* @type {number[]}
|
||||
*/
|
||||
this.pings = [];
|
||||
|
||||
/**
|
||||
* Timeouts set by {@link Client#setTimeout} that are still active
|
||||
* @type {Set<Timeout>}
|
||||
* @private
|
||||
*/
|
||||
this._timeouts = new Set();
|
||||
|
||||
/**
|
||||
* Intervals set by {@link Client#setInterval} that are still active
|
||||
* @type {Set<Timeout>}
|
||||
* @private
|
||||
*/
|
||||
this._intervals = new Set();
|
||||
|
||||
if (this.options.messageSweepInterval > 0) {
|
||||
@@ -157,16 +176,25 @@ class Client extends EventEmitter {
|
||||
}
|
||||
|
||||
/**
|
||||
* The status for the logged in Client.
|
||||
* Timestamp of the latest ping's start time
|
||||
* @type {number}
|
||||
* @private
|
||||
*/
|
||||
get _pingTimestamp() {
|
||||
return this.ws.connection ? this.ws.connection.lastPingTimestamp : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Current status of the client's connection to Discord
|
||||
* @type {?number}
|
||||
* @readonly
|
||||
*/
|
||||
get status() {
|
||||
return this.ws.status;
|
||||
return this.ws.connection.status;
|
||||
}
|
||||
|
||||
/**
|
||||
* The uptime for the logged in Client.
|
||||
* How long it has been since the client last entered the `READY` state
|
||||
* @type {?number}
|
||||
* @readonly
|
||||
*/
|
||||
@@ -175,17 +203,27 @@ class Client extends EventEmitter {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Collection, mapping Guild ID to Voice Connections.
|
||||
* @type {Collection<string, VoiceConnection>}
|
||||
* Average heartbeat ping of the websocket, obtained by averaging the {@link Client#pings} property
|
||||
* @type {number}
|
||||
* @readonly
|
||||
*/
|
||||
get ping() {
|
||||
return this.pings.reduce((prev, p) => prev + p, 0) / this.pings.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* All active voice connections that have been established, mapped by channel ID
|
||||
* @type {Collection<Snowflake, VoiceConnection>}
|
||||
* @readonly
|
||||
*/
|
||||
get voiceConnections() {
|
||||
if (this.browser) return new Collection();
|
||||
return this.voice.connections;
|
||||
}
|
||||
|
||||
/**
|
||||
* The emojis that the client can use. Mapped by emoji ID.
|
||||
* @type {Collection<string, Emoji>}
|
||||
* All custom emojis that the client has access to, mapped by their IDs
|
||||
* @type {Collection<Snowflake, Emoji>}
|
||||
* @readonly
|
||||
*/
|
||||
get emojis() {
|
||||
@@ -197,7 +235,7 @@ class Client extends EventEmitter {
|
||||
}
|
||||
|
||||
/**
|
||||
* The timestamp that the client was last ready at
|
||||
* Timestamp of the time the client was last `READY` at
|
||||
* @type {?number}
|
||||
* @readonly
|
||||
*/
|
||||
@@ -206,31 +244,41 @@ class Client extends EventEmitter {
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs the client in. If successful, resolves with the account's token. <warn>If you're making a bot, it's
|
||||
* much better to use a bot account rather than a user account.
|
||||
* Bot accounts have higher rate limits and have access to some features user accounts don't have. User bots
|
||||
* that are making a lot of API requests can even be banned.</warn>
|
||||
* @param {string} tokenOrEmail The token or email used for the account. If it is an email, a password _must_ be
|
||||
* provided.
|
||||
* @param {string} [password] The password for the account, only needed if an email was provided.
|
||||
* @returns {Promise<string>}
|
||||
* @example
|
||||
* // log the client in using a token
|
||||
* const token = 'my token';
|
||||
* client.login(token);
|
||||
* @example
|
||||
* // log the client in using email and password
|
||||
* const email = 'user@email.com';
|
||||
* const password = 'supersecret123';
|
||||
* client.login(email, password);
|
||||
* Whether the client is in a browser environment
|
||||
* @type {boolean}
|
||||
* @readonly
|
||||
*/
|
||||
login(tokenOrEmail, password = null) {
|
||||
if (password) return this.rest.methods.loginEmailPassword(tokenOrEmail, password);
|
||||
return this.rest.methods.loginToken(tokenOrEmail);
|
||||
get browser() {
|
||||
return typeof window !== 'undefined';
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroys the client and logs out.
|
||||
* Creates a voice broadcast.
|
||||
* @returns {VoiceBroadcast}
|
||||
*/
|
||||
createVoiceBroadcast() {
|
||||
const broadcast = new VoiceBroadcast(this);
|
||||
this.broadcasts.push(broadcast);
|
||||
return broadcast;
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs the client in, establishing a websocket connection to Discord.
|
||||
* <info>Both bot and regular user accounts are supported, but it is highly recommended to use a bot account whenever
|
||||
* possible. User accounts are subject to harsher ratelimits and other restrictions that don't apply to bot accounts.
|
||||
* Bot accounts also have access to many features that user accounts cannot utilise. User accounts that are found to
|
||||
* be abusing/overusing the API will be banned, locking you out of Discord entirely.</info>
|
||||
* @param {string} token Token of the account to log in with
|
||||
* @returns {Promise<string>} Token of the account used
|
||||
* @example
|
||||
* client.login('my token');
|
||||
*/
|
||||
login(token) {
|
||||
return this.rest.methods.login(token);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs out, terminates the connection to Discord, and destroys the client.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
destroy() {
|
||||
@@ -238,40 +286,38 @@ class Client extends EventEmitter {
|
||||
for (const i of this._intervals) clearInterval(i);
|
||||
this._timeouts.clear();
|
||||
this._intervals.clear();
|
||||
this.token = null;
|
||||
this.email = null;
|
||||
this.password = null;
|
||||
return this.manager.destroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* This shouldn't really be necessary to most developers as it is automatically invoked every 30 seconds, however
|
||||
* if you wish to force a sync of Guild data, you can use this. Only applicable to user accounts.
|
||||
* @param {Guild[]|Collection<string, Guild>} [guilds=this.guilds] An array or collection of guilds to sync
|
||||
* Requests a sync of guild data with Discord.
|
||||
* <info>This can be done automatically every 30 seconds by enabling {@link ClientOptions#sync}.</info>
|
||||
* <warn>This is only available when using a user account.</warn>
|
||||
* @param {Guild[]|Collection<Snowflake, Guild>} [guilds=this.guilds] An array or collection of guilds to sync
|
||||
*/
|
||||
syncGuilds(guilds = this.guilds) {
|
||||
if (!this.user.bot) {
|
||||
this.ws.send({
|
||||
op: 12,
|
||||
d: guilds instanceof Collection ? guilds.keyArray() : guilds.map(g => g.id),
|
||||
});
|
||||
}
|
||||
if (this.user.bot) return;
|
||||
this.ws.send({
|
||||
op: 12,
|
||||
d: guilds instanceof Collection ? guilds.keyArray() : guilds.map(g => g.id),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Caches a user, or obtains it from the cache if it's already cached.
|
||||
* If the user isn't already cached, it will only be obtainable by OAuth bot accounts.
|
||||
* @param {string} id The ID of the user to obtain
|
||||
* Obtains a user from Discord, or the user cache if it's already available.
|
||||
* <warn>This is only available when using a bot account.</warn>
|
||||
* @param {Snowflake} id ID of the user
|
||||
* @param {boolean} [cache=true] Whether to cache the new user object if it isn't already
|
||||
* @returns {Promise<User>}
|
||||
*/
|
||||
fetchUser(id) {
|
||||
fetchUser(id, cache = true) {
|
||||
if (this.users.has(id)) return Promise.resolve(this.users.get(id));
|
||||
return this.rest.methods.getUser(id);
|
||||
return this.rest.methods.getUser(id, cache);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches an invite object from an invite code.
|
||||
* @param {InviteResolvable} invite An invite code or URL
|
||||
* Obtains an invite from Discord.
|
||||
* @param {InviteResolvable} invite Invite code or URL
|
||||
* @returns {Promise<Invite>}
|
||||
*/
|
||||
fetchInvite(invite) {
|
||||
@@ -280,19 +326,28 @@ class Client extends EventEmitter {
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a webhook by ID.
|
||||
* @param {string} id ID of the webhook
|
||||
* Obtains a webhook from Discord.
|
||||
* @param {Snowflake} id ID of the webhook
|
||||
* @param {string} [token] Token for the webhook
|
||||
* @returns {Promise<Webhook>}
|
||||
*/
|
||||
fetchWebhook(id) {
|
||||
return this.rest.methods.getWebhook(id);
|
||||
fetchWebhook(id, token) {
|
||||
return this.rest.methods.getWebhook(id, token);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sweeps all channels' messages and removes the ones older than the max message lifetime.
|
||||
* Obtains the available voice regions from Discord.
|
||||
* @returns {Collection<string, VoiceRegion>}
|
||||
*/
|
||||
fetchVoiceRegions() {
|
||||
return this.rest.methods.fetchVoiceRegions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sweeps all text-based channels' messages and removes the ones older than the max message lifetime.
|
||||
* If the message has been edited, the time of the edit is used rather than the time of the original message.
|
||||
* @param {number} [lifetime=this.options.messageCacheLifetime] Messages that are older than this (in seconds)
|
||||
* will be removed from the caches. The default is based on the client's `messageCacheLifetime` option.
|
||||
* will be removed from the caches. The default is based on {@link ClientOptions#messageCacheLifetime}
|
||||
* @returns {number} Amount of messages that were removed from the caches,
|
||||
* or -1 if the message cache lifetime is unlimited
|
||||
*/
|
||||
@@ -324,43 +379,125 @@ class Client extends EventEmitter {
|
||||
return messages;
|
||||
}
|
||||
|
||||
setTimeout(fn, ...params) {
|
||||
/**
|
||||
* Obtains the OAuth Application of the bot from Discord.
|
||||
* @param {Snowflake} [id='@me'] ID of application to fetch
|
||||
* @returns {Promise<OAuth2Application>}
|
||||
*/
|
||||
fetchApplication(id = '@me') {
|
||||
return this.rest.methods.getApplication(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a link that can be used to invite the bot to a guild.
|
||||
* <warn>This is only available when using a bot account.</warn>
|
||||
* @param {PermissionResolvable[]|number} [permissions] Permissions to request
|
||||
* @returns {Promise<string>}
|
||||
* @example
|
||||
* client.generateInvite(['SEND_MESSAGES', 'MANAGE_GUILD', 'MENTION_EVERYONE'])
|
||||
* .then(link => {
|
||||
* console.log(`Generated bot invite link: ${link}`);
|
||||
* });
|
||||
*/
|
||||
generateInvite(permissions) {
|
||||
if (permissions) {
|
||||
if (permissions instanceof Array) permissions = Permissions.resolve(permissions);
|
||||
} else {
|
||||
permissions = 0;
|
||||
}
|
||||
return this.fetchApplication().then(application =>
|
||||
`https://discordapp.com/oauth2/authorize?client_id=${application.id}&permissions=${permissions}&scope=bot`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a timeout that will be automatically cancelled if the client is destroyed.
|
||||
* @param {Function} fn Function to execute
|
||||
* @param {number} delay Time to wait before executing (in milliseconds)
|
||||
* @param {...*} args Arguments for the function
|
||||
* @returns {Timeout}
|
||||
*/
|
||||
setTimeout(fn, delay, ...args) {
|
||||
const timeout = setTimeout(() => {
|
||||
fn();
|
||||
fn(...args);
|
||||
this._timeouts.delete(timeout);
|
||||
}, ...params);
|
||||
}, delay);
|
||||
this._timeouts.add(timeout);
|
||||
return timeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears a timeout.
|
||||
* @param {Timeout} timeout Timeout to cancel
|
||||
*/
|
||||
clearTimeout(timeout) {
|
||||
clearTimeout(timeout);
|
||||
this._timeouts.delete(timeout);
|
||||
}
|
||||
|
||||
setInterval(...params) {
|
||||
const interval = setInterval(...params);
|
||||
/**
|
||||
* Sets an interval that will be automatically cancelled if the client is destroyed.
|
||||
* @param {Function} fn Function to execute
|
||||
* @param {number} delay Time to wait before executing (in milliseconds)
|
||||
* @param {...*} args Arguments for the function
|
||||
* @returns {Timeout}
|
||||
*/
|
||||
setInterval(fn, delay, ...args) {
|
||||
const interval = setInterval(fn, delay, ...args);
|
||||
this._intervals.add(interval);
|
||||
return interval;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears an interval.
|
||||
* @param {Timeout} interval Interval to cancel
|
||||
*/
|
||||
clearInterval(interval) {
|
||||
clearInterval(interval);
|
||||
this._intervals.delete(interval);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a ping to {@link Client#pings}.
|
||||
* @param {number} startTime Starting time of the ping
|
||||
* @private
|
||||
*/
|
||||
_pong(startTime) {
|
||||
this.pings.unshift(Date.now() - startTime);
|
||||
if (this.pings.length > 3) this.pings.length = 3;
|
||||
this.ws.lastHeartbeatAck = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds/updates a friend's presence in {@link Client#presences}.
|
||||
* @param {Snowflake} id ID of the user
|
||||
* @param {Object} presence Raw presence object from Discord
|
||||
* @private
|
||||
*/
|
||||
_setPresence(id, presence) {
|
||||
if (this.presences.get(id)) {
|
||||
if (this.presences.has(id)) {
|
||||
this.presences.get(id).update(presence);
|
||||
return;
|
||||
}
|
||||
this.presences.set(id, new Presence(presence));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval} on a script
|
||||
* with the client as `this`.
|
||||
* @param {string} script Script to eval
|
||||
* @returns {*}
|
||||
* @private
|
||||
*/
|
||||
_eval(script) {
|
||||
return eval(script);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the client options.
|
||||
* @param {ClientOptions} [options=this.options] Options to validate
|
||||
* @private
|
||||
*/
|
||||
_validateOptions(options = this.options) {
|
||||
if (typeof options.shardCount !== 'number' || isNaN(options.shardCount)) {
|
||||
throw new TypeError('The shardCount option must be a number.');
|
||||
@@ -398,13 +535,13 @@ class Client extends EventEmitter {
|
||||
module.exports = Client;
|
||||
|
||||
/**
|
||||
* Emitted for general warnings
|
||||
* Emitted for general warnings.
|
||||
* @event Client#warn
|
||||
* @param {string} The warning
|
||||
* @param {string} info The warning
|
||||
*/
|
||||
|
||||
/**
|
||||
* Emitted for general debugging information
|
||||
* Emitted for general debugging information.
|
||||
* @event Client#debug
|
||||
* @param {string} The debug information
|
||||
* @param {string} info The debug information
|
||||
*/
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
const Constants = require('../util/Constants');
|
||||
const cloneObject = require('../util/CloneObject');
|
||||
const Util = require('../util/Util');
|
||||
const Guild = require('../structures/Guild');
|
||||
const User = require('../structures/User');
|
||||
const CategoryChannel = require('../structures/CategoryChannel');
|
||||
const DMChannel = require('../structures/DMChannel');
|
||||
const Emoji = require('../structures/Emoji');
|
||||
const TextChannel = require('../structures/TextChannel');
|
||||
@@ -15,7 +16,7 @@ class ClientDataManager {
|
||||
}
|
||||
|
||||
get pastReady() {
|
||||
return this.client.ws.status === Constants.Status.READY;
|
||||
return this.client.ws.connection.status === Constants.Status.READY;
|
||||
}
|
||||
|
||||
newGuild(data) {
|
||||
@@ -24,7 +25,7 @@ class ClientDataManager {
|
||||
this.client.guilds.set(guild.id, guild);
|
||||
if (this.pastReady && !already) {
|
||||
/**
|
||||
* Emitted whenever the client joins a Guild.
|
||||
* Emitted whenever the client joins a guild.
|
||||
* @event Client#guildCreate
|
||||
* @param {Guild} guild The created guild
|
||||
*/
|
||||
@@ -50,25 +51,30 @@ class ClientDataManager {
|
||||
let channel;
|
||||
if (data.type === Constants.ChannelTypes.DM) {
|
||||
channel = new DMChannel(this.client, data);
|
||||
} else if (data.type === Constants.ChannelTypes.groupDM) {
|
||||
} else if (data.type === Constants.ChannelTypes.GROUP_DM) {
|
||||
channel = new GroupDMChannel(this.client, data);
|
||||
} else {
|
||||
guild = guild || this.client.guilds.get(data.guild_id);
|
||||
if (guild) {
|
||||
if (data.type === Constants.ChannelTypes.text) {
|
||||
if (data.type === Constants.ChannelTypes.TEXT) {
|
||||
channel = new TextChannel(guild, data);
|
||||
guild.channels.set(channel.id, channel);
|
||||
} else if (data.type === Constants.ChannelTypes.voice) {
|
||||
} else if (data.type === Constants.ChannelTypes.VOICE) {
|
||||
channel = new VoiceChannel(guild, data);
|
||||
guild.channels.set(channel.id, channel);
|
||||
} else if (data.type === Constants.ChannelTypes.CATEGORY) {
|
||||
channel = new CategoryChannel(guild, data);
|
||||
guild.channels.set(channel.id, channel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (channel) {
|
||||
if (this.pastReady && !already) this.client.emit(Constants.Events.CHANNEL_CREATE, channel);
|
||||
if (channel && !already) {
|
||||
if (this.pastReady) this.client.emit(Constants.Events.CHANNEL_CREATE, channel);
|
||||
this.client.channels.set(channel.id, channel);
|
||||
return channel;
|
||||
} else if (already) {
|
||||
return channel;
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -78,7 +84,7 @@ class ClientDataManager {
|
||||
const already = guild.emojis.has(data.id);
|
||||
if (data && !already) {
|
||||
let emoji = new Emoji(guild, data);
|
||||
this.client.emit(Constants.Events.EMOJI_CREATE, emoji);
|
||||
this.client.emit(Constants.Events.GUILD_EMOJI_CREATE, emoji);
|
||||
guild.emojis.set(emoji.id, emoji);
|
||||
return emoji;
|
||||
} else if (already) {
|
||||
@@ -90,7 +96,7 @@ class ClientDataManager {
|
||||
|
||||
killEmoji(emoji) {
|
||||
if (!(emoji instanceof Emoji && emoji.guild)) return;
|
||||
this.client.emit(Constants.Events.EMOJI_DELETE, emoji);
|
||||
this.client.emit(Constants.Events.GUILD_EMOJI_DELETE, emoji);
|
||||
emoji.guild.emojis.delete(emoji.id);
|
||||
}
|
||||
|
||||
@@ -110,7 +116,7 @@ class ClientDataManager {
|
||||
}
|
||||
|
||||
updateGuild(currentGuild, newData) {
|
||||
const oldGuild = cloneObject(currentGuild);
|
||||
const oldGuild = Util.cloneObject(currentGuild);
|
||||
currentGuild.setup(newData);
|
||||
if (this.pastReady) this.client.emit(Constants.Events.GUILD_UPDATE, oldGuild, currentGuild);
|
||||
}
|
||||
@@ -120,9 +126,10 @@ class ClientDataManager {
|
||||
}
|
||||
|
||||
updateEmoji(currentEmoji, newData) {
|
||||
const oldEmoji = cloneObject(currentEmoji);
|
||||
const oldEmoji = Util.cloneObject(currentEmoji);
|
||||
currentEmoji.setup(newData);
|
||||
this.client.emit(Constants.Events.GUILD_EMOJI_UPDATE, oldEmoji, currentEmoji);
|
||||
return currentEmoji;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const request = require('superagent');
|
||||
const snekfetch = require('snekfetch');
|
||||
|
||||
const Constants = require('../util/Constants');
|
||||
const User = require(`../structures/User`);
|
||||
const Message = require(`../structures/Message`);
|
||||
const Guild = require(`../structures/Guild`);
|
||||
const Channel = require(`../structures/Channel`);
|
||||
const GuildMember = require(`../structures/GuildMember`);
|
||||
const convertToBuffer = require('../util/Util').convertToBuffer;
|
||||
const User = require('../structures/User');
|
||||
const Message = require('../structures/Message');
|
||||
const Guild = require('../structures/Guild');
|
||||
const Channel = require('../structures/Channel');
|
||||
const GuildMember = require('../structures/GuildMember');
|
||||
const Emoji = require('../structures/Emoji');
|
||||
const ReactionEmoji = require('../structures/ReactionEmoji');
|
||||
const Role = require('../structures/Role');
|
||||
|
||||
/**
|
||||
* The DataResolver identifies different objects and tries to resolve a specific piece of information from them, e.g.
|
||||
@@ -25,15 +29,15 @@ class ClientDataResolver {
|
||||
/**
|
||||
* Data that resolves to give a User object. This can be:
|
||||
* * A User object
|
||||
* * A User ID
|
||||
* * A Message (resolves to the message author)
|
||||
* * A Guild (owner of the guild)
|
||||
* * A Guild Member
|
||||
* @typedef {User|string|Message|Guild|GuildMember} UserResolvable
|
||||
* * A Snowflake
|
||||
* * A Message object (resolves to the message author)
|
||||
* * A Guild object (owner of the guild)
|
||||
* * A GuildMember object
|
||||
* @typedef {User|Snowflake|Message|Guild|GuildMember} UserResolvable
|
||||
*/
|
||||
|
||||
/**
|
||||
* Resolves a UserResolvable to a User object
|
||||
* Resolves a UserResolvable to a User object.
|
||||
* @param {UserResolvable} user The UserResolvable to identify
|
||||
* @returns {?User}
|
||||
*/
|
||||
@@ -47,9 +51,9 @@ class ClientDataResolver {
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a UserResolvable to a user ID string
|
||||
* Resolves a UserResolvable to a user ID string.
|
||||
* @param {UserResolvable} user The UserResolvable to identify
|
||||
* @returns {?string}
|
||||
* @returns {?Snowflake}
|
||||
*/
|
||||
resolveUserID(user) {
|
||||
if (user instanceof User || user instanceof GuildMember) return user.id;
|
||||
@@ -62,11 +66,12 @@ class ClientDataResolver {
|
||||
/**
|
||||
* Data that resolves to give a Guild object. This can be:
|
||||
* * A Guild object
|
||||
* @typedef {Guild} GuildResolvable
|
||||
* * A Snowflake
|
||||
* @typedef {Guild|Snowflake} GuildResolvable
|
||||
*/
|
||||
|
||||
/**
|
||||
* Resolves a GuildResolvable to a Guild object
|
||||
* Resolves a GuildResolvable to a Guild object.
|
||||
* @param {GuildResolvable} guild The GuildResolvable to identify
|
||||
* @returns {?Guild}
|
||||
*/
|
||||
@@ -80,44 +85,76 @@ class ClientDataResolver {
|
||||
* Data that resolves to give a GuildMember object. This can be:
|
||||
* * A GuildMember object
|
||||
* * A User object
|
||||
* @typedef {Guild} GuildMemberResolvable
|
||||
* @typedef {GuildMember|User} GuildMemberResolvable
|
||||
*/
|
||||
|
||||
/**
|
||||
* Resolves a GuildMemberResolvable to a GuildMember object
|
||||
* Resolves a GuildMemberResolvable to a GuildMember object.
|
||||
* @param {GuildResolvable} guild The guild that the member is part of
|
||||
* @param {UserResolvable} user The user that is part of the guild
|
||||
* @returns {?GuildMember}
|
||||
*/
|
||||
resolveGuildMember(guild, user) {
|
||||
if (user instanceof GuildMember) return user;
|
||||
|
||||
guild = this.resolveGuild(guild);
|
||||
user = this.resolveUser(user);
|
||||
if (!guild || !user) return null;
|
||||
|
||||
return guild.members.get(user.id) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Data that can be resolved to give a Channel. This can be:
|
||||
* * An instance of a Channel
|
||||
* * An instance of a Message (the channel the message was sent in)
|
||||
* * An instance of a Guild (the #general channel)
|
||||
* * An ID of a Channel
|
||||
* @typedef {Channel|Guild|Message|string} ChannelResolvable
|
||||
* Data that can be resolved to a Role object. This can be:
|
||||
* * A Role
|
||||
* * A Snowflake
|
||||
* @typedef {Role|Snowflake} RoleResolvable
|
||||
*/
|
||||
|
||||
/**
|
||||
* Resolves a ChannelResolvable to a Channel object
|
||||
* Resolves a RoleResolvable to a Role object.
|
||||
* @param {GuildResolvable} guild The guild that this role is part of
|
||||
* @param {RoleResolvable} role The role resolvable to resolve
|
||||
* @returns {?Role}
|
||||
*/
|
||||
resolveRole(guild, role) {
|
||||
if (role instanceof Role) return role;
|
||||
guild = this.resolveGuild(guild);
|
||||
if (!guild) return null;
|
||||
if (typeof role === 'string') return guild.roles.get(role);
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Data that can be resolved to give a Channel object. This can be:
|
||||
* * A Channel object
|
||||
* * A Message object (the channel the message was sent in)
|
||||
* * A Guild object (the #general channel)
|
||||
* * A Snowflake
|
||||
* @typedef {Channel|Guild|Message|Snowflake} ChannelResolvable
|
||||
*/
|
||||
|
||||
/**
|
||||
* Resolves a ChannelResolvable to a Channel object.
|
||||
* @param {ChannelResolvable} channel The channel resolvable to resolve
|
||||
* @returns {?Channel}
|
||||
*/
|
||||
resolveChannel(channel) {
|
||||
if (channel instanceof Channel) return channel;
|
||||
if (typeof channel === 'string') return this.client.channels.get(channel) || null;
|
||||
if (channel instanceof Message) return channel.channel;
|
||||
if (channel instanceof Guild) return channel.channels.get(channel.id) || null;
|
||||
if (typeof channel === 'string') return this.client.channels.get(channel.id) || null;
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a ChannelResolvable to a channel ID.
|
||||
* @param {ChannelResolvable} channel The channel resolvable to resolve
|
||||
* @returns {?Snowflake}
|
||||
*/
|
||||
resolveChannelID(channel) {
|
||||
if (channel instanceof Channel) return channel.id;
|
||||
if (typeof channel === 'string') return channel;
|
||||
if (channel instanceof Message) return channel.channel.id;
|
||||
if (channel instanceof Guild) return channel.defaultChannel.id;
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -129,76 +166,27 @@ class ClientDataResolver {
|
||||
*/
|
||||
|
||||
/**
|
||||
* Resolves InviteResolvable to an invite code
|
||||
* Resolves InviteResolvable to an invite code.
|
||||
* @param {InviteResolvable} data The invite resolvable to resolve
|
||||
* @returns {string}
|
||||
*/
|
||||
resolveInviteCode(data) {
|
||||
const inviteRegex = /discord(?:app)?\.(?:gg|com\/invite)\/([a-z0-9]{5})/i;
|
||||
const inviteRegex = /discord(?:app\.com\/invite|\.gg)\/([\w-]{2,255})/i;
|
||||
const match = inviteRegex.exec(data);
|
||||
|
||||
if (match && match[1]) return match[1];
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Data that can be resolved to give a permission number. This can be:
|
||||
* * A string
|
||||
* * A permission number
|
||||
*
|
||||
* Possible strings:
|
||||
* ```js
|
||||
* [
|
||||
* "CREATE_INSTANT_INVITE",
|
||||
* "KICK_MEMBERS",
|
||||
* "BAN_MEMBERS",
|
||||
* "ADMINISTRATOR",
|
||||
* "MANAGE_CHANNELS",
|
||||
* "MANAGE_GUILD",
|
||||
* "READ_MESSAGES",
|
||||
* "SEND_MESSAGES",
|
||||
* "SEND_TTS_MESSAGES",
|
||||
* "MANAGE_MESSAGES",
|
||||
* "EMBED_LINKS",
|
||||
* "ATTACH_FILES",
|
||||
* "READ_MESSAGE_HISTORY",
|
||||
* "MENTION_EVERYONE",
|
||||
* "EXTERNAL_EMOJIS", // use external emojis
|
||||
* "CONNECT", // connect to voice
|
||||
* "SPEAK", // speak on voice
|
||||
* "MUTE_MEMBERS", // globally mute members on voice
|
||||
* "DEAFEN_MEMBERS", // globally deafen members on voice
|
||||
* "MOVE_MEMBERS", // move member's voice channels
|
||||
* "USE_VAD", // use voice activity detection
|
||||
* "CHANGE_NICKNAME",
|
||||
* "MANAGE_NICKNAMES", // change nicknames of others
|
||||
* "MANAGE_ROLES_OR_PERMISSIONS"
|
||||
* ]
|
||||
* ```
|
||||
* @typedef {string|number} PermissionResolvable
|
||||
*/
|
||||
|
||||
/**
|
||||
* Resolves a PermissionResolvable to a permission number
|
||||
* @param {PermissionResolvable} permission The permission resolvable to resolve
|
||||
* @returns {number}
|
||||
*/
|
||||
resolvePermission(permission) {
|
||||
if (typeof permission === 'string') permission = Constants.PermissionFlags[permission];
|
||||
if (typeof permission !== 'number' || permission < 1) throw new Error(Constants.Errors.NOT_A_PERMISSION);
|
||||
return permission;
|
||||
}
|
||||
|
||||
/**
|
||||
* Data that can be resolved to give a string. This can be:
|
||||
* * A string
|
||||
* * An Array (joined with a new line delimiter to give a string)
|
||||
* * An array (joined with a new line delimiter to give a string)
|
||||
* * Any value
|
||||
* @typedef {string|Array|*} StringResolvable
|
||||
*/
|
||||
|
||||
/**
|
||||
* Resolves a StringResolvable to a string
|
||||
* Resolves a StringResolvable to a string.
|
||||
* @param {StringResolvable} data The string resolvable to resolve
|
||||
* @returns {string}
|
||||
*/
|
||||
@@ -208,15 +196,29 @@ class ClientDataResolver {
|
||||
return String(data);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Resolves a Base64Resolvable, a string, or a BufferResolvable to a Base 64 image.
|
||||
* @param {BufferResolvable|Base64Resolvable} image The image to be resolved
|
||||
* @returns {Promise<?string>}
|
||||
*/
|
||||
resolveImage(image) {
|
||||
if (!image) return Promise.resolve(null);
|
||||
if (typeof image === 'string' && image.startsWith('data:')) {
|
||||
return Promise.resolve(image);
|
||||
}
|
||||
return this.resolveFile(image).then(this.resolveBase64);
|
||||
}
|
||||
|
||||
/**
|
||||
* Data that resolves to give a Base64 string, typically for image uploading. This can be:
|
||||
* * A Buffer
|
||||
* * A Base64 string
|
||||
* * A base64 string
|
||||
* @typedef {Buffer|string} Base64Resolvable
|
||||
*/
|
||||
|
||||
/**
|
||||
* Resolves a Base64Resolvable to a Base 64 image
|
||||
* Resolves a Base64Resolvable to a Base 64 image.
|
||||
* @param {Base64Resolvable} data The base 64 resolvable you want to resolve
|
||||
* @returns {?string}
|
||||
*/
|
||||
@@ -226,40 +228,149 @@ class ClientDataResolver {
|
||||
}
|
||||
|
||||
/**
|
||||
* Data that can be resolved to give a Buffer. This can be:
|
||||
* * A Buffer
|
||||
* * The path to a local file
|
||||
* * A URL
|
||||
* @typedef {string|Buffer} FileResolvable
|
||||
*/
|
||||
* Data that can be resolved to give a Buffer. This can be:
|
||||
* * A Buffer
|
||||
* * The path to a local file
|
||||
* * A URL
|
||||
* * A Stream
|
||||
* @typedef {string|Buffer} BufferResolvable
|
||||
*/
|
||||
|
||||
/**
|
||||
* Resolves a FileResolvable to a Buffer
|
||||
* @param {FileResolvable} resource The file resolvable to resolve
|
||||
* @returns {Promise<Buffer>}
|
||||
*/
|
||||
* @external Stream
|
||||
* @see {@link https://nodejs.org/api/stream.html}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Resolves a BufferResolvable to a Buffer.
|
||||
* @param {BufferResolvable|Stream} resource The buffer or stream resolvable to resolve
|
||||
* @returns {Promise<Buffer>}
|
||||
*/
|
||||
resolveFile(resource) {
|
||||
if (resource instanceof Buffer) return Promise.resolve(resource);
|
||||
if (this.client.browser && resource instanceof ArrayBuffer) return Promise.resolve(convertToBuffer(resource));
|
||||
|
||||
if (typeof resource === 'string') {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (/^https?:\/\//.test(resource)) {
|
||||
request.get(resource)
|
||||
.set('Content-Type', 'blob')
|
||||
.end((err, res) => err ? reject(err) : resolve(res.body));
|
||||
snekfetch.get(resource)
|
||||
.end((err, res) => {
|
||||
if (err) return reject(err);
|
||||
if (!(res.body instanceof Buffer)) return reject(new TypeError('The response body isn\'t a Buffer.'));
|
||||
return resolve(res.body);
|
||||
});
|
||||
} else {
|
||||
const file = path.resolve(resource);
|
||||
fs.stat(file, (err, stats) => {
|
||||
if (err) reject(err);
|
||||
if (!stats || !stats.isFile()) throw new Error(`The file could not be found: ${file}`);
|
||||
if (err) return reject(err);
|
||||
if (!stats || !stats.isFile()) return reject(new Error(`The file could not be found: ${file}`));
|
||||
fs.readFile(file, (err2, data) => {
|
||||
if (err2) reject(err2); else resolve(data);
|
||||
});
|
||||
return null;
|
||||
});
|
||||
}
|
||||
});
|
||||
} else if (resource.pipe && typeof resource.pipe === 'function') {
|
||||
return new Promise((resolve, reject) => {
|
||||
const buffers = [];
|
||||
resource.once('error', reject);
|
||||
resource.on('data', data => buffers.push(data));
|
||||
resource.once('end', () => resolve(Buffer.concat(buffers)));
|
||||
});
|
||||
}
|
||||
|
||||
if (resource instanceof Buffer) return Promise.resolve(resource);
|
||||
return Promise.reject(new TypeError('Resource must be a string or Buffer.'));
|
||||
return Promise.reject(new TypeError('The resource must be a string or Buffer.'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Data that can be resolved to give an emoji identifier. This can be:
|
||||
* * The unicode representation of an emoji
|
||||
* * A custom emoji ID
|
||||
* * An Emoji object
|
||||
* * A ReactionEmoji object
|
||||
* @typedef {string|Emoji|ReactionEmoji} EmojiIdentifierResolvable
|
||||
*/
|
||||
|
||||
/**
|
||||
* Resolves an EmojiResolvable to an emoji identifier.
|
||||
* @param {EmojiIdentifierResolvable} emoji The emoji resolvable to resolve
|
||||
* @returns {?string}
|
||||
*/
|
||||
resolveEmojiIdentifier(emoji) {
|
||||
if (emoji instanceof Emoji || emoji instanceof ReactionEmoji) return emoji.identifier;
|
||||
if (typeof emoji === 'string') {
|
||||
if (this.client.emojis.has(emoji)) return this.client.emojis.get(emoji).identifier;
|
||||
else if (!emoji.includes('%')) return encodeURIComponent(emoji);
|
||||
else return emoji;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Can be a Hex Literal, Hex String, Number, RGB Array, or one of the following
|
||||
* ```
|
||||
* [
|
||||
* 'DEFAULT',
|
||||
* 'AQUA',
|
||||
* 'GREEN',
|
||||
* 'BLUE',
|
||||
* 'PURPLE',
|
||||
* 'GOLD',
|
||||
* 'ORANGE',
|
||||
* 'RED',
|
||||
* 'GREY',
|
||||
* 'DARKER_GREY',
|
||||
* 'NAVY',
|
||||
* 'DARK_AQUA',
|
||||
* 'DARK_GREEN',
|
||||
* 'DARK_BLUE',
|
||||
* 'DARK_PURPLE',
|
||||
* 'DARK_GOLD',
|
||||
* 'DARK_ORANGE',
|
||||
* 'DARK_RED',
|
||||
* 'DARK_GREY',
|
||||
* 'LIGHT_GREY',
|
||||
* 'DARK_NAVY',
|
||||
* 'RANDOM',
|
||||
* ]
|
||||
* ```
|
||||
* or something like
|
||||
* ```
|
||||
* [255, 0, 255]
|
||||
* ```
|
||||
* for purple
|
||||
* @typedef {string|number|Array} ColorResolvable
|
||||
*/
|
||||
|
||||
/**
|
||||
* Resolves a ColorResolvable into a color number.
|
||||
* @param {ColorResolvable} color Color to resolve
|
||||
* @returns {number} A color
|
||||
*/
|
||||
static resolveColor(color) {
|
||||
if (typeof color === 'string') {
|
||||
if (color === 'RANDOM') return Math.floor(Math.random() * (0xFFFFFF + 1));
|
||||
color = Constants.Colors[color] || parseInt(color.replace('#', ''), 16);
|
||||
} else if (color instanceof Array) {
|
||||
color = (color[0] << 16) + (color[1] << 8) + color[2];
|
||||
}
|
||||
|
||||
if (color < 0 || color > 0xFFFFFF) {
|
||||
throw new RangeError('Color must be within the range 0 - 16777215 (0xFFFFFF).');
|
||||
} else if (color && isNaN(color)) {
|
||||
throw new TypeError('Unable to convert color to a number.');
|
||||
}
|
||||
|
||||
return color;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ColorResolvable} color Color to resolve
|
||||
* @returns {number} A color
|
||||
*/
|
||||
resolveColor(color) {
|
||||
return this.constructor.resolveColor(color);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,71 +1,72 @@
|
||||
const Constants = require('../util/Constants');
|
||||
const WebSocketConnection = require('./websocket/WebSocketConnection');
|
||||
|
||||
/**
|
||||
* Manages the State and Background Tasks of the Client
|
||||
* Manages the state and background tasks of the client.
|
||||
* @private
|
||||
*/
|
||||
class ClientManager {
|
||||
constructor(client) {
|
||||
/**
|
||||
* The Client that instantiated this Manager
|
||||
* The client that instantiated this Manager
|
||||
* @type {Client}
|
||||
*/
|
||||
this.client = client;
|
||||
|
||||
/**
|
||||
* The heartbeat interval, null if not yet set
|
||||
* The heartbeat interval
|
||||
* @type {?number}
|
||||
*/
|
||||
this.heartbeatInterval = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Connects the Client to the WebSocket
|
||||
* The status of the client
|
||||
* @type {number}
|
||||
*/
|
||||
get status() {
|
||||
return this.connection ? this.connection.status : Constants.Status.IDLE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Connects the client to the WebSocket.
|
||||
* @param {string} token The authorization token
|
||||
* @param {function} resolve Function to run when connection is successful
|
||||
* @param {function} reject Function to run when connection fails
|
||||
* @param {Function} resolve Function to run when connection is successful
|
||||
* @param {Function} reject Function to run when connection fails
|
||||
*/
|
||||
connectToWebSocket(token, resolve, reject) {
|
||||
this.client.emit(Constants.Events.DEBUG, `Authenticated using token ${token}`);
|
||||
this.client.token = token;
|
||||
const timeout = this.client.setTimeout(() => reject(new Error(Constants.Errors.TOOK_TOO_LONG)), 1000 * 300);
|
||||
this.client.rest.methods.getGateway().then(gateway => {
|
||||
this.client.rest.methods.getGateway().then(res => {
|
||||
const protocolVersion = Constants.DefaultOptions.ws.version;
|
||||
const gateway = `${res.url}/?v=${protocolVersion}&encoding=${WebSocketConnection.ENCODING}`;
|
||||
this.client.emit(Constants.Events.DEBUG, `Using gateway ${gateway}`);
|
||||
this.client.ws.connect(gateway);
|
||||
this.client.ws.once('close', event => {
|
||||
this.client.ws.connection.once('close', event => {
|
||||
if (event.code === 4004) reject(new Error(Constants.Errors.BAD_LOGIN));
|
||||
if (event.code === 4010) reject(new Error(Constants.Errors.INVALID_SHARD));
|
||||
if (event.code === 4011) reject(new Error(Constants.Errors.SHARDING_REQUIRED));
|
||||
});
|
||||
this.client.once(Constants.Events.READY, () => {
|
||||
resolve(token);
|
||||
this.client.clearTimeout(timeout);
|
||||
});
|
||||
}).catch(reject);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up a keep-alive interval to keep the Client's connection valid
|
||||
* @param {number} time The interval in milliseconds at which heartbeat packets should be sent
|
||||
*/
|
||||
setupKeepAlive(time) {
|
||||
this.heartbeatInterval = this.client.setInterval(() => {
|
||||
this.client.emit('debug', 'Sending heartbeat');
|
||||
this.client.ws.send({
|
||||
op: Constants.OPCodes.HEARTBEAT,
|
||||
d: this.client.ws.sequence,
|
||||
}, true);
|
||||
}, time);
|
||||
}, reject);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.client.ws.destroy();
|
||||
if (!this.client.user.bot) {
|
||||
this.client.rest.methods.logout().then(resolve, reject);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
this.client.ws.destroy();
|
||||
this.client.rest.destroy();
|
||||
if (!this.client.user) return Promise.resolve();
|
||||
if (this.client.user.bot) {
|
||||
this.client.token = null;
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
return this.client.rest.methods.logout().then(() => {
|
||||
this.client.token = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
const Webhook = require('../structures/Webhook');
|
||||
const RESTManager = require('./rest/RESTManager');
|
||||
const ClientDataResolver = require('./ClientDataResolver');
|
||||
const mergeDefault = require('../util/MergeDefault');
|
||||
const Constants = require('../util/Constants');
|
||||
const Util = require('../util/Util');
|
||||
|
||||
/**
|
||||
* The Webhook Client
|
||||
* The webhook client.
|
||||
* @extends {Webhook}
|
||||
*/
|
||||
class WebhookClient extends Webhook {
|
||||
/**
|
||||
* @param {string} id The id of the webhook.
|
||||
* @param {string} token the token of the webhook.
|
||||
* @param {Snowflake} id ID of the webhook
|
||||
* @param {string} token Token of the webhook
|
||||
* @param {ClientOptions} [options] Options for the client
|
||||
* @example
|
||||
* // create a new webhook and send a message
|
||||
* let hook = new Discord.WebhookClient('1234', 'abcdef')
|
||||
* hook.sendMessage('This will send a message').catch(console.error)
|
||||
* // Create a new webhook and send a message
|
||||
* const hook = new Discord.WebhookClient('1234', 'abcdef');
|
||||
* hook.sendMessage('This will send a message').catch(console.error);
|
||||
*/
|
||||
constructor(id, token, options) {
|
||||
super(null, id, token);
|
||||
@@ -25,7 +25,7 @@ class WebhookClient extends Webhook {
|
||||
* The options the client was instantiated with
|
||||
* @type {ClientOptions}
|
||||
*/
|
||||
this.options = mergeDefault(Constants.DefaultOptions, options);
|
||||
this.options = Util.mergeDefault(Constants.DefaultOptions, options);
|
||||
|
||||
/**
|
||||
* The REST manager of the client
|
||||
@@ -35,11 +35,83 @@ class WebhookClient extends Webhook {
|
||||
this.rest = new RESTManager(this);
|
||||
|
||||
/**
|
||||
* The Data Resolver of the Client
|
||||
* The data resolver of the client
|
||||
* @type {ClientDataResolver}
|
||||
* @private
|
||||
*/
|
||||
this.resolver = new ClientDataResolver(this);
|
||||
|
||||
/**
|
||||
* Timeouts set by {@link WebhookClient#setTimeout} that are still active
|
||||
* @type {Set<Timeout>}
|
||||
* @private
|
||||
*/
|
||||
this._timeouts = new Set();
|
||||
|
||||
/**
|
||||
* Intervals set by {@link WebhookClient#setInterval} that are still active
|
||||
* @type {Set<Timeout>}
|
||||
* @private
|
||||
*/
|
||||
this._intervals = new Set();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a timeout that will be automatically cancelled if the client is destroyed.
|
||||
* @param {Function} fn Function to execute
|
||||
* @param {number} delay Time to wait before executing (in milliseconds)
|
||||
* @param {...*} args Arguments for the function
|
||||
* @returns {Timeout}
|
||||
*/
|
||||
setTimeout(fn, delay, ...args) {
|
||||
const timeout = setTimeout(() => {
|
||||
fn(...args);
|
||||
this._timeouts.delete(timeout);
|
||||
}, delay);
|
||||
this._timeouts.add(timeout);
|
||||
return timeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears a timeout.
|
||||
* @param {Timeout} timeout Timeout to cancel
|
||||
*/
|
||||
clearTimeout(timeout) {
|
||||
clearTimeout(timeout);
|
||||
this._timeouts.delete(timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets an interval that will be automatically cancelled if the client is destroyed.
|
||||
* @param {Function} fn Function to execute
|
||||
* @param {number} delay Time to wait before executing (in milliseconds)
|
||||
* @param {...*} args Arguments for the function
|
||||
* @returns {Timeout}
|
||||
*/
|
||||
setInterval(fn, delay, ...args) {
|
||||
const interval = setInterval(fn, delay, ...args);
|
||||
this._intervals.add(interval);
|
||||
return interval;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears an interval.
|
||||
* @param {Timeout} interval Interval to cancel
|
||||
*/
|
||||
clearInterval(interval) {
|
||||
clearInterval(interval);
|
||||
this._intervals.delete(interval);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Destroys the client.
|
||||
*/
|
||||
destroy() {
|
||||
for (const t of this._timeouts) clearTimeout(t);
|
||||
for (const i of this._intervals) clearInterval(i);
|
||||
this._timeouts.clear();
|
||||
this._intervals.clear();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,33 +2,38 @@ class ActionsManager {
|
||||
constructor(client) {
|
||||
this.client = client;
|
||||
|
||||
this.register('MessageCreate');
|
||||
this.register('MessageDelete');
|
||||
this.register('MessageDeleteBulk');
|
||||
this.register('MessageUpdate');
|
||||
this.register('ChannelCreate');
|
||||
this.register('ChannelDelete');
|
||||
this.register('ChannelUpdate');
|
||||
this.register('GuildDelete');
|
||||
this.register('GuildUpdate');
|
||||
this.register('GuildMemberGet');
|
||||
this.register('GuildMemberRemove');
|
||||
this.register('GuildBanRemove');
|
||||
this.register('GuildRoleCreate');
|
||||
this.register('GuildRoleDelete');
|
||||
this.register('GuildRoleUpdate');
|
||||
this.register('UserGet');
|
||||
this.register('UserUpdate');
|
||||
this.register('GuildSync');
|
||||
this.register('GuildEmojiCreate');
|
||||
this.register('GuildEmojiDelete');
|
||||
this.register('GuildEmojiUpdate');
|
||||
this.register('GuildRolesPositionUpdate');
|
||||
this.register(require('./MessageCreate'));
|
||||
this.register(require('./MessageDelete'));
|
||||
this.register(require('./MessageDeleteBulk'));
|
||||
this.register(require('./MessageUpdate'));
|
||||
this.register(require('./MessageReactionAdd'));
|
||||
this.register(require('./MessageReactionRemove'));
|
||||
this.register(require('./MessageReactionRemoveAll'));
|
||||
this.register(require('./ChannelCreate'));
|
||||
this.register(require('./ChannelDelete'));
|
||||
this.register(require('./ChannelUpdate'));
|
||||
this.register(require('./GuildDelete'));
|
||||
this.register(require('./GuildUpdate'));
|
||||
this.register(require('./GuildMemberGet'));
|
||||
this.register(require('./GuildMemberRemove'));
|
||||
this.register(require('./GuildBanRemove'));
|
||||
this.register(require('./GuildRoleCreate'));
|
||||
this.register(require('./GuildRoleDelete'));
|
||||
this.register(require('./GuildRoleUpdate'));
|
||||
this.register(require('./UserGet'));
|
||||
this.register(require('./UserUpdate'));
|
||||
this.register(require('./UserNoteUpdate'));
|
||||
this.register(require('./GuildSync'));
|
||||
this.register(require('./GuildEmojiCreate'));
|
||||
this.register(require('./GuildEmojiDelete'));
|
||||
this.register(require('./GuildEmojiUpdate'));
|
||||
this.register(require('./GuildEmojisUpdate'));
|
||||
this.register(require('./GuildRolesPositionUpdate'));
|
||||
this.register(require('./GuildChannelsPositionUpdate'));
|
||||
}
|
||||
|
||||
register(name) {
|
||||
const Action = require(`./${name}`);
|
||||
this[name] = new Action(this.client);
|
||||
register(Action) {
|
||||
this[Action.name.replace(/Action$/, '')] = new Action(this.client);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,9 +4,7 @@ class ChannelCreateAction extends Action {
|
||||
handle(data) {
|
||||
const client = this.client;
|
||||
const channel = client.dataManager.newChannel(data);
|
||||
return {
|
||||
channel,
|
||||
};
|
||||
return { channel };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,9 +18,7 @@ class ChannelDeleteAction extends Action {
|
||||
channel = this.deleted.get(data.id) || null;
|
||||
}
|
||||
|
||||
return {
|
||||
channel,
|
||||
};
|
||||
return { channel };
|
||||
}
|
||||
|
||||
scheduleForDeletion(id) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const Action = require('./Action');
|
||||
const Constants = require('../../util/Constants');
|
||||
const cloneObject = require('../../util/CloneObject');
|
||||
const Util = require('../../util/Util');
|
||||
|
||||
class ChannelUpdateAction extends Action {
|
||||
handle(data) {
|
||||
@@ -8,7 +8,7 @@ class ChannelUpdateAction extends Action {
|
||||
|
||||
const channel = client.channels.get(data.id);
|
||||
if (channel) {
|
||||
const oldChannel = cloneObject(channel);
|
||||
const oldChannel = Util.cloneObject(channel);
|
||||
channel.setup(data);
|
||||
client.emit(Constants.Events.CHANNEL_UPDATE, oldChannel, channel);
|
||||
return {
|
||||
|
||||
19
src/client/actions/GuildChannelsPositionUpdate.js
Normal file
19
src/client/actions/GuildChannelsPositionUpdate.js
Normal file
@@ -0,0 +1,19 @@
|
||||
const Action = require('./Action');
|
||||
|
||||
class GuildChannelsPositionUpdate extends Action {
|
||||
handle(data) {
|
||||
const client = this.client;
|
||||
|
||||
const guild = client.guilds.get(data.guild_id);
|
||||
if (guild) {
|
||||
for (const partialChannel of data.channels) {
|
||||
const channel = guild.channels.get(partialChannel.id);
|
||||
if (channel) channel.position = partialChannel.position;
|
||||
}
|
||||
}
|
||||
|
||||
return { guild };
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = GuildChannelsPositionUpdate;
|
||||
@@ -12,19 +12,23 @@ class GuildDeleteAction extends Action {
|
||||
|
||||
let guild = client.guilds.get(data.id);
|
||||
if (guild) {
|
||||
for (const channel of guild.channels.values()) {
|
||||
if (channel.type === 'text') channel.stopTyping(true);
|
||||
}
|
||||
|
||||
if (guild.available && data.unavailable) {
|
||||
// guild is unavailable
|
||||
// Guild is unavailable
|
||||
guild.available = false;
|
||||
client.emit(Constants.Events.GUILD_UNAVAILABLE, guild);
|
||||
|
||||
// stops the GuildDelete packet thinking a guild was actually deleted,
|
||||
// Stops the GuildDelete packet thinking a guild was actually deleted,
|
||||
// handles emitting of event itself
|
||||
return {
|
||||
guild: null,
|
||||
};
|
||||
}
|
||||
|
||||
// delete guild
|
||||
// Delete guild
|
||||
client.guilds.delete(guild.id);
|
||||
this.deleted.set(guild.id, guild);
|
||||
this.scheduleForDeletion(guild.id);
|
||||
@@ -32,9 +36,7 @@ class GuildDeleteAction extends Action {
|
||||
guild = this.deleted.get(data.id) || null;
|
||||
}
|
||||
|
||||
return {
|
||||
guild,
|
||||
};
|
||||
return { guild };
|
||||
}
|
||||
|
||||
scheduleForDeletion(id) {
|
||||
@@ -45,7 +47,7 @@ class GuildDeleteAction extends Action {
|
||||
/**
|
||||
* Emitted whenever a guild becomes unavailable, likely due to a server outage.
|
||||
* @event Client#guildUnavailable
|
||||
* @param {Guild} guild The guild that has become unavailable.
|
||||
* @param {Guild} guild The guild that has become unavailable
|
||||
*/
|
||||
|
||||
module.exports = GuildDeleteAction;
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
const Action = require('./Action');
|
||||
|
||||
class EmojiCreateAction extends Action {
|
||||
handle(data, guild) {
|
||||
class GuildEmojiCreateAction extends Action {
|
||||
handle(guild, createdEmoji) {
|
||||
const client = this.client;
|
||||
const emoji = client.dataManager.newEmoji(data, guild);
|
||||
return {
|
||||
emoji,
|
||||
};
|
||||
const emoji = client.dataManager.newEmoji(createdEmoji, guild);
|
||||
return { emoji };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emitted whenever an emoji is created
|
||||
* @event Client#guildEmojiCreate
|
||||
* @param {Emoji} emoji The emoji that was created.
|
||||
* Emitted whenever a custom emoji is created in a guild.
|
||||
* @event Client#emojiCreate
|
||||
* @param {Emoji} emoji The emoji that was created
|
||||
*/
|
||||
module.exports = EmojiCreateAction;
|
||||
|
||||
module.exports = GuildEmojiCreateAction;
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
const Action = require('./Action');
|
||||
|
||||
class EmojiDeleteAction extends Action {
|
||||
handle(data) {
|
||||
class GuildEmojiDeleteAction extends Action {
|
||||
handle(emoji) {
|
||||
const client = this.client;
|
||||
client.dataManager.killEmoji(data);
|
||||
return {
|
||||
data,
|
||||
};
|
||||
client.dataManager.killEmoji(emoji);
|
||||
return { emoji };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emitted whenever an emoji is deleted
|
||||
* @event Client#guildEmojiDelete
|
||||
* @param {Emoji} emoji The emoji that was deleted.
|
||||
* Emitted whenever a custom guild emoji is deleted.
|
||||
* @event Client#emojiDelete
|
||||
* @param {Emoji} emoji The emoji that was deleted
|
||||
*/
|
||||
module.exports = EmojiDeleteAction;
|
||||
|
||||
module.exports = GuildEmojiDeleteAction;
|
||||
|
||||
@@ -1,29 +1,17 @@
|
||||
const Action = require('./Action');
|
||||
|
||||
class GuildEmojiUpdateAction extends Action {
|
||||
handle(data, guild) {
|
||||
const client = this.client;
|
||||
for (let emoji of data.emojis) {
|
||||
const already = guild.emojis.has(emoji.id);
|
||||
if (already) {
|
||||
client.dataManager.updateEmoji(guild.emojis.get(emoji.id), emoji);
|
||||
} else {
|
||||
emoji = client.dataManager.newEmoji(emoji, guild);
|
||||
}
|
||||
}
|
||||
for (let emoji of guild.emojis) {
|
||||
if (!data.emoijs.has(emoji.id)) client.dataManager.killEmoji(emoji);
|
||||
}
|
||||
return {
|
||||
emojis: data.emojis,
|
||||
};
|
||||
handle(oldEmoji, newEmoji) {
|
||||
const emoji = this.client.dataManager.updateEmoji(oldEmoji, newEmoji);
|
||||
return { emoji };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emitted whenever an emoji is updated
|
||||
* @event Client#guildEmojiUpdate
|
||||
* Emitted whenever a custom guild emoji is updated.
|
||||
* @event Client#emojiUpdate
|
||||
* @param {Emoji} oldEmoji The old emoji
|
||||
* @param {Emoji} newEmoji The new emoji
|
||||
*/
|
||||
|
||||
module.exports = GuildEmojiUpdateAction;
|
||||
|
||||
38
src/client/actions/GuildEmojisUpdate.js
Normal file
38
src/client/actions/GuildEmojisUpdate.js
Normal file
@@ -0,0 +1,38 @@
|
||||
const Action = require('./Action');
|
||||
|
||||
function mappify(iterable) {
|
||||
const map = new Map();
|
||||
for (const x of iterable) map.set(...x);
|
||||
return map;
|
||||
}
|
||||
|
||||
class GuildEmojisUpdateAction extends Action {
|
||||
handle(data) {
|
||||
const guild = this.client.guilds.get(data.guild_id);
|
||||
if (!guild || !guild.emojis) return;
|
||||
|
||||
const deletions = mappify(guild.emojis.entries());
|
||||
|
||||
for (const emoji of data.emojis) {
|
||||
// Determine type of emoji event
|
||||
const cachedEmoji = guild.emojis.get(emoji.id);
|
||||
if (cachedEmoji) {
|
||||
deletions.delete(emoji.id);
|
||||
if (!cachedEmoji.equals(emoji, true)) {
|
||||
// Emoji updated
|
||||
this.client.actions.GuildEmojiUpdate.handle(cachedEmoji, emoji);
|
||||
}
|
||||
} else {
|
||||
// Emoji added
|
||||
this.client.actions.GuildEmojiCreate.handle(guild, emoji);
|
||||
}
|
||||
}
|
||||
|
||||
for (const emoji of deletions.values()) {
|
||||
// Emoji deleted
|
||||
this.client.actions.GuildEmojiDelete.handle(emoji);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = GuildEmojisUpdateAction;
|
||||
@@ -3,9 +3,7 @@ const Action = require('./Action');
|
||||
class GuildMemberGetAction extends Action {
|
||||
handle(guild, data) {
|
||||
const member = guild._addMember(data, false);
|
||||
return {
|
||||
member,
|
||||
};
|
||||
return { member };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,10 +9,10 @@ class GuildMemberRemoveAction extends Action {
|
||||
|
||||
handle(data) {
|
||||
const client = this.client;
|
||||
|
||||
const guild = client.guilds.get(data.guild_id);
|
||||
let member = null;
|
||||
if (guild) {
|
||||
let member = guild.members.get(data.user.id);
|
||||
member = guild.members.get(data.user.id);
|
||||
if (member) {
|
||||
guild.memberCount--;
|
||||
guild._removeMember(member);
|
||||
@@ -22,17 +22,8 @@ class GuildMemberRemoveAction extends Action {
|
||||
} else {
|
||||
member = this.deleted.get(guild.id + data.user.id) || null;
|
||||
}
|
||||
|
||||
return {
|
||||
guild,
|
||||
member,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
guild,
|
||||
member: null,
|
||||
};
|
||||
return { guild, member };
|
||||
}
|
||||
|
||||
scheduleForDeletion(guildID, userID) {
|
||||
@@ -43,7 +34,7 @@ class GuildMemberRemoveAction extends Action {
|
||||
/**
|
||||
* Emitted whenever a member leaves a guild, or is kicked.
|
||||
* @event Client#guildMemberRemove
|
||||
* @param {GuildMember} member The member that has left/been kicked from the guild.
|
||||
* @param {GuildMember} member The member that has left/been kicked from the guild
|
||||
*/
|
||||
|
||||
module.exports = GuildMemberRemoveAction;
|
||||
|
||||
@@ -5,28 +5,22 @@ const Role = require('../../structures/Role');
|
||||
class GuildRoleCreate extends Action {
|
||||
handle(data) {
|
||||
const client = this.client;
|
||||
|
||||
const guild = client.guilds.get(data.guild_id);
|
||||
let role;
|
||||
if (guild) {
|
||||
const already = guild.roles.has(data.role.id);
|
||||
const role = new Role(guild, data.role);
|
||||
role = new Role(guild, data.role);
|
||||
guild.roles.set(role.id, role);
|
||||
if (!already) client.emit(Constants.Events.GUILD_ROLE_CREATE, role);
|
||||
return {
|
||||
role,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
role: null,
|
||||
};
|
||||
return { role };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emitted whenever a role is created.
|
||||
* @event Client#roleCreate
|
||||
* @param {Role} role The role that was created.
|
||||
* @param {Role} role The role that was created
|
||||
*/
|
||||
|
||||
module.exports = GuildRoleCreate;
|
||||
|
||||
@@ -9,10 +9,11 @@ class GuildRoleDeleteAction extends Action {
|
||||
|
||||
handle(data) {
|
||||
const client = this.client;
|
||||
|
||||
const guild = client.guilds.get(data.guild_id);
|
||||
let role;
|
||||
|
||||
if (guild) {
|
||||
let role = guild.roles.get(data.role_id);
|
||||
role = guild.roles.get(data.role_id);
|
||||
if (role) {
|
||||
guild.roles.delete(data.role_id);
|
||||
this.deleted.set(guild.id + data.role_id, role);
|
||||
@@ -21,15 +22,9 @@ class GuildRoleDeleteAction extends Action {
|
||||
} else {
|
||||
role = this.deleted.get(guild.id + data.role_id) || null;
|
||||
}
|
||||
|
||||
return {
|
||||
role,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
role: null,
|
||||
};
|
||||
return { role };
|
||||
}
|
||||
|
||||
scheduleForDeletion(guildID, roleID) {
|
||||
@@ -40,7 +35,7 @@ class GuildRoleDeleteAction extends Action {
|
||||
/**
|
||||
* Emitted whenever a guild role is deleted.
|
||||
* @event Client#roleDelete
|
||||
* @param {Role} role The role that was deleted.
|
||||
* @param {Role} role The role that was deleted
|
||||
*/
|
||||
|
||||
module.exports = GuildRoleDeleteAction;
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
const Action = require('./Action');
|
||||
const Constants = require('../../util/Constants');
|
||||
const cloneObject = require('../../util/CloneObject');
|
||||
const Util = require('../../util/Util');
|
||||
|
||||
class GuildRoleUpdateAction extends Action {
|
||||
handle(data) {
|
||||
const client = this.client;
|
||||
|
||||
const guild = client.guilds.get(data.guild_id);
|
||||
|
||||
if (guild) {
|
||||
const roleData = data.role;
|
||||
let oldRole = null;
|
||||
|
||||
const role = guild.roles.get(roleData.id);
|
||||
if (role) {
|
||||
oldRole = cloneObject(role);
|
||||
oldRole = Util.cloneObject(role);
|
||||
role.setup(data.role);
|
||||
client.emit(Constants.Events.GUILD_ROLE_UPDATE, oldRole, role);
|
||||
}
|
||||
@@ -34,8 +34,8 @@ class GuildRoleUpdateAction extends Action {
|
||||
/**
|
||||
* Emitted whenever a guild role is updated.
|
||||
* @event Client#roleUpdate
|
||||
* @param {Role} oldRole The role before the update.
|
||||
* @param {Role} newRole The role after the update.
|
||||
* @param {Role} oldRole The role before the update
|
||||
* @param {Role} newRole The role after the update
|
||||
*/
|
||||
|
||||
module.exports = GuildRoleUpdateAction;
|
||||
|
||||
@@ -1,23 +1,19 @@
|
||||
const Action = require('./Action');
|
||||
|
||||
class GuildRolesPositionUpdate extends Action {
|
||||
handle(data) {
|
||||
const client = this.client;
|
||||
|
||||
const guild = client.guilds.get(data.guild_id);
|
||||
if (guild) {
|
||||
for (const partialRole of data.roles) {
|
||||
const role = guild.roles.get(partialRole.id);
|
||||
if (role) {
|
||||
role.position = partialRole.position;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
guild,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = GuildRolesPositionUpdate;
|
||||
const Action = require('./Action');
|
||||
|
||||
class GuildRolesPositionUpdate extends Action {
|
||||
handle(data) {
|
||||
const client = this.client;
|
||||
|
||||
const guild = client.guilds.get(data.guild_id);
|
||||
if (guild) {
|
||||
for (const partialRole of data.roles) {
|
||||
const role = guild.roles.get(partialRole.id);
|
||||
if (role) role.position = partialRole.position;
|
||||
}
|
||||
}
|
||||
|
||||
return { guild };
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = GuildRolesPositionUpdate;
|
||||
|
||||
@@ -6,20 +6,22 @@ class GuildSync extends Action {
|
||||
|
||||
const guild = client.guilds.get(data.id);
|
||||
if (guild) {
|
||||
data.presences = data.presences || [];
|
||||
for (const presence of data.presences) {
|
||||
guild._setPresence(presence.user.id, presence);
|
||||
if (data.presences) {
|
||||
for (const presence of data.presences) guild._setPresence(presence.user.id, presence);
|
||||
}
|
||||
|
||||
data.members = data.members || [];
|
||||
for (const syncMember of data.members) {
|
||||
const member = guild.members.get(syncMember.user.id);
|
||||
if (member) {
|
||||
guild._updateMember(member, syncMember);
|
||||
} else {
|
||||
guild._addMember(syncMember);
|
||||
if (data.members) {
|
||||
for (const syncMember of data.members) {
|
||||
const member = guild.members.get(syncMember.user.id);
|
||||
if (member) {
|
||||
guild._updateMember(member, syncMember);
|
||||
} else {
|
||||
guild._addMember(syncMember, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ('large' in data) guild.large = data.large;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const Action = require('./Action');
|
||||
const Constants = require('../../util/Constants');
|
||||
const cloneObject = require('../../util/CloneObject');
|
||||
const Util = require('../../util/Util');
|
||||
|
||||
class GuildUpdateAction extends Action {
|
||||
handle(data) {
|
||||
@@ -8,7 +8,7 @@ class GuildUpdateAction extends Action {
|
||||
|
||||
const guild = client.guilds.get(data.id);
|
||||
if (guild) {
|
||||
const oldGuild = cloneObject(guild);
|
||||
const oldGuild = Util.cloneObject(guild);
|
||||
guild.setup(data);
|
||||
client.emit(Constants.Events.GUILD_UPDATE, oldGuild, guild);
|
||||
return {
|
||||
@@ -27,8 +27,8 @@ class GuildUpdateAction extends Action {
|
||||
/**
|
||||
* Emitted whenever a guild is updated - e.g. name change.
|
||||
* @event Client#guildUpdate
|
||||
* @param {Guild} oldGuild The guild before the update.
|
||||
* @param {Guild} newGuild The guild after the update.
|
||||
* @param {Guild} oldGuild The guild before the update
|
||||
* @param {Guild} newGuild The guild after the update
|
||||
*/
|
||||
|
||||
module.exports = GuildUpdateAction;
|
||||
|
||||
@@ -6,17 +6,40 @@ class MessageCreateAction extends Action {
|
||||
const client = this.client;
|
||||
|
||||
const channel = client.channels.get((data instanceof Array ? data[0] : data).channel_id);
|
||||
const user = client.users.get((data instanceof Array ? data[0] : data).author.id);
|
||||
if (channel) {
|
||||
const member = channel.guild ? channel.guild.member(user) : null;
|
||||
if (data instanceof Array) {
|
||||
const messages = new Array(data.length);
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
messages[i] = channel._cacheMessage(new Message(channel, data[i], client));
|
||||
}
|
||||
const lastMessage = messages[messages.length - 1];
|
||||
channel.lastMessageID = lastMessage.id;
|
||||
channel.lastMessage = lastMessage;
|
||||
if (user) {
|
||||
user.lastMessageID = lastMessage.id;
|
||||
user.lastMessage = lastMessage;
|
||||
}
|
||||
if (member) {
|
||||
member.lastMessageID = lastMessage.id;
|
||||
member.lastMessage = lastMessage;
|
||||
}
|
||||
return {
|
||||
messages,
|
||||
};
|
||||
} else {
|
||||
const message = channel._cacheMessage(new Message(channel, data, client));
|
||||
channel.lastMessageID = data.id;
|
||||
channel.lastMessage = message;
|
||||
if (user) {
|
||||
user.lastMessageID = data.id;
|
||||
user.lastMessage = message;
|
||||
}
|
||||
if (member) {
|
||||
member.lastMessageID = data.id;
|
||||
member.lastMessage = message;
|
||||
}
|
||||
return {
|
||||
message,
|
||||
};
|
||||
|
||||
@@ -8,11 +8,11 @@ class MessageDeleteAction extends Action {
|
||||
|
||||
handle(data) {
|
||||
const client = this.client;
|
||||
|
||||
const channel = client.channels.get(data.channel_id);
|
||||
if (channel) {
|
||||
let message = channel.messages.get(data.id);
|
||||
let message;
|
||||
|
||||
if (channel) {
|
||||
message = channel.messages.get(data.id);
|
||||
if (message) {
|
||||
channel.messages.delete(message.id);
|
||||
this.deleted.set(channel.id + message.id, message);
|
||||
@@ -20,15 +20,9 @@ class MessageDeleteAction extends Action {
|
||||
} else {
|
||||
message = this.deleted.get(channel.id + data.id) || null;
|
||||
}
|
||||
|
||||
return {
|
||||
message,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
message: null,
|
||||
};
|
||||
return { message };
|
||||
}
|
||||
|
||||
scheduleForDeletion(channelID, messageID) {
|
||||
|
||||
@@ -15,9 +15,7 @@ class MessageDeleteBulkAction extends Action {
|
||||
}
|
||||
|
||||
if (messages.size > 0) client.emit(Constants.Events.MESSAGE_BULK_DELETE, messages);
|
||||
return {
|
||||
messages,
|
||||
};
|
||||
return { messages };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
37
src/client/actions/MessageReactionAdd.js
Normal file
37
src/client/actions/MessageReactionAdd.js
Normal file
@@ -0,0 +1,37 @@
|
||||
const Action = require('./Action');
|
||||
const Constants = require('../../util/Constants');
|
||||
|
||||
/*
|
||||
{ user_id: 'id',
|
||||
message_id: 'id',
|
||||
emoji: { name: '<27>', id: null },
|
||||
channel_id: 'id' } }
|
||||
*/
|
||||
|
||||
class MessageReactionAdd extends Action {
|
||||
handle(data) {
|
||||
const user = this.client.users.get(data.user_id);
|
||||
if (!user) return false;
|
||||
// Verify channel
|
||||
const channel = this.client.channels.get(data.channel_id);
|
||||
if (!channel || channel.type === 'voice') return false;
|
||||
// Verify message
|
||||
const message = channel.messages.get(data.message_id);
|
||||
if (!message) return false;
|
||||
if (!data.emoji) return false;
|
||||
// Verify reaction
|
||||
const reaction = message._addReaction(data.emoji, user);
|
||||
if (reaction) this.client.emit(Constants.Events.MESSAGE_REACTION_ADD, reaction, user);
|
||||
|
||||
return { message, reaction, user };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emitted whenever a reaction is added to a message.
|
||||
* @event Client#messageReactionAdd
|
||||
* @param {MessageReaction} messageReaction The reaction object
|
||||
* @param {User} user The user that applied the emoji or reaction emoji
|
||||
*/
|
||||
|
||||
module.exports = MessageReactionAdd;
|
||||
37
src/client/actions/MessageReactionRemove.js
Normal file
37
src/client/actions/MessageReactionRemove.js
Normal file
@@ -0,0 +1,37 @@
|
||||
const Action = require('./Action');
|
||||
const Constants = require('../../util/Constants');
|
||||
|
||||
/*
|
||||
{ user_id: 'id',
|
||||
message_id: 'id',
|
||||
emoji: { name: '<27>', id: null },
|
||||
channel_id: 'id' } }
|
||||
*/
|
||||
|
||||
class MessageReactionRemove extends Action {
|
||||
handle(data) {
|
||||
const user = this.client.users.get(data.user_id);
|
||||
if (!user) return false;
|
||||
// Verify channel
|
||||
const channel = this.client.channels.get(data.channel_id);
|
||||
if (!channel || channel.type === 'voice') return false;
|
||||
// Verify message
|
||||
const message = channel.messages.get(data.message_id);
|
||||
if (!message) return false;
|
||||
if (!data.emoji) return false;
|
||||
// Verify reaction
|
||||
const reaction = message._removeReaction(data.emoji, user);
|
||||
if (reaction) this.client.emit(Constants.Events.MESSAGE_REACTION_REMOVE, reaction, user);
|
||||
|
||||
return { message, reaction, user };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emitted whenever a reaction is removed from a message.
|
||||
* @event Client#messageReactionRemove
|
||||
* @param {MessageReaction} messageReaction The reaction object
|
||||
* @param {User} user The user that removed the emoji or reaction emoji
|
||||
*/
|
||||
|
||||
module.exports = MessageReactionRemove;
|
||||
25
src/client/actions/MessageReactionRemoveAll.js
Normal file
25
src/client/actions/MessageReactionRemoveAll.js
Normal file
@@ -0,0 +1,25 @@
|
||||
const Action = require('./Action');
|
||||
const Constants = require('../../util/Constants');
|
||||
|
||||
class MessageReactionRemoveAll extends Action {
|
||||
handle(data) {
|
||||
const channel = this.client.channels.get(data.channel_id);
|
||||
if (!channel || channel.type === 'voice') return false;
|
||||
|
||||
const message = channel.messages.get(data.message_id);
|
||||
if (!message) return false;
|
||||
|
||||
message._clearReactions();
|
||||
this.client.emit(Constants.Events.MESSAGE_REACTION_REMOVE_ALL, message);
|
||||
|
||||
return { message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emitted whenever all reactions are removed from a message.
|
||||
* @event Client#messageReactionRemoveAll
|
||||
* @param {Message} message The message the reactions were removed from
|
||||
*/
|
||||
|
||||
module.exports = MessageReactionRemoveAll;
|
||||
@@ -1,6 +1,5 @@
|
||||
const Action = require('./Action');
|
||||
const Constants = require('../../util/Constants');
|
||||
const cloneObject = require('../../util/CloneObject');
|
||||
|
||||
class MessageUpdateAction extends Action {
|
||||
handle(data) {
|
||||
@@ -10,12 +9,10 @@ class MessageUpdateAction extends Action {
|
||||
if (channel) {
|
||||
const message = channel.messages.get(data.id);
|
||||
if (message) {
|
||||
const oldMessage = cloneObject(message);
|
||||
message.patch(data);
|
||||
message._edits.unshift(oldMessage);
|
||||
client.emit(Constants.Events.MESSAGE_UPDATE, oldMessage, message);
|
||||
client.emit(Constants.Events.MESSAGE_UPDATE, message._edits[0], message);
|
||||
return {
|
||||
old: oldMessage,
|
||||
old: message._edits[0],
|
||||
updated: message,
|
||||
};
|
||||
}
|
||||
@@ -36,8 +33,8 @@ class MessageUpdateAction extends Action {
|
||||
/**
|
||||
* Emitted whenever a message is updated - e.g. embed or content change.
|
||||
* @event Client#messageUpdate
|
||||
* @param {Message} oldMessage The message before the update.
|
||||
* @param {Message} newMessage The message after the update.
|
||||
* @param {Message} oldMessage The message before the update
|
||||
* @param {Message} newMessage The message after the update
|
||||
*/
|
||||
|
||||
module.exports = MessageUpdateAction;
|
||||
|
||||
@@ -4,9 +4,7 @@ class UserGetAction extends Action {
|
||||
handle(data) {
|
||||
const client = this.client;
|
||||
const user = client.dataManager.newUser(data);
|
||||
return {
|
||||
user,
|
||||
};
|
||||
return { user };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
30
src/client/actions/UserNoteUpdate.js
Normal file
30
src/client/actions/UserNoteUpdate.js
Normal file
@@ -0,0 +1,30 @@
|
||||
const Action = require('./Action');
|
||||
const Constants = require('../../util/Constants');
|
||||
|
||||
class UserNoteUpdateAction extends Action {
|
||||
handle(data) {
|
||||
const client = this.client;
|
||||
|
||||
const oldNote = client.user.notes.get(data.id);
|
||||
const note = data.note.length ? data.note : null;
|
||||
|
||||
client.user.notes.set(data.id, note);
|
||||
|
||||
client.emit(Constants.Events.USER_NOTE_UPDATE, data.id, oldNote, note);
|
||||
|
||||
return {
|
||||
old: oldNote,
|
||||
updated: note,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emitted whenever a note is updated.
|
||||
* @event Client#userNoteUpdate
|
||||
* @param {User} user The user the note belongs to
|
||||
* @param {string} oldNote The note content before the update
|
||||
* @param {string} newNote The note content after the update
|
||||
*/
|
||||
|
||||
module.exports = UserNoteUpdateAction;
|
||||
@@ -1,6 +1,6 @@
|
||||
const Action = require('./Action');
|
||||
const Constants = require('../../util/Constants');
|
||||
const cloneObject = require('../../util/CloneObject');
|
||||
const Util = require('../../util/Util');
|
||||
|
||||
class UserUpdateAction extends Action {
|
||||
handle(data) {
|
||||
@@ -14,7 +14,7 @@ class UserUpdateAction extends Action {
|
||||
};
|
||||
}
|
||||
|
||||
const oldUser = cloneObject(client.user);
|
||||
const oldUser = Util.cloneObject(client.user);
|
||||
client.user.patch(data);
|
||||
client.emit(Constants.Events.USER_UPDATE, oldUser, client.user);
|
||||
return {
|
||||
|
||||
@@ -1,52 +1,51 @@
|
||||
const request = require('superagent');
|
||||
const snekfetch = require('snekfetch');
|
||||
const Constants = require('../../util/Constants');
|
||||
|
||||
function getRoute(url) {
|
||||
let route = url.split('?')[0];
|
||||
if (route.includes('/channels/') || route.includes('/guilds/')) {
|
||||
const startInd = ~route.indexOf('/channels/') ? route.indexOf('/channels/') : route.indexOf('/guilds/');
|
||||
const majorID = route.substring(startInd).split('/')[2];
|
||||
route = route.replace(/(\d{8,})/g, ':id').replace(':id', majorID);
|
||||
}
|
||||
return route;
|
||||
}
|
||||
|
||||
class APIRequest {
|
||||
constructor(rest, method, url, auth, data, file) {
|
||||
constructor(rest, method, path, auth, data, files, reason) {
|
||||
this.rest = rest;
|
||||
this.client = rest.client;
|
||||
this.method = method;
|
||||
this.url = url;
|
||||
this.path = path.toString();
|
||||
this.auth = auth;
|
||||
this.data = data;
|
||||
this.file = file;
|
||||
this.route = getRoute(this.url);
|
||||
this.files = files;
|
||||
this.route = this.getRoute(this.path);
|
||||
this.reason = reason;
|
||||
}
|
||||
|
||||
getRoute(url) {
|
||||
let route = url.split('?')[0];
|
||||
if (route.includes('/channels/') || route.includes('/guilds/')) {
|
||||
const startInd = route.includes('/channels/') ? route.indexOf('/channels/') : route.indexOf('/guilds/');
|
||||
const majorID = route.substring(startInd).split('/')[2];
|
||||
route = route.replace(/(\d{8,})/g, ':id').replace(':id', majorID);
|
||||
}
|
||||
return route;
|
||||
}
|
||||
|
||||
getAuth() {
|
||||
if (this.rest.client.token && this.rest.client.user && this.rest.client.user.bot) {
|
||||
return `Bot ${this.rest.client.token}`;
|
||||
} else if (this.rest.client.token) {
|
||||
return this.rest.client.token;
|
||||
if (this.client.token && this.client.user && this.client.user.bot) {
|
||||
return `Bot ${this.client.token}`;
|
||||
} else if (this.client.token) {
|
||||
return this.client.token;
|
||||
}
|
||||
throw new Error(Constants.Errors.NO_TOKEN);
|
||||
}
|
||||
|
||||
gen() {
|
||||
const apiRequest = request[this.method](this.url);
|
||||
if (this.auth) apiRequest.set('authorization', this.getAuth());
|
||||
if (this.file && this.file.file) {
|
||||
apiRequest.attach('file', this.file.file, this.file.name);
|
||||
this.data = this.data || {};
|
||||
for (const key in this.data) {
|
||||
if (this.data[key]) {
|
||||
apiRequest.field(key, this.data[key]);
|
||||
}
|
||||
}
|
||||
const API = `${this.client.options.http.host}/api/v${this.client.options.http.version}`;
|
||||
const request = snekfetch[this.method](`${API}${this.path}`);
|
||||
if (this.auth) request.set('Authorization', this.getAuth());
|
||||
if (this.reason) request.set('X-Audit-Log-Reason', encodeURIComponent(this.reason));
|
||||
if (!this.rest.client.browser) request.set('User-Agent', this.rest.userAgentManager.userAgent);
|
||||
if (this.files) {
|
||||
for (const file of this.files) if (file && file.file) request.attach(file.name, file.file, file.name);
|
||||
if (typeof this.data !== 'undefined') request.attach('payload_json', JSON.stringify(this.data));
|
||||
} else if (this.data) {
|
||||
apiRequest.send(this.data);
|
||||
request.send(this.data);
|
||||
}
|
||||
apiRequest.set('User-Agent', this.rest.userAgentManager.userAgent);
|
||||
return apiRequest;
|
||||
return request;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
54
src/client/rest/DiscordAPIError.js
Normal file
54
src/client/rest/DiscordAPIError.js
Normal file
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Represents an error from the Discord API.
|
||||
* @extends Error
|
||||
*/
|
||||
class DiscordAPIError extends Error {
|
||||
constructor(path, error) {
|
||||
super();
|
||||
const flattened = this.constructor.flattenErrors(error.errors || error).join('\n');
|
||||
this.name = 'DiscordAPIError';
|
||||
this.message = error.message && flattened ? `${error.message}\n${flattened}` : error.message || flattened;
|
||||
|
||||
/**
|
||||
* The path of the request relative to the HTTP endpoint
|
||||
* @type {string}
|
||||
*/
|
||||
this.path = path;
|
||||
|
||||
/**
|
||||
* HTTP error code returned by Discord
|
||||
* @type {number}
|
||||
*/
|
||||
this.code = error.code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flattens an errors object returned from the API into an array.
|
||||
* @param {Object} obj Discord errors object
|
||||
* @param {string} [key] Used internally to determine key names of nested fields
|
||||
* @returns {string[]}
|
||||
* @private
|
||||
*/
|
||||
static flattenErrors(obj, key = '') {
|
||||
let messages = [];
|
||||
|
||||
for (const k of Object.keys(obj)) {
|
||||
if (k === 'message') continue;
|
||||
const newKey = key ? isNaN(k) ? `${key}.${k}` : `${key}[${k}]` : k;
|
||||
|
||||
if (obj[k]._errors) {
|
||||
messages.push(`${newKey}: ${obj[k]._errors.map(e => e.message).join(' ')}`);
|
||||
} else if (obj[k].code || obj[k].message) {
|
||||
messages.push(`${obj[k].code ? `${obj[k].code}: ` : ''}: ${obj[k].message}`.trim());
|
||||
} else if (typeof obj[k] === 'string') {
|
||||
messages.push(obj[k]);
|
||||
} else {
|
||||
messages = messages.concat(this.flattenErrors(obj[k], newKey));
|
||||
}
|
||||
}
|
||||
|
||||
return messages;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = DiscordAPIError;
|
||||
@@ -15,6 +15,12 @@ class RESTManager {
|
||||
this.globallyRateLimited = false;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
for (const handlerID in this.handlers) {
|
||||
this.handlers[handlerID].destroy();
|
||||
}
|
||||
}
|
||||
|
||||
push(handler, apiRequest) {
|
||||
return new Promise((resolve, reject) => {
|
||||
handler.push({
|
||||
@@ -36,9 +42,8 @@ class RESTManager {
|
||||
}
|
||||
}
|
||||
|
||||
makeRequest(method, url, auth, data, file) {
|
||||
const apiRequest = new APIRequest(this, method, url, auth, data, file);
|
||||
|
||||
makeRequest(method, url, auth, data, file, reason) {
|
||||
const apiRequest = new APIRequest(this, method, url, auth, data, file, reason);
|
||||
if (!this.handlers[apiRequest.route]) {
|
||||
const RequestHandlerType = this.getRequestHandler();
|
||||
this.handlers[apiRequest.route] = new RequestHandlerType(this, apiRequest.route);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,18 @@
|
||||
const RequestHandler = require('./RequestHandler');
|
||||
const DiscordAPIError = require('../DiscordAPIError');
|
||||
|
||||
class BurstRequestHandler extends RequestHandler {
|
||||
constructor(restManager, endpoint) {
|
||||
super(restManager, endpoint);
|
||||
this.requestRemaining = 1;
|
||||
this.first = true;
|
||||
|
||||
this.client = restManager.client;
|
||||
|
||||
this.limit = Infinity;
|
||||
this.resetTime = null;
|
||||
this.remaining = 1;
|
||||
this.timeDifference = 0;
|
||||
|
||||
this.resetTimeout = null;
|
||||
}
|
||||
|
||||
push(request) {
|
||||
@@ -12,58 +20,51 @@ class BurstRequestHandler extends RequestHandler {
|
||||
this.handle();
|
||||
}
|
||||
|
||||
handleNext(time) {
|
||||
if (this.waiting) return;
|
||||
this.waiting = true;
|
||||
this.restManager.client.setTimeout(() => {
|
||||
this.requestRemaining = this.requestLimit;
|
||||
this.waiting = false;
|
||||
this.handle();
|
||||
}, time);
|
||||
}
|
||||
|
||||
execute(item) {
|
||||
if (!item) return;
|
||||
item.request.gen().end((err, res) => {
|
||||
if (res && res.headers) {
|
||||
this.requestLimit = res.headers['x-ratelimit-limit'];
|
||||
this.requestResetTime = Number(res.headers['x-ratelimit-reset']) * 1000;
|
||||
this.requestRemaining = Number(res.headers['x-ratelimit-remaining']);
|
||||
this.limit = Number(res.headers['x-ratelimit-limit']);
|
||||
this.resetTime = Number(res.headers['x-ratelimit-reset']) * 1000;
|
||||
this.remaining = Number(res.headers['x-ratelimit-remaining']);
|
||||
this.timeDifference = Date.now() - new Date(res.headers.date).getTime();
|
||||
this.handleNext((this.requestResetTime - Date.now()) + this.timeDifference + 1000);
|
||||
}
|
||||
if (err) {
|
||||
if (err.status === 429) {
|
||||
this.requestRemaining = 0;
|
||||
this.queue.unshift(item);
|
||||
this.restManager.client.setTimeout(() => {
|
||||
if (res.headers['x-ratelimit-global']) this.globalLimit = true;
|
||||
if (this.resetTimeout) return;
|
||||
this.resetTimeout = this.client.setTimeout(() => {
|
||||
this.remaining = this.limit;
|
||||
this.globalLimit = false;
|
||||
this.handle();
|
||||
}, Number(res.headers['retry-after']) + 500);
|
||||
if (res.headers['x-ratelimit-global']) {
|
||||
this.globalLimit = true;
|
||||
}
|
||||
this.resetTimeout = null;
|
||||
}, Number(res.headers['retry-after']) + this.client.options.restTimeOffset);
|
||||
} else if (err.status >= 500 && err.status < 600) {
|
||||
this.queue.unshift(item);
|
||||
this.resetTimeout = this.client.setTimeout(() => {
|
||||
this.handle();
|
||||
this.resetTimeout = null;
|
||||
}, 1e3 + this.client.options.restTimeOffset);
|
||||
} else {
|
||||
item.reject(err);
|
||||
item.reject(err.status >= 400 && err.status < 500 ? new DiscordAPIError(res.request.path, res.body) : err);
|
||||
this.handle();
|
||||
}
|
||||
} else {
|
||||
this.globalLimit = false;
|
||||
const data = res && res.body ? res.body : {};
|
||||
item.resolve(data);
|
||||
if (this.first) {
|
||||
this.first = false;
|
||||
this.handle();
|
||||
}
|
||||
this.handle();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
handle() {
|
||||
super.handle();
|
||||
if (this.requestRemaining < 1 || this.queue.length === 0 || this.globalLimit) return;
|
||||
while (this.queue.length > 0 && this.requestRemaining > 0) {
|
||||
this.execute(this.queue.shift());
|
||||
this.requestRemaining--;
|
||||
}
|
||||
if (this.remaining <= 0 || this.queue.length === 0 || this.globalLimit) return;
|
||||
this.execute(this.queue.shift());
|
||||
this.remaining--;
|
||||
this.handle();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,15 +14,16 @@ class RequestHandler {
|
||||
this.restManager = restManager;
|
||||
|
||||
/**
|
||||
* A list of requests that have yet to be processed.
|
||||
* A list of requests that have yet to be processed
|
||||
* @type {APIRequest[]}
|
||||
*/
|
||||
this.queue = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not the client is being rate limited on every endpoint.
|
||||
* Whether or not the client is being rate limited on every endpoint
|
||||
* @type {boolean}
|
||||
* @readonly
|
||||
*/
|
||||
get globalLimit() {
|
||||
return this.restManager.globallyRateLimited;
|
||||
@@ -33,7 +34,7 @@ class RequestHandler {
|
||||
}
|
||||
|
||||
/**
|
||||
* Push a new API request into this bucket
|
||||
* Push a new API request into this bucket.
|
||||
* @param {APIRequest} request The new request to push into the queue
|
||||
*/
|
||||
push(request) {
|
||||
@@ -41,10 +42,12 @@ class RequestHandler {
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to get this RequestHandler to process its current queue
|
||||
* Attempts to get this RequestHandler to process its current queue.
|
||||
*/
|
||||
handle() {
|
||||
return;
|
||||
handle() {} // eslint-disable-line no-empty-function
|
||||
|
||||
destroy() {
|
||||
this.queue = [];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const RequestHandler = require('./RequestHandler');
|
||||
const DiscordAPIError = require('../DiscordAPIError');
|
||||
|
||||
/**
|
||||
* Handles API Requests sequentially, i.e. we wait until the current request is finished before moving onto
|
||||
@@ -15,12 +16,6 @@ class SequentialRequestHandler extends RequestHandler {
|
||||
constructor(restManager, endpoint) {
|
||||
super(restManager, endpoint);
|
||||
|
||||
/**
|
||||
* Whether this rate limiter is waiting for a response from a request
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.waiting = false;
|
||||
|
||||
/**
|
||||
* The endpoint that this handler is handling
|
||||
* @type {string}
|
||||
@@ -29,10 +24,16 @@ class SequentialRequestHandler extends RequestHandler {
|
||||
|
||||
/**
|
||||
* The time difference between Discord's Dates and the local computer's Dates. A positive number means the local
|
||||
* computer's time is ahead of Discord's.
|
||||
* computer's time is ahead of Discord's
|
||||
* @type {number}
|
||||
*/
|
||||
this.timeDifference = 0;
|
||||
|
||||
/**
|
||||
* Whether the queue is being processed or not
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.busy = false;
|
||||
}
|
||||
|
||||
push(request) {
|
||||
@@ -41,47 +42,45 @@ class SequentialRequestHandler extends RequestHandler {
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a request then resolves a promise to indicate its readiness for a new request
|
||||
* Performs a request then resolves a promise to indicate its readiness for a new request.
|
||||
* @param {APIRequest} item The item to execute
|
||||
* @returns {Promise<?Object|Error>}
|
||||
*/
|
||||
execute(item) {
|
||||
this.busy = true;
|
||||
return new Promise(resolve => {
|
||||
item.request.gen().end((err, res) => {
|
||||
if (res && res.headers) {
|
||||
this.requestLimit = res.headers['x-ratelimit-limit'];
|
||||
this.requestLimit = Number(res.headers['x-ratelimit-limit']);
|
||||
this.requestResetTime = Number(res.headers['x-ratelimit-reset']) * 1000;
|
||||
this.requestRemaining = Number(res.headers['x-ratelimit-remaining']);
|
||||
this.timeDifference = Date.now() - new Date(res.headers.date).getTime();
|
||||
}
|
||||
if (err) {
|
||||
if (err.status === 429) {
|
||||
this.queue.unshift(item);
|
||||
this.restManager.client.setTimeout(() => {
|
||||
this.waiting = false;
|
||||
this.globalLimit = false;
|
||||
resolve();
|
||||
}, Number(res.headers['retry-after']) + 500);
|
||||
if (res.headers['x-ratelimit-global']) {
|
||||
this.globalLimit = true;
|
||||
}
|
||||
}, Number(res.headers['retry-after']) + this.restManager.client.options.restTimeOffset);
|
||||
if (res.headers['x-ratelimit-global']) this.globalLimit = true;
|
||||
} else if (err.status >= 500 && err.status < 600) {
|
||||
this.queue.unshift(item);
|
||||
this.restManager.client.setTimeout(resolve, 1e3 + this.restManager.client.options.restTimeOffset);
|
||||
} else {
|
||||
this.queue.shift();
|
||||
this.waiting = false;
|
||||
item.reject(err);
|
||||
item.reject(err.status >= 400 && err.status < 500 ? new DiscordAPIError(res.request.path, res.body) : err);
|
||||
resolve(err);
|
||||
}
|
||||
} else {
|
||||
this.queue.shift();
|
||||
this.globalLimit = false;
|
||||
const data = res && res.body ? res.body : {};
|
||||
item.resolve(data);
|
||||
if (this.requestRemaining === 0) {
|
||||
this.restManager.client.setTimeout(() => {
|
||||
this.waiting = false;
|
||||
resolve(data);
|
||||
}, (this.requestResetTime - Date.now()) + this.timeDifference + 1000);
|
||||
this.restManager.client.setTimeout(
|
||||
() => resolve(data),
|
||||
this.requestResetTime - Date.now() + this.timeDifference + this.restManager.client.options.restTimeOffset
|
||||
);
|
||||
} else {
|
||||
this.waiting = false;
|
||||
resolve(data);
|
||||
}
|
||||
}
|
||||
@@ -91,12 +90,11 @@ class SequentialRequestHandler extends RequestHandler {
|
||||
|
||||
handle() {
|
||||
super.handle();
|
||||
|
||||
if (this.waiting || this.queue.length === 0 || this.globalLimit) return;
|
||||
this.waiting = true;
|
||||
|
||||
const item = this.queue[0];
|
||||
this.execute(item).then(() => this.handle());
|
||||
if (this.busy || this.remaining === 0 || this.queue.length === 0 || this.globalLimit) return;
|
||||
this.execute(this.queue.shift()).then(() => {
|
||||
this.busy = false;
|
||||
this.handle();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,22 +1,25 @@
|
||||
const Constants = require('../../util/Constants');
|
||||
|
||||
class UserAgentManager {
|
||||
constructor(restManager) {
|
||||
this.restManager = restManager;
|
||||
this._userAgent = {
|
||||
url: 'https://github.com/hydrabolt/discord.js',
|
||||
version: Constants.Package.version,
|
||||
};
|
||||
constructor() {
|
||||
this.build(this.constructor.DEFAULT);
|
||||
}
|
||||
|
||||
set(info) {
|
||||
this._userAgent.url = info.url || 'https://github.com/hydrabolt/discord.js';
|
||||
this._userAgent.version = info.version || Constants.Package.version;
|
||||
set({ url, version } = {}) {
|
||||
this.build({
|
||||
url: url || this.constructor.DFEAULT.url,
|
||||
version: version || this.constructor.DEFAULT.version,
|
||||
});
|
||||
}
|
||||
|
||||
get userAgent() {
|
||||
return `DiscordBot (${this._userAgent.url}, ${this._userAgent.version})`;
|
||||
build(ua) {
|
||||
this.userAgent = `DiscordBot (${ua.url}, ${ua.version}) Node.js/${process.version}`;
|
||||
}
|
||||
}
|
||||
|
||||
UserAgentManager.DEFAULT = {
|
||||
url: Constants.Package.homepage.split('#')[0],
|
||||
version: Constants.Package.version,
|
||||
};
|
||||
|
||||
module.exports = UserAgentManager;
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
const Collection = require('../../util/Collection');
|
||||
const mergeDefault = require('../../util/MergeDefault');
|
||||
const Constants = require('../../util/Constants');
|
||||
const VoiceConnection = require('./VoiceConnection');
|
||||
const EventEmitter = require('events').EventEmitter;
|
||||
|
||||
/**
|
||||
* Manages all the voice stuff for the Client
|
||||
* Manages all the voice stuff for the client.
|
||||
* @private
|
||||
*/
|
||||
class ClientVoiceManager {
|
||||
@@ -18,231 +15,67 @@ class ClientVoiceManager {
|
||||
|
||||
/**
|
||||
* A collection mapping connection IDs to the Connection objects
|
||||
* @type {Collection<string, VoiceConnection>}
|
||||
* @type {Collection<Snowflake, VoiceConnection>}
|
||||
*/
|
||||
this.connections = new Collection();
|
||||
|
||||
/**
|
||||
* Pending connection attempts, maps Guild ID to VoiceChannel
|
||||
* @type {Collection<string, VoiceChannel>}
|
||||
*/
|
||||
this.pending = new Collection();
|
||||
|
||||
this.client.on('self.voiceServer', this.onVoiceServer.bind(this));
|
||||
this.client.on('self.voiceStateUpdate', this.onVoiceStateUpdate.bind(this));
|
||||
}
|
||||
|
||||
onVoiceServer(data) {
|
||||
if (this.pending.has(data.guild_id)) this.pending.get(data.guild_id).setTokenAndEndpoint(data.token, data.endpoint);
|
||||
onVoiceServer({ guild_id, token, endpoint }) {
|
||||
const connection = this.connections.get(guild_id);
|
||||
if (connection) connection.setTokenAndEndpoint(token, endpoint);
|
||||
}
|
||||
|
||||
onVoiceStateUpdate(data) {
|
||||
if (this.pending.has(data.guild_id)) this.pending.get(data.guild_id).setSessionID(data.session_id);
|
||||
onVoiceStateUpdate({ guild_id, session_id, channel_id }) {
|
||||
const connection = this.connections.get(guild_id);
|
||||
if (connection) {
|
||||
connection.channel = this.client.channels.get(channel_id);
|
||||
connection.setSessionID(session_id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a request to the main gateway to join a voice channel
|
||||
* @param {VoiceChannel} channel The channel to join
|
||||
* @param {Object} [options] The options to provide
|
||||
*/
|
||||
sendVoiceStateUpdate(channel, options = {}) {
|
||||
if (!this.client.user) throw new Error('Unable to join because there is no client user.');
|
||||
if (!channel.permissionsFor) {
|
||||
throw new Error('Channel does not support permissionsFor; is it really a voice channel?');
|
||||
}
|
||||
const permissions = channel.permissionsFor(this.client.user);
|
||||
if (!permissions) {
|
||||
throw new Error('There is no permission set for the client user in this channel - are they part of the guild?');
|
||||
}
|
||||
if (!permissions.hasPermission('CONNECT')) {
|
||||
throw new Error('You do not have permission to connect to this voice channel.');
|
||||
}
|
||||
|
||||
options = mergeDefault({
|
||||
guild_id: channel.guild.id,
|
||||
channel_id: channel.id,
|
||||
self_mute: false,
|
||||
self_deaf: false,
|
||||
}, options);
|
||||
|
||||
this.client.ws.send({
|
||||
op: Constants.OPCodes.VOICE_STATE_UPDATE,
|
||||
d: options,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up a request to join a voice channel
|
||||
* Sets up a request to join a voice channel.
|
||||
* @param {VoiceChannel} channel The voice channel to join
|
||||
* @returns {Promise<VoiceConnection>}
|
||||
*/
|
||||
joinChannel(channel) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this.pending.get(channel.guild.id)) throw new Error('Already connecting to this guild\'s voice server.');
|
||||
|
||||
if (!channel.joinable) {
|
||||
throw new Error('You do not have permission to join this voice channel');
|
||||
}
|
||||
|
||||
const existingConnection = this.connections.get(channel.guild.id);
|
||||
if (existingConnection) {
|
||||
if (existingConnection.channel.id !== channel.id) {
|
||||
this.sendVoiceStateUpdate(channel);
|
||||
this.connections.get(channel.guild.id).channel = channel;
|
||||
if (channel.full) {
|
||||
throw new Error('You do not have permission to join this voice channel; it is full.');
|
||||
} else {
|
||||
throw new Error('You do not have permission to join this voice channel.');
|
||||
}
|
||||
resolve(existingConnection);
|
||||
return;
|
||||
}
|
||||
|
||||
const pendingConnection = new PendingVoiceConnection(this, channel);
|
||||
this.pending.set(channel.guild.id, pendingConnection);
|
||||
let connection = this.connections.get(channel.guild.id);
|
||||
|
||||
pendingConnection.on('fail', reason => {
|
||||
this.pending.delete(channel.guild.id);
|
||||
if (connection) {
|
||||
if (connection.channel.id !== channel.id) {
|
||||
this.connections.get(channel.guild.id).updateChannel(channel);
|
||||
}
|
||||
resolve(connection);
|
||||
return;
|
||||
} else {
|
||||
connection = new VoiceConnection(this, channel);
|
||||
this.connections.set(channel.guild.id, connection);
|
||||
}
|
||||
|
||||
connection.once('failed', reason => {
|
||||
this.connections.delete(channel.guild.id);
|
||||
reject(reason);
|
||||
});
|
||||
|
||||
pendingConnection.on('pass', voiceConnection => {
|
||||
this.pending.delete(channel.guild.id);
|
||||
this.connections.set(channel.guild.id, voiceConnection);
|
||||
voiceConnection.once('ready', () => resolve(voiceConnection));
|
||||
voiceConnection.once('error', reject);
|
||||
voiceConnection.once('disconnect', () => this.connections.delete(channel.guild.id));
|
||||
connection.once('authenticated', () => {
|
||||
connection.once('ready', () => resolve(connection));
|
||||
connection.once('error', reject);
|
||||
connection.once('disconnect', () => this.connections.delete(channel.guild.id));
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a Pending Voice Connection
|
||||
* @private
|
||||
*/
|
||||
class PendingVoiceConnection extends EventEmitter {
|
||||
constructor(voiceManager, channel) {
|
||||
super();
|
||||
|
||||
/**
|
||||
* The ClientVoiceManager that instantiated this pending connection
|
||||
* @type {ClientVoiceManager}
|
||||
*/
|
||||
this.voiceManager = voiceManager;
|
||||
|
||||
/**
|
||||
* The channel that this pending voice connection will attempt to join
|
||||
* @type {VoiceChannel}
|
||||
*/
|
||||
this.channel = channel;
|
||||
|
||||
/**
|
||||
* The timeout that will be invoked after 15 seconds signifying a failure to connect
|
||||
* @type {Timeout}
|
||||
*/
|
||||
this.deathTimer = this.voiceManager.client.setTimeout(
|
||||
() => this.fail(new Error('Connection not established within 15 seconds.')), 15000);
|
||||
|
||||
/**
|
||||
* An object containing data required to connect to the voice servers with
|
||||
* @type {object}
|
||||
*/
|
||||
this.data = {};
|
||||
|
||||
this.sendVoiceStateUpdate();
|
||||
}
|
||||
|
||||
checkReady() {
|
||||
if (this.data.token && this.data.endpoint && this.data.session_id) {
|
||||
this.pass();
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the token and endpoint required to connect to the the voice servers
|
||||
* @param {string} token the token
|
||||
* @param {string} endpoint the endpoint
|
||||
* @returns {void}
|
||||
*/
|
||||
setTokenAndEndpoint(token, endpoint) {
|
||||
if (!token) {
|
||||
this.fail(new Error('Token not provided from voice server packet.'));
|
||||
return;
|
||||
}
|
||||
if (!endpoint) {
|
||||
this.fail(new Error('Endpoint not provided from voice server packet.'));
|
||||
return;
|
||||
}
|
||||
if (this.data.token) {
|
||||
this.fail(new Error('There is already a registered token for this connection.'));
|
||||
return;
|
||||
}
|
||||
if (this.data.endpoint) {
|
||||
this.fail(new Error('There is already a registered endpoint for this connection.'));
|
||||
return;
|
||||
}
|
||||
|
||||
endpoint = endpoint.match(/([^:]*)/)[0];
|
||||
|
||||
if (!endpoint) {
|
||||
this.fail(new Error('Failed to find an endpoint.'));
|
||||
return;
|
||||
}
|
||||
|
||||
this.data.token = token;
|
||||
this.data.endpoint = endpoint;
|
||||
|
||||
this.checkReady();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Session ID for the connection
|
||||
* @param {string} sessionID the session ID
|
||||
*/
|
||||
setSessionID(sessionID) {
|
||||
if (!sessionID) {
|
||||
this.fail(new Error('Session ID not supplied.'));
|
||||
return;
|
||||
}
|
||||
if (this.data.session_id) {
|
||||
this.fail(new Error('There is already a registered session ID for this connection.'));
|
||||
return;
|
||||
}
|
||||
this.data.session_id = sessionID;
|
||||
|
||||
this.checkReady();
|
||||
}
|
||||
|
||||
clean() {
|
||||
clearInterval(this.deathTimer);
|
||||
this.emit('fail', new Error('Clean-up triggered :fourTriggered:'));
|
||||
}
|
||||
|
||||
pass() {
|
||||
clearInterval(this.deathTimer);
|
||||
this.emit('pass', this.upgrade());
|
||||
}
|
||||
|
||||
fail(reason) {
|
||||
this.emit('fail', reason);
|
||||
this.clean();
|
||||
}
|
||||
|
||||
sendVoiceStateUpdate() {
|
||||
try {
|
||||
this.voiceManager.sendVoiceStateUpdate(this.channel);
|
||||
} catch (error) {
|
||||
this.fail(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Upgrades this Pending Connection to a full Voice Connection
|
||||
* @returns {VoiceConnection}
|
||||
*/
|
||||
upgrade() {
|
||||
return new VoiceConnection(this);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ClientVoiceManager;
|
||||
|
||||
366
src/client/voice/VoiceBroadcast.js
Normal file
366
src/client/voice/VoiceBroadcast.js
Normal file
@@ -0,0 +1,366 @@
|
||||
const VolumeInterface = require('./util/VolumeInterface');
|
||||
const Prism = require('prism-media');
|
||||
const OpusEncoders = require('./opus/OpusEngineList');
|
||||
const Collection = require('../../util/Collection');
|
||||
|
||||
const ffmpegArguments = [
|
||||
'-analyzeduration', '0',
|
||||
'-loglevel', '0',
|
||||
'-f', 's16le',
|
||||
'-ar', '48000',
|
||||
'-ac', '2',
|
||||
];
|
||||
|
||||
/**
|
||||
* A voice broadcast can be played across multiple voice connections for improved shared-stream efficiency.
|
||||
*
|
||||
* Example usage:
|
||||
* ```js
|
||||
* const broadcast = client.createVoiceBroadcast();
|
||||
* broadcast.playFile('./music.mp3');
|
||||
* // Play "music.mp3" in all voice connections that the client is in
|
||||
* for (const connection of client.voiceConnections.values()) {
|
||||
* connection.playBroadcast(broadcast);
|
||||
* }
|
||||
* ```
|
||||
* @implements {VolumeInterface}
|
||||
*/
|
||||
class VoiceBroadcast extends VolumeInterface {
|
||||
constructor(client) {
|
||||
super();
|
||||
/**
|
||||
* The client that created the broadcast
|
||||
* @type {Client}
|
||||
*/
|
||||
this.client = client;
|
||||
this._dispatchers = new Collection();
|
||||
this._encoders = new Collection();
|
||||
/**
|
||||
* The audio transcoder that this broadcast uses
|
||||
* @type {Prism}
|
||||
*/
|
||||
this.prism = new Prism();
|
||||
/**
|
||||
* The current audio transcoder that is being used
|
||||
* @type {Object}
|
||||
*/
|
||||
this.currentTranscoder = null;
|
||||
this.tickInterval = null;
|
||||
this._volume = 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* An array of subscribed dispatchers
|
||||
* @type {StreamDispatcher[]}
|
||||
* @readonly
|
||||
*/
|
||||
get dispatchers() {
|
||||
let d = [];
|
||||
for (const container of this._dispatchers.values()) {
|
||||
d = d.concat(Array.from(container.values()));
|
||||
}
|
||||
return d;
|
||||
}
|
||||
|
||||
get _playableStream() {
|
||||
const currentTranscoder = this.currentTranscoder;
|
||||
if (!currentTranscoder) return null;
|
||||
const transcoder = currentTranscoder.transcoder;
|
||||
const options = currentTranscoder.options;
|
||||
return (transcoder && transcoder.output) || options.stream;
|
||||
}
|
||||
|
||||
unregisterDispatcher(dispatcher, old) {
|
||||
const volume = old || dispatcher.volume;
|
||||
|
||||
/**
|
||||
* Emitted whenever a stream dispatcher unsubscribes from the broadcast.
|
||||
* @event VoiceBroadcast#unsubscribe
|
||||
* @param {StreamDispatcher} dispatcher The unsubscribed dispatcher
|
||||
*/
|
||||
this.emit('unsubscribe', dispatcher);
|
||||
for (const container of this._dispatchers.values()) {
|
||||
container.delete(dispatcher);
|
||||
|
||||
if (!container.size) {
|
||||
this._encoders.get(volume).destroy();
|
||||
this._dispatchers.delete(volume);
|
||||
this._encoders.delete(volume);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
registerDispatcher(dispatcher) {
|
||||
if (!this._dispatchers.has(dispatcher.volume)) {
|
||||
this._dispatchers.set(dispatcher.volume, new Set());
|
||||
this._encoders.set(dispatcher.volume, OpusEncoders.fetch());
|
||||
}
|
||||
const container = this._dispatchers.get(dispatcher.volume);
|
||||
if (!container.has(dispatcher)) {
|
||||
container.add(dispatcher);
|
||||
dispatcher.once('end', () => this.unregisterDispatcher(dispatcher));
|
||||
dispatcher.on('volumeChange', (o, n) => {
|
||||
this.unregisterDispatcher(dispatcher, o);
|
||||
if (!this._dispatchers.has(n)) {
|
||||
this._dispatchers.set(n, new Set());
|
||||
this._encoders.set(n, OpusEncoders.fetch());
|
||||
}
|
||||
this._dispatchers.get(n).add(dispatcher);
|
||||
});
|
||||
/**
|
||||
* Emitted whenever a stream dispatcher subscribes to the broadcast.
|
||||
* @event VoiceBroadcast#subscribe
|
||||
* @param {StreamDispatcher} dispatcher The subscribed dispatcher
|
||||
*/
|
||||
this.emit('subscribe', dispatcher);
|
||||
}
|
||||
}
|
||||
|
||||
killCurrentTranscoder() {
|
||||
if (this.currentTranscoder) {
|
||||
if (this.currentTranscoder.transcoder) this.currentTranscoder.transcoder.kill();
|
||||
this.currentTranscoder = null;
|
||||
this.emit('end');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Plays any audio stream across the broadcast.
|
||||
* @param {ReadableStream} stream The audio stream to play
|
||||
* @param {StreamOptions} [options] Options for playing the stream
|
||||
* @returns {VoiceBroadcast}
|
||||
* @example
|
||||
* // Play streams using ytdl-core
|
||||
* const ytdl = require('ytdl-core');
|
||||
* const streamOptions = { seek: 0, volume: 1 };
|
||||
* const broadcast = client.createVoiceBroadcast();
|
||||
*
|
||||
* voiceChannel.join()
|
||||
* .then(connection => {
|
||||
* const stream = ytdl('https://www.youtube.com/watch?v=XAWgeLF9EVQ', { filter : 'audioonly' });
|
||||
* broadcast.playStream(stream);
|
||||
* const dispatcher = connection.playBroadcast(broadcast);
|
||||
* })
|
||||
* .catch(console.error);
|
||||
*/
|
||||
playStream(stream, options = {}) {
|
||||
this.setVolume(options.volume || 1);
|
||||
return this._playTranscodable(stream, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Play the given file in the voice connection.
|
||||
* @param {string} file The absolute path to the file
|
||||
* @param {StreamOptions} [options] Options for playing the stream
|
||||
* @returns {StreamDispatcher}
|
||||
* @example
|
||||
* // Play files natively
|
||||
* const broadcast = client.createVoiceBroadcast();
|
||||
*
|
||||
* voiceChannel.join()
|
||||
* .then(connection => {
|
||||
* broadcast.playFile('C:/Users/Discord/Desktop/music.mp3');
|
||||
* const dispatcher = connection.playBroadcast(broadcast);
|
||||
* })
|
||||
* .catch(console.error);
|
||||
*/
|
||||
playFile(file, options = {}) {
|
||||
this.setVolume(options.volume || 1);
|
||||
return this._playTranscodable(`file:${file}`, options);
|
||||
}
|
||||
|
||||
_playTranscodable(media, options) {
|
||||
this.killCurrentTranscoder();
|
||||
const transcoder = this.prism.transcode({
|
||||
type: 'ffmpeg',
|
||||
media,
|
||||
ffmpegArguments: ffmpegArguments.concat(['-ss', String(options.seek || 0)]),
|
||||
});
|
||||
/**
|
||||
* Emitted whenever an error occurs.
|
||||
* @event VoiceBroadcast#error
|
||||
* @param {Error} error The error that occurred
|
||||
*/
|
||||
transcoder.once('error', e => {
|
||||
if (this.listenerCount('error') > 0) this.emit('error', e);
|
||||
/**
|
||||
* Emitted whenever the VoiceBroadcast has any warnings.
|
||||
* @event VoiceBroadcast#warn
|
||||
* @param {string|Error} warning The warning that was raised
|
||||
*/
|
||||
else this.emit('warn', e);
|
||||
});
|
||||
/**
|
||||
* Emitted once the broadcast (the audio stream) ends.
|
||||
* @event VoiceBroadcast#end
|
||||
*/
|
||||
transcoder.once('end', () => this.killCurrentTranscoder());
|
||||
this.currentTranscoder = {
|
||||
transcoder,
|
||||
options,
|
||||
};
|
||||
transcoder.output.once('readable', () => this._startPlaying());
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Plays a stream of 16-bit signed stereo PCM.
|
||||
* @param {ReadableStream} stream The audio stream to play
|
||||
* @param {StreamOptions} [options] Options for playing the stream
|
||||
* @returns {VoiceBroadcast}
|
||||
*/
|
||||
playConvertedStream(stream, options = {}) {
|
||||
this.killCurrentTranscoder();
|
||||
this.setVolume(options.volume || 1);
|
||||
this.currentTranscoder = { options: { stream } };
|
||||
stream.once('readable', () => this._startPlaying());
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Plays an Opus encoded stream.
|
||||
* <warn>Note that inline volume is not compatible with this method.</warn>
|
||||
* @param {ReadableStream} stream The Opus audio stream to play
|
||||
* @param {StreamOptions} [options] Options for playing the stream
|
||||
* @returns {StreamDispatcher}
|
||||
*/
|
||||
playOpusStream(stream) {
|
||||
this.currentTranscoder = { options: { stream }, opus: true };
|
||||
stream.once('readable', () => this._startPlaying());
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Play an arbitrary input that can be [handled by ffmpeg](https://ffmpeg.org/ffmpeg-protocols.html#Description)
|
||||
* @param {string} input The arbitrary input
|
||||
* @param {StreamOptions} [options] Options for playing the stream
|
||||
* @returns {VoiceBroadcast}
|
||||
*/
|
||||
playArbitraryInput(input, options = {}) {
|
||||
this.setVolume(options.volume || 1);
|
||||
options.input = input;
|
||||
return this._playTranscodable(input, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pauses the entire broadcast - all dispatchers also pause.
|
||||
*/
|
||||
pause() {
|
||||
this.paused = true;
|
||||
for (const container of this._dispatchers.values()) {
|
||||
for (const dispatcher of container.values()) {
|
||||
dispatcher.pause();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resumes the entire broadcast - all dispatchers also resume.
|
||||
*/
|
||||
resume() {
|
||||
this.paused = false;
|
||||
for (const container of this._dispatchers.values()) {
|
||||
for (const dispatcher of container.values()) {
|
||||
dispatcher.resume();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_startPlaying() {
|
||||
if (this.tickInterval) clearInterval(this.tickInterval);
|
||||
// Old code?
|
||||
// this.tickInterval = this.client.setInterval(this.tick.bind(this), 20);
|
||||
this._startTime = Date.now();
|
||||
this._count = 0;
|
||||
this._pausedTime = 0;
|
||||
this._missed = 0;
|
||||
this.tick();
|
||||
}
|
||||
|
||||
tick() {
|
||||
if (!this._playableStream) return;
|
||||
if (this.paused) {
|
||||
this._pausedTime += 20;
|
||||
setTimeout(() => this.tick(), 20);
|
||||
return;
|
||||
}
|
||||
|
||||
const opus = this.currentTranscoder.opus;
|
||||
const buffer = this.readStreamBuffer();
|
||||
|
||||
if (!buffer) {
|
||||
this._missed++;
|
||||
if (this._missed < 5) {
|
||||
this._pausedTime += 200;
|
||||
setTimeout(() => this.tick(), 200);
|
||||
} else {
|
||||
this.killCurrentTranscoder();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this._missed = 0;
|
||||
|
||||
let packetMatrix = {};
|
||||
|
||||
const getOpusPacket = volume => {
|
||||
if (packetMatrix[volume]) return packetMatrix[volume];
|
||||
|
||||
const opusEncoder = this._encoders.get(volume);
|
||||
const opusPacket = opusEncoder.encode(this.applyVolume(buffer, this._volume * volume));
|
||||
packetMatrix[volume] = opusPacket;
|
||||
return opusPacket;
|
||||
};
|
||||
|
||||
for (const dispatcher of this.dispatchers) {
|
||||
if (opus) {
|
||||
dispatcher.processPacket(buffer);
|
||||
continue;
|
||||
}
|
||||
|
||||
const volume = dispatcher.volume;
|
||||
dispatcher.processPacket(getOpusPacket(volume));
|
||||
}
|
||||
|
||||
const next = 20 + (this._startTime + this._pausedTime + (this._count * 20) - Date.now());
|
||||
this._count++;
|
||||
setTimeout(() => this.tick(), next);
|
||||
}
|
||||
|
||||
readStreamBuffer() {
|
||||
const opus = this.currentTranscoder.opus;
|
||||
const bufferLength = (opus ? 80 : 1920) * 2;
|
||||
let buffer = this._playableStream.read(bufferLength);
|
||||
if (opus) return buffer;
|
||||
if (!buffer) return null;
|
||||
|
||||
if (buffer.length !== bufferLength) {
|
||||
const newBuffer = Buffer.alloc(bufferLength).fill(0);
|
||||
buffer.copy(newBuffer);
|
||||
buffer = newBuffer;
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the current stream from playing without unsubscribing dispatchers.
|
||||
*/
|
||||
end() {
|
||||
this.killCurrentTranscoder();
|
||||
}
|
||||
|
||||
/**
|
||||
* End the current broadcast, all subscribed dispatchers will also end.
|
||||
*/
|
||||
destroy() {
|
||||
this.end();
|
||||
for (const container of this._dispatchers.values()) {
|
||||
for (const dispatcher of container.values()) {
|
||||
dispatcher.destroy('end', 'broadcast ended');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = VoiceBroadcast;
|
||||
@@ -1,36 +1,67 @@
|
||||
const VoiceWebSocket = require('./VoiceWebSocket');
|
||||
const VoiceUDP = require('./VoiceUDPClient');
|
||||
const Util = require('../../util/Util');
|
||||
const Constants = require('../../util/Constants');
|
||||
const AudioPlayer = require('./player/AudioPlayer');
|
||||
const VoiceReceiver = require('./receiver/VoiceReceiver');
|
||||
const EventEmitter = require('events').EventEmitter;
|
||||
const fs = require('fs');
|
||||
const Prism = require('prism-media');
|
||||
|
||||
/**
|
||||
* Represents a connection to a Voice Channel in Discord.
|
||||
* Represents a connection to a guild's voice server.
|
||||
* ```js
|
||||
* // obtained using:
|
||||
* voiceChannel.join().then(connection => {
|
||||
* // Obtained using:
|
||||
* voiceChannel.join()
|
||||
* .then(connection => {
|
||||
*
|
||||
* });
|
||||
* });
|
||||
* ```
|
||||
* @extends {EventEmitter}
|
||||
*/
|
||||
class VoiceConnection extends EventEmitter {
|
||||
|
||||
constructor(pendingConnection) {
|
||||
constructor(voiceManager, channel) {
|
||||
super();
|
||||
|
||||
/**
|
||||
* The Voice Manager that instantiated this connection
|
||||
* The voice manager that instantiated this connection
|
||||
* @type {ClientVoiceManager}
|
||||
*/
|
||||
this.voiceManager = pendingConnection.voiceManager;
|
||||
this.voiceManager = voiceManager;
|
||||
|
||||
/**
|
||||
* The client that instantiated this connection
|
||||
* @type {Client}
|
||||
*/
|
||||
this.client = voiceManager.client;
|
||||
|
||||
/**
|
||||
* @external Prism
|
||||
* @see {@link https://github.com/hydrabolt/prism-media}
|
||||
*/
|
||||
|
||||
/**
|
||||
* The audio transcoder for this connection
|
||||
* @type {Prism}
|
||||
*/
|
||||
this.prism = new Prism();
|
||||
|
||||
/**
|
||||
* The voice channel this connection is currently serving
|
||||
* @type {VoiceChannel}
|
||||
*/
|
||||
this.channel = pendingConnection.channel;
|
||||
this.channel = channel;
|
||||
|
||||
/**
|
||||
* The current status of the voice connection
|
||||
* @type {number}
|
||||
*/
|
||||
this.status = Constants.VoiceStatus.AUTHENTICATING;
|
||||
|
||||
/**
|
||||
* Whether we're currently transmitting audio
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.speaking = false;
|
||||
|
||||
/**
|
||||
* An array of Voice Receivers that have been created for this connection
|
||||
@@ -40,10 +71,10 @@ class VoiceConnection extends EventEmitter {
|
||||
|
||||
/**
|
||||
* The authentication data needed to connect to the voice server
|
||||
* @type {object}
|
||||
* @type {Object}
|
||||
* @private
|
||||
*/
|
||||
this.authentication = pendingConnection.data;
|
||||
this.authentication = {};
|
||||
|
||||
/**
|
||||
* The audio player for this voice connection
|
||||
@@ -53,21 +84,20 @@ class VoiceConnection extends EventEmitter {
|
||||
|
||||
this.player.on('debug', m => {
|
||||
/**
|
||||
* Debug info from the connection
|
||||
* Debug info from the connection.
|
||||
* @event VoiceConnection#debug
|
||||
* @param {string} message the debug message
|
||||
* @param {string} message The debug message
|
||||
*/
|
||||
this.emit('debug', `audio player - ${m}`);
|
||||
});
|
||||
|
||||
this.player.on('error', e => {
|
||||
/**
|
||||
* Warning info from the connection
|
||||
* Warning info from the connection.
|
||||
* @event VoiceConnection#warn
|
||||
* @param {string|error} warning the warning
|
||||
* @param {string|Error} warning The warning
|
||||
*/
|
||||
this.emit('warn', e);
|
||||
this.player.cleanup();
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -79,20 +109,31 @@ class VoiceConnection extends EventEmitter {
|
||||
|
||||
/**
|
||||
* Object that wraps contains the `ws` and `udp` sockets of this voice connection
|
||||
* @type {object}
|
||||
* @type {Object}
|
||||
* @private
|
||||
*/
|
||||
this.sockets = {};
|
||||
this.connect();
|
||||
|
||||
this.authenticate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether the voice connection should display as "speaking" or not
|
||||
* @param {boolean} value whether or not to speak
|
||||
* The current stream dispatcher (if any)
|
||||
* @type {?StreamDispatcher}
|
||||
* @readonly
|
||||
*/
|
||||
get dispatcher() {
|
||||
return this.player.dispatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether the voice connection should display as "speaking" or not.
|
||||
* @param {boolean} value Whether or not to speak
|
||||
* @private
|
||||
*/
|
||||
setSpeaking(value) {
|
||||
if (this.speaking === value) return;
|
||||
if (this.status !== Constants.VoiceStatus.CONNECTED) return;
|
||||
this.speaking = value;
|
||||
this.sockets.ws.sendPacket({
|
||||
op: Constants.VoiceOPCodes.SPEAKING,
|
||||
@@ -100,97 +141,289 @@ class VoiceConnection extends EventEmitter {
|
||||
speaking: true,
|
||||
delay: 0,
|
||||
},
|
||||
})
|
||||
.catch(e => {
|
||||
}).catch(e => {
|
||||
this.emit('debug', e);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a request to the main gateway to join a voice channel.
|
||||
* @param {Object} [options] The options to provide
|
||||
*/
|
||||
sendVoiceStateUpdate(options = {}) {
|
||||
options = Util.mergeDefault({
|
||||
guild_id: this.channel.guild.id,
|
||||
channel_id: this.channel.id,
|
||||
self_mute: false,
|
||||
self_deaf: false,
|
||||
}, options);
|
||||
|
||||
this.client.ws.send({
|
||||
op: Constants.OPCodes.VOICE_STATE_UPDATE,
|
||||
d: options,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the token and endpoint required to connect to the voice servers.
|
||||
* @param {string} token The voice token
|
||||
* @param {string} endpoint The voice endpoint
|
||||
* @returns {void}
|
||||
*/
|
||||
setTokenAndEndpoint(token, endpoint) {
|
||||
if (!endpoint) {
|
||||
// Signifies awaiting endpoint stage
|
||||
return;
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
this.authenticateFailed('Token not provided from voice server packet.');
|
||||
return;
|
||||
}
|
||||
|
||||
endpoint = endpoint.match(/([^:]*)/)[0];
|
||||
|
||||
if (!endpoint) {
|
||||
this.authenticateFailed('Invalid endpoint received.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.status === Constants.VoiceStatus.AUTHENTICATING) {
|
||||
this.authentication.token = token;
|
||||
this.authentication.endpoint = endpoint;
|
||||
this.checkAuthenticated();
|
||||
} else if (token !== this.authentication.token || endpoint !== this.authentication.endpoint) {
|
||||
this.reconnect(token, endpoint);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Session ID for the connection.
|
||||
* @param {string} sessionID The voice session ID
|
||||
*/
|
||||
setSessionID(sessionID) {
|
||||
if (!sessionID) {
|
||||
this.authenticateFailed('Session ID not supplied.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.status === Constants.VoiceStatus.AUTHENTICATING) {
|
||||
this.authentication.sessionID = sessionID;
|
||||
this.checkAuthenticated();
|
||||
} else if (sessionID !== this.authentication.sessionID) {
|
||||
this.authentication.sessionID = sessionID;
|
||||
/**
|
||||
* Emitted when a new session ID is received.
|
||||
* @event VoiceConnection#newSession
|
||||
* @private
|
||||
*/
|
||||
this.emit('newSession', sessionID);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the voice connection is authenticated.
|
||||
* @private
|
||||
*/
|
||||
checkAuthenticated() {
|
||||
const { token, endpoint, sessionID } = this.authentication;
|
||||
|
||||
if (token && endpoint && sessionID) {
|
||||
clearTimeout(this.connectTimeout);
|
||||
this.status = Constants.VoiceStatus.CONNECTING;
|
||||
/**
|
||||
* Emitted when we successfully initiate a voice connection.
|
||||
* @event VoiceConnection#authenticated
|
||||
*/
|
||||
this.emit('authenticated');
|
||||
this.connect();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when we fail to initiate a voice connection.
|
||||
* @param {string} reason The reason for failure
|
||||
* @private
|
||||
*/
|
||||
authenticateFailed(reason) {
|
||||
clearTimeout(this.connectTimeout);
|
||||
if (this.status === Constants.VoiceStatus.AUTHENTICATING) {
|
||||
/**
|
||||
* Emitted when we fail to initiate a voice connection.
|
||||
* @event VoiceConnection#failed
|
||||
* @param {Error} error The encountered error
|
||||
*/
|
||||
this.emit('failed', new Error(reason));
|
||||
} else {
|
||||
this.emit('error', new Error(reason));
|
||||
}
|
||||
this.status = Constants.VoiceStatus.DISCONNECTED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move to a different voice channel in the same guild.
|
||||
* @param {VoiceChannel} channel The channel to move to
|
||||
* @private
|
||||
*/
|
||||
updateChannel(channel) {
|
||||
this.channel = channel;
|
||||
this.sendVoiceStateUpdate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to authenticate to the voice server.
|
||||
* @private
|
||||
*/
|
||||
authenticate() {
|
||||
this.sendVoiceStateUpdate();
|
||||
this.connectTimeout = this.client.setTimeout(
|
||||
() => this.authenticateFailed(new Error('Connection not established within 15 seconds.')), 15000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to reconnect to the voice server (typically after a region change).
|
||||
* @param {string} token The voice token
|
||||
* @param {string} endpoint The voice endpoint
|
||||
* @private
|
||||
*/
|
||||
reconnect(token, endpoint) {
|
||||
this.authentication.token = token;
|
||||
this.authentication.endpoint = endpoint;
|
||||
|
||||
this.status = Constants.VoiceStatus.RECONNECTING;
|
||||
/**
|
||||
* Emitted when the voice connection is reconnecting (typically after a region change).
|
||||
* @event VoiceConnection#reconnecting
|
||||
*/
|
||||
this.emit('reconnecting');
|
||||
this.connect();
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect the voice connection, causing a disconnect and closing event to be emitted.
|
||||
*/
|
||||
disconnect() {
|
||||
this.emit('closing');
|
||||
this.voiceManager.client.ws.send({
|
||||
op: Constants.OPCodes.VOICE_STATE_UPDATE,
|
||||
d: {
|
||||
guild_id: this.channel.guild.id,
|
||||
channel_id: null,
|
||||
self_mute: false,
|
||||
self_deaf: false,
|
||||
},
|
||||
this.sendVoiceStateUpdate({
|
||||
channel_id: null,
|
||||
});
|
||||
this.player.destroy();
|
||||
this.cleanup();
|
||||
this.status = Constants.VoiceStatus.DISCONNECTED;
|
||||
/**
|
||||
* Emitted when the voice connection disconnects
|
||||
* Emitted when the voice connection disconnects.
|
||||
* @event VoiceConnection#disconnect
|
||||
*/
|
||||
this.emit('disconnect');
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect the voice connection
|
||||
* Cleans up after disconnect.
|
||||
* @private
|
||||
*/
|
||||
cleanup() {
|
||||
const { ws, udp } = this.sockets;
|
||||
|
||||
if (ws) {
|
||||
ws.removeAllListeners('error');
|
||||
ws.removeAllListeners('ready');
|
||||
ws.removeAllListeners('sessionDescription');
|
||||
ws.removeAllListeners('speaking');
|
||||
}
|
||||
|
||||
if (udp) udp.removeAllListeners('error');
|
||||
|
||||
this.sockets.ws = null;
|
||||
this.sockets.udp = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect the voice connection.
|
||||
* @private
|
||||
*/
|
||||
connect() {
|
||||
if (this.sockets.ws) throw new Error('There is already an existing WebSocket connection.');
|
||||
if (this.sockets.udp) throw new Error('There is already an existing UDP connection.');
|
||||
if (this.status !== Constants.VoiceStatus.RECONNECTING) {
|
||||
if (this.sockets.ws) throw new Error('There is already an existing WebSocket connection.');
|
||||
if (this.sockets.udp) throw new Error('There is already an existing UDP connection.');
|
||||
}
|
||||
|
||||
if (this.sockets.ws) this.sockets.ws.shutdown();
|
||||
if (this.sockets.udp) this.sockets.udp.shutdown();
|
||||
|
||||
this.sockets.ws = new VoiceWebSocket(this);
|
||||
this.sockets.udp = new VoiceUDP(this);
|
||||
this.sockets.ws.on('error', e => this.emit('error', e));
|
||||
this.sockets.udp.on('error', e => this.emit('error', e));
|
||||
this.sockets.ws.once('ready', d => {
|
||||
this.authentication.port = d.port;
|
||||
this.authentication.ssrc = d.ssrc;
|
||||
/**
|
||||
* Emitted whenever the connection encounters an error.
|
||||
* @event VoiceConnection#error
|
||||
* @param {Error} error the encountered error
|
||||
*/
|
||||
this.sockets.udp.findEndpointAddress()
|
||||
.then(address => {
|
||||
this.sockets.udp.createUDPSocket(address);
|
||||
})
|
||||
.catch(e => this.emit('error', e));
|
||||
});
|
||||
this.sockets.ws.once('sessionDescription', (mode, secret) => {
|
||||
this.authentication.encryptionMode = mode;
|
||||
this.authentication.secretKey = secret;
|
||||
/**
|
||||
* Emitted once the connection is ready, when a promise to join a voice channel resolves,
|
||||
* the connection will already be ready.
|
||||
* @event VoiceConnection#ready
|
||||
*/
|
||||
this.emit('ready');
|
||||
});
|
||||
this.sockets.ws.on('speaking', data => {
|
||||
const guild = this.channel.guild;
|
||||
const user = this.voiceManager.client.users.get(data.user_id);
|
||||
this.ssrcMap.set(+data.ssrc, user);
|
||||
if (!data.speaking) {
|
||||
for (const receiver of this.receivers) {
|
||||
const opusStream = receiver.opusStreams.get(user.id);
|
||||
const pcmStream = receiver.pcmStreams.get(user.id);
|
||||
if (opusStream) {
|
||||
opusStream.push(null);
|
||||
opusStream.open = false;
|
||||
receiver.opusStreams.delete(user.id);
|
||||
}
|
||||
if (pcmStream) {
|
||||
pcmStream.push(null);
|
||||
pcmStream.open = false;
|
||||
receiver.pcmStreams.delete(user.id);
|
||||
}
|
||||
}
|
||||
|
||||
const { ws, udp } = this.sockets;
|
||||
|
||||
ws.on('error', err => this.emit('error', err));
|
||||
udp.on('error', err => this.emit('error', err));
|
||||
ws.on('ready', this.onReady.bind(this));
|
||||
ws.on('sessionDescription', this.onSessionDescription.bind(this));
|
||||
ws.on('speaking', this.onSpeaking.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when the voice websocket is ready.
|
||||
* @param {Object} data The received data
|
||||
* @private
|
||||
*/
|
||||
onReady({ port, ssrc }) {
|
||||
this.authentication.port = port;
|
||||
this.authentication.ssrc = ssrc;
|
||||
|
||||
const udp = this.sockets.udp;
|
||||
/**
|
||||
* Emitted whenever the connection encounters an error.
|
||||
* @event VoiceConnection#error
|
||||
* @param {Error} error The encountered error
|
||||
*/
|
||||
udp.findEndpointAddress()
|
||||
.then(address => {
|
||||
udp.createUDPSocket(address);
|
||||
}, e => this.emit('error', e));
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when a session description is received.
|
||||
* @param {string} mode The encryption mode
|
||||
* @param {string} secret The secret key
|
||||
* @private
|
||||
*/
|
||||
onSessionDescription(mode, secret) {
|
||||
this.authentication.encryptionMode = mode;
|
||||
this.authentication.secretKey = secret;
|
||||
|
||||
this.status = Constants.VoiceStatus.CONNECTED;
|
||||
/**
|
||||
* Emitted once the connection is ready, when a promise to join a voice channel resolves,
|
||||
* the connection will already be ready.
|
||||
* @event VoiceConnection#ready
|
||||
*/
|
||||
this.emit('ready');
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when a speaking event is received.
|
||||
* @param {Object} data The received data
|
||||
* @private
|
||||
*/
|
||||
onSpeaking({ user_id, ssrc, speaking }) {
|
||||
const guild = this.channel.guild;
|
||||
const user = this.client.users.get(user_id);
|
||||
this.ssrcMap.set(+ssrc, user);
|
||||
if (!speaking) {
|
||||
for (const receiver of this.receivers) {
|
||||
receiver.stoppedSpeaking(user);
|
||||
}
|
||||
/**
|
||||
* Emitted whenever a user starts/stops speaking
|
||||
* @event VoiceConnection#speaking
|
||||
* @param {User} user The user that has started/stopped speaking
|
||||
* @param {boolean} speaking Whether or not the user is speaking
|
||||
*/
|
||||
if (this.ready) this.emit('speaking', user, data.speaking);
|
||||
guild._memberSpeakUpdate(data.user_id, data.speaking);
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Emitted whenever a user starts/stops speaking.
|
||||
* @event VoiceConnection#speaking
|
||||
* @param {User} user The user that has started/stopped speaking
|
||||
* @param {boolean} speaking Whether or not the user is speaking
|
||||
*/
|
||||
if (this.status === Constants.VoiceStatus.CONNECTED) this.emit('speaking', user, speaking);
|
||||
guild._memberSpeakUpdate(user_id, speaking);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -199,23 +432,35 @@ class VoiceConnection extends EventEmitter {
|
||||
* @property {number} [seek=0] The time to seek to
|
||||
* @property {number} [volume=1] The volume to play at
|
||||
* @property {number} [passes=1] How many times to send the voice packet to reduce packet loss
|
||||
* @property {number|string} [bitrate=48000] The bitrate (quality) of the audio.
|
||||
* If set to 'auto', the voice channel's bitrate will be used
|
||||
*/
|
||||
|
||||
/**
|
||||
* Play the given file in the voice connection.
|
||||
* @param {string} file The path to the file
|
||||
* @param {string} file The absolute path to the file
|
||||
* @param {StreamOptions} [options] Options for playing the stream
|
||||
* @returns {StreamDispatcher}
|
||||
* @example
|
||||
* // play files natively
|
||||
* // Play files natively
|
||||
* voiceChannel.join()
|
||||
* .then(connection => {
|
||||
* const dispatcher = connection.playFile('C:/Users/Discord/Desktop/music.mp3');
|
||||
* })
|
||||
* .catch(console.error);
|
||||
* .then(connection => {
|
||||
* const dispatcher = connection.playFile('C:/Users/Discord/Desktop/music.mp3');
|
||||
* })
|
||||
* .catch(console.error);
|
||||
*/
|
||||
playFile(file, options) {
|
||||
return this.playStream(fs.createReadStream(file), options);
|
||||
return this.player.playUnknownStream(`file:${file}`, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Play an arbitrary input that can be [handled by ffmpeg](https://ffmpeg.org/ffmpeg-protocols.html#Description)
|
||||
* @param {string} input the arbitrary input
|
||||
* @param {StreamOptions} [options] Options for playing the stream
|
||||
* @returns {StreamDispatcher}
|
||||
*/
|
||||
playArbitraryInput(input, options) {
|
||||
return this.player.playUnknownStream(input, options);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -224,34 +469,60 @@ class VoiceConnection extends EventEmitter {
|
||||
* @param {StreamOptions} [options] Options for playing the stream
|
||||
* @returns {StreamDispatcher}
|
||||
* @example
|
||||
* // play streams using ytdl-core
|
||||
* // Play streams using ytdl-core
|
||||
* const ytdl = require('ytdl-core');
|
||||
* const streamOptions = { seek: 0, volume: 1 };
|
||||
* voiceChannel.join()
|
||||
* .then(connection => {
|
||||
* const stream = ytdl('https://www.youtube.com/watch?v=XAWgeLF9EVQ', {filter : 'audioonly'});
|
||||
* const dispatcher = connection.playStream(stream, streamOptions);
|
||||
* })
|
||||
* .catch(console.error);
|
||||
* .then(connection => {
|
||||
* const stream = ytdl('https://www.youtube.com/watch?v=XAWgeLF9EVQ', { filter : 'audioonly' });
|
||||
* const dispatcher = connection.playStream(stream, streamOptions);
|
||||
* })
|
||||
* .catch(console.error);
|
||||
*/
|
||||
playStream(stream, { seek = 0, volume = 1, passes = 1 } = {}) {
|
||||
const options = { seek, volume, passes };
|
||||
playStream(stream, options) {
|
||||
return this.player.playUnknownStream(stream, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Plays a stream of 16-bit signed stereo PCM at 48KHz.
|
||||
* @param {ReadableStream} stream The audio stream to play.
|
||||
* Plays a stream of 16-bit signed stereo PCM.
|
||||
* @param {ReadableStream} stream The audio stream to play
|
||||
* @param {StreamOptions} [options] Options for playing the stream
|
||||
* @returns {StreamDispatcher}
|
||||
*/
|
||||
playConvertedStream(stream, { seek = 0, volume = 1, passes = 1 } = {}) {
|
||||
const options = { seek, volume, passes };
|
||||
playConvertedStream(stream, options) {
|
||||
return this.player.playPCMStream(stream, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a VoiceReceiver so you can start listening to voice data. It's recommended to only create one of these.
|
||||
* Plays an Opus encoded stream.
|
||||
* <warn>Note that inline volume is not compatible with this method.</warn>
|
||||
* @param {ReadableStream} stream The Opus audio stream to play
|
||||
* @param {StreamOptions} [options] Options for playing the stream
|
||||
* @returns {StreamDispatcher}
|
||||
*/
|
||||
playOpusStream(stream, options) {
|
||||
return this.player.playOpusStream(stream, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Plays a voice broadcast.
|
||||
* @param {VoiceBroadcast} broadcast The broadcast to play
|
||||
* @param {StreamOptions} [options] Options for playing the stream
|
||||
* @returns {StreamDispatcher}
|
||||
* @example
|
||||
* // Play a broadcast
|
||||
* const broadcast = client
|
||||
* .createVoiceBroadcast()
|
||||
* .playFile('./test.mp3');
|
||||
* const dispatcher = voiceConnection.playBroadcast(broadcast);
|
||||
*/
|
||||
playBroadcast(broadcast, options) {
|
||||
return this.player.playBroadcast(broadcast, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a VoiceReceiver so you can start listening to voice data.
|
||||
* It's recommended to only create one of these.
|
||||
* @returns {VoiceReceiver}
|
||||
*/
|
||||
createReceiver() {
|
||||
|
||||
@@ -3,20 +3,8 @@ const dns = require('dns');
|
||||
const Constants = require('../../util/Constants');
|
||||
const EventEmitter = require('events').EventEmitter;
|
||||
|
||||
function parseLocalPacket(message) {
|
||||
try {
|
||||
const packet = new Buffer(message);
|
||||
let address = '';
|
||||
for (let i = 4; i < packet.indexOf(0, i); i++) address += String.fromCharCode(packet[i]);
|
||||
const port = parseInt(packet.readUIntLE(packet.length - 2, 2).toString(10), 10);
|
||||
return { address, port };
|
||||
} catch (error) {
|
||||
return { error };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a UDP Client for a Voice Connection
|
||||
* Represents a UDP client for a Voice Connection.
|
||||
* @extends {EventEmitter}
|
||||
* @private
|
||||
*/
|
||||
@@ -37,7 +25,7 @@ class VoiceConnectionUDPClient extends EventEmitter {
|
||||
this.socket = null;
|
||||
|
||||
/**
|
||||
* The address of the discord voice server
|
||||
* The address of the Discord voice server
|
||||
* @type {?string}
|
||||
*/
|
||||
this.discordAddress = null;
|
||||
@@ -59,17 +47,17 @@ class VoiceConnectionUDPClient extends EventEmitter {
|
||||
|
||||
shutdown() {
|
||||
if (this.socket) {
|
||||
this.socket.removeAllListeners('message');
|
||||
try {
|
||||
this.socket.close();
|
||||
} catch (e) {
|
||||
return;
|
||||
} finally {
|
||||
this.socket = null;
|
||||
}
|
||||
this.socket = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The port of the discord voice server
|
||||
* The port of the Discord voice server
|
||||
* @type {number}
|
||||
* @readonly
|
||||
*/
|
||||
@@ -78,7 +66,7 @@ class VoiceConnectionUDPClient extends EventEmitter {
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to resolve the voice server endpoint to an address
|
||||
* Tries to resolve the voice server endpoint to an address.
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
findEndpointAddress() {
|
||||
@@ -95,8 +83,8 @@ class VoiceConnectionUDPClient extends EventEmitter {
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a packet to the UDP client
|
||||
* @param {Object} packet the packet to send
|
||||
* Send a packet to the UDP client.
|
||||
* @param {Object} packet The packet to send
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
send(packet) {
|
||||
@@ -136,10 +124,22 @@ class VoiceConnectionUDPClient extends EventEmitter {
|
||||
});
|
||||
});
|
||||
|
||||
const blankMessage = new Buffer(70);
|
||||
const blankMessage = Buffer.alloc(70);
|
||||
blankMessage.writeUIntBE(this.voiceConnection.authentication.ssrc, 0, 4);
|
||||
this.send(blankMessage);
|
||||
}
|
||||
}
|
||||
|
||||
function parseLocalPacket(message) {
|
||||
try {
|
||||
const packet = Buffer.from(message);
|
||||
let address = '';
|
||||
for (let i = 4; i < packet.indexOf(0, i); i++) address += String.fromCharCode(packet[i]);
|
||||
const port = parseInt(packet.readUIntLE(packet.length - 2, 2).toString(10), 10);
|
||||
return { address, port };
|
||||
} catch (error) {
|
||||
return { error };
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = VoiceConnectionUDPClient;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user