Compare commits
783
Commits
v3.5
...
v26.2-pre-3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4eef1867e3 | ||
|
|
a7598d26aa | ||
|
|
762fcf45c3 | ||
|
|
ea2a8c97ae | ||
|
|
004b638dbf | ||
|
|
69371ae619 | ||
|
|
b145d5fa01 | ||
|
|
8c9fcb32e0 | ||
|
|
17d45de378 | ||
|
|
b96c60912c | ||
|
|
e97e281bda | ||
|
|
00cebb7bb5 | ||
|
|
014f263bf1 | ||
|
|
942d71040b | ||
|
|
92d21e7fc0 | ||
|
|
07e4705a6a | ||
|
|
6320296f0f | ||
|
|
f4b14e6c01 | ||
|
|
7e22831d08 | ||
|
|
9332fc9254 | ||
|
|
018c949104 | ||
|
|
b55976508e | ||
|
|
264dcb3053 | ||
|
|
22c33c0f7a | ||
|
|
57d420a9f4 | ||
|
|
f8cc08ba3e | ||
|
|
60d5c4cb1f | ||
|
|
0288e2f4de | ||
|
|
33d5a79759 | ||
|
|
30b89a3cf9 | ||
|
|
427dc2d040 | ||
|
|
033f32d596 | ||
|
|
d5fa42b55d | ||
|
|
4bb35b1e50 | ||
|
|
d451857d7b | ||
|
|
78b804176a | ||
|
|
15b780ab62 | ||
|
|
9448c4e3d6 | ||
|
|
4af9759700 | ||
|
|
835cbbe937 | ||
|
|
491bb9ba65 | ||
|
|
ced29535f9 | ||
|
|
d99e9a105b | ||
|
|
c72c7c5c34 | ||
|
|
23960bc30d | ||
|
|
abb48c281b | ||
|
|
7acb825713 | ||
|
|
dd2f329485 | ||
|
|
75577ec0bd | ||
|
|
7366bcda71 | ||
|
|
23e43cfad3 | ||
|
|
ad9a8085aa | ||
|
|
2f23e8f23d | ||
|
|
4c07b8a236 | ||
|
|
571bd586fa | ||
|
|
eac2d88873 | ||
|
|
93c793f393 | ||
|
|
1aa97b63f9 | ||
|
|
c1b76a2b84 | ||
|
|
a1429aa723 | ||
|
|
2de7c7b19c | ||
|
|
7383bd2c48 | ||
|
|
a05d65099b | ||
|
|
c6db929d12 | ||
|
|
dc88f6c51a | ||
|
|
d69fb0e26d | ||
|
|
4b2d53d3f4 | ||
|
|
e3fa8b6d72 | ||
|
|
5c6a9f7306 | ||
|
|
71e33dbf66 | ||
|
|
d468ab1575 | ||
|
|
74482af471 | ||
|
|
0b8b092cb9 | ||
|
|
3cdfaef942 | ||
|
|
fcf441a190 | ||
|
|
5a1c4ad99c | ||
|
|
d9cb72f22e | ||
|
|
58eb37124e | ||
|
|
afeeda3a8d | ||
|
|
fece90d2bd | ||
|
|
357a85fa3d | ||
|
|
f34158caa6 | ||
|
|
1bb343e49a | ||
|
|
dffa5d4161 | ||
|
|
13590b08a8 | ||
|
|
6e14ce209d | ||
|
|
72b8c2247e | ||
|
|
925504a928 | ||
|
|
236d67707f | ||
|
|
753f432c95 | ||
|
|
c824bc51b2 | ||
|
|
39ee540b55 | ||
|
|
903b214051 | ||
|
|
ac0daf5fe7 | ||
|
|
80e38a07e9 | ||
|
|
d2598e09f7 | ||
|
|
28aab47150 | ||
|
|
81bda67ae4 | ||
|
|
25bbd1ff85 | ||
|
|
164d695a5f | ||
|
|
57e4a3e77c | ||
|
|
a6ac532b2b | ||
|
|
4f14722e14 | ||
|
|
9a6eece7a1 | ||
|
|
fd65d0a9a7 | ||
|
|
48500fe8a4 | ||
|
|
8d8d9e4bee | ||
|
|
dcbddf9f23 | ||
|
|
7449c0a61a | ||
|
|
c95b8ccb32 | ||
|
|
2c5bfe8e33 | ||
|
|
a3bbd97333 | ||
|
|
4ea488951d | ||
|
|
481424d5a6 | ||
|
|
fe82e96cc1 | ||
|
|
976258c959 | ||
|
|
83cf0689e2 | ||
|
|
c911cb4423 | ||
|
|
4eadef8b65 | ||
|
|
4e081358ff | ||
|
|
209ec0b435 | ||
|
|
6d363abb54 | ||
|
|
6f8b84aeda | ||
|
|
5b8c5da33a | ||
|
|
b6ef0f95cb | ||
|
|
cce1480a42 | ||
|
|
44fbe9f2d6 | ||
|
|
29db6c7d67 | ||
|
|
26e9b56961 | ||
|
|
979271cfb5 | ||
|
|
69f0b00ed3 | ||
|
|
c1a1839457 | ||
|
|
dd4b53800e | ||
|
|
1fc94528fc | ||
|
|
bb1a817980 | ||
|
|
53305acbc7 | ||
|
|
d0f1bae0bd | ||
|
|
c0cf3b1bb7 | ||
|
|
ce67b668d5 | ||
|
|
3273cfd0e1 | ||
|
|
fd992a6514 | ||
|
|
550d681fe1 | ||
|
|
7d29226953 | ||
|
|
38da72ca91 | ||
|
|
eab11361cb | ||
|
|
b281bba36f | ||
|
|
5e38573aed | ||
|
|
da7b6d487c | ||
|
|
6bf4e5f4bb | ||
|
|
213f532a3c | ||
|
|
45bd88e4d2 | ||
|
|
4eb8658459 | ||
|
|
b37118fee2 | ||
|
|
026caba68a | ||
|
|
2003f8801a | ||
|
|
473bacf13a | ||
|
|
8b3f2bc61a | ||
|
|
3aa373c33c | ||
|
|
9b6263e583 | ||
|
|
3e066d08ce | ||
|
|
aa48e28162 | ||
|
|
dc660ed94a | ||
|
|
e02ecf9553 | ||
|
|
23acc0926f | ||
|
|
3fc1cd18d5 | ||
|
|
ebe29b403e | ||
|
|
dcba3312dd | ||
|
|
7fddeeec37 | ||
|
|
b5341b023f | ||
|
|
bbb7443ef2 | ||
|
|
22df347bb9 | ||
|
|
9aed6b8f35 | ||
|
|
9e7ec9f1bd | ||
|
|
373f43eac5 | ||
|
|
6c91b2791a | ||
|
|
bde62a431f | ||
|
|
615a074540 | ||
|
|
09b335ac46 | ||
|
|
2701880c77 | ||
|
|
24c8f2fb38 | ||
|
|
c3ec158ac3 | ||
|
|
b3690222d0 | ||
|
|
4d7583785b | ||
|
|
17ad1a15db | ||
|
|
4c89de9e76 | ||
|
|
5f25af41f9 | ||
|
|
3a3ee357fc | ||
|
|
08a6d27903 | ||
|
|
33f7675aac | ||
|
|
1178a0a611 | ||
|
|
96ded77bec | ||
|
|
e83ca8cb2b | ||
|
|
e507176d39 | ||
|
|
4e554b8bee | ||
|
|
bc649caa99 | ||
|
|
f7a4c7b616 | ||
|
|
2c7be5882c | ||
|
|
6107d268f7 | ||
|
|
7760699155 | ||
|
|
acf349c72e | ||
|
|
8096e5b882 | ||
|
|
873cfc7dc2 | ||
|
|
038174bc7e | ||
|
|
14881096bb | ||
|
|
ed1905d8f4 | ||
|
|
72c0a2efa3 | ||
|
|
e599d48713 | ||
|
|
7f44634d4f | ||
|
|
44170caac6 | ||
|
|
adedfadd92 | ||
|
|
6b01d0c8fe | ||
|
|
d4d96f59ab | ||
|
|
a1bf7ed6bd | ||
|
|
306e20efc9 | ||
|
|
f593f6e0a8 | ||
|
|
74646a4318 | ||
|
|
8bfcba8832 | ||
|
|
4ba5621a4d | ||
|
|
1f928324c2 | ||
|
|
3771872cd2 | ||
|
|
f359a60343 | ||
|
|
f53afd7bc9 | ||
|
|
e7dcc2ab29 | ||
|
|
0118b9e591 | ||
|
|
ffe42f9777 | ||
|
|
5a467599e9 | ||
|
|
c56e78484a | ||
|
|
1ee0d4a09e | ||
|
|
ffdce5820b | ||
|
|
9fed504f37 | ||
|
|
6c5799be0d | ||
|
|
b6747a0340 | ||
|
|
cd6df02e30 | ||
|
|
edbef6a36f | ||
|
|
b6a0016f5f | ||
|
|
e740c17837 | ||
|
|
7eb949f109 | ||
|
|
02394e310d | ||
|
|
ef83832819 | ||
|
|
a82101ab30 | ||
|
|
007c78930e | ||
|
|
300c83512c | ||
|
|
b674872c56 | ||
|
|
12a7680275 | ||
|
|
c9cf7c942b | ||
|
|
8e0326c8f8 | ||
|
|
13972119a8 | ||
|
|
acf165a119 | ||
|
|
bdff7c6069 | ||
|
|
e2e3376c87 | ||
|
|
3c613088e5 | ||
|
|
7e9ece50a0 | ||
|
|
af81b6536a | ||
|
|
4cd3ecf81f | ||
|
|
31f6b9e08e | ||
|
|
02de920cb3 | ||
|
|
84c0905297 | ||
|
|
250583f859 | ||
|
|
ee7d42196d | ||
|
|
a397de2b9f | ||
|
|
0610df9400 | ||
|
|
ed583deb31 | ||
|
|
e039048fc6 | ||
|
|
f2ea9650c4 | ||
|
|
1e89057006 | ||
|
|
019bb4842f | ||
|
|
4cb45beded | ||
|
|
8a2059f087 | ||
|
|
a6b105a2e1 | ||
|
|
6c14a6a06c | ||
|
|
cddd5d0026 | ||
|
|
78dea91e1d | ||
|
|
3df591ec52 | ||
|
|
1c4032caa6 | ||
|
|
465c422171 | ||
|
|
284e8dc6f5 | ||
|
|
3fc905c82e | ||
|
|
55a42b88ae | ||
|
|
bd26162e4a | ||
|
|
62e7e33406 | ||
|
|
2f0ddcec3f | ||
|
|
7cdfa68e0e | ||
|
|
f8939a964d | ||
|
|
7830e5dda5 | ||
|
|
6aa2f22294 | ||
|
|
50b14e3d44 | ||
|
|
e09c2192c2 | ||
|
|
b725d2cc62 | ||
|
|
abf3f617a4 | ||
|
|
1eda7e66fe | ||
|
|
b5000531c4 | ||
|
|
d4f95e752b | ||
|
|
e31c611f70 | ||
|
|
085481bee6 | ||
|
|
cf610fbe49 | ||
|
|
b681eb3cd2 | ||
|
|
f8ffdf0e81 | ||
|
|
ba38bce1da | ||
|
|
3a39b05fe4 | ||
|
|
27d2982b28 | ||
|
|
33e4d97be5 | ||
|
|
654064d7c5 | ||
|
|
aeb0da9e0d | ||
|
|
cb1a2fe067 | ||
|
|
85667ddfad | ||
|
|
7ad5016966 | ||
|
|
45c3315fd0 | ||
|
|
c5f550dd00 | ||
|
|
37c870c7ab | ||
|
|
155b768f73 | ||
|
|
09f0873f27 | ||
|
|
19a12f6c8f | ||
|
|
79a498d1d6 | ||
|
|
12a03c36f3 | ||
|
|
ddc8bcab9b | ||
|
|
35dacfd48a | ||
|
|
611cef34ab | ||
|
|
8a8f9fe890 | ||
|
|
5a65b70c80 | ||
|
|
8bea3faa1f | ||
|
|
4125954d53 | ||
|
|
3d9c5a729e | ||
|
|
51d775ea6d | ||
|
|
e461c47c4b | ||
|
|
bbdb38e45a | ||
|
|
083e047aec | ||
|
|
84e0b504ab | ||
|
|
bedff6832c | ||
|
|
6c29e04de2 | ||
|
|
ba674323e1 | ||
|
|
6d756a161e | ||
|
|
66a5cd95ec | ||
|
|
9f70648264 | ||
|
|
b975b79691 | ||
|
|
3cab896954 | ||
|
|
b9680e5b35 | ||
|
|
43a003ca7d | ||
|
|
fbe1cf2009 | ||
|
|
fdee61cc11 | ||
|
|
8b51200d9c | ||
|
|
0631ab536a | ||
|
|
0d76e76519 | ||
|
|
9e9f83d211 | ||
|
|
0829d477d9 | ||
|
|
1b7109fa23 | ||
|
|
366669ccd8 | ||
|
|
e05edd89fa | ||
|
|
a405df66c9 | ||
|
|
28a57ead9f | ||
|
|
72bfacae75 | ||
|
|
10f8821b3b | ||
|
|
92a3745ce1 | ||
|
|
aff7ff1451 | ||
|
|
9e63e0ff09 | ||
|
|
2c5ebd8620 | ||
|
|
e6d63db3a9 | ||
|
|
2cc316aafb | ||
|
|
4f25cfa0dd | ||
|
|
0385de3882 | ||
|
|
2e1253c54e | ||
|
|
14df15caeb | ||
|
|
af481a2ec8 | ||
|
|
96c1d80844 | ||
|
|
57ea2e25bd | ||
|
|
cd17b0a6d7 | ||
|
|
c5dd8823f6 | ||
|
|
c27f16f753 | ||
|
|
4842610229 | ||
|
|
bd33669dba | ||
|
|
e81684d322 | ||
|
|
8209dfa26c | ||
|
|
0a82c62a4e | ||
|
|
4399dfe6b8 | ||
|
|
defd0cc78c | ||
|
|
fd5fc8294c | ||
|
|
10823b416c | ||
|
|
4490920ac6 | ||
|
|
e0b8ad4a32 | ||
|
|
e11063832b | ||
|
|
d598fdd753 | ||
|
|
157b93239a | ||
|
|
3d7058dccd | ||
|
|
1ec654d1ea | ||
|
|
8ce2d4269d | ||
|
|
49e8aa7869 | ||
|
|
0827f55d43 | ||
|
|
ba6ff8c342 | ||
|
|
c0b7dc2737 | ||
|
|
ab6abd453b | ||
|
|
77921aca75 | ||
|
|
a8f91c4814 | ||
|
|
7780fa792c | ||
|
|
41e4c376ec | ||
|
|
09aad61cfe | ||
|
|
31ac81428f | ||
|
|
60daab82d2 | ||
|
|
e7a0544004 | ||
|
|
ff0364bba8 | ||
|
|
a3b64be351 | ||
|
|
eae370e346 | ||
|
|
0d86b50a83 | ||
|
|
543b9028a3 | ||
|
|
867f12d11f | ||
|
|
462781ea6a | ||
|
|
c300ab6459 | ||
|
|
448d8f3007 | ||
|
|
faa53b0af1 | ||
|
|
8e53b8d9a6 | ||
|
|
5678567baa | ||
|
|
1c30f73bb1 | ||
|
|
bd3dbc3033 | ||
|
|
3c312ab8d3 | ||
|
|
7284785db0 | ||
|
|
4e543ea490 | ||
|
|
4d3540126f | ||
|
|
6c29f0d792 | ||
|
|
c85df7c40f | ||
|
|
4f6ff575ce | ||
|
|
aaf3890f71 | ||
|
|
4f9837d991 | ||
|
|
2e2eb008c6 | ||
|
|
fa7074bb59 | ||
|
|
18a7def050 | ||
|
|
40e9cf5fe5 | ||
|
|
6ce049a57e | ||
|
|
d4c4eac710 | ||
|
|
28ec96e5e8 | ||
|
|
a20fe8eb06 | ||
|
|
7d1d9b881d | ||
|
|
c116e6cbb0 | ||
|
|
a3ebf0a33b | ||
|
|
ea6ef022e7 | ||
|
|
683e29196a | ||
|
|
71694d7082 | ||
|
|
c5d5656fb6 | ||
|
|
b1fdbee5f8 | ||
|
|
70405a1cd0 | ||
|
|
3d7e81169f | ||
|
|
fa7c8c7219 | ||
|
|
358a25f522 | ||
|
|
690169a409 | ||
|
|
0ec4cc5a51 | ||
|
|
0b7dbaf368 | ||
|
|
940c445182 | ||
|
|
d8a90c9aa5 | ||
|
|
89ee8fceca | ||
|
|
68b55a2d33 | ||
|
|
8cc2e7747f | ||
|
|
4e6ac3a294 | ||
|
|
732e22ea7b | ||
|
|
ac0d9981f8 | ||
|
|
23f7310760 | ||
|
|
02a9ce6bac | ||
|
|
069665358f | ||
|
|
2cf6d69127 | ||
|
|
870435061d | ||
|
|
53ad906491 | ||
|
|
01399e343b | ||
|
|
a0bdec4c42 | ||
|
|
914f2057d8 | ||
|
|
ed9b9dca79 | ||
|
|
f6d4383ff6 | ||
|
|
b6ccf30ae4 | ||
|
|
48e44a0d38 | ||
|
|
735998b48c | ||
|
|
d94eefddb7 | ||
|
|
5a65b4df17 | ||
|
|
15a839a310 | ||
|
|
9730c61962 | ||
|
|
1805c041a6 | ||
|
|
f0bde93d5f | ||
|
|
1ed7412f92 | ||
|
|
1f5ea2d25b | ||
|
|
ff4c8f0e85 | ||
|
|
5c3dd878b8 | ||
|
|
20f0d91c75 | ||
|
|
894e48756f | ||
|
|
aef199b292 | ||
|
|
9b884cd941 | ||
|
|
b22d813cee | ||
|
|
0981566cbe | ||
|
|
0647aa435a | ||
|
|
58179f98e9 | ||
|
|
05fe62536b | ||
|
|
241d41e220 | ||
|
|
34d6134c24 | ||
|
|
702d1d1998 | ||
|
|
e43a5fd5a3 | ||
|
|
4ed9a7b7f5 | ||
|
|
d8ba76e395 | ||
|
|
448004abca | ||
|
|
7b0db34fb1 | ||
|
|
a7e7d5b372 | ||
|
|
9768628e8b | ||
|
|
6c0328b2fd | ||
|
|
6bf7d879e2 | ||
|
|
f25490f1ad | ||
|
|
31eee78535 | ||
|
|
4ed2aff1b7 | ||
|
|
6f05545be6 | ||
|
|
dfec8a0e4f | ||
|
|
47b730111b | ||
|
|
59ba27c36f | ||
|
|
b501164c38 | ||
|
|
1b795087bf | ||
|
|
7063edc10f | ||
|
|
c02abcb760 | ||
|
|
2d1a20dcc6 | ||
|
|
65156d4b20 | ||
|
|
951cedb190 | ||
|
|
11442f320d | ||
|
|
33df21f656 | ||
|
|
19ab208678 | ||
|
|
59b9651575 | ||
|
|
c1c1f1767a | ||
|
|
dda8a5180c | ||
|
|
34d558412f | ||
|
|
be77ee1903 | ||
|
|
fc571d6c73 | ||
|
|
7ca5a97535 | ||
|
|
730239ba8d | ||
|
|
9069e23188 | ||
|
|
366203fe21 | ||
|
|
f23f15c972 | ||
|
|
8432290a70 | ||
|
|
ceddd503f8 | ||
|
|
51ad35f72b | ||
|
|
5631b24a14 | ||
|
|
f9da25643d | ||
|
|
75bed3f5dd | ||
|
|
d4576dfc21 | ||
|
|
31bad3c2e5 | ||
|
|
a3c0e81e04 | ||
|
|
b5cf721efb | ||
|
|
95063398aa | ||
|
|
2da25c5566 | ||
|
|
a72c1ab380 | ||
|
|
87b9301e20 | ||
|
|
d0a6bed727 | ||
|
|
29881fbf28 | ||
|
|
4f05ad6546 | ||
|
|
36fc9aae6c | ||
|
|
fb3f87c685 | ||
|
|
a4c0eeb9b3 | ||
|
|
c64f249fd0 | ||
|
|
48b542293f | ||
|
|
95e92970c9 | ||
|
|
afdb16da07 | ||
|
|
0a230cfab9 | ||
|
|
c752a07ad0 | ||
|
|
cc5a6b7317 | ||
|
|
d93991ad86 | ||
|
|
5c98376a8c | ||
|
|
28699f0ff0 | ||
|
|
e3576b17b1 | ||
|
|
17c49b2b03 | ||
|
|
dcc00d8d1f | ||
|
|
05a772eee2 | ||
|
|
59dd291dc6 | ||
|
|
f0a791e354 | ||
|
|
94db6cff7b | ||
|
|
72a02bf6d2 | ||
|
|
cf99209887 | ||
|
|
ff7ed49905 | ||
|
|
ecd17a51df | ||
|
|
63b08f8c12 | ||
|
|
3077772aff | ||
|
|
f40aa9ed14 | ||
|
|
4ae6dc3a69 | ||
|
|
bdb8389d8d | ||
|
|
f7254c7ec7 | ||
|
|
3701156362 | ||
|
|
575d25f5ad | ||
|
|
698b82c7d3 | ||
|
|
eaa9da6d2e | ||
|
|
ca7294a0e3 | ||
|
|
5281ffce60 | ||
|
|
2f84fd3b33 | ||
|
|
bef270da74 | ||
|
|
bdf839ffca | ||
|
|
b1b2ead0d8 | ||
|
|
0b74473c64 | ||
|
|
2fb189cf5c | ||
|
|
24a29639e9 | ||
|
|
50258dc359 | ||
|
|
d9c6b7698f | ||
|
|
f042f34ae2 | ||
|
|
c087edc14f | ||
|
|
d96ccc3265 | ||
|
|
018ef19efd | ||
|
|
159317a15a | ||
|
|
34583c96bf | ||
|
|
887d3d3ab4 | ||
|
|
f2de39de33 | ||
|
|
8173c31a60 | ||
|
|
80a2871741 | ||
|
|
3b37e9c420 | ||
|
|
730ba561bc | ||
|
|
4d7ddc9d6f | ||
|
|
680f71a602 | ||
|
|
170a7b50b9 | ||
|
|
805c48c1bb | ||
|
|
7d92ed51bb | ||
|
|
f849897f29 | ||
|
|
75e0f1ad1e | ||
|
|
2f015aff2b | ||
|
|
bb322adb2f | ||
|
|
6ee0981885 | ||
|
|
877fb0f201 | ||
|
|
ded4b9094a | ||
|
|
36ab931998 | ||
|
|
20baddfcca | ||
|
|
d180c147db | ||
|
|
86fa91cc52 | ||
|
|
b8c12a0307 | ||
|
|
eff2f9c195 | ||
|
|
8a8597afef | ||
|
|
067e619488 | ||
|
|
f7f8bc8c55 | ||
|
|
0c71fd7e59 | ||
|
|
5b2bd21c06 | ||
|
|
9ac68f68e2 | ||
|
|
aa32f00b0d | ||
|
|
bd5f5dffad | ||
|
|
645ad63f4f | ||
|
|
c83c6afecc | ||
|
|
689a191d4c | ||
|
|
5767ba0de5 | ||
|
|
cdf78f2598 | ||
|
|
d23f46ad9e | ||
|
|
8453fa5007 | ||
|
|
0bd990a9e3 | ||
|
|
9c8ee1471b | ||
|
|
87262e82a3 | ||
|
|
953db88ed2 | ||
|
|
a44bb949eb | ||
|
|
9d11369059 | ||
|
|
8c92f39349 | ||
|
|
bce9d733d3 | ||
|
|
525c216401 | ||
|
|
c145e9f11d | ||
|
|
53700003f1 | ||
|
|
03094a4c35 | ||
|
|
934ebed24f | ||
|
|
9f469e1637 | ||
|
|
77fe39774d | ||
|
|
186d688ec8 | ||
|
|
de3181b5c7 | ||
|
|
6d53234f7a | ||
|
|
b0d3673f4b | ||
|
|
e40083f585 | ||
|
|
f7b702fa29 | ||
|
|
5089a19a56 | ||
|
|
d77e9842aa | ||
|
|
88ae76a25a | ||
|
|
9b0b0d10a1 | ||
|
|
68340a8b02 | ||
|
|
d14f0966d5 | ||
|
|
e4b83f28c4 | ||
|
|
1925deec90 | ||
|
|
e9ba960b29 | ||
|
|
821f0f5b60 | ||
|
|
8fa8915673 | ||
|
|
5405aa469e | ||
|
|
cfa50b4e8c | ||
|
|
c5014baeb4 | ||
|
|
c14f120ec2 | ||
|
|
b72fcf39da | ||
|
|
6da3b0cba0 | ||
|
|
752387a02d | ||
|
|
32d3e1b957 | ||
|
|
a029dff202 | ||
|
|
89e1b85a56 | ||
|
|
4350eb709c | ||
|
|
2b1b6e916e | ||
|
|
3f6f9045bd | ||
|
|
8294496faa | ||
|
|
9f58118ecd | ||
|
|
3ba9d42587 | ||
|
|
e496a5f3d0 | ||
|
|
04e2bb29b0 | ||
|
|
131a7a015c | ||
|
|
6f430f5f14 | ||
|
|
0154c106ef | ||
|
|
04de6e6fc0 | ||
|
|
2b17e5f842 | ||
|
|
0dc8c1e68a | ||
|
|
67b291eb09 | ||
|
|
d7865a5a4e | ||
|
|
d1809587a8 | ||
|
|
64ec4dfd53 | ||
|
|
6e07d21a3d | ||
|
|
018e47bd53 | ||
|
|
065a2e9347 | ||
|
|
b53307c7fd | ||
|
|
e4cc0d960e | ||
|
|
3336824df7 | ||
|
|
166173d53c | ||
|
|
45da2d7924 | ||
|
|
af14a65ef5 | ||
|
|
9049a4b9ad | ||
|
|
2f4873ab9a | ||
|
|
0fb25d71dc | ||
|
|
34b6901434 | ||
|
|
a90b43aba3 | ||
|
|
0ac5c2c300 | ||
|
|
6290fee7bd | ||
|
|
320992f3fa | ||
|
|
bd714f0c93 | ||
|
|
45d3867c1f | ||
|
|
deafa66565 | ||
|
|
ce8fcdaa7a | ||
|
|
b83965c144 | ||
|
|
2505a8cab8 | ||
|
|
21e598bb31 | ||
|
|
ee2f3c0dfc | ||
|
|
197bda14b8 | ||
|
|
d416c2e421 | ||
|
|
6bc2e669e4 | ||
|
|
9accb65256 | ||
|
|
d9212584ca | ||
|
|
1a7effb422 | ||
|
|
1480435f15 | ||
|
|
64c6a97657 | ||
|
|
7ae2ff9837 | ||
|
|
f19dec405d | ||
|
|
bbd4e9d162 | ||
|
|
58deff0869 | ||
|
|
628632f43e | ||
|
|
436586117c | ||
|
|
7234ec728c | ||
|
|
fc4d68be39 | ||
|
|
bcf2df4bff | ||
|
|
70a469a951 | ||
|
|
981995cb32 | ||
|
|
406c7d368b | ||
|
|
5c266d8b8d | ||
|
|
4f5b94a491 | ||
|
|
284c83cc1d | ||
|
|
23f06af202 | ||
|
|
82be317595 | ||
|
|
a43c76529f | ||
|
|
26065c2a5c | ||
|
|
6b1dfe01cf | ||
|
|
72443fb7c6 | ||
|
|
28cad23746 | ||
|
|
ebc0d4b137 | ||
|
|
4b198acda7 | ||
|
|
b84c3c75a5 | ||
|
|
de0ec97d9b | ||
|
|
a8302cd276 | ||
|
|
35d8d77f15 | ||
|
|
f853394d9d | ||
|
|
500c01dd1f | ||
|
|
71bfccfb0c | ||
|
|
71e1b64818 | ||
|
|
2118a00a36 | ||
|
|
1c69c21f21 | ||
|
|
31c76fc1c0 | ||
|
|
d31cdf8af2 | ||
|
|
29b5fd2c6f | ||
|
|
aa32525f8c | ||
|
|
f6a67a3373 | ||
|
|
4f78858641 | ||
|
|
c7e70bdaf8 | ||
|
|
5479e0957b | ||
|
|
8500554cfb | ||
|
|
368727cece | ||
|
|
742faf3805 | ||
|
|
7d08cbed84 | ||
|
|
f413bb5f3c | ||
|
|
2b3edc7b45 | ||
|
|
83f982f8d2 | ||
|
|
890a1d41b1 | ||
|
|
996a2c26ca | ||
|
|
fadf042101 | ||
|
|
a819078d7f | ||
|
|
2c9b4c663d | ||
|
|
22e6803e14 | ||
|
|
61cecb112f | ||
|
|
0c18aff34d | ||
|
|
599649a97f | ||
|
|
45194b82da |
No files matched your search
+2
-2
@@ -1,2 +1,2 @@
|
||||
# Auto detect text files and perform LF normalization
|
||||
* text=auto
|
||||
Configs/supportbot.yml filter=supportbot-token
|
||||
Configs/api.yml filter=api-secrets
|
||||
@@ -1,23 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a bug report to help us improve SupportBot.
|
||||
title: "[BUG]"
|
||||
labels: bug
|
||||
assignees: EmeraldRusher
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of the bug you found and how you found it.
|
||||
|
||||
**How did you find it**
|
||||
Please could explain what you did to find the bug?
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Additional context**
|
||||
Feel free to notify with us with anything else about the problem here.
|
||||
+19
-71
@@ -1,76 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
node_modules
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
# Local database, runtime data, and transcripts
|
||||
Data/*.db
|
||||
Data/*.db-shm
|
||||
Data/*.db-wal
|
||||
Data/Transcripts/*
|
||||
Data/dashboard-groups.json
|
||||
Data/dashboard-users.json
|
||||
Data/transcript-settings.json
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
# Local log files
|
||||
Logs/Output/*
|
||||
Logs/Errors/*
|
||||
Logs/Warns/*
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
# Dashboard UI source at repo root only (do not ignore public/dashboard/)
|
||||
/dashboard/
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
# Local git hooks + secret-stripping scripts (run npm run git:setup)
|
||||
scripts/
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
|
||||
# next.js build output
|
||||
.next
|
||||
|
||||
# nuxt.js build output
|
||||
.nuxt
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
# Local secrets — use *.example as templates
|
||||
Configs/api.yml
|
||||
+2123
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,26 @@
|
||||
const Discord = require("discord.js");
|
||||
const fs = require("fs");
|
||||
const yaml = require("js-yaml");
|
||||
const { Command } = require('../Structures/Addon.js'); // Adjust the path as needed
|
||||
const supportbot = require("../Structures/ConfigStore").supportbot;
|
||||
|
||||
module.exports = new Command({
|
||||
name: "addon",
|
||||
description: "This is a sample addon!",
|
||||
options: [],
|
||||
permissions: ["Administrator"],
|
||||
|
||||
async run(interaction) {
|
||||
let disableCommand = true;
|
||||
|
||||
const PingEmbed = new Discord.EmbedBuilder()
|
||||
.setDescription(
|
||||
`This is a sample addon.\n\nPong! \`${interaction.client.ws.ping} ms\``
|
||||
)
|
||||
.setColor(supportbot.Embed.Colours.General);
|
||||
|
||||
interaction.reply({
|
||||
embeds: [PingEmbed],
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,122 @@
|
||||
// SupportBot | Emerald Services
|
||||
// AI Command (Consolidated)
|
||||
|
||||
const fs = require("fs");
|
||||
const { ApplicationCommandOptionType, ApplicationCommandType, EmbedBuilder, MessageFlags } = require("discord.js");
|
||||
const yaml = require("js-yaml");
|
||||
|
||||
const supportbot = require("../../Structures/ConfigStore").supportbot;
|
||||
const cmdconfig = require("../../Structures/ConfigStore").commands;
|
||||
const supportbotai = require("../../Structures/ConfigStore").supportbotAi;
|
||||
|
||||
const Command = require("../../Structures/Command.js");
|
||||
const AIDatabase = require("../../Structures/AIDatabase.js");
|
||||
|
||||
module.exports = new Command({
|
||||
name: cmdconfig.AI?.Command || "ai",
|
||||
description: cmdconfig.AI?.Description || "Manage SupportBot AI settings and memory.",
|
||||
type: ApplicationCommandType.ChatInput,
|
||||
options: [
|
||||
{
|
||||
name: cmdconfig.AI?.Train?.Command || "train",
|
||||
description: cmdconfig.AI?.Train?.Description || "Train the AI by adding knowledge to its database.",
|
||||
type: ApplicationCommandOptionType.Subcommand,
|
||||
options: [
|
||||
{
|
||||
name: "content",
|
||||
description: "The text/knowledge you want the bot to learn",
|
||||
type: ApplicationCommandOptionType.String,
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: cmdconfig.AI?.Knowledge?.Command || "knowledge",
|
||||
description: cmdconfig.AI?.Knowledge?.Description || "Manage the AI knowledge base.",
|
||||
type: ApplicationCommandOptionType.SubcommandGroup,
|
||||
options: [
|
||||
{
|
||||
name: cmdconfig.AI?.Knowledge?.Clear?.Command || "clear",
|
||||
description: cmdconfig.AI?.Knowledge?.Clear?.Description || "Clear all trained knowledge from the AI.",
|
||||
type: ApplicationCommandOptionType.Subcommand,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: cmdconfig.AI?.Memory?.Command || "memory",
|
||||
description: cmdconfig.AI?.Memory?.Description || "Manage the AI conversational memory.",
|
||||
type: ApplicationCommandOptionType.SubcommandGroup,
|
||||
options: [
|
||||
{
|
||||
name: cmdconfig.AI?.Memory?.Clear?.Command || "clear",
|
||||
description: cmdconfig.AI?.Memory?.Clear?.Description || "Clear recent conversation memory for this channel.",
|
||||
type: ApplicationCommandOptionType.Subcommand,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
permissions: cmdconfig.AI?.Permission || ["Administrator"],
|
||||
|
||||
async run(interaction) {
|
||||
if (!supportbotai.Enabled) {
|
||||
return interaction.reply({
|
||||
content: "SupportBot AI is currently disabled.",
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
|
||||
const subcommandGroup = interaction.options.getSubcommandGroup(false);
|
||||
const subcommand = interaction.options.getSubcommand(false);
|
||||
|
||||
try {
|
||||
if (subcommand === "train" || subcommand === cmdconfig.AI?.Train?.Command) {
|
||||
const content = interaction.options.getString("content");
|
||||
|
||||
AIDatabase.addKnowledge({
|
||||
sourceType: "user_command",
|
||||
sourceName: interaction.user.tag,
|
||||
title: "Trained Knowledge",
|
||||
content: content,
|
||||
});
|
||||
|
||||
const SuccessEmbed = new EmbedBuilder()
|
||||
.setDescription(`✅ **Knowledge Added!**\n\nThe AI has learned the following information:\n\`\`\`${content}\`\`\``)
|
||||
.setColor(supportbot.Embed.Colours.Success);
|
||||
|
||||
return interaction.reply({ embeds: [SuccessEmbed], flags: MessageFlags.Ephemeral });
|
||||
}
|
||||
|
||||
if (subcommandGroup === (cmdconfig.AI?.Knowledge?.Command || "knowledge")) {
|
||||
if (subcommand === (cmdconfig.AI?.Knowledge?.Clear?.Command || "clear")) {
|
||||
AIDatabase.clearKnowledge();
|
||||
|
||||
const SuccessEmbed = new EmbedBuilder()
|
||||
.setDescription(`✅ **Knowledge Cleared!**\n\nAll manually trained knowledge has been wiped from the database.`)
|
||||
.setColor(supportbot.Embed.Colours.Success);
|
||||
|
||||
return interaction.reply({ embeds: [SuccessEmbed], flags: MessageFlags.Ephemeral });
|
||||
}
|
||||
}
|
||||
|
||||
if (subcommandGroup === (cmdconfig.AI?.Memory?.Command || "memory")) {
|
||||
if (subcommand === (cmdconfig.AI?.Memory?.Clear?.Command || "clear")) {
|
||||
AIDatabase.clearConversations(interaction.channel.id);
|
||||
|
||||
const SuccessEmbed = new EmbedBuilder()
|
||||
.setDescription(`✅ **Memory Cleared!**\n\nThe AI's recent conversation memory in this channel has been wiped. It will no longer remember previous messages sent here.`)
|
||||
.setColor(supportbot.Embed.Colours.Success);
|
||||
|
||||
return interaction.reply({ embeds: [SuccessEmbed], flags: MessageFlags.Ephemeral });
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error("AI Command Error:", error);
|
||||
const ErrorEmbed = new EmbedBuilder()
|
||||
.setDescription(`❌ **Error:** Failed to process the AI command.`)
|
||||
.setColor(supportbot.Embed.Colours.Error);
|
||||
|
||||
return interaction.reply({ embeds: [ErrorEmbed], flags: MessageFlags.Ephemeral });
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
const fs = require("fs");
|
||||
const Discord = require("discord.js");
|
||||
const yaml = require("js-yaml");
|
||||
|
||||
const supportbot = require("../../Structures/ConfigStore").supportbot;
|
||||
const cmdconfig = require("../../Structures/ConfigStore").commands;
|
||||
|
||||
const Command = require("../../Structures/Command.js");
|
||||
|
||||
module.exports = new Command({
|
||||
name: cmdconfig.Help.Command,
|
||||
description: cmdconfig.Help.Description,
|
||||
type: Discord.ApplicationCommandType.ChatInput,
|
||||
options: [],
|
||||
permissions: cmdconfig.Help.Permission,
|
||||
|
||||
async run(interaction) {
|
||||
const { getRole } = interaction.client;
|
||||
let SupportStaff = await getRole(supportbot.Roles.StaffMember.Staff, interaction.guild);
|
||||
let Admin = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild);
|
||||
if (!SupportStaff || !Admin) {
|
||||
return interaction.reply(
|
||||
"Some roles seem to be missing!\nPlease check for errors when starting the bot."
|
||||
);
|
||||
}
|
||||
|
||||
// Separate regular commands and addon commands
|
||||
let botCommands = "";
|
||||
let addonCommands = "";
|
||||
|
||||
interaction.client.commands.forEach((command) => {
|
||||
if (command.permissions) {
|
||||
if (command.addon) {
|
||||
addonCommands += `\`${command.name}\` `;
|
||||
} else {
|
||||
botCommands += `\`${command.name}\` `;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Create embed
|
||||
const HelpEmbed1 = new Discord.EmbedBuilder()
|
||||
.setTitle(supportbot.General.Name + " Commands")
|
||||
.setThumbnail(interaction.user.displayAvatarURL())
|
||||
.setColor(supportbot.Embed.Colours.General)
|
||||
.addFields({
|
||||
name: "🖥️ Commands\n",
|
||||
value: botCommands || "No commands available.",
|
||||
inline: false,
|
||||
});
|
||||
|
||||
HelpEmbed1.setFooter({
|
||||
text: supportbot.Embed.Footer,
|
||||
iconURL: interaction.user.displayAvatarURL(),
|
||||
});
|
||||
|
||||
await interaction.reply({
|
||||
flags: Discord.MessageFlags.Ephemeral,
|
||||
embeds: [HelpEmbed1],
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
// SupportBot | Emerald Services
|
||||
// Info Command
|
||||
|
||||
const Discord = require("discord.js");
|
||||
|
||||
const supportbot = require("../../Structures/ConfigStore").supportbot;
|
||||
const cmdconfig = require("../../Structures/ConfigStore").commands;
|
||||
const msgconfig = require("../../Structures/ConfigStore").messages;
|
||||
|
||||
const Command = require("../../Structures/Command.js");
|
||||
|
||||
module.exports = new Command({
|
||||
name: cmdconfig.Info.Command,
|
||||
description: cmdconfig.Info.Description,
|
||||
options: [],
|
||||
permissions: cmdconfig.Info.Permission,
|
||||
|
||||
async run(interaction) {
|
||||
let disableCommand = true;
|
||||
|
||||
const InfoButton = new Discord.ButtonBuilder()
|
||||
.setLabel(msgconfig.Info.Button)
|
||||
.setURL(msgconfig.Info.URL)
|
||||
.setStyle("Link");
|
||||
|
||||
const inforow = new Discord.ActionRowBuilder().addComponents(InfoButton);
|
||||
|
||||
const InfoEmbed = new Discord.EmbedBuilder()
|
||||
.setURL(msgconfig.Info.URL)
|
||||
.setTitle(msgconfig.Info.Title)
|
||||
.setDescription(msgconfig.Info.Description)
|
||||
.setColor(msgconfig.Info.Colour)
|
||||
|
||||
interaction.reply({
|
||||
embeds: [InfoEmbed],
|
||||
components: [inforow],
|
||||
});
|
||||
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
// SupportBot | Emerald Services
|
||||
// Ping Command
|
||||
|
||||
const fs = require("fs");
|
||||
|
||||
const Discord = require("discord.js");
|
||||
const yaml = require("js-yaml");
|
||||
const supportbot = require("../../Structures/ConfigStore").supportbot;
|
||||
const cmdconfig = require("../../Structures/ConfigStore").commands;
|
||||
|
||||
const Command = require("../../Structures/Command.js");
|
||||
|
||||
module.exports = new Command({
|
||||
name: cmdconfig.Ping.Command,
|
||||
description: cmdconfig.Ping.Description,
|
||||
options: [],
|
||||
permissions: cmdconfig.Ping.Permission,
|
||||
|
||||
async run(interaction) {
|
||||
const PingEmbed = new Discord.EmbedBuilder()
|
||||
.setDescription(
|
||||
`:ping_pong: **Ping:** \`${interaction.client.ws.ping} ms\``,
|
||||
)
|
||||
.setColor(supportbot.Embed.Colours.General);
|
||||
|
||||
await interaction.reply({
|
||||
embeds: [PingEmbed],
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,527 @@
|
||||
const {
|
||||
ModalBuilder,
|
||||
TextInputBuilder,
|
||||
TextInputStyle,
|
||||
ApplicationCommandType,
|
||||
ActionRowBuilder,
|
||||
ButtonBuilder,
|
||||
ButtonStyle,
|
||||
ApplicationCommandOptionType,
|
||||
MessageFlags,
|
||||
ContainerBuilder,
|
||||
TextDisplayBuilder,
|
||||
SeparatorBuilder,
|
||||
ThumbnailBuilder,
|
||||
MediaGalleryBuilder,
|
||||
MediaGalleryItemBuilder,
|
||||
} = require("discord.js");
|
||||
const fs = require("fs");
|
||||
const yaml = require("js-yaml");
|
||||
const db = require("../../Structures/Database.js");
|
||||
const Command = require("../../Structures/Command.js");
|
||||
|
||||
const supportbot = require("../../Structures/ConfigStore").supportbot;
|
||||
const cmdconfig = require("../../Structures/ConfigStore").commands;
|
||||
const msgconfig = require("../../Structures/ConfigStore").messages;
|
||||
|
||||
const clockedInUsers = new Set();
|
||||
|
||||
function safeGetProfile(userId) {
|
||||
try {
|
||||
if (typeof db.getProfile !== "function") {
|
||||
console.error("db.getProfile is not a function");
|
||||
return {
|
||||
bio: "",
|
||||
timezone: "",
|
||||
clockedIn: false,
|
||||
};
|
||||
}
|
||||
|
||||
const profile = db.getProfile(userId);
|
||||
|
||||
return {
|
||||
bio: profile?.bio || "",
|
||||
timezone: profile?.timezone || "",
|
||||
clockedIn: Boolean(profile?.clockedIn),
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error getting profile:", error);
|
||||
return {
|
||||
bio: "",
|
||||
timezone: "",
|
||||
clockedIn: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function safeSetClockedIn(userId, value) {
|
||||
try {
|
||||
if (typeof db.setClockedIn !== "function") {
|
||||
console.error("db.setClockedIn is not a function");
|
||||
return false;
|
||||
}
|
||||
|
||||
db.setClockedIn(userId, value);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("Error setting clocked in status:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function buildProfileContainer({
|
||||
viewingUser,
|
||||
viewingUserId,
|
||||
bio,
|
||||
timezone,
|
||||
clockedIn,
|
||||
isStaff,
|
||||
}) {
|
||||
const container = new ContainerBuilder();
|
||||
|
||||
if (supportbot.Embed?.Colours?.General) {
|
||||
container.setAccentColor(
|
||||
parseInt(String(supportbot.Embed.Colours.General).replace("#", ""), 16),
|
||||
);
|
||||
}
|
||||
|
||||
const avatarUrl = viewingUser.displayAvatarURL({ size: 128, extension: "png" });
|
||||
|
||||
const gallery = new MediaGalleryBuilder().addItems(
|
||||
new MediaGalleryItemBuilder().setURL(avatarUrl)
|
||||
);
|
||||
|
||||
container.addMediaGalleryComponents(gallery);
|
||||
|
||||
container.addTextDisplayComponents(
|
||||
new TextDisplayBuilder().setContent(`## ${viewingUser.username}'s Profile`)
|
||||
);
|
||||
|
||||
container.addSeparatorComponents(new SeparatorBuilder().setDivider(true));
|
||||
|
||||
container.addTextDisplayComponents(
|
||||
new TextDisplayBuilder().setContent(
|
||||
[
|
||||
`**Bio:** ${bio || "No bio set."}`,
|
||||
`**Timezone:** ${timezone || "No timezone set."}`,
|
||||
...(isStaff
|
||||
? [`**Clocked In:** ${clockedIn ? "✅ Clocked In" : "❌ Clocked Out"}`]
|
||||
: []),
|
||||
].join("\n")
|
||||
)
|
||||
);
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
function buildProfileButtons({ isOwnProfile, isStaff, clockedIn }) {
|
||||
const row = new ActionRowBuilder();
|
||||
|
||||
if (isOwnProfile) {
|
||||
row.addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId("editProfile")
|
||||
.setLabel("Edit Profile")
|
||||
.setStyle(ButtonStyle.Secondary),
|
||||
);
|
||||
|
||||
if (isStaff) {
|
||||
row.addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId("clockInOut")
|
||||
.setLabel(clockedIn ? "Clock Out" : "Clock In")
|
||||
.setStyle(ButtonStyle.Secondary),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (isStaff && supportbot.Ticket?.ClaimTickets?.Enabled) {
|
||||
row.addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId("viewTicketStats")
|
||||
.setLabel("View Ticket Stats")
|
||||
.setStyle(ButtonStyle.Secondary),
|
||||
);
|
||||
}
|
||||
|
||||
return row.components.length ? [row] : [];
|
||||
}
|
||||
|
||||
function buildTicketStatsContainer(viewingUserId, ticketsClaimed, ticketsOpen, avgMinutes) {
|
||||
const container = new ContainerBuilder();
|
||||
|
||||
if (supportbot.Embed?.Colours?.General) {
|
||||
container.setAccentColor(
|
||||
parseInt(String(supportbot.Embed.Colours.General).replace("#", ""), 16),
|
||||
);
|
||||
}
|
||||
|
||||
container.addTextDisplayComponents(
|
||||
new TextDisplayBuilder().setContent(`# ${msgconfig.TicketStats.Title || "Ticket Stats"}`),
|
||||
);
|
||||
|
||||
container.addSeparatorComponents(new SeparatorBuilder().setDivider(true));
|
||||
|
||||
container.addTextDisplayComponents(
|
||||
new TextDisplayBuilder().setContent(
|
||||
[
|
||||
`> Ticket stats for <@${viewingUserId}>`,
|
||||
``,
|
||||
`**${msgconfig.TicketStats.OpenTickets || "Open Tickets"}:** ${ticketsOpen.length}`,
|
||||
`**${msgconfig.TicketStats.TotalTickets || "Total Tickets"}:** ${ticketsClaimed.length}`,
|
||||
`**${msgconfig.TicketStats.ResponseTime || "Average Response Time"}:** ${avgMinutes} minutes`,
|
||||
].join("\n"),
|
||||
),
|
||||
);
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
function buildOpenTicketsContainer(ticketsOpen) {
|
||||
const container = new ContainerBuilder();
|
||||
|
||||
if (supportbot.Embed?.Colours?.General) {
|
||||
container.setAccentColor(
|
||||
parseInt(String(supportbot.Embed.Colours.General).replace("#", ""), 16),
|
||||
);
|
||||
}
|
||||
|
||||
container.addTextDisplayComponents(new TextDisplayBuilder().setContent(`# Active Tickets`));
|
||||
|
||||
container.addSeparatorComponents(new SeparatorBuilder().setDivider(true));
|
||||
|
||||
container.addTextDisplayComponents(
|
||||
new TextDisplayBuilder().setContent(
|
||||
ticketsOpen.length ? ticketsOpen.map((t) => `<#${t.id}>`).join("\n") : "No active tickets.",
|
||||
),
|
||||
);
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
module.exports = new Command({
|
||||
name: cmdconfig.Profile.Command,
|
||||
description: cmdconfig.Profile.Description,
|
||||
type: ApplicationCommandType.ChatInput,
|
||||
permissions: cmdconfig.Profile.Permission,
|
||||
options: [
|
||||
{
|
||||
name: "user",
|
||||
description: "The user to view the profile of",
|
||||
type: ApplicationCommandOptionType.User,
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
|
||||
async run(interaction) {
|
||||
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
||||
|
||||
try {
|
||||
const viewingUser = interaction.options.getUser("user") || interaction.user;
|
||||
const viewingUserId = viewingUser.id;
|
||||
const interactionUserId = interaction.user.id;
|
||||
|
||||
const member = await interaction.guild.members.fetch(viewingUserId);
|
||||
const { getRole } = interaction.client;
|
||||
|
||||
const Staff = await getRole(supportbot.Roles.StaffMember.Staff, interaction.guild);
|
||||
const Admin = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild);
|
||||
|
||||
if (!Staff || !Admin) {
|
||||
return interaction.editReply({
|
||||
content: "Some roles seem to be missing!\nPlease check for errors when starting the bot.",
|
||||
});
|
||||
}
|
||||
|
||||
const isStaff = member.roles.cache.has(Staff.id) || member.roles.cache.has(Admin.id);
|
||||
const isOwnProfile = interactionUserId === viewingUserId;
|
||||
|
||||
if (typeof db.ensureProfile === "function") {
|
||||
db.ensureProfile(viewingUserId);
|
||||
}
|
||||
|
||||
let { bio, timezone, clockedIn } = safeGetProfile(viewingUserId);
|
||||
|
||||
const profileContainer = buildProfileContainer({
|
||||
viewingUser,
|
||||
viewingUserId,
|
||||
bio,
|
||||
timezone,
|
||||
clockedIn,
|
||||
isStaff,
|
||||
});
|
||||
|
||||
const buttonRows = buildProfileButtons({
|
||||
isOwnProfile,
|
||||
isStaff,
|
||||
clockedIn,
|
||||
});
|
||||
|
||||
await interaction.editReply({
|
||||
flags: MessageFlags.IsComponentsV2,
|
||||
components: [profileContainer, ...buttonRows],
|
||||
});
|
||||
|
||||
const filter = (i) =>
|
||||
[
|
||||
"editProfile",
|
||||
"clockInOut",
|
||||
"viewTicketStats",
|
||||
"showOpenTickets",
|
||||
"backToStats",
|
||||
"backToProfile",
|
||||
].includes(i.customId) && i.user.id === interactionUserId;
|
||||
|
||||
const collector = interaction.channel.createMessageComponentCollector({
|
||||
filter,
|
||||
time: 60000,
|
||||
});
|
||||
|
||||
collector.on("collect", async (i) => {
|
||||
if (i.customId === "editProfile") {
|
||||
const modal = new ModalBuilder()
|
||||
.setCustomId("editProfileModal")
|
||||
.setTitle("Edit Profile")
|
||||
.addComponents(
|
||||
new ActionRowBuilder().addComponents(
|
||||
new TextInputBuilder()
|
||||
.setCustomId("bio")
|
||||
.setLabel("Bio")
|
||||
.setStyle(TextInputStyle.Paragraph)
|
||||
.setPlaceholder("Enter your bio")
|
||||
.setValue(String(bio || ""))
|
||||
.setRequired(false),
|
||||
),
|
||||
new ActionRowBuilder().addComponents(
|
||||
new TextInputBuilder()
|
||||
.setCustomId("timezone")
|
||||
.setLabel("Timezone")
|
||||
.setStyle(TextInputStyle.Short)
|
||||
.setPlaceholder("Enter your timezone")
|
||||
.setValue(String(timezone || ""))
|
||||
.setRequired(false),
|
||||
),
|
||||
);
|
||||
|
||||
return i.showModal(modal);
|
||||
}
|
||||
|
||||
if (i.customId === "clockInOut") {
|
||||
const newStatus = !clockedIn;
|
||||
const saved = safeSetClockedIn(viewingUserId, newStatus);
|
||||
|
||||
if (!saved) {
|
||||
return i.reply({
|
||||
content:
|
||||
"Could not update clock in status because the profile database is not available.",
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
|
||||
clockedIn = newStatus;
|
||||
|
||||
if (newStatus) clockedInUsers.add(viewingUserId);
|
||||
else clockedInUsers.delete(viewingUserId);
|
||||
|
||||
const updatedProfileContainer = buildProfileContainer({
|
||||
viewingUser,
|
||||
viewingUserId,
|
||||
bio,
|
||||
timezone,
|
||||
clockedIn,
|
||||
isStaff,
|
||||
});
|
||||
|
||||
const updatedButtonRows = buildProfileButtons({
|
||||
isOwnProfile,
|
||||
isStaff,
|
||||
clockedIn,
|
||||
});
|
||||
|
||||
await i.update({
|
||||
flags: MessageFlags.IsComponentsV2,
|
||||
components: [updatedProfileContainer, ...updatedButtonRows],
|
||||
});
|
||||
|
||||
return interaction.followUp({
|
||||
content: newStatus
|
||||
? "You have successfully clocked in."
|
||||
: "You have successfully clocked out.",
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
|
||||
if (i.customId === "viewTicketStats") {
|
||||
let TicketData = { tickets: [] };
|
||||
|
||||
try {
|
||||
TicketData = JSON.parse(fs.readFileSync("./Data/TicketData.json", "utf8"));
|
||||
} catch (error) {
|
||||
console.error("Error reading TicketData.json:", error);
|
||||
}
|
||||
|
||||
const ticketsClaimed = TicketData.tickets.filter((t) => t.claimedBy === viewingUserId);
|
||||
const ticketsOpen = ticketsClaimed.filter((t) => t.open);
|
||||
const totalTickets = ticketsClaimed.length;
|
||||
|
||||
let totalResponseTime = 0;
|
||||
let responseCount = 0;
|
||||
|
||||
ticketsClaimed.forEach((t) => {
|
||||
if (t.claimedAt && t.createdAt) {
|
||||
totalResponseTime += new Date(t.claimedAt) - new Date(t.createdAt);
|
||||
responseCount++;
|
||||
}
|
||||
});
|
||||
|
||||
const avgMinutes = responseCount
|
||||
? Math.round(totalResponseTime / responseCount / 60000)
|
||||
: 0;
|
||||
|
||||
const statsContainer = buildTicketStatsContainer(
|
||||
viewingUserId,
|
||||
ticketsClaimed,
|
||||
ticketsOpen,
|
||||
avgMinutes,
|
||||
);
|
||||
|
||||
const statsRow = new ActionRowBuilder().addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId("showOpenTickets")
|
||||
.setLabel("Show Open Tickets")
|
||||
.setStyle(ButtonStyle.Secondary),
|
||||
new ButtonBuilder()
|
||||
.setCustomId("backToProfile")
|
||||
.setLabel("Back to Profile")
|
||||
.setStyle(ButtonStyle.Secondary),
|
||||
);
|
||||
|
||||
return i.update({
|
||||
flags: MessageFlags.IsComponentsV2,
|
||||
components: [statsContainer, statsRow],
|
||||
});
|
||||
}
|
||||
|
||||
if (i.customId === "showOpenTickets") {
|
||||
let TicketData = { tickets: [] };
|
||||
|
||||
try {
|
||||
TicketData = JSON.parse(fs.readFileSync("./Data/TicketData.json", "utf8"));
|
||||
} catch (error) {
|
||||
console.error("Error reading TicketData.json:", error);
|
||||
}
|
||||
|
||||
const ticketsOpen = TicketData.tickets.filter(
|
||||
(t) => t.claimedBy === viewingUserId && t.open,
|
||||
);
|
||||
|
||||
const openContainer = buildOpenTicketsContainer(ticketsOpen);
|
||||
|
||||
const backRow = new ActionRowBuilder().addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId("backToStats")
|
||||
.setLabel("Back to Stats")
|
||||
.setStyle(ButtonStyle.Secondary),
|
||||
);
|
||||
|
||||
return i.update({
|
||||
flags: MessageFlags.IsComponentsV2,
|
||||
components: [openContainer, backRow],
|
||||
});
|
||||
}
|
||||
|
||||
if (i.customId === "backToStats") {
|
||||
let TicketData = { tickets: [] };
|
||||
|
||||
try {
|
||||
TicketData = JSON.parse(fs.readFileSync("./Data/TicketData.json", "utf8"));
|
||||
} catch (error) {
|
||||
console.error("Error reading TicketData.json:", error);
|
||||
}
|
||||
|
||||
const ticketsClaimed = TicketData.tickets.filter((t) => t.claimedBy === viewingUserId);
|
||||
const ticketsOpen = ticketsClaimed.filter((t) => t.open);
|
||||
|
||||
let totalResponseTime = 0;
|
||||
let responseCount = 0;
|
||||
|
||||
ticketsClaimed.forEach((t) => {
|
||||
if (t.claimedAt && t.createdAt) {
|
||||
totalResponseTime += new Date(t.claimedAt) - new Date(t.createdAt);
|
||||
responseCount++;
|
||||
}
|
||||
});
|
||||
|
||||
const avgMinutes = responseCount
|
||||
? Math.round(totalResponseTime / responseCount / 60000)
|
||||
: 0;
|
||||
|
||||
const statsContainer = buildTicketStatsContainer(
|
||||
viewingUserId,
|
||||
ticketsClaimed,
|
||||
ticketsOpen,
|
||||
avgMinutes,
|
||||
);
|
||||
|
||||
const statsRow = new ActionRowBuilder().addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId("showOpenTickets")
|
||||
.setLabel("Show Open Tickets")
|
||||
.setStyle(ButtonStyle.Secondary),
|
||||
new ButtonBuilder()
|
||||
.setCustomId("backToProfile")
|
||||
.setLabel("Back to Profile")
|
||||
.setStyle(ButtonStyle.Secondary),
|
||||
);
|
||||
|
||||
return i.update({
|
||||
flags: MessageFlags.IsComponentsV2,
|
||||
components: [statsContainer, statsRow],
|
||||
});
|
||||
}
|
||||
|
||||
if (i.customId === "backToProfile") {
|
||||
const updatedProfileContainer = buildProfileContainer({
|
||||
viewingUser,
|
||||
viewingUserId,
|
||||
bio,
|
||||
timezone,
|
||||
clockedIn,
|
||||
isStaff,
|
||||
});
|
||||
|
||||
const updatedButtonRows = buildProfileButtons({
|
||||
isOwnProfile,
|
||||
isStaff,
|
||||
clockedIn,
|
||||
});
|
||||
|
||||
return i.update({
|
||||
flags: MessageFlags.IsComponentsV2,
|
||||
components: [updatedProfileContainer, ...updatedButtonRows],
|
||||
});
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error executing profile command:", error);
|
||||
|
||||
if (interaction.deferred || interaction.replied) {
|
||||
return interaction
|
||||
.editReply({
|
||||
content: "An error occurred while loading the profile.",
|
||||
components: [],
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
return interaction
|
||||
.reply({
|
||||
content: "An error occurred while loading the profile.",
|
||||
flags: MessageFlags.Ephemeral,
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
module.exports.clockedInUsers = clockedInUsers;
|
||||
@@ -0,0 +1,206 @@
|
||||
// SupportBot | Emerald Services
|
||||
// Translate Command
|
||||
|
||||
const fs = require("fs");
|
||||
const Discord = require("discord.js");
|
||||
const yaml = require("js-yaml");
|
||||
|
||||
const { translate } = require("@vitalets/google-translate-api");
|
||||
|
||||
const supportbot = require("../../Structures/ConfigStore").supportbot;
|
||||
const cmdconfig = require("../../Structures/ConfigStore").commands;
|
||||
const msgconfig = require("../../Structures/ConfigStore").messages;
|
||||
|
||||
const Command = require("../../Structures/Command.js");
|
||||
|
||||
const LANGUAGES = [
|
||||
{ name: "Auto Detect", value: "auto" },
|
||||
{ name: "English", value: "en" },
|
||||
{ name: "Spanish", value: "es" },
|
||||
{ name: "French", value: "fr" },
|
||||
{ name: "German", value: "de" },
|
||||
{ name: "Italian", value: "it" },
|
||||
{ name: "Portuguese", value: "pt" },
|
||||
{ name: "Dutch", value: "nl" },
|
||||
{ name: "Polish", value: "pl" },
|
||||
{ name: "Russian", value: "ru" },
|
||||
{ name: "Ukrainian", value: "uk" },
|
||||
{ name: "Turkish", value: "tr" },
|
||||
{ name: "Arabic", value: "ar" },
|
||||
{ name: "Hebrew", value: "he" },
|
||||
{ name: "Hindi", value: "hi" },
|
||||
{ name: "Japanese", value: "ja" },
|
||||
{ name: "Korean", value: "ko" },
|
||||
{ name: "Chinese (Simplified)", value: "zh-cn" },
|
||||
{ name: "Chinese (Traditional)", value: "zh-tw" },
|
||||
{ name: "Thai", value: "th" },
|
||||
{ name: "Vietnamese", value: "vi" },
|
||||
{ name: "Indonesian", value: "id" },
|
||||
{ name: "Greek", value: "el" },
|
||||
{ name: "Swedish", value: "sv" },
|
||||
{ name: "Romanian", value: "ro" },
|
||||
];
|
||||
|
||||
function buildContainer(title, lines = [], color = null) {
|
||||
const container = new Discord.ContainerBuilder();
|
||||
|
||||
if (color) {
|
||||
container.setAccentColor(parseInt(color.replace("#", ""), 16));
|
||||
}
|
||||
|
||||
container.addTextDisplayComponents(new Discord.TextDisplayBuilder().setContent(`## ${title}`));
|
||||
|
||||
container.addSeparatorComponents(new Discord.SeparatorBuilder().setDivider(true));
|
||||
|
||||
container.addTextDisplayComponents(new Discord.TextDisplayBuilder().setContent(lines.join("\n")));
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
function trim(text, max = 1800) {
|
||||
if (text.length <= max) return text;
|
||||
return text.substring(0, max) + "...";
|
||||
}
|
||||
|
||||
module.exports = new Command({
|
||||
name: cmdconfig.Translate.Command,
|
||||
description: cmdconfig.Translate.Description,
|
||||
type: Discord.ApplicationCommandType.ChatInput,
|
||||
|
||||
options: [
|
||||
{
|
||||
type: Discord.ApplicationCommandOptionType.String,
|
||||
name: "from",
|
||||
description: "Language to translate FROM",
|
||||
required: true,
|
||||
choices: LANGUAGES,
|
||||
},
|
||||
{
|
||||
type: Discord.ApplicationCommandOptionType.String,
|
||||
name: "to",
|
||||
description: "Language to translate TO",
|
||||
required: true,
|
||||
choices: LANGUAGES.filter((l) => l.value !== "auto"),
|
||||
},
|
||||
{
|
||||
type: Discord.ApplicationCommandOptionType.String,
|
||||
name: "text",
|
||||
description: "Text to translate",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
|
||||
permissions: cmdconfig.Translate.Permission,
|
||||
|
||||
async run(interaction) {
|
||||
const { getRole, getChannel } = interaction.client;
|
||||
|
||||
const Staff = await getRole(supportbot.Roles.StaffMember.Staff, interaction.guild);
|
||||
|
||||
const Admin = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild);
|
||||
|
||||
if (!Staff || !Admin) {
|
||||
return interaction.reply({
|
||||
content: "Some roles seem to be missing!",
|
||||
flags: Discord.MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
!interaction.member.roles.cache.has(Staff.id) &&
|
||||
!interaction.member.roles.cache.has(Admin.id)
|
||||
) {
|
||||
return interaction.reply({
|
||||
flags: Discord.MessageFlags.IsComponentsV2 | Discord.MessageFlags.Ephemeral,
|
||||
components: [
|
||||
buildContainer(
|
||||
"Invalid Permissions",
|
||||
[
|
||||
msgconfig.Error.IncorrectPerms,
|
||||
"",
|
||||
`Required role: \`${supportbot.Roles.StaffMember.Staff}\` or \`${supportbot.Roles.StaffMember.Admin}\``,
|
||||
],
|
||||
supportbot.Embed.Colours.Warn,
|
||||
),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
await interaction.deferReply({ flags: Discord.MessageFlags.Ephemeral });
|
||||
|
||||
const fromLang = interaction.options.getString("from");
|
||||
const toLang = interaction.options.getString("to");
|
||||
const text = interaction.options.getString("text");
|
||||
|
||||
const translateLog = await getChannel(supportbot.Translate?.TranslateLog, interaction.guild);
|
||||
|
||||
let result;
|
||||
|
||||
try {
|
||||
result = await translate(text, {
|
||||
from: fromLang === "auto" ? undefined : fromLang,
|
||||
to: toLang,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Translate error:", err);
|
||||
|
||||
return interaction.editReply({
|
||||
flags: Discord.MessageFlags.IsComponentsV2,
|
||||
components: [
|
||||
buildContainer(
|
||||
"Translation Failed",
|
||||
["The translation service returned an error."],
|
||||
supportbot.Embed.Colours.Warn,
|
||||
),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
const detectedLanguage = result.from?.language?.iso || result.from?.language || "unknown";
|
||||
|
||||
const container = buildContainer(
|
||||
"Translation",
|
||||
[
|
||||
`**Requested By:** <@${interaction.user.id}>`,
|
||||
`**From:** \`${fromLang === "auto" ? detectedLanguage : fromLang}\``,
|
||||
`**To:** \`${toLang}\``,
|
||||
"",
|
||||
"**Original Text**",
|
||||
trim(text),
|
||||
"",
|
||||
"**Translated Text**",
|
||||
trim(result.text),
|
||||
],
|
||||
supportbot.Embed.Colours.Success,
|
||||
);
|
||||
|
||||
await interaction.editReply({
|
||||
flags: Discord.MessageFlags.IsComponentsV2,
|
||||
components: [container],
|
||||
});
|
||||
|
||||
if (translateLog) {
|
||||
const logContainer = buildContainer(
|
||||
"Translation Log",
|
||||
[
|
||||
`User: <@${interaction.user.id}>`,
|
||||
`Channel: <#${interaction.channel.id}>`,
|
||||
`From: ${fromLang}`,
|
||||
`To: ${toLang}`,
|
||||
"",
|
||||
trim(text),
|
||||
"",
|
||||
trim(result.text),
|
||||
],
|
||||
supportbot.Embed.Colours.General,
|
||||
);
|
||||
|
||||
translateLog
|
||||
.send({
|
||||
flags: Discord.MessageFlags.IsComponentsV2,
|
||||
components: [logContainer],
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,147 @@
|
||||
const fs = require("fs");
|
||||
const Discord = require("discord.js");
|
||||
const yaml = require("js-yaml");
|
||||
const path = require("path");
|
||||
|
||||
const supportbot = require("../../Structures/ConfigStore").supportbot;
|
||||
const cmdconfig = require("../../Structures/ConfigStore").commands;
|
||||
const msgconfig = require("../../Structures/ConfigStore").messages;
|
||||
|
||||
const Command = require("../../Structures/Command.js");
|
||||
const DB = require("../../Structures/Database.js");
|
||||
|
||||
module.exports = new Command({
|
||||
name: "suggest",
|
||||
description: cmdconfig.Suggestion?.Description || "Submit a new suggestion",
|
||||
type: Discord.ApplicationCommandType.ChatInput,
|
||||
options: [
|
||||
{
|
||||
name: "text",
|
||||
description: "Your suggestion text",
|
||||
type: Discord.ApplicationCommandOptionType.String,
|
||||
required: true
|
||||
}
|
||||
],
|
||||
|
||||
async run(interaction) {
|
||||
const suggestion = interaction.options.getString("text");
|
||||
const mode = supportbot.Suggestions.Mode || "Channel";
|
||||
|
||||
const embed = new Discord.EmbedBuilder()
|
||||
.addFields(
|
||||
{ name: "Suggestion", value: suggestion, inline: true },
|
||||
{ name: "From", value: `<@${interaction.user.id}>` },
|
||||
{ name: `${supportbot.Suggestions.UpvoteEmoji} ${supportbot.Suggestions.UpvoteTitle}`, value: "0", inline: true },
|
||||
{ name: `${supportbot.Suggestions.DownvoteEmoji} ${supportbot.Suggestions.DownvoteTitle}`, value: "0", inline: true }
|
||||
)
|
||||
.setThumbnail(interaction.user.displayAvatarURL())
|
||||
.setColor(supportbot.Embed.Colours.General);
|
||||
|
||||
const row = new Discord.ActionRowBuilder().addComponents(
|
||||
new Discord.ButtonBuilder()
|
||||
.setCustomId("upvote")
|
||||
.setEmoji(supportbot.Suggestions.UpvoteEmoji)
|
||||
.setStyle(supportbot.Suggestions.Buttons.Upvote),
|
||||
new Discord.ButtonBuilder()
|
||||
.setCustomId("downvote")
|
||||
.setEmoji(supportbot.Suggestions.DownvoteEmoji)
|
||||
.setStyle(supportbot.Suggestions.Buttons.Downvote),
|
||||
new Discord.ButtonBuilder()
|
||||
.setCustomId("removevote")
|
||||
.setLabel(supportbot.Suggestions.Buttons.RemoveVote_Title)
|
||||
.setStyle(supportbot.Suggestions.Buttons.RemoveVote)
|
||||
);
|
||||
|
||||
if (mode === "Forum") {
|
||||
const forum = await interaction.guild.channels
|
||||
.fetch(supportbot.Suggestions.ForumChannel)
|
||||
.catch(() => null);
|
||||
|
||||
if (!forum || forum.type !== Discord.ChannelType.GuildForum) {
|
||||
return interaction.reply({
|
||||
embeds: [
|
||||
new Discord.EmbedBuilder()
|
||||
.setTitle("⚠ Missing Forum Channel")
|
||||
.setDescription(msgconfig.Error.MissingChannel)
|
||||
.setColor(supportbot.Embed.Colours.Error)
|
||||
],
|
||||
flags: Discord.MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
|
||||
const appliedTags = supportbot.Suggestions.Tags?.Created
|
||||
? [supportbot.Suggestions.Tags.Created]
|
||||
: [];
|
||||
|
||||
const thread = await forum.threads.create({
|
||||
name: suggestion.slice(0, 100),
|
||||
message: {
|
||||
content: "\u200B",
|
||||
embeds: [embed],
|
||||
components: [row]
|
||||
},
|
||||
appliedTags,
|
||||
autoArchiveDuration: Discord.ThreadAutoArchiveDuration.OneDay,
|
||||
reason: "New Suggestion"
|
||||
});
|
||||
|
||||
embed.setFooter({
|
||||
text: `${supportbot.Embed.Footer} | Suggestion ID: ${thread.id}`,
|
||||
iconURL: interaction.user.displayAvatarURL()
|
||||
});
|
||||
const starter = await thread.fetchStarterMessage();
|
||||
await starter.edit({ embeds: [embed], components: [row] });
|
||||
|
||||
DB.add(thread.id, interaction.user.id, suggestion);
|
||||
|
||||
return interaction.reply({
|
||||
embeds: [
|
||||
new Discord.EmbedBuilder()
|
||||
.setTitle(msgconfig.Suggestions.Sent_Title)
|
||||
.setDescription(`${msgconfig.Suggestions.Sent}`)
|
||||
.addFields({ name: 'Posted in:', value: `<#${forum.id}>` })
|
||||
.setColor(supportbot.Embed.Colours.Success)
|
||||
],
|
||||
flags: Discord.MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
|
||||
// ---------- Channel Mode ----------
|
||||
const channel = await interaction.guild.channels
|
||||
.fetch(supportbot.Suggestions.Channel)
|
||||
.catch(() => null);
|
||||
|
||||
if (!channel) {
|
||||
return interaction.reply({
|
||||
embeds: [
|
||||
new Discord.EmbedBuilder()
|
||||
.setTitle("⚠ Missing Suggestion Channel")
|
||||
.setDescription(msgconfig.Error.MissingChannel)
|
||||
.setColor(supportbot.Embed.Colours.Error)
|
||||
],
|
||||
flags: Discord.MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
|
||||
const message = await channel.send({ embeds: [embed], components: [row] });
|
||||
|
||||
embed.setFooter({
|
||||
text: `${supportbot.Embed.Footer} | Suggestion ID: ${message.id}`,
|
||||
iconURL: interaction.user.displayAvatarURL()
|
||||
});
|
||||
await message.edit({ embeds: [embed], components: [row] });
|
||||
|
||||
DB.add(message.id, interaction.user.id, suggestion);
|
||||
|
||||
return interaction.reply({
|
||||
embeds: [
|
||||
new Discord.EmbedBuilder()
|
||||
.setTitle(msgconfig.Suggestions.Sent_Title)
|
||||
.setDescription(`${msgconfig.Suggestions.Sent}`)
|
||||
.addFields({ name: 'Posted in:', value: `<#${channel.id}>` })
|
||||
.setColor(supportbot.Embed.Colours.Success)
|
||||
],
|
||||
flags: Discord.MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,183 @@
|
||||
const Discord = require("discord.js");
|
||||
const fs = require("fs");
|
||||
const yaml = require("js-yaml");
|
||||
|
||||
const supportbot = require("../../Structures/ConfigStore").supportbot;
|
||||
const msgconfig = require("../../Structures/ConfigStore").messages;
|
||||
const cmdconfig = require("../../Structures/ConfigStore").commands;
|
||||
const SuggestionDB = require("../../Structures/Database.js");
|
||||
const Command = require("../../Structures/Command.js");
|
||||
|
||||
module.exports = new Command({
|
||||
name: cmdconfig.SuggestionAdmin.Command,
|
||||
description: cmdconfig.SuggestionAdmin.Description,
|
||||
type: Discord.ApplicationCommandType.ChatInput,
|
||||
permissions: cmdconfig.SuggestionAdmin.Permission,
|
||||
options: [
|
||||
{
|
||||
name: "status",
|
||||
description: "New status",
|
||||
type: Discord.ApplicationCommandOptionType.String,
|
||||
required: true,
|
||||
choices: [
|
||||
{ name: "Solved", value: "Solved" },
|
||||
{ name: "Denied", value: "Denied" },
|
||||
{ name: "Considering", value: "Considering" }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "id",
|
||||
description: "Message ID (required in Channel mode)",
|
||||
type: Discord.ApplicationCommandOptionType.String,
|
||||
required: false
|
||||
}
|
||||
],
|
||||
|
||||
async run(interaction) {
|
||||
const status = interaction.options.getString("status");
|
||||
const idOption = interaction.options.getString("id");
|
||||
const mode = supportbot.Suggestions.Mode || "Channel";
|
||||
|
||||
const { getRole } = interaction.client;
|
||||
const staffRole = await getRole(supportbot.Roles.StaffMember.Staff, interaction.guild);
|
||||
const adminRole = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild);
|
||||
if (
|
||||
!interaction.member.roles.cache.has(staffRole?.id) &&
|
||||
!interaction.member.roles.cache.has(adminRole?.id)
|
||||
) {
|
||||
return interaction.reply({
|
||||
embeds: [
|
||||
new Discord.EmbedBuilder()
|
||||
.setTitle("Invalid Permissions!")
|
||||
.setDescription(
|
||||
`${msgconfig.Error.IncorrectPerms}\nRequired: ${supportbot.Roles.StaffMember.Staff} or ${supportbot.Roles.StaffMember.Admin}`
|
||||
)
|
||||
.setColor(supportbot.Embed.Colours.Warn)
|
||||
],
|
||||
flags: Discord.MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
|
||||
if (mode === "Forum") {
|
||||
const thread = interaction.channel;
|
||||
if (!thread.isThread() || thread.parentId !== supportbot.Suggestions.ForumChannel) {
|
||||
return interaction.reply({
|
||||
embeds: [
|
||||
new Discord.EmbedBuilder()
|
||||
.setTitle("Not a Suggestion Thread")
|
||||
.setDescription(msgconfig.Error.MissingChannel)
|
||||
.setColor(supportbot.Embed.Colours.Error)
|
||||
],
|
||||
flags: Discord.MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
|
||||
const tagMap = {
|
||||
Solved: supportbot.Suggestions.Tags.Solved,
|
||||
Denied: supportbot.Suggestions.Tags.Denied,
|
||||
Considering: supportbot.Suggestions.Tags.Considering
|
||||
};
|
||||
const tagId = tagMap[status];
|
||||
if (!tagId) {
|
||||
return interaction.reply({
|
||||
embeds: [
|
||||
new Discord.EmbedBuilder()
|
||||
.setTitle("Tag Missing")
|
||||
.setDescription("No forum tag configured for this status.")
|
||||
.setColor(supportbot.Embed.Colours.Error)
|
||||
],
|
||||
flags: Discord.MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
|
||||
await thread.setAppliedTags([tagId]).catch(() => null);
|
||||
SuggestionDB.setStatus(thread.id, status);
|
||||
|
||||
return interaction.reply({
|
||||
embeds: [
|
||||
new Discord.EmbedBuilder()
|
||||
.setTitle("Suggestion Updated")
|
||||
.setDescription(`Status set to **${status}**.`)
|
||||
.setColor(supportbot.Embed.Colours.Success)
|
||||
],
|
||||
flags: Discord.MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
|
||||
if (!idOption) {
|
||||
return interaction.reply({
|
||||
embeds: [
|
||||
new Discord.EmbedBuilder()
|
||||
.setTitle("Message ID Required")
|
||||
.setDescription("Provide the suggestion’s message ID when in Channel mode.")
|
||||
.setColor(supportbot.Embed.Colours.Error)
|
||||
],
|
||||
flags: Discord.MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
|
||||
const channel = await interaction.guild.channels
|
||||
.fetch(supportbot.Suggestions.Channel)
|
||||
.catch(() => null);
|
||||
if (!channel) {
|
||||
return interaction.reply({
|
||||
embeds: [
|
||||
new Discord.EmbedBuilder()
|
||||
.setTitle("Missing Suggestion Channel")
|
||||
.setDescription(msgconfig.Error.MissingChannel)
|
||||
.setColor(supportbot.Embed.Colours.Error)
|
||||
],
|
||||
flags: Discord.MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
|
||||
const targetMsg = await channel.messages.fetch(idOption).catch(() => null);
|
||||
if (!targetMsg) {
|
||||
return interaction.reply({
|
||||
embeds: [
|
||||
new Discord.EmbedBuilder()
|
||||
.setTitle("Message Not Found")
|
||||
.setDescription("Could not fetch a suggestion with that ID.")
|
||||
.setColor(supportbot.Embed.Colours.Error)
|
||||
],
|
||||
flags: Discord.MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
|
||||
const oldEmbed = targetMsg.embeds[0];
|
||||
if (!oldEmbed) {
|
||||
return interaction.reply({
|
||||
embeds: [
|
||||
new Discord.EmbedBuilder()
|
||||
.setTitle("No Embed Found")
|
||||
.setDescription("The target message has no suggestion embed.")
|
||||
.setColor(supportbot.Embed.Colours.Error)
|
||||
],
|
||||
flags: Discord.MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
|
||||
const colorMap = {
|
||||
Solved: supportbot.Embed.Colours.Success,
|
||||
Denied: supportbot.Embed.Colours.Error,
|
||||
Considering: supportbot.Embed.Colours.Warn
|
||||
};
|
||||
|
||||
const newEmbed = Discord.EmbedBuilder.from(oldEmbed)
|
||||
.setColor(colorMap[status])
|
||||
.addFields({ name: "Status", value: status, inline: true });
|
||||
|
||||
await targetMsg.edit({ embeds: [newEmbed], components: targetMsg.components });
|
||||
SuggestionDB.setStatus(idOption, status);
|
||||
|
||||
return interaction.reply({
|
||||
embeds: [
|
||||
new Discord.EmbedBuilder()
|
||||
.setTitle("Suggestion Updated")
|
||||
.setDescription(`Suggestion **${idOption}** marked as **${status}**.`)
|
||||
.setColor(supportbot.Embed.Colours.Success)
|
||||
],
|
||||
flags: Discord.MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,494 @@
|
||||
|
||||
|
||||
const fs = require("fs");
|
||||
const Discord = require("discord.js");
|
||||
const yaml = require("js-yaml");
|
||||
|
||||
const db = require("../../Structures/Database.js");
|
||||
const TicketManager = require("../../Structures/TicketManager.js");
|
||||
const Command = require("../../Structures/Command.js");
|
||||
|
||||
const supportbot = require("../../Structures/ConfigStore").supportbot;
|
||||
const cmdconfig = require("../../Structures/ConfigStore").commands;
|
||||
const msgconfig = require("../../Structures/ConfigStore").messages;
|
||||
|
||||
function getDepartmentConfig(departmentKey) {
|
||||
return supportbot.Ticket?.DepartmentSystem?.Departments?.[departmentKey] || null;
|
||||
}
|
||||
|
||||
function getTicketUserId(ticket) {
|
||||
return ticket?.user || ticket?.user_id || null;
|
||||
}
|
||||
|
||||
function buildContainer(title, lines = []) {
|
||||
const container = new Discord.ContainerBuilder();
|
||||
|
||||
if (supportbot.Embed?.Colours?.General) {
|
||||
container.setAccentColor(
|
||||
parseInt(String(supportbot.Embed.Colours.General).replace("#", ""), 16),
|
||||
);
|
||||
}
|
||||
|
||||
container.addTextDisplayComponents(new Discord.TextDisplayBuilder().setContent(`## ${title}`));
|
||||
|
||||
container.addSeparatorComponents(new Discord.SeparatorBuilder().setDivider(true));
|
||||
|
||||
const textContent = lines.filter(Boolean).join("\n").trim();
|
||||
if (textContent.length > 0) {
|
||||
container.addTextDisplayComponents(
|
||||
new Discord.TextDisplayBuilder().setContent(textContent),
|
||||
);
|
||||
}
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
function buildReviewSummaryContainer(ticket, rating, comment) {
|
||||
const departmentConfig = ticket.department ? getDepartmentConfig(ticket.department) : null;
|
||||
const departmentName = departmentConfig?.Name || ticket.department || null;
|
||||
const departmentEmoji = departmentConfig?.Emoji || "🎫";
|
||||
|
||||
const claimedByEnabled = supportbot.Ticket?.ClaimTickets?.Enabled === true;
|
||||
const claimedBy = claimedByEnabled && ticket.claimedBy ? `<@${ticket.claimedBy}>` : null;
|
||||
|
||||
const ticketUserId = getTicketUserId(ticket);
|
||||
const lines = [
|
||||
`**${msgconfig.ReviewSystem.ReviewEmbed.ReviewedByTitle}** ${ticketUserId ? `<@${ticketUserId}>` : "Unknown User"}`,
|
||||
];
|
||||
|
||||
if (claimedBy) {
|
||||
lines.unshift(`**${msgconfig.ReviewSystem.ReviewEmbed.ReviewedStaffTitle}** ${claimedBy}`);
|
||||
}
|
||||
|
||||
if (supportbot.Ticket?.DepartmentSystem?.Enabled === true && departmentName) {
|
||||
lines.push(`**Department** ${departmentEmoji} ${departmentName}`);
|
||||
}
|
||||
|
||||
lines.push("");
|
||||
lines.push(`**${msgconfig.ReviewSystem.ReviewEmbed.RatingTitle}**`);
|
||||
lines.push(`${msgconfig.ReviewSystem.ReviewEmbed.ReviewEmoji.repeat(Number(rating))}`);
|
||||
lines.push("");
|
||||
lines.push(`**${msgconfig.ReviewSystem.ReviewEmbed.CommentTitle}**`);
|
||||
lines.push(comment || "No Comment Provided");
|
||||
|
||||
const container = new Discord.ContainerBuilder();
|
||||
|
||||
if (msgconfig.ReviewSystem?.ReviewEmbed?.Color) {
|
||||
container.setAccentColor(
|
||||
parseInt(String(msgconfig.ReviewSystem.ReviewEmbed.Color).replace("#", ""), 16),
|
||||
);
|
||||
} else if (supportbot.Embed?.Colours?.General) {
|
||||
container.setAccentColor(
|
||||
parseInt(String(supportbot.Embed.Colours.General).replace("#", ""), 16),
|
||||
);
|
||||
}
|
||||
|
||||
container.addTextDisplayComponents(
|
||||
new Discord.TextDisplayBuilder().setContent(`## ${msgconfig.ReviewSystem.ReviewEmbed.Title}`),
|
||||
);
|
||||
|
||||
container.addSeparatorComponents(new Discord.SeparatorBuilder().setDivider(true));
|
||||
|
||||
container.addTextDisplayComponents(new Discord.TextDisplayBuilder().setContent(lines.join("\n")));
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
async function collectReview(interaction, ticket) {
|
||||
const ticketUserId = getTicketUserId(ticket);
|
||||
if (!ticketUserId) return null;
|
||||
|
||||
const ratingContainer = buildContainer(msgconfig.ReviewSystem.Rate.Title, [
|
||||
msgconfig.ReviewSystem.Rate.Description,
|
||||
"",
|
||||
]);
|
||||
|
||||
const ratingRow = new Discord.ActionRowBuilder().addComponents(
|
||||
new Discord.StringSelectMenuBuilder()
|
||||
.setCustomId(`closeReviewRating:${interaction.id}`)
|
||||
.setPlaceholder("Select a rating")
|
||||
.addOptions([
|
||||
{ label: msgconfig.ReviewSystem.Stars.One, value: "1" },
|
||||
{ label: msgconfig.ReviewSystem.Stars.Two, value: "2" },
|
||||
{ label: msgconfig.ReviewSystem.Stars.Three, value: "3" },
|
||||
{ label: msgconfig.ReviewSystem.Stars.Four, value: "4" },
|
||||
{ label: msgconfig.ReviewSystem.Stars.Five, value: "5" },
|
||||
]),
|
||||
);
|
||||
|
||||
ratingContainer.addActionRowComponents(ratingRow);
|
||||
|
||||
const ratingPrompt = await interaction.followUp({
|
||||
flags: Discord.MessageFlags.IsComponentsV2,
|
||||
components: [ratingContainer],
|
||||
});
|
||||
|
||||
let rating = null;
|
||||
|
||||
try {
|
||||
const ratingInteraction = await ratingPrompt.awaitMessageComponent({
|
||||
componentType: Discord.ComponentType.StringSelect,
|
||||
time: 120000,
|
||||
filter: (i) =>
|
||||
i.customId === `closeReviewRating:${interaction.id}` && i.user.id === ticketUserId,
|
||||
});
|
||||
|
||||
rating = ratingInteraction.values[0];
|
||||
await ratingInteraction.deferUpdate().catch(() => {});
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
const commentContainer = buildContainer(msgconfig.ReviewSystem.Comment.Title, [
|
||||
msgconfig.ReviewSystem.Comment.Description,
|
||||
"",
|
||||
]);
|
||||
|
||||
const commentRow = new Discord.ActionRowBuilder().addComponents(
|
||||
new Discord.ButtonBuilder()
|
||||
.setCustomId(`closeReviewWrite:${interaction.id}`)
|
||||
.setLabel("Write Comment")
|
||||
.setStyle(Discord.ButtonStyle.Secondary),
|
||||
new Discord.ButtonBuilder()
|
||||
.setCustomId(`closeReviewSkip:${interaction.id}`)
|
||||
.setLabel("Skip Comment")
|
||||
.setStyle(Discord.ButtonStyle.Danger),
|
||||
);
|
||||
|
||||
commentContainer.addActionRowComponents(commentRow);
|
||||
|
||||
const commentPrompt = await interaction.followUp({
|
||||
flags: Discord.MessageFlags.IsComponentsV2,
|
||||
components: [commentContainer],
|
||||
});
|
||||
|
||||
let comment = "No Comment Provided";
|
||||
|
||||
try {
|
||||
const buttonInteraction = await commentPrompt.awaitMessageComponent({
|
||||
componentType: Discord.ComponentType.Button,
|
||||
time: 120000,
|
||||
filter: (i) =>
|
||||
(i.customId === `closeReviewWrite:${interaction.id}` ||
|
||||
i.customId === `closeReviewSkip:${interaction.id}`) &&
|
||||
i.user.id === ticketUserId,
|
||||
});
|
||||
|
||||
if (buttonInteraction.customId === `closeReviewSkip:${interaction.id}`) {
|
||||
await buttonInteraction.deferUpdate().catch(() => {});
|
||||
return { rating, comment };
|
||||
}
|
||||
|
||||
await buttonInteraction
|
||||
.update({
|
||||
flags: Discord.MessageFlags.IsComponentsV2,
|
||||
components: [
|
||||
buildContainer(msgconfig.ReviewSystem.Comment.Title, [
|
||||
`Requested from: <@${ticketUserId}>`,
|
||||
"",
|
||||
"Please send your review comment in the channel now.",
|
||||
"Type `no` to skip.",
|
||||
]),
|
||||
],
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
const collected = await interaction.channel.awaitMessages({
|
||||
max: 1,
|
||||
time: 120000,
|
||||
filter: (m) => m.author.id === ticketUserId,
|
||||
});
|
||||
|
||||
const message = collected.first();
|
||||
|
||||
if (message) {
|
||||
const rawComment = message.content?.trim();
|
||||
if (rawComment && rawComment.toLowerCase() !== "no") {
|
||||
comment = rawComment;
|
||||
}
|
||||
|
||||
await message.delete().catch(() => {});
|
||||
}
|
||||
} catch {
|
||||
return { rating, comment };
|
||||
}
|
||||
|
||||
return { rating, comment };
|
||||
}
|
||||
|
||||
function getTranscriptAttachmentFromResult(result) {
|
||||
if (!result) return null;
|
||||
|
||||
if (typeof result === "string") {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (result.filePath) return result.filePath;
|
||||
if (result.attachment) return result.attachment;
|
||||
if (result.path) return result.path;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function maybeDMTranscript(interaction, ticket, transcriptResult) {
|
||||
if (supportbot.Ticket?.UserManagement?.DMTranscript !== true) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ticketUserId = getTicketUserId(ticket);
|
||||
if (!ticketUserId) return;
|
||||
|
||||
const ticketUser = await interaction.client.users.fetch(ticketUserId).catch(() => null);
|
||||
|
||||
if (!ticketUser) return;
|
||||
|
||||
const attachmentPath = getTranscriptAttachmentFromResult(transcriptResult);
|
||||
if (!attachmentPath) return;
|
||||
|
||||
try {
|
||||
await ticketUser.send({
|
||||
content: `Here is a copy of your ticket transcript from \`${interaction.guild.name}\`.`,
|
||||
files: [attachmentPath],
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to DM transcript to user:", error);
|
||||
}
|
||||
}
|
||||
|
||||
async function collectCloseConfirmation(interaction, ticket, reason) {
|
||||
const ticketUserId = getTicketUserId(ticket);
|
||||
|
||||
const lines = [
|
||||
"Are you sure you want to close this ticket?",
|
||||
"",
|
||||
`**Ticket:** ${interaction.channel.name}`,
|
||||
`**Reason:** ${reason || "No Reason Provided."}`,
|
||||
];
|
||||
|
||||
if (ticketUserId) {
|
||||
lines.push(`**Ticket User:** <@${ticketUserId}>`);
|
||||
}
|
||||
|
||||
if (supportbot.Ticket?.ReviewSystem?.Enabled === true) {
|
||||
lines.push("");
|
||||
lines.push("A review will be requested before the ticket is closed.");
|
||||
}
|
||||
|
||||
const confirmContainer = buildContainer("Close Confirmation", lines);
|
||||
|
||||
const confirmRow = new Discord.ActionRowBuilder().addComponents(
|
||||
new Discord.ButtonBuilder()
|
||||
.setCustomId(`confirmClose:${interaction.id}`)
|
||||
.setLabel("Confirm Close")
|
||||
.setStyle(Discord.ButtonStyle.Danger),
|
||||
new Discord.ButtonBuilder()
|
||||
.setCustomId(`cancelClose:${interaction.id}`)
|
||||
.setLabel("Cancel")
|
||||
.setStyle(Discord.ButtonStyle.Secondary),
|
||||
);
|
||||
|
||||
confirmContainer.addActionRowComponents(confirmRow);
|
||||
|
||||
const confirmMessage = await interaction.editReply({
|
||||
flags: Discord.MessageFlags.IsComponentsV2,
|
||||
components: [confirmContainer],
|
||||
});
|
||||
|
||||
try {
|
||||
const buttonInteraction = await confirmMessage.awaitMessageComponent({
|
||||
componentType: Discord.ComponentType.Button,
|
||||
time: 120000,
|
||||
filter: (i) =>
|
||||
(i.customId === `confirmClose:${interaction.id}` ||
|
||||
i.customId === `cancelClose:${interaction.id}`) &&
|
||||
i.user.id === interaction.user.id,
|
||||
});
|
||||
|
||||
if (buttonInteraction.customId === `cancelClose:${interaction.id}`) {
|
||||
await buttonInteraction
|
||||
.update({
|
||||
flags: Discord.MessageFlags.IsComponentsV2,
|
||||
components: [
|
||||
buildContainer("Close Cancelled", ["The ticket close process has been cancelled."]),
|
||||
],
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
await buttonInteraction.deferUpdate().catch(() => {});
|
||||
return true;
|
||||
} catch {
|
||||
await interaction
|
||||
.editReply({
|
||||
flags: Discord.MessageFlags.IsComponentsV2,
|
||||
components: [buildContainer("Close Cancelled", ["The close confirmation timed out."])],
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function postReview(interaction, ticket, reviewData) {
|
||||
const { getChannel } = interaction.client;
|
||||
const reviewChannel = await getChannel(supportbot.Ticket.ReviewSystem.Channel, interaction.guild);
|
||||
|
||||
if (!reviewChannel || !reviewData) return;
|
||||
|
||||
const reviewContainer = buildReviewSummaryContainer(
|
||||
ticket,
|
||||
reviewData.rating,
|
||||
reviewData.comment,
|
||||
);
|
||||
|
||||
await reviewChannel
|
||||
.send({
|
||||
flags: Discord.MessageFlags.IsComponentsV2,
|
||||
components: [reviewContainer],
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
async function handleCloseTicket(interaction, reason, ticket) {
|
||||
try {
|
||||
if (typeof db.updateTicketStatus === "function") {
|
||||
db.updateTicketStatus(interaction.channel.id, "closed");
|
||||
}
|
||||
|
||||
const transcriptResult = await TicketManager.createTranscript(interaction, ticket, reason);
|
||||
|
||||
await maybeDMTranscript(interaction, ticket, transcriptResult);
|
||||
|
||||
await interaction
|
||||
.editReply({
|
||||
flags: Discord.MessageFlags.IsComponentsV2,
|
||||
components: [
|
||||
buildContainer("Ticket Closed", [
|
||||
`**Ticket:** ${interaction.channel.name}`,
|
||||
`**Closed By:** <@${interaction.user.id}>`,
|
||||
`**Reason:** ${reason || "No Reason Provided."}`,
|
||||
]),
|
||||
],
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
await interaction.channel.delete().catch(() => {});
|
||||
} catch (err) {
|
||||
console.error("Error handling ticket close:", err);
|
||||
|
||||
if (interaction.deferred || interaction.replied) {
|
||||
await interaction
|
||||
.editReply({
|
||||
content: "An error occurred while closing the ticket.",
|
||||
components: [],
|
||||
})
|
||||
.catch(() => {});
|
||||
return;
|
||||
}
|
||||
|
||||
await interaction
|
||||
.reply({
|
||||
content: "An error occurred while closing the ticket.",
|
||||
flags: Discord.MessageFlags.Ephemeral,
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new Command({
|
||||
name: cmdconfig.CloseTicket.Command,
|
||||
description: cmdconfig.CloseTicket.Description,
|
||||
type: Discord.ApplicationCommandType.ChatInput,
|
||||
options: [
|
||||
{
|
||||
name: "reason",
|
||||
description: "Ticket Close Reason",
|
||||
type: Discord.ApplicationCommandOptionType.String,
|
||||
},
|
||||
],
|
||||
permissions: cmdconfig.CloseTicket.Permission,
|
||||
|
||||
async run(interaction) {
|
||||
const { getRole } = interaction.client;
|
||||
|
||||
if (supportbot.Ticket.Close.StaffOnly) {
|
||||
const SupportStaff = await getRole(supportbot.Roles.StaffMember.Staff, interaction.guild);
|
||||
const Admin = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild);
|
||||
|
||||
if (!SupportStaff || !Admin) {
|
||||
return interaction.reply({
|
||||
content: "Some roles seem to be missing! Please check for errors when starting the bot.",
|
||||
flags: Discord.MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
!interaction.member.roles.cache.has(SupportStaff.id) &&
|
||||
!interaction.member.roles.cache.has(Admin.id)
|
||||
) {
|
||||
return interaction.reply({
|
||||
flags: Discord.MessageFlags.IsComponentsV2 | Discord.MessageFlags.Ephemeral,
|
||||
components: [
|
||||
buildContainer("Invalid Permissions!", [
|
||||
msgconfig.Error.IncorrectPerms,
|
||||
"",
|
||||
`Role Required: \`${supportbot.Roles.StaffMember.Staff}\` or \`${supportbot.Roles.StaffMember.Admin}\``,
|
||||
]),
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const isThread = interaction.channel.type === Discord.ChannelType.PrivateThread;
|
||||
|
||||
if (
|
||||
(supportbot.Ticket.TicketType === "threads" && !isThread) ||
|
||||
(supportbot.Ticket.TicketType === "channels" &&
|
||||
interaction.channel.type !== Discord.ChannelType.GuildText)
|
||||
) {
|
||||
return interaction.reply({
|
||||
flags: Discord.MessageFlags.IsComponentsV2 | Discord.MessageFlags.Ephemeral,
|
||||
components: [
|
||||
buildContainer("Invalid Channel!", [
|
||||
`This command can only be used in a ${
|
||||
supportbot.Ticket.TicketType === "threads" ? "ticket thread" : "ticket channel"
|
||||
}.`,
|
||||
]),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
await interaction.deferReply({ flags: Discord.MessageFlags.Ephemeral });
|
||||
|
||||
const ticket = db.getTicket(interaction.channel.id);
|
||||
|
||||
if (!ticket) {
|
||||
return interaction.editReply({
|
||||
flags: Discord.MessageFlags.IsComponentsV2,
|
||||
components: [buildContainer("No Ticket Found!", [msgconfig.Error.NoValidTicket])],
|
||||
});
|
||||
}
|
||||
|
||||
const reason = interaction.options?.getString("reason") || "No Reason Provided.";
|
||||
|
||||
const confirmed = await collectCloseConfirmation(interaction, ticket, reason);
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (supportbot.Ticket?.ReviewSystem?.Enabled === true) {
|
||||
const reviewData = await collectReview(interaction, ticket);
|
||||
|
||||
if (reviewData) {
|
||||
await postReview(interaction, ticket, reviewData);
|
||||
}
|
||||
|
||||
await handleCloseTicket(interaction, reason, ticket);
|
||||
return;
|
||||
}
|
||||
|
||||
await handleCloseTicket(interaction, reason, ticket);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,664 @@
|
||||
const fs = require("fs");
|
||||
const yaml = require("js-yaml");
|
||||
const {
|
||||
EmbedBuilder,
|
||||
ApplicationCommandOptionType,
|
||||
ApplicationCommandType,
|
||||
ChannelType,
|
||||
MessageFlags,
|
||||
ModalBuilder,
|
||||
ActionRowBuilder,
|
||||
TextInputBuilder,
|
||||
TextInputStyle,
|
||||
} = require("discord.js");
|
||||
|
||||
const supportbot = require("../../Structures/ConfigStore").supportbot;
|
||||
const msgconfig = require("../../Structures/ConfigStore").messages;
|
||||
const cmdconfig = require("../../Structures/ConfigStore").commands;
|
||||
|
||||
const TicketManager = require("../../Structures/TicketManager.js");
|
||||
const Command = require("../../Structures/Command.js");
|
||||
const db = require("../../Structures/Database.js");
|
||||
|
||||
function getDepartments() {
|
||||
return supportbot.Ticket?.DepartmentSystem?.Departments || {};
|
||||
}
|
||||
|
||||
function getPriorities() {
|
||||
return supportbot.Ticket?.PrioritySystem?.Priorities || {};
|
||||
}
|
||||
|
||||
function prioritiesEnabled() {
|
||||
return (
|
||||
supportbot.Ticket?.TicketType !== "threads" &&
|
||||
supportbot.Ticket?.PrioritySystem?.Enabled === true
|
||||
);
|
||||
}
|
||||
|
||||
function departmentsEnabled() {
|
||||
return (
|
||||
supportbot.Ticket?.TicketType !== "threads" &&
|
||||
supportbot.Ticket?.DepartmentSystem?.Enabled === true
|
||||
);
|
||||
}
|
||||
|
||||
function buildTicketChannelName(ticketNumberID, priorityKey, departmentConfig) {
|
||||
const defaultPrefix = supportbot.Ticket?.ChannelPrefix || supportbot.Ticket?.Channel || "ticket-";
|
||||
|
||||
const departmentPrefix = departmentConfig?.ChannelPrefix || defaultPrefix;
|
||||
|
||||
const safePrefix = String(departmentPrefix).trim() || "ticket-";
|
||||
const baseName = `${safePrefix}${ticketNumberID}`;
|
||||
|
||||
if (!prioritiesEnabled() || !priorityKey) {
|
||||
return baseName;
|
||||
}
|
||||
|
||||
return `${baseName}-${String(priorityKey).toLowerCase()}`;
|
||||
}
|
||||
|
||||
module.exports = new Command({
|
||||
name: cmdconfig.TicketManage?.Command || "ticketmanage",
|
||||
description: cmdconfig.TicketManage?.Description || "Manage tickets",
|
||||
type: ApplicationCommandType.ChatInput,
|
||||
permissions: cmdconfig.TicketManage?.Permission || ["ManageChannels"],
|
||||
|
||||
options: [
|
||||
{
|
||||
name: "transfer",
|
||||
description: "Transfer a ticket to another department",
|
||||
type: ApplicationCommandOptionType.Subcommand,
|
||||
options: [
|
||||
{
|
||||
name: "department",
|
||||
description: "Department to transfer this ticket to",
|
||||
type: ApplicationCommandOptionType.String,
|
||||
required: true,
|
||||
choices: Object.entries(getDepartments())
|
||||
.slice(0, 25)
|
||||
.map(([key, dept]) => ({
|
||||
name: dept.Name || key,
|
||||
value: key,
|
||||
})),
|
||||
},
|
||||
{
|
||||
name: "priority",
|
||||
description: "Optional new priority",
|
||||
type: ApplicationCommandOptionType.String,
|
||||
required: false,
|
||||
choices: Object.entries(getPriorities())
|
||||
.slice(0, 25)
|
||||
.map(([key, priority]) => ({
|
||||
name: priority.Name || key,
|
||||
value: key,
|
||||
})),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "priority",
|
||||
description: "Change a ticket priority",
|
||||
type: ApplicationCommandOptionType.Subcommand,
|
||||
options: [
|
||||
{
|
||||
name: "level",
|
||||
description: "New priority level",
|
||||
type: ApplicationCommandOptionType.String,
|
||||
required: true,
|
||||
choices: Object.entries(getPriorities())
|
||||
.slice(0, 25)
|
||||
.map(([key, priority]) => ({
|
||||
name: priority.Name || key,
|
||||
value: key,
|
||||
})),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: cmdconfig.TicketManage?.ForceAdd?.Command || "forceadd",
|
||||
description:
|
||||
cmdconfig.TicketManage?.ForceAdd?.Description || "Force add a user to the current ticket",
|
||||
type: ApplicationCommandOptionType.Subcommand,
|
||||
options: [
|
||||
{
|
||||
name: "user",
|
||||
description: "The user to add",
|
||||
type: ApplicationCommandOptionType.User,
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: cmdconfig.TicketManage?.ForceRemove?.Command || "forceremove",
|
||||
description:
|
||||
cmdconfig.TicketManage?.ForceRemove?.Description || "Remove a user from the current ticket",
|
||||
type: ApplicationCommandOptionType.Subcommand,
|
||||
options: [
|
||||
{
|
||||
name: "user",
|
||||
description: "The user to remove",
|
||||
type: ApplicationCommandOptionType.User,
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: cmdconfig.TicketManage?.Rename?.Command || "rename",
|
||||
description: cmdconfig.TicketManage?.Rename?.Description || "Rename the current ticket",
|
||||
type: ApplicationCommandOptionType.Subcommand,
|
||||
options: [],
|
||||
},
|
||||
{
|
||||
name: cmdconfig.TicketManage?.Close?.Command || "close",
|
||||
description: cmdconfig.TicketManage?.Close?.Description || "Close the current ticket",
|
||||
type: ApplicationCommandOptionType.Subcommand,
|
||||
options: [
|
||||
{
|
||||
name: "reason",
|
||||
description: "Reason for closing the ticket",
|
||||
type: ApplicationCommandOptionType.String,
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
async run(interaction) {
|
||||
try {
|
||||
const subcommand = interaction.options.getSubcommand(false);
|
||||
const ticket = db.getTicket(interaction.channel.id);
|
||||
|
||||
if (!ticket) {
|
||||
return interaction.reply({
|
||||
content: "This channel is not a ticket.",
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
|
||||
const { getRole } = interaction.client;
|
||||
|
||||
let SupportStaff = await getRole(supportbot.Roles.StaffMember.Staff, interaction.guild);
|
||||
let Admin = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild);
|
||||
let Moderator = await getRole(supportbot.Roles.StaffMember.Moderator, interaction.guild);
|
||||
|
||||
if (!SupportStaff || !Admin || !Moderator) {
|
||||
const missingRolesEmbed = new EmbedBuilder()
|
||||
.setDescription(msgconfig.Error.InvalidChannel || "Required roles are missing!")
|
||||
.setColor(supportbot.Embed.Colours.Warn);
|
||||
|
||||
return interaction.reply({
|
||||
embeds: [missingRolesEmbed],
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
|
||||
const NoPerms = new EmbedBuilder()
|
||||
.setDescription(
|
||||
msgconfig.Error.IncorrectPerms || "You do not have the correct permissions!",
|
||||
)
|
||||
.setColor(supportbot.Embed.Colours.Warn);
|
||||
|
||||
if (
|
||||
!interaction.member.roles.cache.has(Admin.id) &&
|
||||
!interaction.member.roles.cache.has(Moderator.id) &&
|
||||
(!supportbot.Roles?.Mod?.AllowSupportStaff || !interaction.member.roles.cache.has(SupportStaff.id))
|
||||
) {
|
||||
return interaction.reply({
|
||||
embeds: [NoPerms],
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
|
||||
if (subcommand === "transfer") {
|
||||
if (!departmentsEnabled()) {
|
||||
return interaction.reply({
|
||||
content: "Departments are disabled, or ticket mode is set to threads.",
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
|
||||
if (interaction.channel.type !== ChannelType.GuildText) {
|
||||
return interaction.reply({
|
||||
content: "Ticket transfer only works for ticket channels.",
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
|
||||
const departmentKey = interaction.options.getString("department");
|
||||
const requestedPriority = interaction.options.getString("priority");
|
||||
|
||||
const departments = getDepartments();
|
||||
const departmentConfig = departments[departmentKey];
|
||||
|
||||
if (!departmentConfig) {
|
||||
return interaction.reply({
|
||||
content: "That department does not exist.",
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
|
||||
const targetCategoryId = departmentConfig.Category;
|
||||
const targetRoleId = departmentConfig.Role || null;
|
||||
|
||||
if (!targetCategoryId) {
|
||||
return interaction.reply({
|
||||
content: "That department does not have a category configured.",
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
|
||||
const targetCategory = interaction.guild.channels.cache.get(targetCategoryId);
|
||||
if (!targetCategory) {
|
||||
return interaction.reply({
|
||||
content: "The target department category could not be found.",
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
|
||||
let finalPriority = ticket.priority || null;
|
||||
|
||||
if (requestedPriority) {
|
||||
finalPriority = requestedPriority;
|
||||
} else if (!finalPriority && departmentConfig.DefaultPriority) {
|
||||
finalPriority = departmentConfig.DefaultPriority;
|
||||
}
|
||||
|
||||
const oldDepartmentKey = ticket.department || null;
|
||||
const oldDepartmentConfig = oldDepartmentKey ? departments[oldDepartmentKey] : null;
|
||||
const oldRoleId = oldDepartmentConfig?.Role || null;
|
||||
|
||||
await interaction.channel.setParent(targetCategory.id);
|
||||
|
||||
if (oldRoleId && oldRoleId !== targetRoleId) {
|
||||
await interaction.channel.permissionOverwrites.delete(oldRoleId).catch(() => { });
|
||||
}
|
||||
|
||||
if (targetRoleId) {
|
||||
await interaction.channel.permissionOverwrites.create(targetRoleId, {
|
||||
ViewChannel: true,
|
||||
SendMessages: true,
|
||||
ReadMessageHistory: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof db.updateTicketDepartment === "function") {
|
||||
db.updateTicketDepartment(interaction.channel.id, departmentKey);
|
||||
}
|
||||
|
||||
if (finalPriority && typeof db.updateTicketPriority === "function") {
|
||||
db.updateTicketPriority(interaction.channel.id, finalPriority);
|
||||
}
|
||||
|
||||
const ticketNumber = ticket.number || interaction.channel.name.replace(/\D/g, "");
|
||||
|
||||
const newChannelName = buildTicketChannelName(
|
||||
ticketNumber,
|
||||
finalPriority,
|
||||
departmentConfig,
|
||||
);
|
||||
|
||||
if (interaction.channel.name !== newChannelName) {
|
||||
await interaction.channel.setName(newChannelName).catch(() => { });
|
||||
}
|
||||
|
||||
const deptEmoji = departmentConfig.Emoji || "🎫";
|
||||
const deptName = departmentConfig.Name || departmentKey;
|
||||
|
||||
let description = `> **Department:** ${deptEmoji} ${deptName}`;
|
||||
|
||||
if (finalPriority && prioritiesEnabled()) {
|
||||
const priorityConfig = getPriorities()[finalPriority];
|
||||
const priorityEmoji = priorityConfig?.Emoji || "🟡";
|
||||
const priorityName = priorityConfig?.Name || finalPriority;
|
||||
description += `\n> **Priority:** ${priorityEmoji} ${priorityName}`;
|
||||
}
|
||||
|
||||
description += `\n> **Transferred By:** <@${interaction.user.id}>`;
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle("Ticket Updated")
|
||||
.setDescription(description)
|
||||
.setColor(supportbot.Embed.Colours.Success);
|
||||
|
||||
return interaction.reply({
|
||||
embeds: [embed],
|
||||
});
|
||||
}
|
||||
|
||||
if (subcommand === "priority") {
|
||||
if (!prioritiesEnabled()) {
|
||||
return interaction.reply({
|
||||
content: "Priority is disabled, or ticket mode is set to threads.",
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
|
||||
const level = interaction.options.getString("level");
|
||||
const priorityConfig = getPriorities()[level];
|
||||
|
||||
if (!priorityConfig) {
|
||||
return interaction.reply({
|
||||
content: "That priority does not exist.",
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof db.updateTicketPriority === "function") {
|
||||
db.updateTicketPriority(interaction.channel.id, level);
|
||||
}
|
||||
|
||||
const departments = getDepartments();
|
||||
const departmentConfig = ticket.department ? departments[ticket.department] : null;
|
||||
const ticketNumber = ticket.number || interaction.channel.name.replace(/\D/g, "");
|
||||
|
||||
const newChannelName = buildTicketChannelName(ticketNumber, level, departmentConfig);
|
||||
|
||||
if (interaction.channel.name !== newChannelName) {
|
||||
await interaction.channel.setName(newChannelName).catch(() => { });
|
||||
}
|
||||
|
||||
const priorityEmoji = priorityConfig.Emoji || "🟡";
|
||||
const priorityName = priorityConfig.Name || level;
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle("Ticket Updated")
|
||||
.setDescription(
|
||||
`> **Priority:** ${priorityEmoji} ${priorityName}\n> **Updated By:** <@${interaction.user.id}>`,
|
||||
)
|
||||
.setColor(supportbot.Embed.Colours.Success);
|
||||
|
||||
return interaction.reply({
|
||||
embeds: [embed],
|
||||
});
|
||||
}
|
||||
|
||||
if (subcommand === (cmdconfig.TicketManage?.ForceAdd?.Command || "forceadd")) {
|
||||
const userToAdd = interaction.options.getUser("user");
|
||||
const ticketChannel = interaction.channel;
|
||||
|
||||
if (
|
||||
(supportbot.Ticket.TicketType === "threads" && !ticketChannel.isThread()) ||
|
||||
(supportbot.Ticket.TicketType === "channels" &&
|
||||
ticketChannel.type !== ChannelType.GuildText)
|
||||
) {
|
||||
const onlyInTicket = new EmbedBuilder()
|
||||
.setTitle(msgconfig.ForceAddUser.NotInTicket_Title)
|
||||
.setDescription(msgconfig.ForceAddUser.NotInTicket_Description)
|
||||
.setColor(supportbot.Embed.Colours.Error);
|
||||
|
||||
return interaction.reply({
|
||||
embeds: [onlyInTicket],
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
|
||||
const ticketRow = db.getTicket(ticketChannel.id);
|
||||
if (!ticketRow) {
|
||||
const notFound = new EmbedBuilder()
|
||||
.setTitle(msgconfig.ForceAddUser.NotInTicket_Title)
|
||||
.setDescription(msgconfig.ForceAddUser.NotInTicket_Description)
|
||||
.setColor(supportbot.Embed.Colours.Error);
|
||||
|
||||
return interaction.reply({
|
||||
embeds: [notFound],
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
if (supportbot.Ticket.TicketType === "threads") {
|
||||
const botMember = await ticketChannel.members
|
||||
.fetch(interaction.client.user.id)
|
||||
.catch(() => null);
|
||||
|
||||
if (!botMember) await ticketChannel.join();
|
||||
|
||||
await new Promise((res) => setTimeout(res, 1000));
|
||||
await ticketChannel.members.add(userToAdd.id);
|
||||
} else {
|
||||
await ticketChannel.permissionOverwrites.create(userToAdd.id, {
|
||||
ViewChannel: true,
|
||||
SendMessages: true,
|
||||
ReadMessageHistory: true,
|
||||
});
|
||||
}
|
||||
|
||||
db.addUserToTicket(ticketChannel.id, userToAdd.id);
|
||||
|
||||
const addedEmbed = new EmbedBuilder()
|
||||
.setTitle(msgconfig.ForceAddUser.Added_Title)
|
||||
.setDescription(
|
||||
msgconfig.ForceAddUser.Added_Description.replace("%username%", userToAdd.username),
|
||||
)
|
||||
.setColor(supportbot.Embed.Colours.Success);
|
||||
|
||||
await interaction.reply({
|
||||
embeds: [addedEmbed],
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
|
||||
if (supportbot.Ticket?.UserManagement?.DMOnAdd !== false) {
|
||||
const addedToTicketEmbed = new EmbedBuilder()
|
||||
.setTitle(msgconfig.ForceAddUser.AddedToTicket_Title)
|
||||
.setDescription(
|
||||
msgconfig.ForceAddUser.AddedToTicket_Description.replace(
|
||||
"%channel_link%",
|
||||
`[${ticketChannel.name}](https://discord.com/channels/${ticketChannel.guild.id}/${ticketChannel.id})`,
|
||||
),
|
||||
)
|
||||
.setColor(supportbot.Embed.Colours.General);
|
||||
|
||||
await userToAdd.send({ embeds: [addedToTicketEmbed] }).catch(() => { });
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error adding user to the ticket:", err);
|
||||
|
||||
const errorEmbed = new EmbedBuilder()
|
||||
.setTitle(msgconfig.ForceAddUser.Error_Title)
|
||||
.setDescription(msgconfig.ForceAddUser.Error_Adding)
|
||||
.setColor(supportbot.Embed.Colours.Error);
|
||||
|
||||
await interaction.reply({
|
||||
embeds: [errorEmbed],
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (subcommand === (cmdconfig.TicketManage?.ForceRemove?.Command || "forceremove")) {
|
||||
const userToRemove = interaction.options.getUser("user");
|
||||
const ticketChannel = interaction.channel;
|
||||
|
||||
if (
|
||||
(supportbot.Ticket.TicketType === "threads" && !ticketChannel.isThread()) ||
|
||||
(supportbot.Ticket.TicketType === "channels" &&
|
||||
ticketChannel.type !== ChannelType.GuildText)
|
||||
) {
|
||||
const onlyInTicket = new EmbedBuilder()
|
||||
.setTitle(msgconfig.RemoveUser.NotInTicket_Title)
|
||||
.setDescription(msgconfig.RemoveUser.NotInTicket_Description)
|
||||
.setColor(supportbot.Embed.Colours.Error);
|
||||
|
||||
return interaction.reply({
|
||||
embeds: [onlyInTicket],
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
if (supportbot.Ticket.TicketType === "threads") {
|
||||
await ticketChannel.members.remove(userToRemove.id);
|
||||
} else {
|
||||
await ticketChannel.permissionOverwrites.delete(userToRemove.id);
|
||||
}
|
||||
|
||||
db.removeUserFromTicket(ticketChannel.id, userToRemove.id);
|
||||
|
||||
const removedEmbed = new EmbedBuilder()
|
||||
.setTitle(msgconfig.RemoveUser.Removed_Title)
|
||||
.setDescription(msgconfig.RemoveUser.Removed_Message)
|
||||
.setColor(supportbot.Embed.Colours.Success);
|
||||
|
||||
await interaction.reply({
|
||||
embeds: [removedEmbed],
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
|
||||
if (supportbot.Ticket?.UserManagement?.DMOnRemove !== false) {
|
||||
const removedFromTicketEmbed = new EmbedBuilder()
|
||||
.setTitle(msgconfig.RemoveUser.RemovedFromTicket_Title)
|
||||
.setDescription(
|
||||
msgconfig.RemoveUser.RemovedFromTicket_Description.replace(
|
||||
"%channel_link%",
|
||||
`[${ticketChannel.name}](https://discord.com/channels/${ticketChannel.guild.id}/${ticketChannel.id})`,
|
||||
),
|
||||
)
|
||||
.setColor(supportbot.Embed.Colours.General);
|
||||
|
||||
await userToRemove.send({ embeds: [removedFromTicketEmbed] }).catch(() => { });
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error removing user from the ticket:", err);
|
||||
|
||||
const errorEmbed = new EmbedBuilder()
|
||||
.setTitle(msgconfig.RemoveUser.Error_Title)
|
||||
.setDescription(msgconfig.RemoveUser.Error_Removing)
|
||||
.setColor(supportbot.Embed.Colours.Error);
|
||||
|
||||
await interaction.reply({
|
||||
embeds: [errorEmbed],
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (subcommand === (cmdconfig.TicketManage?.Rename?.Command || "rename")) {
|
||||
const modal = new ModalBuilder()
|
||||
.setCustomId("renameTicketModal")
|
||||
.setTitle("Rename Channel")
|
||||
.addComponents(
|
||||
new ActionRowBuilder().addComponents(
|
||||
new TextInputBuilder()
|
||||
.setCustomId("renameTicket")
|
||||
.setLabel("New Channel Name")
|
||||
.setStyle(TextInputStyle.Short)
|
||||
.setRequired(true),
|
||||
),
|
||||
);
|
||||
|
||||
return interaction.showModal(modal);
|
||||
}
|
||||
|
||||
if (subcommand === (cmdconfig.TicketManage?.Close?.Command || "close")) {
|
||||
const reason = interaction.options.getString("reason") || "Closed by staff.";
|
||||
|
||||
if (supportbot.Ticket.Close.StaffOnly) {
|
||||
let CloseSupportStaff = await getRole(
|
||||
supportbot.Roles.StaffMember.Staff,
|
||||
interaction.guild,
|
||||
);
|
||||
let CloseAdmin = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild);
|
||||
|
||||
if (!CloseSupportStaff || !CloseAdmin) {
|
||||
return interaction.reply({
|
||||
content:
|
||||
"Some roles seem to be missing! Please check for errors when starting the bot.",
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
|
||||
const noClosePerms = new EmbedBuilder()
|
||||
.setTitle("Invalid Permissions!")
|
||||
.setDescription(
|
||||
`${msgconfig.Error.IncorrectPerms}\n\nRole Required: \`${supportbot.Roles.StaffMember.Staff}\` or \`${supportbot.Roles.StaffMember.Admin}\``,
|
||||
)
|
||||
.setColor(supportbot.Embed.Colours.Warn);
|
||||
|
||||
if (
|
||||
!interaction.member.roles.cache.has(CloseSupportStaff.id) &&
|
||||
!interaction.member.roles.cache.has(CloseAdmin.id)
|
||||
) {
|
||||
return interaction.reply({
|
||||
embeds: [noClosePerms],
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const isThread = interaction.channel.type === ChannelType.PrivateThread;
|
||||
|
||||
if (
|
||||
(supportbot.Ticket.TicketType === "threads" && !isThread) ||
|
||||
(supportbot.Ticket.TicketType === "channels" &&
|
||||
interaction.channel.type !== ChannelType.GuildText)
|
||||
) {
|
||||
const notTicketChannel = new EmbedBuilder()
|
||||
.setTitle("Invalid Channel!")
|
||||
.setDescription(
|
||||
`This command can only be used in a ${supportbot.Ticket.TicketType === "threads" ? "ticket thread" : "ticket channel"}.`,
|
||||
)
|
||||
.setColor(supportbot.Embed.Colours.Warn);
|
||||
|
||||
return interaction.reply({
|
||||
embeds: [notTicketChannel],
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
|
||||
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
||||
|
||||
const ticketData = db.getTicket(interaction.channel.id);
|
||||
|
||||
if (!ticketData) {
|
||||
const exists = new EmbedBuilder()
|
||||
.setTitle("No Ticket Found!")
|
||||
.setDescription(msgconfig.Error.NoValidTicket)
|
||||
.setColor(supportbot.Embed.Colours.Warn);
|
||||
|
||||
return interaction.followUp({
|
||||
embeds: [exists],
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
await TicketManager.createTranscript(interaction, ticketData, reason);
|
||||
|
||||
await interaction.followUp({
|
||||
content: "Ticket closed successfully.",
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
|
||||
await interaction.channel.delete().catch(() => { });
|
||||
} catch (err) {
|
||||
console.error("Error closing ticket:", err);
|
||||
|
||||
await interaction.followUp({
|
||||
content: "An error occurred while closing the ticket.",
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
return interaction.reply({
|
||||
content: "Unknown ticket management action.",
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error in ticket manage command:", error);
|
||||
|
||||
if (!interaction.replied && !interaction.deferred) {
|
||||
return interaction.reply({
|
||||
content: "An error occurred while managing the ticket.",
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,343 @@
|
||||
// SupportBot | Emerald Services
|
||||
// Ticket Panel Command
|
||||
|
||||
const fs = require("fs");
|
||||
const {
|
||||
ApplicationCommandType,
|
||||
ActionRowBuilder,
|
||||
ButtonBuilder,
|
||||
ButtonStyle,
|
||||
MessageFlags,
|
||||
ContainerBuilder,
|
||||
TextDisplayBuilder,
|
||||
MediaGalleryBuilder,
|
||||
SeparatorBuilder,
|
||||
SectionBuilder,
|
||||
StringSelectMenuBuilder,
|
||||
ThumbnailBuilder,
|
||||
EmbedBuilder,
|
||||
} = require("discord.js");
|
||||
const yaml = require("js-yaml");
|
||||
|
||||
const panelconfig = require("../../Structures/ConfigStore").ticketPanel;
|
||||
const supportbot = require("../../Structures/ConfigStore").supportbot;
|
||||
const cmdconfig = require("../../Structures/ConfigStore").commands;
|
||||
|
||||
const Command = require("../../Structures/Command.js");
|
||||
const db = require("../../Structures/Database.js");
|
||||
|
||||
function parseButtonStyle(style) {
|
||||
switch (String(style || "1")) {
|
||||
case "1":
|
||||
return ButtonStyle.Primary;
|
||||
case "2":
|
||||
return ButtonStyle.Secondary;
|
||||
case "3":
|
||||
return ButtonStyle.Danger;
|
||||
case "4":
|
||||
return ButtonStyle.Success;
|
||||
default:
|
||||
return ButtonStyle.Primary;
|
||||
}
|
||||
}
|
||||
|
||||
function getPanelMessage(key, fallback) {
|
||||
return panelconfig.Messages?.[key] || fallback;
|
||||
}
|
||||
|
||||
module.exports = new Command({
|
||||
name: cmdconfig.TicketPanel.Command,
|
||||
description: cmdconfig.TicketPanel.Description,
|
||||
type: ApplicationCommandType.ChatInput,
|
||||
options: [],
|
||||
permissions: cmdconfig.TicketPanel.Permission,
|
||||
|
||||
async run(interaction) {
|
||||
if (!panelconfig.Panel) {
|
||||
console.log(
|
||||
"\u001b[31m",
|
||||
`[TICKET PANEL]`,
|
||||
"\u001b[33m",
|
||||
"Ticket Panel is not setup, You can set this in",
|
||||
`\u001b[31m`,
|
||||
"/Configs/ticket-panel.yml",
|
||||
`\n `,
|
||||
);
|
||||
|
||||
return interaction.reply({
|
||||
content: getPanelMessage(
|
||||
"NotConfigured",
|
||||
"Ticket Panel is not set up. Please configure it in `/Configs/ticket-panel.yml`.",
|
||||
),
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
|
||||
const { getChannel } = interaction.client;
|
||||
|
||||
const channel = await getChannel(supportbot.Ticket.TicketHome, interaction.guild);
|
||||
|
||||
if (!channel) {
|
||||
console.log(`[TICKET PANEL] TicketHome channel not found. Please check your config file.`);
|
||||
return interaction.reply({
|
||||
content: getPanelMessage(
|
||||
"InvalidChannel",
|
||||
"TicketHome channel not found. Please check your config file.",
|
||||
),
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
|
||||
const panelRow = db.getTicketPanel();
|
||||
|
||||
if (panelRow) {
|
||||
try {
|
||||
const oldMessage = await channel.messages.fetch(panelRow.message_id);
|
||||
if (oldMessage) {
|
||||
await oldMessage.delete().catch(() => { });
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(`[TICKET PANEL] Previous panel message not found, creating a new one.`);
|
||||
}
|
||||
}
|
||||
|
||||
const departmentSystem = supportbot.Ticket?.DepartmentSystem || {};
|
||||
const prioritySystem = supportbot.Ticket?.PrioritySystem || {};
|
||||
const usingThreads = supportbot.Ticket?.TicketType === "threads";
|
||||
|
||||
const departmentsEnabled = departmentSystem.Enabled === true && !usingThreads;
|
||||
const prioritiesEnabled = prioritySystem.Enabled === true && !usingThreads;
|
||||
const departments = departmentSystem.Departments || {};
|
||||
|
||||
if (usingThreads && (departmentSystem.Enabled || prioritySystem.Enabled)) {
|
||||
console.log(
|
||||
"[SupportBot] Departments and Priority are disabled because TicketType is set to 'threads'.",
|
||||
);
|
||||
}
|
||||
|
||||
const departmentOptions = Object.entries(departments)
|
||||
.filter(([_, dept]) => dept && dept.Enabled !== false)
|
||||
.slice(0, 25)
|
||||
.map(([key, dept]) => ({
|
||||
label: dept.Name || key,
|
||||
description:
|
||||
dept.PanelDescription ||
|
||||
dept.Description ||
|
||||
panelconfig.DepartmentMenu?.DefaultDescription ||
|
||||
`Open a ${String(dept.Name || key).toLowerCase()} ticket`,
|
||||
value: key,
|
||||
emoji:
|
||||
dept.Emoji ||
|
||||
panelconfig.DepartmentMenu?.DefaultEmoji ||
|
||||
panelconfig.Button?.Emoji ||
|
||||
undefined,
|
||||
}));
|
||||
|
||||
const shouldUseDepartments = departmentsEnabled && departmentOptions.length > 0;
|
||||
|
||||
let openerComponent;
|
||||
|
||||
if (shouldUseDepartments) {
|
||||
openerComponent = new StringSelectMenuBuilder()
|
||||
.setCustomId(panelconfig.DepartmentMenu?.CustomId || "ticketdepartmentselect")
|
||||
.setPlaceholder(
|
||||
panelconfig.DepartmentMenu?.Placeholder ||
|
||||
departmentSystem.Placeholder ||
|
||||
panelconfig.Button?.Text ||
|
||||
"Choose a ticket department",
|
||||
)
|
||||
.addOptions(departmentOptions);
|
||||
} else {
|
||||
openerComponent = new ButtonBuilder()
|
||||
.setCustomId(panelconfig.Button?.CustomId || "createticket")
|
||||
.setLabel(panelconfig.Button?.Text || "Create Ticket")
|
||||
.setStyle(parseButtonStyle(panelconfig.Button?.Color));
|
||||
|
||||
if (panelconfig.Button?.Emoji) {
|
||||
openerComponent.setEmoji(panelconfig.Button.Emoji);
|
||||
}
|
||||
}
|
||||
|
||||
const panelContainer = new ContainerBuilder();
|
||||
|
||||
if (panelconfig.Style?.Color) {
|
||||
panelContainer.setAccentColor(parseInt(String(panelconfig.Style.Color).replace("#", ""), 16));
|
||||
}
|
||||
|
||||
const layout = String(panelconfig.Style?.Layout || "3");
|
||||
const showImage = panelconfig.Settings?.Image === true && !!panelconfig.Style?.Image;
|
||||
const showTopTitle = panelconfig.Settings?.TopTitle === true;
|
||||
const showTopDivider = panelconfig.Settings?.TopDivider === true;
|
||||
const showBottomTitle = panelconfig.Settings?.BottomTitle === true;
|
||||
const showBottomDivider = panelconfig.Settings?.BottomDivider === true;
|
||||
|
||||
const panelTitle = new TextDisplayBuilder().setContent(
|
||||
panelconfig.Style?.Title || "## SupportBot Ticket Creation",
|
||||
);
|
||||
|
||||
const rawTitle = (panelconfig.Style?.Title || "SupportBot Ticket Creation").replace(/^#+\s*/, "");
|
||||
|
||||
const panelDesc = new TextDisplayBuilder().setContent(
|
||||
panelconfig.Style?.Description || "Click on the button below to create a support ticket.",
|
||||
);
|
||||
|
||||
if (showTopTitle) {
|
||||
panelContainer.addTextDisplayComponents(panelTitle);
|
||||
}
|
||||
|
||||
if (showTopDivider) {
|
||||
panelContainer.addSeparatorComponents(new SeparatorBuilder().setDivider(true));
|
||||
}
|
||||
|
||||
if (layout === "1" || layout === "3") {
|
||||
panelContainer.addTextDisplayComponents(panelDesc);
|
||||
}
|
||||
|
||||
if (showImage && layout === "2") {
|
||||
const panelImage = new MediaGalleryBuilder().addItems([
|
||||
{
|
||||
media: {
|
||||
url: panelconfig.Style.Image,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
panelContainer.addMediaGalleryComponents(panelImage);
|
||||
}
|
||||
|
||||
if (layout === "1") {
|
||||
const middleSection = new SectionBuilder().addTextDisplayComponents(panelDesc);
|
||||
|
||||
if (!shouldUseDepartments) {
|
||||
middleSection.setButtonAccessory(openerComponent);
|
||||
panelContainer.addSectionComponents(middleSection);
|
||||
} else {
|
||||
panelContainer.addSectionComponents(middleSection);
|
||||
panelContainer.addActionRowComponents(
|
||||
new ActionRowBuilder().addComponents(openerComponent),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (showImage && layout === "3") {
|
||||
const panelImage = new MediaGalleryBuilder().addItems([
|
||||
{
|
||||
media: {
|
||||
url: panelconfig.Style.Image,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
panelContainer.addMediaGalleryComponents(panelImage);
|
||||
}
|
||||
|
||||
if (showBottomTitle) {
|
||||
panelContainer.addTextDisplayComponents(panelTitle);
|
||||
}
|
||||
|
||||
if (showBottomDivider) {
|
||||
panelContainer.addSeparatorComponents(new SeparatorBuilder().setDivider(true));
|
||||
}
|
||||
|
||||
if (layout === "2") {
|
||||
const middleSection = new SectionBuilder().addTextDisplayComponents(panelDesc);
|
||||
|
||||
if (!shouldUseDepartments) {
|
||||
middleSection.setButtonAccessory(openerComponent);
|
||||
panelContainer.addSectionComponents(middleSection);
|
||||
} else {
|
||||
panelContainer.addSectionComponents(middleSection);
|
||||
panelContainer.addActionRowComponents(
|
||||
new ActionRowBuilder().addComponents(openerComponent),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (layout === "3") {
|
||||
panelContainer.addActionRowComponents(new ActionRowBuilder().addComponents(openerComponent));
|
||||
}
|
||||
|
||||
if (layout === "4") {
|
||||
// Create a classic Discord Embed for Layout 4
|
||||
const oldEmbed = new EmbedBuilder()
|
||||
.setTitle(rawTitle)
|
||||
.setDescription(panelconfig.Style?.Description || "Click on the button below to create a support ticket.");
|
||||
|
||||
if (panelconfig.Style?.Color) {
|
||||
oldEmbed.setColor(panelconfig.Style.Color);
|
||||
}
|
||||
|
||||
if (showImage && panelconfig.Style?.Image) {
|
||||
oldEmbed.setThumbnail(panelconfig.Style.Image);
|
||||
}
|
||||
|
||||
try {
|
||||
const message = await channel.send({
|
||||
embeds: [oldEmbed],
|
||||
components: [new ActionRowBuilder().addComponents(openerComponent)],
|
||||
});
|
||||
|
||||
db.saveTicketPanel(message.id, channel.id);
|
||||
|
||||
return interaction.reply({
|
||||
content: shouldUseDepartments
|
||||
? getPanelMessage(
|
||||
"PanelSentWithDepartments",
|
||||
"Ticket panel message with department selection has been sent!",
|
||||
)
|
||||
: getPanelMessage("PanelSent", "Ticket panel message has been sent!"),
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
} catch (e) {
|
||||
console.log("Error sending message:", e);
|
||||
return interaction.reply({
|
||||
content: getPanelMessage(
|
||||
"SendError",
|
||||
"An error occurred while sending the ticket panel message.",
|
||||
),
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (showImage && layout === "1") {
|
||||
const panelImage = new MediaGalleryBuilder().addItems([
|
||||
{
|
||||
media: {
|
||||
url: panelconfig.Style.Image,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
panelContainer.addMediaGalleryComponents(panelImage);
|
||||
}
|
||||
|
||||
try {
|
||||
const message = await channel.send({
|
||||
flags: MessageFlags.IsComponentsV2,
|
||||
components: [panelContainer],
|
||||
});
|
||||
|
||||
db.saveTicketPanel(message.id, channel.id);
|
||||
|
||||
return interaction.reply({
|
||||
content: shouldUseDepartments
|
||||
? getPanelMessage(
|
||||
"PanelSentWithDepartments",
|
||||
"Ticket panel message with department selection has been sent!",
|
||||
)
|
||||
: getPanelMessage("PanelSent", "Ticket panel message has been sent!"),
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
} catch (e) {
|
||||
console.log("Error sending message:", e);
|
||||
return interaction.reply({
|
||||
content: getPanelMessage(
|
||||
"SendError",
|
||||
"An error occurred while sending the ticket panel message.",
|
||||
),
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,468 @@
|
||||
const {
|
||||
ApplicationCommandType,
|
||||
ApplicationCommandOptionType,
|
||||
ActionRowBuilder,
|
||||
ButtonBuilder,
|
||||
EmbedBuilder,
|
||||
StringSelectMenuBuilder,
|
||||
ModalBuilder,
|
||||
TextInputBuilder,
|
||||
TextInputStyle,
|
||||
ButtonStyle,
|
||||
ComponentType,
|
||||
MessageFlags,
|
||||
} = require('discord.js');
|
||||
const Command = require('../../Structures/Command.js');
|
||||
const fs = require('fs');
|
||||
const yaml = require('js-yaml');
|
||||
|
||||
const supportbot = require('../../Structures/ConfigStore').supportbot;
|
||||
const cmdconfig = require("../../Structures/ConfigStore").commands;
|
||||
const msgconfig = require("../../Structures/ConfigStore").messages;
|
||||
|
||||
function parseColor(color) {
|
||||
if (!color) return 0x5865F2;
|
||||
if (typeof color === 'number') return color;
|
||||
if (typeof color !== 'string') return 0x5865F2;
|
||||
color = color.replace('#', '');
|
||||
if (/^[0-9A-F]{6}$/i.test(color)) {
|
||||
return parseInt(color, 16);
|
||||
}
|
||||
return 0x5865F2;
|
||||
}
|
||||
|
||||
module.exports = new Command({
|
||||
name: cmdconfig.Embed.Command,
|
||||
description: cmdconfig.Embed.Description,
|
||||
type: ApplicationCommandType.ChatInput,
|
||||
options: [
|
||||
{
|
||||
name: 'channel',
|
||||
description: 'Select channel to send the message',
|
||||
type: ApplicationCommandOptionType.Channel,
|
||||
required: true
|
||||
}
|
||||
],
|
||||
permissions: cmdconfig.Embed.Permission,
|
||||
|
||||
async run(interaction) {
|
||||
const { getRole } = interaction.client;
|
||||
let SupportStaff = await getRole(supportbot.Roles.StaffMember.Staff, interaction.guild);
|
||||
let Admin = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild);
|
||||
|
||||
if (!SupportStaff || !Admin) {
|
||||
return interaction.reply('Some roles seem to be missing!\nPlease check for errors when starting the bot.');
|
||||
}
|
||||
|
||||
if (!interaction.member.roles.cache.has(SupportStaff.id) &&
|
||||
!interaction.member.roles.cache.has(Admin.id)) {
|
||||
const NoPerms = new EmbedBuilder()
|
||||
.setTitle('Invalid Permissions!')
|
||||
.setDescription(`${msgconfig.Error.IncorrectPerms}\n\nRole Required: \`${supportbot.Roles.StaffMember.Staff}\` or \`${supportbot.Roles.StaffMember.Admin}\``)
|
||||
.setColor(0xFF0000);
|
||||
return interaction.reply({ embeds: [NoPerms], flags: MessageFlags.Ephemeral });
|
||||
}
|
||||
|
||||
const channel = interaction.options.getChannel('channel');
|
||||
|
||||
const messageData = {
|
||||
type: 'embed',
|
||||
title: 'New Embed',
|
||||
description: 'Click the buttons below to edit this embed',
|
||||
color: 0x5865F2,
|
||||
fields: [],
|
||||
footer: null,
|
||||
thumbnail: null,
|
||||
image: null,
|
||||
timestamp: false
|
||||
};
|
||||
|
||||
function createEmbed() {
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle(messageData.title)
|
||||
.setDescription(messageData.description)
|
||||
.setColor(messageData.color);
|
||||
|
||||
if (messageData.fields.length > 0) {
|
||||
embed.addFields(messageData.fields);
|
||||
}
|
||||
if (messageData.footer) {
|
||||
embed.setFooter({ text: messageData.footer });
|
||||
}
|
||||
if (messageData.thumbnail) {
|
||||
embed.setThumbnail(messageData.thumbnail);
|
||||
}
|
||||
if (messageData.image) {
|
||||
embed.setImage(messageData.image);
|
||||
}
|
||||
if (messageData.timestamp) {
|
||||
embed.setTimestamp();
|
||||
}
|
||||
|
||||
return embed;
|
||||
}
|
||||
|
||||
const typeRow = new ActionRowBuilder()
|
||||
.addComponents(
|
||||
new StringSelectMenuBuilder()
|
||||
.setCustomId('message_type')
|
||||
.setPlaceholder('Select message type')
|
||||
.addOptions([
|
||||
{
|
||||
label: 'Embed Message',
|
||||
description: 'Send as an embedded message',
|
||||
value: 'embed'
|
||||
},
|
||||
{
|
||||
label: 'Text Message',
|
||||
description: 'Send as a plain text message',
|
||||
value: 'text'
|
||||
}
|
||||
])
|
||||
);
|
||||
|
||||
function createButtons(messageType) {
|
||||
const buttons = [];
|
||||
|
||||
if (messageType === 'embed') {
|
||||
const row1 = new ActionRowBuilder()
|
||||
.addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId('edit_title')
|
||||
.setLabel('Edit Title')
|
||||
.setStyle(ButtonStyle.Secondary),
|
||||
new ButtonBuilder()
|
||||
.setCustomId('edit_description')
|
||||
.setLabel('Edit Description')
|
||||
.setStyle(ButtonStyle.Secondary),
|
||||
new ButtonBuilder()
|
||||
.setCustomId('edit_color')
|
||||
.setLabel('Edit Color')
|
||||
.setStyle(ButtonStyle.Secondary),
|
||||
new ButtonBuilder()
|
||||
.setCustomId('add_field')
|
||||
.setLabel('Add Field')
|
||||
.setStyle(ButtonStyle.Secondary)
|
||||
);
|
||||
|
||||
const row2 = new ActionRowBuilder()
|
||||
.addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId('edit_footer')
|
||||
.setLabel('Edit Footer')
|
||||
.setStyle(ButtonStyle.Secondary),
|
||||
new ButtonBuilder()
|
||||
.setCustomId('edit_image')
|
||||
.setLabel('Add Image')
|
||||
.setStyle(ButtonStyle.Secondary),
|
||||
new ButtonBuilder()
|
||||
.setCustomId('edit_thumbnail')
|
||||
.setLabel('Add Thumbnail')
|
||||
.setStyle(ButtonStyle.Secondary),
|
||||
new ButtonBuilder()
|
||||
.setCustomId('toggle_timestamp')
|
||||
.setLabel('Toggle Timestamp')
|
||||
.setStyle(ButtonStyle.Secondary)
|
||||
);
|
||||
|
||||
buttons.push(row1, row2);
|
||||
} else {
|
||||
const textRow = new ActionRowBuilder()
|
||||
.addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId('edit_description')
|
||||
.setLabel('Edit Text')
|
||||
.setStyle(ButtonStyle.Secondary)
|
||||
);
|
||||
|
||||
buttons.push(textRow);
|
||||
}
|
||||
|
||||
// Send button always shows
|
||||
const sendRow = new ActionRowBuilder()
|
||||
.addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId('send_message')
|
||||
.setLabel('Send')
|
||||
.setStyle(ButtonStyle.Success)
|
||||
);
|
||||
|
||||
buttons.push(sendRow);
|
||||
return buttons;
|
||||
}
|
||||
|
||||
async function updatePreview(i) {
|
||||
if (messageData.type === 'embed') {
|
||||
await i.update({
|
||||
content: 'Customize your message:',
|
||||
embeds: [createEmbed()],
|
||||
components: [typeRow, ...createButtons('embed')]
|
||||
});
|
||||
} else {
|
||||
await i.update({
|
||||
content: messageData.description,
|
||||
embeds: [],
|
||||
components: [typeRow, ...createButtons('text')]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const message = await interaction.reply({
|
||||
content: messageData.type === 'text' ? messageData.description : 'Customize your message:',
|
||||
components: [typeRow, ...createButtons(messageData.type)],
|
||||
embeds: messageData.type === 'embed' ? [createEmbed()] : [],
|
||||
flags: MessageFlags.Ephemeral
|
||||
});
|
||||
|
||||
const collector = message.createMessageComponentCollector({
|
||||
time: 900000
|
||||
});
|
||||
|
||||
collector.on('collect', async (i) => {
|
||||
if (i.user.id !== interaction.user.id) {
|
||||
return i.reply({
|
||||
content: 'You cannot edit this message.',
|
||||
flags: MessageFlags.Ephemeral
|
||||
});
|
||||
}
|
||||
|
||||
switch (i.customId) {
|
||||
case 'message_type':
|
||||
messageData.type = i.values[0];
|
||||
await updatePreview(i);
|
||||
break;
|
||||
|
||||
case 'edit_description':
|
||||
const descModal = new ModalBuilder()
|
||||
.setCustomId('desc_modal')
|
||||
.setTitle(messageData.type === 'embed' ? 'Edit Description' : 'Edit Text');
|
||||
|
||||
descModal.addComponents(
|
||||
new ActionRowBuilder()
|
||||
.addComponents(
|
||||
new TextInputBuilder()
|
||||
.setCustomId('desc_input')
|
||||
.setLabel(messageData.type === 'embed' ? 'Description' : 'Message Text')
|
||||
.setStyle(TextInputStyle.Paragraph)
|
||||
.setValue(messageData.description)
|
||||
.setMaxLength(4000)
|
||||
)
|
||||
);
|
||||
|
||||
await i.showModal(descModal);
|
||||
break;
|
||||
|
||||
case 'edit_title':
|
||||
const titleModal = new ModalBuilder()
|
||||
.setCustomId('title_modal')
|
||||
.setTitle('Edit Title');
|
||||
|
||||
titleModal.addComponents(
|
||||
new ActionRowBuilder()
|
||||
.addComponents(
|
||||
new TextInputBuilder()
|
||||
.setCustomId('title_input')
|
||||
.setLabel('Title')
|
||||
.setStyle(TextInputStyle.Short)
|
||||
.setValue(messageData.title)
|
||||
.setMaxLength(256)
|
||||
)
|
||||
);
|
||||
|
||||
await i.showModal(titleModal);
|
||||
break;
|
||||
|
||||
case 'edit_color':
|
||||
const colorModal = new ModalBuilder()
|
||||
.setCustomId('color_modal')
|
||||
.setTitle('Edit Color');
|
||||
|
||||
colorModal.addComponents(
|
||||
new ActionRowBuilder()
|
||||
.addComponents(
|
||||
new TextInputBuilder()
|
||||
.setCustomId('color_input')
|
||||
.setLabel('Color (HEX)')
|
||||
.setStyle(TextInputStyle.Short)
|
||||
.setPlaceholder('#FF0000')
|
||||
.setMinLength(6)
|
||||
.setMaxLength(7)
|
||||
.setValue(messageData.color.toString(16).padStart(6, '0'))
|
||||
)
|
||||
);
|
||||
|
||||
await i.showModal(colorModal);
|
||||
break;
|
||||
|
||||
case 'add_field':
|
||||
const fieldModal = new ModalBuilder()
|
||||
.setCustomId('field_modal')
|
||||
.setTitle('Add Field');
|
||||
|
||||
fieldModal.addComponents(
|
||||
new ActionRowBuilder()
|
||||
.addComponents(
|
||||
new TextInputBuilder()
|
||||
.setCustomId('field_name')
|
||||
.setLabel('Field Name')
|
||||
.setStyle(TextInputStyle.Short)
|
||||
.setMaxLength(256)
|
||||
),
|
||||
new ActionRowBuilder()
|
||||
.addComponents(
|
||||
new TextInputBuilder()
|
||||
.setCustomId('field_value')
|
||||
.setLabel('Field Value')
|
||||
.setStyle(TextInputStyle.Paragraph)
|
||||
.setMaxLength(1024)
|
||||
),
|
||||
new ActionRowBuilder()
|
||||
.addComponents(
|
||||
new TextInputBuilder()
|
||||
.setCustomId('field_inline')
|
||||
.setLabel('Inline? (true/false)')
|
||||
.setStyle(TextInputStyle.Short)
|
||||
.setValue('false')
|
||||
.setPlaceholder('true or false')
|
||||
)
|
||||
);
|
||||
|
||||
await i.showModal(fieldModal);
|
||||
break;
|
||||
|
||||
case 'edit_footer':
|
||||
const footerModal = new ModalBuilder()
|
||||
.setCustomId('footer_modal')
|
||||
.setTitle('Edit Footer');
|
||||
|
||||
footerModal.addComponents(
|
||||
new ActionRowBuilder()
|
||||
.addComponents(
|
||||
new TextInputBuilder()
|
||||
.setCustomId('footer_input')
|
||||
.setLabel('Footer Text')
|
||||
.setStyle(TextInputStyle.Short)
|
||||
.setValue(messageData.footer || '')
|
||||
.setMaxLength(2048)
|
||||
)
|
||||
);
|
||||
|
||||
await i.showModal(footerModal);
|
||||
break;
|
||||
|
||||
case 'edit_image':
|
||||
const imageModal = new ModalBuilder()
|
||||
.setCustomId('image_modal')
|
||||
.setTitle('Add Image');
|
||||
|
||||
imageModal.addComponents(
|
||||
new ActionRowBuilder()
|
||||
.addComponents(
|
||||
new TextInputBuilder()
|
||||
.setCustomId('image_input')
|
||||
.setLabel('Image URL')
|
||||
.setStyle(TextInputStyle.Short)
|
||||
.setValue(messageData.image || '')
|
||||
.setPlaceholder('https://example.com/image.png')
|
||||
)
|
||||
);
|
||||
|
||||
await i.showModal(imageModal);
|
||||
break;
|
||||
|
||||
case 'edit_thumbnail':
|
||||
const thumbnailModal = new ModalBuilder()
|
||||
.setCustomId('thumbnail_modal')
|
||||
.setTitle('Add Thumbnail');
|
||||
|
||||
thumbnailModal.addComponents(
|
||||
new ActionRowBuilder()
|
||||
.addComponents(
|
||||
new TextInputBuilder()
|
||||
.setCustomId('thumbnail_input')
|
||||
.setLabel('Thumbnail URL')
|
||||
.setStyle(TextInputStyle.Short)
|
||||
.setValue(messageData.thumbnail || '')
|
||||
.setPlaceholder('https://example.com/thumbnail.png')
|
||||
)
|
||||
);
|
||||
|
||||
await i.showModal(thumbnailModal);
|
||||
break;
|
||||
|
||||
case 'toggle_timestamp':
|
||||
messageData.timestamp = !messageData.timestamp;
|
||||
await updatePreview(i);
|
||||
break;
|
||||
|
||||
case 'send_message':
|
||||
try {
|
||||
if (messageData.type === 'embed') {
|
||||
await channel.send({ embeds: [createEmbed()] });
|
||||
} else {
|
||||
await channel.send(messageData.description);
|
||||
}
|
||||
await i.update({
|
||||
content: 'Message sent successfully!',
|
||||
components: [],
|
||||
embeds: []
|
||||
});
|
||||
collector.stop();
|
||||
} catch (error) {
|
||||
await i.reply({
|
||||
content: 'Failed to send message. Please check channel permissions.',
|
||||
flags: MessageFlags.Ephemeral
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// Handle modal submissions
|
||||
interaction.client.on('interactionCreate', async (modal) => {
|
||||
if (!modal.isModalSubmit()) return;
|
||||
if (modal.user.id !== interaction.user.id) return;
|
||||
|
||||
switch (modal.customId) {
|
||||
case 'title_modal':
|
||||
messageData.title = modal.fields.getTextInputValue('title_input');
|
||||
break;
|
||||
case 'desc_modal':
|
||||
messageData.description = modal.fields.getTextInputValue('desc_input');
|
||||
break;
|
||||
case 'color_modal':
|
||||
messageData.color = parseColor(modal.fields.getTextInputValue('color_input'));
|
||||
break;
|
||||
case 'field_modal':
|
||||
const inlineValue = modal.fields.getTextInputValue('field_inline').toLowerCase();
|
||||
messageData.fields.push({
|
||||
name: modal.fields.getTextInputValue('field_name'),
|
||||
value: modal.fields.getTextInputValue('field_value'),
|
||||
inline: inlineValue === 'true'
|
||||
});
|
||||
break;
|
||||
case 'footer_modal':
|
||||
messageData.footer = modal.fields.getTextInputValue('footer_input');
|
||||
break;
|
||||
case 'image_modal':
|
||||
messageData.image = modal.fields.getTextInputValue('image_input');
|
||||
break;
|
||||
case 'thumbnail_modal':
|
||||
messageData.thumbnail = modal.fields.getTextInputValue('thumbnail_input');
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
await updatePreview(modal);
|
||||
});
|
||||
|
||||
collector.on('end', () => {
|
||||
interaction.editReply({
|
||||
content: 'Message editor timed out.',
|
||||
components: [],
|
||||
embeds: []
|
||||
}).catch(() => {});
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,235 @@
|
||||
const fs = require("fs");
|
||||
const { ApplicationCommandOptionType, ApplicationCommandType, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, ComponentType, MessageFlags } = require("discord.js");
|
||||
const yaml = require("js-yaml");
|
||||
const db = require("../../Structures/Database.js");
|
||||
const supportbot = require("../../Structures/ConfigStore").supportbot;
|
||||
const cmdconfig = require("../../Structures/ConfigStore").commands;
|
||||
const msgconfig = require("../../Structures/ConfigStore").messages;
|
||||
|
||||
const Command = require("../../Structures/Command.js");
|
||||
|
||||
// Function to chunk array into smaller arrays
|
||||
function chunkArray(arr, size) {
|
||||
const chunks = [];
|
||||
for (let i = 0; i < arr.length; i += size) {
|
||||
chunks.push(arr.slice(i, i + size));
|
||||
}
|
||||
return chunks;
|
||||
}
|
||||
|
||||
module.exports = new Command({
|
||||
name: cmdconfig.Mod.Command,
|
||||
description: cmdconfig.Mod.Description || "Moderation commands",
|
||||
type: ApplicationCommandType.ChatInput,
|
||||
options: [
|
||||
{
|
||||
name: cmdconfig.Mod.TicketBlacklist.Command,
|
||||
description: cmdconfig.Mod.TicketBlacklist.Description || "Ticket Blacklist Commands",
|
||||
type: ApplicationCommandOptionType.SubcommandGroup,
|
||||
options: [
|
||||
{
|
||||
name: cmdconfig.Mod.TicketBlacklist.Add.Command,
|
||||
description: msgconfig.Mod.TicketBlacklist.Add.Description || "Add a user to the blacklist",
|
||||
type: ApplicationCommandOptionType.Subcommand,
|
||||
options: [
|
||||
{
|
||||
name: 'user',
|
||||
description: 'The user to blacklist',
|
||||
type: ApplicationCommandOptionType.User,
|
||||
required: true
|
||||
},
|
||||
{
|
||||
name: 'reason',
|
||||
description: 'Reason for blacklisting',
|
||||
type: ApplicationCommandOptionType.String,
|
||||
required: false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: cmdconfig.Mod.TicketBlacklist.Remove.Command,
|
||||
description: cmdconfig.Mod.TicketBlacklist.Remove.Description || "Remove a user from the blacklist",
|
||||
type: ApplicationCommandOptionType.Subcommand,
|
||||
options: [
|
||||
{
|
||||
name: 'user',
|
||||
description: 'The user to remove from the blacklist',
|
||||
type: ApplicationCommandOptionType.User,
|
||||
required: true
|
||||
},
|
||||
{
|
||||
name: 'reason',
|
||||
description: 'Reason for unblacklisting',
|
||||
type: ApplicationCommandOptionType.String,
|
||||
required: false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "view",
|
||||
description: msgconfig.Mod.TicketBlacklist.View.Description || "View all blacklisted users",
|
||||
type: ApplicationCommandOptionType.Subcommand
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
run: async ({ interaction }) => {
|
||||
const subcommandGroup = interaction.options.getSubcommandGroup();
|
||||
const subcommand = interaction.options.getSubcommand();
|
||||
const user = interaction.options.getUser('user');
|
||||
const reason = interaction.options.getString('reason') || "No reason provided";
|
||||
|
||||
try {
|
||||
const blacklistedUsers = typeof db.getBlacklistedUsers === "function" ? db.getBlacklistedUsers() : [];
|
||||
|
||||
if (subcommand === cmdconfig.Mod.TicketBlacklist.Add.Command) {
|
||||
if (db.isUserBlacklisted && db.isUserBlacklisted(user.id)) {
|
||||
const alreadyBlacklistedEmbed = new EmbedBuilder()
|
||||
.setDescription(msgconfig.Mod.TicketBlacklist.Add.AlreadyBlacklisted.replace("{userTag}", user.tag) || ":x: User is already blacklisted.")
|
||||
.setColor(supportbot.Embed.Colours.Warn);
|
||||
return interaction.reply({ embeds: [alreadyBlacklistedEmbed], flags: MessageFlags.Ephemeral });
|
||||
}
|
||||
|
||||
if (typeof db.addBlacklistedUser === "function") {
|
||||
db.addBlacklistedUser(user.id, reason, interaction.user.tag);
|
||||
}
|
||||
|
||||
const successEmbed = new EmbedBuilder()
|
||||
.setDescription(msgconfig.Mod.TicketBlacklist.Add.Success.replace("{userTag}", user.tag) || ":white_check_mark: User has been blacklisted.")
|
||||
.setColor(supportbot.Embed.Colours.Success);
|
||||
|
||||
const blacklistChannel = interaction.guild.channels.cache.get(supportbot.Ticket.Log.TicketBlacklistLog);
|
||||
if (blacklistChannel) {
|
||||
const blacklistLogEmbed = new EmbedBuilder()
|
||||
.setTitle(msgconfig.TicketBlacklistLog.Title || "Ticket Blacklist Log")
|
||||
.setColor(msgconfig.TicketBlacklistLog.Colour || supportbot.Embed.Colours.Success)
|
||||
.setFooter({ text: supportbot.Embed.Footer, iconURL: interaction.user.displayAvatarURL() })
|
||||
.setDescription(`> **User:** ${user.tag} (\`${user.id}\`)\n> **Actioned by:** <@${interaction.user.id}>`)
|
||||
.addFields({ name: "Action", value: "\`\`\`Added to blacklist\`\`\`", inline: false })
|
||||
.addFields({ name: "Reason", value: `\`\`\`${reason}\`\`\``, inline: false });
|
||||
|
||||
blacklistChannel.send({ embeds: [blacklistLogEmbed] });
|
||||
}
|
||||
|
||||
return interaction.reply({
|
||||
embeds: [successEmbed],
|
||||
flags: MessageFlags.Ephemeral
|
||||
});
|
||||
|
||||
} else if (subcommand === cmdconfig.Mod.TicketBlacklist.Remove.Command) {
|
||||
if (db.isUserBlacklisted && !db.isUserBlacklisted(user.id)) {
|
||||
const notBlacklistedEmbed = new EmbedBuilder()
|
||||
.setDescription(msgconfig.Mod.TicketBlacklist.Remove.NotBlacklisted.replace("{userTag}", user.tag) || ":x: User is not blacklisted.")
|
||||
.setColor(supportbot.Embed.Colours.Warn);
|
||||
return interaction.reply({ embeds: [notBlacklistedEmbed], flags: MessageFlags.Ephemeral });
|
||||
}
|
||||
|
||||
if (typeof db.removeBlacklistedUser === "function") {
|
||||
db.removeBlacklistedUser(user.id);
|
||||
}
|
||||
|
||||
const removedEmbed = new EmbedBuilder()
|
||||
.setDescription(msgconfig.Mod.TicketBlacklist.Remove.Success.replace("{userTag}", user.tag) || ":white_check_mark: User has been removed from the blacklist.")
|
||||
.setColor(supportbot.Embed.Colours.Success);
|
||||
|
||||
const blacklistChannel = interaction.guild.channels.cache.get(supportbot.Ticket.Log.TicketBlacklistLog);
|
||||
if (blacklistChannel) {
|
||||
const blacklistLogEmbed = new EmbedBuilder()
|
||||
.setTitle(msgconfig.TicketBlacklistLog.Title || "Ticket Blacklist Log")
|
||||
.setColor(msgconfig.TicketBlacklistLog.Colour || supportbot.Embed.Colours.Success)
|
||||
.setFooter({ text: supportbot.Embed.Footer, iconURL: interaction.user.displayAvatarURL() })
|
||||
.setDescription(`> **User:** ${user.tag} (\`${user.id}\`)\n> **Actioned by:** <@${interaction.user.id}>`)
|
||||
.addFields({ name: "Action", value: "\`\`\`Removed from blacklist\`\`\`", inline: false })
|
||||
.addFields({ name: "Reason", value: `\`\`\`${reason}\`\`\``, inline: false });
|
||||
|
||||
blacklistChannel.send({ embeds: [blacklistLogEmbed] });
|
||||
}
|
||||
|
||||
return interaction.reply({ embeds: [removedEmbed], flags: MessageFlags.Ephemeral });
|
||||
|
||||
} else if (subcommand === "view") {
|
||||
if (blacklistedUsers.length === 0) {
|
||||
const noBlacklistedUsersEmbed = new EmbedBuilder()
|
||||
.setDescription(msgconfig.Mod.TicketBlacklist.View.NoBlacklistedUsers || "No users are blacklisted.")
|
||||
.setColor(supportbot.Embed.Colours.Success);
|
||||
return interaction.reply({ embeds: [noBlacklistedUsersEmbed], flags: MessageFlags.Ephemeral });
|
||||
}
|
||||
|
||||
const chunkedUsers = chunkArray(blacklistedUsers, 5);
|
||||
let currentPage = 0;
|
||||
|
||||
const createEmbed = (page) => {
|
||||
return new EmbedBuilder()
|
||||
.setTitle(msgconfig.Mod.TicketBlacklist.View.EmbedTitle || "Blacklisted Users")
|
||||
.setDescription(chunkedUsers[page].map(id => `<@${id}>`).join("\n"))
|
||||
.setColor(supportbot.Embed.Colours.Success)
|
||||
.setFooter({ text: `Page ${page + 1} of ${chunkedUsers.length}` });
|
||||
};
|
||||
|
||||
const buttons = new ActionRowBuilder()
|
||||
.addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId('previous')
|
||||
.setLabel('Previous')
|
||||
.setStyle(ButtonStyle.Secondary)
|
||||
.setDisabled(currentPage === 0),
|
||||
new ButtonBuilder()
|
||||
.setCustomId('next')
|
||||
.setLabel('Next')
|
||||
.setStyle(ButtonStyle.Secondary)
|
||||
.setDisabled(currentPage === chunkedUsers.length - 1)
|
||||
);
|
||||
|
||||
const response = await interaction.reply({
|
||||
embeds: [createEmbed(currentPage)],
|
||||
components: chunkedUsers.length > 1 ? [buttons] : [],
|
||||
flags: MessageFlags.Ephemeral
|
||||
});
|
||||
|
||||
if (chunkedUsers.length <= 1) return;
|
||||
|
||||
const collector = response.createMessageComponentCollector({
|
||||
componentType: ComponentType.Button,
|
||||
time: 60000
|
||||
});
|
||||
|
||||
collector.on('collect', async (i) => {
|
||||
if (i.user.id !== interaction.user.id) {
|
||||
return i.reply({
|
||||
content: 'You cannot use these buttons.',
|
||||
flags: MessageFlags.Ephemeral
|
||||
});
|
||||
}
|
||||
|
||||
if (i.customId === 'previous' && currentPage > 0) {
|
||||
currentPage--;
|
||||
} else if (i.customId === 'next' && currentPage < chunkedUsers.length - 1) {
|
||||
currentPage++;
|
||||
}
|
||||
|
||||
buttons.components[0].setDisabled(currentPage === 0);
|
||||
buttons.components[1].setDisabled(currentPage === chunkedUsers.length - 1);
|
||||
|
||||
await i.update({
|
||||
embeds: [createEmbed(currentPage)],
|
||||
components: [buttons]
|
||||
});
|
||||
});
|
||||
|
||||
collector.on('end', () => {
|
||||
buttons.components.forEach(button => button.setDisabled(true));
|
||||
interaction.editReply({
|
||||
components: [buttons]
|
||||
}).catch(() => {});
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error in mod command:", error);
|
||||
const errorEmbed = new EmbedBuilder()
|
||||
.setDescription(msgconfig.Error.ActionFailed || "An error occurred while processing your request.")
|
||||
.setColor(supportbot.Embed.Colours.Warn);
|
||||
interaction.reply({ embeds: [errorEmbed], flags: MessageFlags.Ephemeral });
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,237 @@
|
||||
const { Command } = require('../../Structures/Addon.js');
|
||||
const Discord = require("discord.js");
|
||||
const fs = require("fs");
|
||||
const yaml = require("js-yaml");
|
||||
const db = require("../../Structures/Database.js");
|
||||
|
||||
const supportbot = require("../../Structures/ConfigStore").supportbot;
|
||||
const cmdconfig = require("../../Structures/ConfigStore").commands;
|
||||
|
||||
async function loadSettings() {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.get("SELECT data FROM settings WHERE id = 1", (err, row) => {
|
||||
if (err) return reject(err);
|
||||
if (!row) return resolve({});
|
||||
try {
|
||||
resolve(JSON.parse(row.data));
|
||||
} catch (e) {
|
||||
resolve({});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function saveSettings(settings) {
|
||||
const data = JSON.stringify(settings);
|
||||
return new Promise((resolve, reject) => {
|
||||
db.run(
|
||||
"INSERT INTO settings (id, data) VALUES (1, ?) ON CONFLICT(id) DO UPDATE SET data = excluded.data",
|
||||
[data],
|
||||
(err) => (err ? reject(err) : resolve())
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = new Command({
|
||||
name: cmdconfig.Settings.Command,
|
||||
description: cmdconfig.Settings.Description,
|
||||
permissions: cmdconfig.Settings.Permission,
|
||||
options: [
|
||||
{
|
||||
name: 'setstatus',
|
||||
type: Discord.ApplicationCommandOptionType.Subcommand,
|
||||
description: 'Set the bot status',
|
||||
options: [
|
||||
{
|
||||
name: 'status',
|
||||
type: Discord.ApplicationCommandOptionType.String,
|
||||
description: 'Choose the bot status',
|
||||
required: true,
|
||||
choices: [
|
||||
{ name: 'Online 🟢', value: 'online' },
|
||||
{ name: 'Idle 🌙', value: 'idle' },
|
||||
{ name: 'Do Not Disturb ⛔', value: 'dnd' },
|
||||
{ name: 'Invisible 👻', value: 'invisible' }
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'setactivity',
|
||||
type: Discord.ApplicationCommandOptionType.Subcommand,
|
||||
description: 'Set the bot activity',
|
||||
options: [
|
||||
{
|
||||
name: 'type',
|
||||
type: Discord.ApplicationCommandOptionType.String,
|
||||
description: 'Choose the activity type',
|
||||
required: true,
|
||||
choices: [
|
||||
{ name: 'Playing 🎮', value: 'PLAYING' },
|
||||
{ name: 'Watching 👀', value: 'WATCHING' },
|
||||
{ name: 'Listening 🎧', value: 'LISTENING' },
|
||||
{ name: 'Competing 🏆', value: 'COMPETING' }
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'message',
|
||||
type: Discord.ApplicationCommandOptionType.String,
|
||||
description: 'The activity message',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'streammode',
|
||||
type: Discord.ApplicationCommandOptionType.Subcommand,
|
||||
description: 'Set the bot streaming status',
|
||||
options: [
|
||||
{
|
||||
name: 'toggle',
|
||||
type: Discord.ApplicationCommandOptionType.String,
|
||||
description: 'Turn streaming mode on or off',
|
||||
required: true,
|
||||
choices: [
|
||||
{ name: 'On', value: 'on' },
|
||||
{ name: 'Off', value: 'off' }
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'twitchurl',
|
||||
type: Discord.ApplicationCommandOptionType.String,
|
||||
description: 'Twitch stream URL (Required if turning on)',
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'setnickname',
|
||||
type: Discord.ApplicationCommandOptionType.Subcommand,
|
||||
description: 'Set the bot nickname',
|
||||
options: [
|
||||
{
|
||||
name: 'nickname',
|
||||
type: Discord.ApplicationCommandOptionType.String,
|
||||
description: 'The new nickname for the bot',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
async run(interaction) {
|
||||
const subcommand = interaction.options.getSubcommand();
|
||||
const settings = await loadSettings();
|
||||
|
||||
if (subcommand === 'setstatus') {
|
||||
const status = interaction.options.getString('status');
|
||||
|
||||
try {
|
||||
interaction.client.user.setPresence({
|
||||
status: status,
|
||||
activities: interaction.client.user.presence.activities || [],
|
||||
});
|
||||
|
||||
settings.status = status;
|
||||
await saveSettings(settings);
|
||||
|
||||
const embed = new Discord.EmbedBuilder()
|
||||
.setDescription(`Status set to **${status.charAt(0).toUpperCase() + status.slice(1)}**`)
|
||||
.setColor(supportbot.Embed.Colours.General);
|
||||
|
||||
return interaction.reply({ embeds: [embed], flags: Discord.MessageFlags.Ephemeral });
|
||||
} catch (error) {
|
||||
return interaction.reply({ content: `Failed to set status: ${error.message}`, flags: Discord.MessageFlags.Ephemeral });
|
||||
}
|
||||
}
|
||||
|
||||
if (subcommand === 'setactivity') {
|
||||
const activityType = interaction.options.getString('type');
|
||||
const activityMessage = interaction.options.getString('message');
|
||||
|
||||
try {
|
||||
interaction.client.user.setPresence({
|
||||
activities: [{ name: activityMessage, type: Discord.ActivityType[activityType] }],
|
||||
status: interaction.client.user.presence.status || 'online',
|
||||
});
|
||||
|
||||
settings.activity = { type: activityType, message: activityMessage };
|
||||
await saveSettings(settings);
|
||||
|
||||
const embed = new Discord.EmbedBuilder()
|
||||
.setDescription(`Activity set to **${activityType.toLowerCase()} ${activityMessage}**`)
|
||||
.setColor(supportbot.Embed.Colours.General);
|
||||
|
||||
return interaction.reply({ embeds: [embed], flags: Discord.MessageFlags.Ephemeral });
|
||||
} catch (error) {
|
||||
return interaction.reply({ content: `Failed to set activity: ${error.message}`, flags: Discord.MessageFlags.Ephemeral });
|
||||
}
|
||||
} else if (subcommand === 'streammode') {
|
||||
const toggle = interaction.options.getString('toggle');
|
||||
const twitchUrl = interaction.options.getString('twitchurl');
|
||||
|
||||
if (toggle === 'on') {
|
||||
if (!twitchUrl) {
|
||||
return interaction.reply({ content: 'Please provide a Twitch URL to stream.', flags: Discord.MessageFlags.Ephemeral });
|
||||
}
|
||||
|
||||
try {
|
||||
interaction.client.user.setPresence({
|
||||
activities: [{ name: 'Streaming', type: Discord.ActivityType.Streaming, url: twitchUrl }],
|
||||
status: 'online',
|
||||
});
|
||||
|
||||
settings.streaming = { active: true, url: twitchUrl };
|
||||
await saveSettings(settings);
|
||||
|
||||
const embed = new Discord.EmbedBuilder()
|
||||
.setDescription(`**Streaming mode enabled!**\nStreaming: [Twitch](${twitchUrl})`)
|
||||
.setColor(supportbot.Embed.Colours.General);
|
||||
|
||||
return interaction.reply({ embeds: [embed], flags: Discord.MessageFlags.Ephemeral });
|
||||
} catch (error) {
|
||||
return interaction.reply({ content: `Failed to enable streaming mode: ${error.message}`, flags: Discord.MessageFlags.Ephemeral });
|
||||
}
|
||||
} else if (toggle === 'off') {
|
||||
try {
|
||||
interaction.client.user.setPresence({
|
||||
activities: interaction.client.user.presence.activities.filter(a => a.type !== Discord.ActivityType.Streaming),
|
||||
status: settings.status || 'online',
|
||||
});
|
||||
|
||||
settings.streaming = { active: false };
|
||||
await saveSettings(settings);
|
||||
|
||||
const embed = new Discord.EmbedBuilder()
|
||||
.setDescription(`**Streaming mode disabled!**`)
|
||||
.setColor(supportbot.Embed.Colours.General);
|
||||
|
||||
return interaction.reply({ embeds: [embed], flags: Discord.MessageFlags.Ephemeral });
|
||||
} catch (error) {
|
||||
return interaction.reply({ content: `Failed to disable streaming mode: ${error.message}`, flags: Discord.MessageFlags.Ephemeral });
|
||||
}
|
||||
}
|
||||
} else if (subcommand === 'setnickname') {
|
||||
const nickname = interaction.options.getString('nickname');
|
||||
|
||||
try {
|
||||
const guild = interaction.guild;
|
||||
const botMember = guild.members.cache.get(interaction.client.user.id);
|
||||
await botMember.setNickname(nickname);
|
||||
|
||||
settings.nickname = nickname;
|
||||
await saveSettings(settings);
|
||||
|
||||
const embed = new Discord.EmbedBuilder()
|
||||
.setDescription(`Bot nickname set to **${nickname}**`)
|
||||
.setColor(supportbot.Embed.Colours.General);
|
||||
|
||||
return interaction.reply({ embeds: [embed], flags: Discord.MessageFlags.Ephemeral });
|
||||
} catch (error) {
|
||||
return interaction.reply({ content: `Failed to set nickname: ${error.message}`, flags: Discord.MessageFlags.Ephemeral });
|
||||
}
|
||||
} else {
|
||||
return interaction.reply({ content: 'Invalid command usage.', flags: Discord.MessageFlags.Ephemeral });
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
# ___ _ ___ _
|
||||
# / __> _ _ ___ ___ ___ _ _ _| |_ | . > ___ _| |_
|
||||
# \__ \| | || . \| . \/ . \| '_> | | | . \/ . \ | |
|
||||
# <___/`___|| _/| _/\___/|_| |_| |___/\___/ |_|
|
||||
# |_| |_|
|
||||
#
|
||||
# SupportBot created by Emerald Services
|
||||
# Installed with MIT License
|
||||
#
|
||||
# Discord Support: https://discord.gg/emeraldsrv
|
||||
# Official Website & Marketplace: https://emeraldsrv.com
|
||||
#
|
||||
# Messages Configuration (api.yml)
|
||||
|
||||
|
||||
# -------------------------------
|
||||
# API Server Configuration
|
||||
# -------------------------------
|
||||
|
||||
API:
|
||||
Enabled: true
|
||||
Port: 3000
|
||||
|
||||
SecretKey: "SECRET_KEY"
|
||||
|
||||
# Set to 1 when the dashboard is behind nginx, Cloudflare, Pterodactyl proxy, etc.
|
||||
# so http/https is detected from X-Forwarded-Proto.
|
||||
|
||||
TrustProxy: false
|
||||
|
||||
EmeraldAPIKey: "EMERALDSRV_API_KEY"
|
||||
# This is required for the one-click addon installer. Get API key from https://emeraldsrv.dev/
|
||||
|
||||
OAuth:
|
||||
Enabled: true
|
||||
ClientId: "CLIENT_ID"
|
||||
ClientSecret: "CLIENT_SECRET"
|
||||
# Get Client ID and Client Secret from https://discord.com/developers/applications
|
||||
|
||||
# Canonical callback URL (host + path). Protocol (http/https) can follow the
|
||||
# browser when UseRequestOrigin is true (default).
|
||||
|
||||
RedirectUri: "http://localhost:3000/api/auth/discord/callback"
|
||||
|
||||
# Optional: register extra callback URLs in Discord and list them here.
|
||||
|
||||
# RedirectUris:
|
||||
# - "https://bot.example.com/api/auth/discord/callback"
|
||||
# UseRequestOrigin: true
|
||||
# CookieSecure: false
|
||||
|
||||
OwnerUserIds:
|
||||
- "OWNER_USER_ID"
|
||||
|
||||
AllowedUserIds: []
|
||||
|
||||
# Dashboard Features
|
||||
|
||||
LiveChat:
|
||||
Mode: "webhook"
|
||||
# "webhook" - Webhook to send messages with staff member's avatar and Discord username
|
||||
# "bot" - Send messages as Bot account with discord username prefix.
|
||||
# "bot_noprefix" - Send messages as Bot account without discord username prefix.
|
||||
@@ -0,0 +1,196 @@
|
||||
# ___ _ ___ _
|
||||
# / __> _ _ ___ ___ ___ _ _ _| |_ | . > ___ _| |_
|
||||
# \__ \| | || . \| . \/ . \| '_> | | | . \/ . \ | |
|
||||
# <___/`___|| _/| _/\___/|_| |_| |___/\___/ |_|
|
||||
# |_| |_|
|
||||
#
|
||||
# SupportBot created by Emerald Services
|
||||
# Installed with MIT License
|
||||
#
|
||||
# Discord Support: https://discord.gg/emeraldsrv
|
||||
# Official Documentation: https://docs.emeraldsrv.com
|
||||
# Addons Marketplace: https://community.emeraldsrv.com
|
||||
#
|
||||
# Commands Configuration (commands.yml)
|
||||
|
||||
Help:
|
||||
Command: "help"
|
||||
Description: "Get a list of all the commands."
|
||||
Permission: ["SendMessages"]
|
||||
Enabled: true
|
||||
|
||||
Info:
|
||||
Command: "info"
|
||||
Description: "Get information about the **ServerName**."
|
||||
Permission: ["SendMessages"]
|
||||
Enabled: true
|
||||
|
||||
TicketPanel:
|
||||
Command: "ticketpanel"
|
||||
Description: "Create the ticket creation panel"
|
||||
Permission: ["Administrator"]
|
||||
Enabled: true
|
||||
|
||||
Profile:
|
||||
Command: "profile"
|
||||
Description: "Shows the profile for a discord member."
|
||||
Permission: ["SendMessages"]
|
||||
# Command is automatically disabled when the TicketClaims is disabled.
|
||||
|
||||
OpenTicket:
|
||||
Command: "ticket"
|
||||
Description: "Create a support ticket."
|
||||
Permission: ["SendMessages"]
|
||||
Enabled: true
|
||||
|
||||
AddUser:
|
||||
Command: "add"
|
||||
Description: "Add a user to a support ticket."
|
||||
Permission: ["SendMessages"]
|
||||
Enabled: true
|
||||
|
||||
CloseTicket:
|
||||
Command: "close"
|
||||
Description: "Close a support ticket"
|
||||
Permission: ["SendMessages"]
|
||||
Enabled: true
|
||||
|
||||
TicketManage:
|
||||
Command: "ticketmanage"
|
||||
Description: "Manage an existing support ticket."
|
||||
Permission: ["SendMessages"]
|
||||
Enabled: true
|
||||
|
||||
Close:
|
||||
Command: "close"
|
||||
Description: "Close a support ticket."
|
||||
|
||||
ForceAdd:
|
||||
Command: "forceadd"
|
||||
Description: "Add a user to a support ticket by bypassing the invite system."
|
||||
|
||||
ForceRemove:
|
||||
Command: "forceremove"
|
||||
Description: "Remove a user from a support ticket by bypassing the normal restrictions."
|
||||
|
||||
Rename:
|
||||
Command: "rename"
|
||||
Description: "Rename a support ticket by bypassing the normal restrictions."
|
||||
|
||||
Suggestion:
|
||||
Command: "suggest"
|
||||
Description: "Create a suggestion."
|
||||
Permission: ["SendMessages"]
|
||||
Enabled: true
|
||||
|
||||
SuggestionAdmin:
|
||||
Command: "suggestadmin"
|
||||
Description: "Update a suggestion’s status."
|
||||
Permission: ['SendMessages']
|
||||
# Disable "Suggestion" to disable this command.
|
||||
|
||||
Embed:
|
||||
Command: "embed"
|
||||
Description: "Build a discord embed."
|
||||
Permission: ["SendMessages"]
|
||||
Enabled: true
|
||||
|
||||
Translate:
|
||||
Command: "translate"
|
||||
Description: "Translate something to english"
|
||||
Permission: ["SendMessages"]
|
||||
Enabled: true
|
||||
|
||||
UserInfo:
|
||||
Command: "userinfo"
|
||||
Description: "Get info about a user."
|
||||
Permission: ["SendMessages"]
|
||||
Enabled: true
|
||||
|
||||
Ping:
|
||||
Command: "ping"
|
||||
Description: "Pong."
|
||||
Permission: ["SendMessages"]
|
||||
Enabled: true
|
||||
|
||||
# ---------------------------------------
|
||||
# Bot Moderator Commands
|
||||
# ---------------------------------------
|
||||
|
||||
Mod:
|
||||
Command: "mod"
|
||||
Description: "Execute a moderation action."
|
||||
Permission: ["BanMembers", "KickMembers"]
|
||||
Enabled: true
|
||||
|
||||
TicketBlacklist:
|
||||
Command: "ticket-blacklist"
|
||||
Description: "Blacklist a user from creating Tickets."
|
||||
|
||||
Add:
|
||||
Command: "add"
|
||||
Description: "Add a user to the blacklist."
|
||||
|
||||
Remove:
|
||||
Command: "remove"
|
||||
Description: "Remove a user from the blacklist."
|
||||
|
||||
View:
|
||||
Command: "view"
|
||||
Description: "View all blacklisted users."
|
||||
|
||||
# ---------------------------------------
|
||||
# Bot Administrator Commands
|
||||
# ---------------------------------------
|
||||
|
||||
Settings:
|
||||
Command: "settings"
|
||||
Description: "Update bot settings."
|
||||
Permission: ["Administrator"]
|
||||
Enabled: true
|
||||
|
||||
AI:
|
||||
Command: "ai"
|
||||
Description: "Manage SupportBot AI settings and memory."
|
||||
Permission: ["Administrator"]
|
||||
Enabled: true
|
||||
|
||||
Train:
|
||||
Command: "train"
|
||||
Description: "Train the AI by adding knowledge to its database."
|
||||
|
||||
Knowledge:
|
||||
Command: "knowledge"
|
||||
Description: "Manage the AI knowledge base."
|
||||
Clear:
|
||||
Command: "clear"
|
||||
Description: "Clear all trained knowledge from the AI."
|
||||
|
||||
Memory:
|
||||
Command: "memory"
|
||||
Description: "Manage the AI conversational memory."
|
||||
Clear:
|
||||
Command: "clear"
|
||||
Description: "Clear recent conversation memory for this channel."
|
||||
|
||||
# ---------------------------------------
|
||||
# Command Description Configuration
|
||||
# ---------------------------------------
|
||||
|
||||
BlacklistDesc: "Blacklist a user from creating Tickets."
|
||||
TicketAddDesc: "Add a user to a support ticket."
|
||||
TicketRemoveDesc: "Remove a user from a support ticket."
|
||||
TranslateCommandDesc: "Translates the language provided, along with the abbreviation (2 char) long language code"
|
||||
OpenTicketDesc: "Open a new support ticket."
|
||||
CloseTicketDesc: "Close your support ticket."
|
||||
InfoCommandDesc: "Get information about the brand."
|
||||
PingCommandDesc: "Pong!"
|
||||
HelpCommandDesc: "Get a list of all the commands."
|
||||
EmbedCommandDesc: "Send a message as an embed."
|
||||
SuggestCommandDesc: "Suggest a feature for the server."
|
||||
UserInfoCommandDesc: "Get info about a user."
|
||||
TicketManageDesc: "Manage an existing support ticket."
|
||||
TicketManageCloseDesc: "Close a support ticket."
|
||||
TicketManageForceAddDesc: "Add a user to a support ticket by bypassing the invite system."
|
||||
TicketManageForceRemoveDesc: "Remove a user from a support ticket by bypassing the normal restrictions."
|
||||
TicketManageForceRenameDesc: "Rename a support ticket by bypassing the normal restrictions."
|
||||
@@ -0,0 +1,325 @@
|
||||
# ___ _ ___ _
|
||||
# / __> _ _ ___ ___ ___ _ _ _| |_ | . > ___ _| |_
|
||||
# \__ \| | || . \| . \/ . \| '_> | | | . \/ . \ | |
|
||||
# <___/`___|| _/| _/\___/|_| |_| |___/\___/ |_|
|
||||
# |_| |_|
|
||||
#
|
||||
# SupportBot created by Emerald Services
|
||||
# Installed with MIT License
|
||||
#
|
||||
# Discord Support: https://discord.gg/emeraldsrv
|
||||
# Official Documentation: https://docs.emeraldsrv.com
|
||||
# Addons Marketplace: https://community.emeraldsrv.com
|
||||
#
|
||||
# Messages Configuration (messages.yml)
|
||||
|
||||
# ----------------------------
|
||||
# Info Command
|
||||
# ----------------------------
|
||||
|
||||
Info:
|
||||
Title: "About Us"
|
||||
Description: "SupportBot is an open-sourced support system for discord. It's completely free to use with high amounts of customization to match your own brand."
|
||||
Button: "Visit Site"
|
||||
URL: "https://emeraldsrv.dev"
|
||||
Colour: "#45b463"
|
||||
|
||||
# ----------------------------
|
||||
# Welcome/Leave Messages
|
||||
# ----------------------------
|
||||
|
||||
Welcome:
|
||||
Enabled: true
|
||||
LayoutStyle: "classic"
|
||||
# Available Layout Styles
|
||||
# "modern" - For the new Discord v2 components
|
||||
# "classic" - Use the classic Discord look.
|
||||
|
||||
Embed:
|
||||
Colour: "#45b463"
|
||||
Title: "Welcome!"
|
||||
Message: ":smile: %joined_user% has just landed!"
|
||||
Icon:
|
||||
Mode: "USER" # USER | BOT | CUSTOM
|
||||
CustomURL: "https://example.com/custom.png"
|
||||
Image:
|
||||
Enabled: false
|
||||
URL: "https://example.com/banner.png"
|
||||
Buttons:
|
||||
Enabled: true
|
||||
List:
|
||||
- label: "Website"
|
||||
style: "Link"
|
||||
url: "https://emeraldsrv.dev"
|
||||
emoji: "⌨️"
|
||||
|
||||
Modern:
|
||||
Colour: "#45b463"
|
||||
Title: "**Welcome to ServerName**"
|
||||
Body: ":smile: %joined_user% has just landed!\n\n🔨 **Server Rules:** <#CHANNEL_ID>"
|
||||
Icon:
|
||||
Mode: "BOT" # USER | BOT | CUSTOM
|
||||
CustomURL: "https://example.com/custom-icon.png"
|
||||
Image:
|
||||
Enabled: false
|
||||
URL: "https://example.com/banner.png"
|
||||
Buttons:
|
||||
- label: "BuiltByBit"
|
||||
style: "Link"
|
||||
url: "https://your-website.com/how-to-play-java"
|
||||
emoji: "⌨️"
|
||||
- label: "GitHub"
|
||||
style: "Link"
|
||||
url: "https://your-website.com/how-to-play-bedrock"
|
||||
emoji: "👾"
|
||||
- label: "Tebex"
|
||||
style: "Link"
|
||||
url: "https://your-store.tebex.io/"
|
||||
emoji: "🛒"
|
||||
|
||||
Leave:
|
||||
Embed:
|
||||
Colour: "#f74343"
|
||||
Title: "Goodbye!"
|
||||
Message: ":slight_frown: %left_user% has recently dropped out!"
|
||||
Thumbnail: "BOT"
|
||||
|
||||
ImageEnabled: false
|
||||
ImageURL: "https://animagelink.png/"
|
||||
|
||||
# ----------------------------
|
||||
# Suggestion Messages
|
||||
# ----------------------------
|
||||
|
||||
Suggestions:
|
||||
Sent_Title: "Suggestion Sent!"
|
||||
Sent: ":white_check_mark: **You have successfully sent a suggestion.**"
|
||||
NoSelfVote: ":x: **You cannot vote on your own suggestion.**"
|
||||
RemoveVoteButton: "Remove Vote"
|
||||
|
||||
# ----------------------------
|
||||
# Ticket Messages
|
||||
# ----------------------------
|
||||
|
||||
Ticket:
|
||||
|
||||
# Available Placeholders:
|
||||
|
||||
# %ticketusername% - User who created the ticket.
|
||||
# <@%ticketauthor%> - Tags the user who created the ticket.
|
||||
# <#%ticketid%> - Mentions the ticket channel.
|
||||
|
||||
# You can use Markdown formatting in this message:
|
||||
# # Heading 1
|
||||
# ## Heading 2
|
||||
# ### Heading 3
|
||||
# **Bold** text
|
||||
# *Italic* text
|
||||
# -# Small text
|
||||
# - Bullet points
|
||||
# Links: [text](https://example.com)
|
||||
|
||||
TicketAuthorTitle: "-# %ticketusername%'s Ticket"
|
||||
TicketTitle: "## Support Ticket"
|
||||
TicketMessage: "Hello <@%ticketauthor%>, Thank you for creating a support ticket\nPlease wait patiently whilst a member of our team reaches out to you."
|
||||
TicketThumbnail: "https://images.emojiterra.com/google/android-pie/512px/1f39f.png"
|
||||
TicketCreatedAlert: ":ticket: Ticked Created: <#%ticketid%>"
|
||||
TicketExists: "❌ Ticket already exists"
|
||||
TicketClaim: ":white_check_mark: Ticket claimed by" #The user who claimed the ticket will be added to the end of this message
|
||||
TicketUnclaim: ":white_check_mark: Ticket unclaimed by" #The user who claimed the ticket will be added to the end of this message
|
||||
|
||||
TicketArchived: ":white_check_mark: Ticket archived."
|
||||
TicketUnarchived: ":white_check_mark: Ticket unarchived."
|
||||
TicketLocked: ":lock: Ticket is locked."
|
||||
DisableInvites: "🔒 Invites have been disabled for this ticket."
|
||||
EnableInvites: "🔓 Invites have been enabled for this ticket."
|
||||
|
||||
# Available Placeholders:
|
||||
|
||||
# <@%user%> - Tags the user who was added to the ticket.
|
||||
|
||||
Blacklisted: ":x: You are not allowed to open a ticket"
|
||||
Muted: ":x: You are `Muted` and therefore not allowed to open any tickets"
|
||||
InvalidSubject: "No Reason Provided!"
|
||||
|
||||
ConfirmClose: "Please confirm closing the ticket by clicking the button below."
|
||||
CloseTimeout: "Ticket close confirmation timed out."
|
||||
|
||||
RenamedSuccess: "🏷️ Ticket renamed successfully!"
|
||||
|
||||
ClaimTickets:
|
||||
ClaimTitle: "Incoming Support Request!"
|
||||
ClaimMessage: "A new ticket has been created by <@!%user%>. Click the button below to claim this ticket."
|
||||
ClaimMessage_Edit: "<#%channel%> was claimed by <@!%user%>."
|
||||
|
||||
ClaimedTitle: "Ticket Claimed!"
|
||||
Claimed: "You have successfully claimed this ticket. <#%channel%>"
|
||||
NoPermsToClaimTitle: "Error, no permissions!"
|
||||
NoPermsToClaim: ":x: **Err!** You do not permissions to claim this ticket."
|
||||
|
||||
ReassigningTitle: "Reassigning!..."
|
||||
ReassigningMessage: "<@%user%> did not claim the ticket. **Reassigning...**"
|
||||
|
||||
TicketQuestions:
|
||||
Details:
|
||||
Title: "Ticket Details"
|
||||
Description: "Questions by <@%user%>" # Use <@%user%> to display the username.
|
||||
|
||||
AIMode:
|
||||
Title: "SupportBot AI"
|
||||
Description: "Use the buttons below to enable or disable AI chat in this ticket."
|
||||
EnabledMessage: "AI Mode has been enabled for this ticket."
|
||||
DisabledMessage: "AI Mode has been disabled for this ticket."
|
||||
|
||||
ThreadWelcomeTitle: "SupportBot AI"
|
||||
ThreadWelcomeMessage: "Hi! I’m SupportBot AI. You can chat with me here about your issue."
|
||||
|
||||
EnableButton: "Enable AI Mode"
|
||||
DisableButton: "Disable AI Mode"
|
||||
|
||||
TicketNotFound: "Ticket not found."
|
||||
EnableError: "There was an error enabling SupportBot AI."
|
||||
DisableError: "There was an error disabling SupportBot AI."
|
||||
|
||||
# ----------------------------
|
||||
# Review Messages
|
||||
# ----------------------------
|
||||
|
||||
ReviewSystem:
|
||||
Rate:
|
||||
Title: "Review us!"
|
||||
Description: "Please rate our support for this ticket (1-5 stars)."
|
||||
|
||||
Stars:
|
||||
One: "⭐ 1 Star"
|
||||
Two: "⭐⭐ 2 Stars"
|
||||
Three: "⭐⭐⭐ 3 Stars"
|
||||
Four: "⭐⭐⭐⭐ 4 Stars"
|
||||
Five: "⭐⭐⭐⭐⭐ 5 Stars"
|
||||
|
||||
Comment:
|
||||
Title: "Write a comment."
|
||||
Description: "Write a comment about your experience."
|
||||
|
||||
ReviewEmbed:
|
||||
Title: "Ticket Review"
|
||||
CommentTitle: "Comment"
|
||||
RatingTitle: "Rating"
|
||||
ReviewedStaffTitle: "Reviewed Staff:"
|
||||
ReviewedByTitle: "Reviewed By:"
|
||||
ReviewEmoji: "⭐"
|
||||
Color: "#f7a943"
|
||||
|
||||
# ----------------------------
|
||||
# Mod Command Messages
|
||||
# ----------------------------
|
||||
|
||||
Mod:
|
||||
|
||||
TicketBlacklist:
|
||||
Add:
|
||||
Description: "Add a user to the ticket blacklist"
|
||||
UserDescription: "User to blacklist"
|
||||
AlreadyBlacklisted: ":x: {userTag} is already blacklisted."
|
||||
Success: ":white_check_mark: {userTag} has been added to the blacklist. They can no longer use the ticket system."
|
||||
|
||||
Remove:
|
||||
Description: "Remove a user from the ticket blacklist"
|
||||
UserDescription: "User to remove from blacklist"
|
||||
NotBlacklisted: ":x: {userTag} is not blacklisted."
|
||||
Success: ":white_check_mark: {userTag} has been removed from the blacklist. They can now use the ticket system again."
|
||||
|
||||
View:
|
||||
Description: "View the list of all blacklisted users"
|
||||
NoBlacklistedUsers: ":information_source: There are currently no users on the blacklist."
|
||||
|
||||
|
||||
# ----------------------------
|
||||
# Logging Messages
|
||||
# ----------------------------
|
||||
|
||||
TicketLog:
|
||||
Title: "Ticket Log"
|
||||
Colour: "#0cc559"
|
||||
|
||||
TicketBlacklistLog:
|
||||
Title: "Ticket Blacklist Log"
|
||||
Colour: "#0cc559"
|
||||
|
||||
# ----------------------------
|
||||
# Misc Messages
|
||||
# ----------------------------
|
||||
|
||||
AddUser:
|
||||
NotInTicket_Title: "Ticket Invitation"
|
||||
NotInTicket_Description: "This command can only be used within a ticket thread."
|
||||
|
||||
Invite_Title: "User Invited"
|
||||
Invite_Description: "%username% has been invited to join the ticket."
|
||||
|
||||
DM_Title: "Ticket Invitation"
|
||||
DM_Description: "You have been invited to join the ticket %channelname%. Do you accept?"
|
||||
|
||||
Accept_Label: "Accept"
|
||||
Reject_Label: "Reject"
|
||||
ViewTicket_Label: "Go to Ticket"
|
||||
|
||||
Accepted_Title: "Invitation Accepted"
|
||||
Accepted_Message: "You have been added to the ticket."
|
||||
AddedToTicket_Title: "Added to Ticket"
|
||||
AddedToTicket_Description: "You have been added to the ticket %channel_link%."
|
||||
|
||||
# Added Message for inside the ticket, this informs everyone inside the ticket that the user has been added.
|
||||
Added_Title: "User Added"
|
||||
Added_Description: "Successfully added **%username%** to the ticket."
|
||||
|
||||
Declined_Title: "Invitation Declined"
|
||||
Declined_Message: "You have declined the invitation."
|
||||
|
||||
NoResponse_Title: "No Response"
|
||||
NoResponse_Message: "You did not respond in time. Please contact staff if you wish to join the ticket."
|
||||
|
||||
Error_Title: "Error"
|
||||
Error_Adding: "There was an error adding you to the ticket. Please try again."
|
||||
|
||||
DM_Error_Title: "Error"
|
||||
DM_Error_Description: "Unable to send a DM to %username%. Please ensure your DMs are enabled."
|
||||
|
||||
Unexpected_Error_Title: "Error"
|
||||
Unexpected_Error_Description: "An unexpected error occurred: %error%"
|
||||
|
||||
RemoveUser:
|
||||
NotInTicket_Title: "Error"
|
||||
NotInTicket_Description: "This command can only be used in ticket threads."
|
||||
|
||||
Removed_Title: "User Removed"
|
||||
Removed_Message: "The user has been successfully removed from the ticket."
|
||||
|
||||
RemovedFromTicket_Title: "Removed from Ticket"
|
||||
RemovedFromTicket_Description: "You have been removed from the ticket channel %channel_link%."
|
||||
|
||||
Error_Title: "Error"
|
||||
Error_Removing: "There was an error removing the user from the ticket."
|
||||
|
||||
ForceAddUser:
|
||||
NotInTicket_Title: "Not a Ticket"
|
||||
NotInTicket_Description: "This command can only be used within a ticket thread."
|
||||
|
||||
Added_Title: "User Added"
|
||||
Added_Description: "Successfully added %username% to the ticket."
|
||||
|
||||
AddedToTicket_Title: "You have been added to a Ticket"
|
||||
AddedToTicket_Description: "You have been added to the ticket channel [%channel_link%]."
|
||||
|
||||
Error_Title: "Error"
|
||||
Error_Adding: "There was an error adding the user to the ticket."
|
||||
|
||||
Error:
|
||||
IncorrectPermissions: ":x: **Error!** You do not have the correct permissions to use this command."
|
||||
MissingChannel: ":x: **Error!** Required channel not found. Please ensure it is set up correctly!"
|
||||
UserNotFound: ":x: **Error!** Specified user not found. Please check if they are in this server."
|
||||
CommandNotAllowedHere: ":warning: This command cannot be used in this channel."
|
||||
EntryAlreadyExists: ":warning: Cannot complete the action, as the entry already exists."
|
||||
EntryDoesNotExist: ":warning: Cannot complete the action, as the entry does not exist."
|
||||
ActionFailed: ":x: **Error!** An error occurred while processing your request. Please try again later."
|
||||
InvalidChannel: ":x: **Error!** The suggestions channel has not been set up correctly."
|
||||
@@ -0,0 +1,89 @@
|
||||
# -------------------------
|
||||
# SupportBot AI
|
||||
#
|
||||
# In order to use the SupportBot AI, You need to setup an OpenAI or Groq Developer Account.
|
||||
# this can be done at https://platform.openai.com/ or https://groq.com/
|
||||
# -------------------------
|
||||
|
||||
Enabled: true
|
||||
|
||||
General:
|
||||
Provider: "openai"
|
||||
Model: "MODEL"
|
||||
Model_API_Key: "MODEL_API_KEY"
|
||||
# Providers Available:
|
||||
# "openai" - Use OpenAI's GPT models. Example: "gpt-5.4" "gpt-5-mini"
|
||||
# "groq" - Use Groq's GroqAI models. Example: "meta-llama/llama-4-scout-17b-16e-instruct"
|
||||
# "claude" - Use Anthropic's Claude models. Example: "claude-sonnet-4-6"
|
||||
|
||||
Tokens: 1200
|
||||
|
||||
Temperature: 0.4
|
||||
Name: "SupportBot AI"
|
||||
|
||||
PastebinAPI_Key: "YOUR_PASTEBIN_API_KEY"
|
||||
PastebinAPI_URL: "https://pastebin.com/api/api_post.php"
|
||||
|
||||
Style:
|
||||
MessageType: "components_v2"
|
||||
# Available Styles for AI:
|
||||
# "embed" - AI responses will be sent as Discord embeds.
|
||||
# "text" - AI responses will be sent as plain messages.
|
||||
# "components_v2" - AI responses will be sent with the new components style.
|
||||
|
||||
Features:
|
||||
SuggestTickets: true # Should SupportBot AI suggest creating a ticket if the user needs more help?
|
||||
|
||||
Channels:
|
||||
AIChannel: "CHANNEL_ID"
|
||||
|
||||
Embed:
|
||||
Color: "#45b463"
|
||||
Footer: "SupportBot AI"
|
||||
|
||||
Memory:
|
||||
Enabled: true
|
||||
StoreFacts: true
|
||||
# StoreFacts - Enables saving important facts the AI learns.
|
||||
|
||||
StoreSummaries: true
|
||||
# StoreSummaries - Enables saving conversation summaries.
|
||||
|
||||
MaxRecentMessages: 15
|
||||
# MaxRecentMessages - Number of recent messages used for context.
|
||||
|
||||
Scope: "guild" # channel | guild
|
||||
# Available Scopes for Memory:
|
||||
# "channel" - Memory is per channel only.
|
||||
# "guild" - Memory is shared across the server.
|
||||
|
||||
CustomInstructions:
|
||||
- "You are SupportBot AI made by Emerald Services"
|
||||
- "You are built with DiscordJS/NodeJS and currently running on dev build 26.0
|
||||
build 2"
|
||||
- "You're current build can be found here
|
||||
https://github.com/Emerald-Services/SupportBot/tree/26.0"
|
||||
- "You're main build can be found here
|
||||
https://builtbybit.com/resources/supportbot-free-advanced-ticket-bot.8940/
|
||||
and Polymart at
|
||||
https://polymart.org/product/518/supportbot-1-discord-ticket-bot which is
|
||||
version 8.4"
|
||||
- "dev build 26.0 build 2 includes all-new AI features for SupportBot such as
|
||||
memory system that allows you to remember facts and summaries of
|
||||
conversations, which can be used to provide better responses."
|
||||
- "SupportBot AI also have a new tickets AI Mode which is enabled in
|
||||
supportbot.ymll, which allows you to assist users in support tickets and
|
||||
provide better responses based on the context of the ticket."
|
||||
- "Your main purpose is to assist users in the support server and provide
|
||||
helpful responses to their questions and issues. Always be accurate and if
|
||||
you do not have answer then simply ask the person to create a ticket if they
|
||||
need more help."
|
||||
#- "Add custom instructions here"
|
||||
|
||||
Messages:
|
||||
ErrorResponse: "I couldn't generate a response."
|
||||
AIError: "There was an error contacting the AI provider."
|
||||
AIDisabled: "AI is currently disabled."
|
||||
MessageTooLong: "My response was too long, so I uploaded it here: {pastebin}"
|
||||
SuggestTicketText: "-# Need more help? React with 🎫 to open a ticket."
|
||||
SuggestTicketReaction: "🎫"
|
||||
@@ -0,0 +1,389 @@
|
||||
# ___ _ ___ _
|
||||
# / __> _ _ ___ ___ ___ _ _ _| |_ | . > ___ _| |_
|
||||
# \__ \| | || . \| . \/ . \| '_> | | | . \/ . \ | |
|
||||
# <___/`___|| _/| _/\___/|_| |_| |___/\___/ |_|
|
||||
# |_| |_|
|
||||
#
|
||||
# SupportBot created by Emerald Services
|
||||
# Installed with MIT License
|
||||
#
|
||||
# Discord Support: https://discord.gg/emeraldsrv
|
||||
# Website: https://emeraldsrv.dev
|
||||
#
|
||||
# SupportBot Configuration (supportbot.yml)
|
||||
|
||||
SupportBot_Version: "v26.2-pre2"
|
||||
|
||||
# -------------------------
|
||||
# General
|
||||
# -------------------------
|
||||
|
||||
General:
|
||||
Name: "SupportBot"
|
||||
Token: "YOUR_BOT_TOKEN_HERE"
|
||||
Addons:
|
||||
Enabled: true
|
||||
GuildId: "YOUR_GUILD_ID_HERE"
|
||||
|
||||
# -------------------------
|
||||
# Database Configuration
|
||||
# -------------------------
|
||||
|
||||
Database:
|
||||
# Driver options: "mysql" (works for MariaDB & MySQL) or "sqlite"
|
||||
Driver: "mysql"
|
||||
|
||||
# Connection Settings for MariaDB / MySQL (used when Driver is "mysql" or "mariadb")
|
||||
MySQL:
|
||||
Host: "localhost"
|
||||
Port: 3306
|
||||
User: "root"
|
||||
Password: "password"
|
||||
Database: "supportbot"
|
||||
ConnectionLimit: 10
|
||||
|
||||
# Connection Settings for SQLite (used when Driver is "sqlite")
|
||||
SQLite:
|
||||
File: "./Data/supportbot.db"
|
||||
|
||||
# Activity
|
||||
# -------------------------
|
||||
|
||||
Activity:
|
||||
Status: "SupportBot"
|
||||
Type: "Watching"
|
||||
StreamingURL: "https://www.twitch.tv/YOUR_TWITCH_NAME"
|
||||
# Available Types:
|
||||
# Playing
|
||||
# Watching
|
||||
# Listening
|
||||
# Competing
|
||||
# Streaming (URL required)
|
||||
|
||||
# You can also update the activity via Discord using: /settings
|
||||
|
||||
# -------------------------
|
||||
# Embeds
|
||||
# -------------------------
|
||||
|
||||
Embed:
|
||||
Colours:
|
||||
General: "#0cc559"
|
||||
Success: "#04b545"
|
||||
Error: "#d13030"
|
||||
Warn: "#fca117"
|
||||
|
||||
Footer: "SupportBot | Developed by Emerald Development"
|
||||
# Use 0x<hexcode> to change the colour of the embeds.
|
||||
|
||||
# -------------------------
|
||||
# Roles
|
||||
# -------------------------
|
||||
|
||||
Roles:
|
||||
AutoRole:
|
||||
Enabled: true
|
||||
Role: "ROLE_ID"
|
||||
|
||||
StaffMember:
|
||||
Staff: "ROLE_ID"
|
||||
Admin: "ROLE_ID"
|
||||
Moderator: "ROLE_ID"
|
||||
|
||||
Mod:
|
||||
AllowSupportStaff: true
|
||||
# Allow Support Staff to use the /mod command. (More in detail customisation in commands.yml)
|
||||
|
||||
# ----------------------------
|
||||
# System Messages
|
||||
# ----------------------------
|
||||
|
||||
Welcome:
|
||||
Enabled: false
|
||||
Channel: "CHANNEL_ID"
|
||||
|
||||
Leave:
|
||||
Enabled: false
|
||||
Channel: "CHANNEL_ID"
|
||||
|
||||
# ----------------------------
|
||||
# Suggestions
|
||||
# ----------------------------
|
||||
|
||||
Suggestions:
|
||||
Mode: "Channel" # Channel or Forum
|
||||
ForumChannel: "FORUM_CHANNEL_ID" # A forum channel, disabled if mode is set to Channel.
|
||||
Channel: "CHANNEL_ID" # A text channel, disabled if mode is set to Forum.
|
||||
|
||||
UpvoteEmoji: "✅"
|
||||
DownvoteEmoji: "❌"
|
||||
|
||||
UpvoteTitle: "Yes"
|
||||
DownvoteTitle: "No"
|
||||
|
||||
OwnSuggestion: true
|
||||
|
||||
# If false this will not allow the user to vote for a suggestion they created
|
||||
# If true this will allow the user to vote for a suggestion they created
|
||||
|
||||
ShowUsers: true
|
||||
|
||||
Threads:
|
||||
Enabled: true
|
||||
Title: "Suggestion-releated Thread"
|
||||
Reason: "A thread releted to this suggestion"
|
||||
|
||||
Buttons:
|
||||
Upvote: "2"
|
||||
Downvote: "2"
|
||||
RemoveVote: "4"
|
||||
RemoveVote_Title: "Remove Vote"
|
||||
# Available Button Styles
|
||||
# 1 - Blurple
|
||||
# 2 - Grey
|
||||
# 3 - Green
|
||||
# 4 - Red
|
||||
|
||||
Tags:
|
||||
Created: "TAG_ID"
|
||||
Considering: "TAG_ID"
|
||||
Solved: "TAG_ID"
|
||||
Denied: "TAG_ID"
|
||||
|
||||
# -------------------------
|
||||
# Tickets
|
||||
# -------------------------
|
||||
|
||||
VoiceTickets:
|
||||
Name: "%username%'s VC"
|
||||
Category: "CATEGORY_ID"
|
||||
|
||||
Ticket:
|
||||
TicketType: "channels"
|
||||
|
||||
# Available TicketTypes:
|
||||
# channels - Tickets will be created as channels in the TicketChannelsCategory
|
||||
# threads - Tickets will be created as threads through the "TicketHome" channel.
|
||||
|
||||
UserManagement:
|
||||
DMOnAdd: true # DM the user when they are added to the ticket
|
||||
DMOnRemove: true # DM the user when they are removed from the ticket
|
||||
DMTranscript: true # DM the user a transcript of the ticket when it is closed
|
||||
|
||||
TicketHome: "CHANNEL_ID"
|
||||
|
||||
# What is TicketHome?
|
||||
# - Channel where the Ticket creation panel is sent.
|
||||
# - Channel where ticket threads are made. (only if ticket threads is enabled above.)
|
||||
|
||||
# For TicketType: channels
|
||||
TicketChannelsCategory: "CATEGORY_ID"
|
||||
TicketChannelsCategory2: "CATEGORY_ID" # This will be used if the first category has reached the max channel limit.
|
||||
TicketArchiveCategory: "CATEGORY_ID"
|
||||
# ---
|
||||
|
||||
Channel: "ticket-" # The prefix for ticket channel names
|
||||
TicketsPerUser: 100
|
||||
TicketReason: true
|
||||
# If ticket reason is set to false, It will not prompt to create a reason
|
||||
|
||||
TicketSubject: "description"
|
||||
|
||||
# Available TicketSubjects:
|
||||
# "description" will display the ticket reason within the channel description
|
||||
# "embed" will display the ticket reason within the ticket embed
|
||||
|
||||
Timeout: 5
|
||||
# Set to true to allow all staff to access the ticket
|
||||
|
||||
Close:
|
||||
StaffOnly: true
|
||||
# Set to false is anyone is able to close a ticket.
|
||||
|
||||
Invites:
|
||||
# Invites is for ticket threads.
|
||||
StaffOnly: true
|
||||
|
||||
ClaimTickets:
|
||||
Enabled: false
|
||||
Channel: "CHANNEL_ID"
|
||||
# Channel where the ticket assignments are sent for staff to claim.
|
||||
|
||||
ClaimAcceptTime: 120000
|
||||
# How long the assigned user has to accept the ticket before it gets re-assigned to someone else.
|
||||
# 120000 = 2 Minutes
|
||||
# 180000 = 3 Minutes
|
||||
# 240000 = 4 Minutes
|
||||
# 300000 = 5 Minutes
|
||||
# Time in Mliseconds
|
||||
|
||||
ButtonTitle: "Claim Ticket"
|
||||
ButtonEmoji: "🎫"
|
||||
Button: "1"
|
||||
# Available Button Styles
|
||||
# 1 - Blurple
|
||||
# 2 - Grey
|
||||
# 3 - Green
|
||||
# 4 - Red
|
||||
|
||||
ReviewSystem:
|
||||
Enabled: false
|
||||
Channel: "CHANNEL_ID"
|
||||
UseModal: true
|
||||
|
||||
Questions:
|
||||
# Default questions asked when a ticket is created, these are used if departments are disabled.
|
||||
Enabled: false
|
||||
List:
|
||||
- "Please provide your account email?"
|
||||
- "What is the nature of your issue?"
|
||||
- "Have you tried resolving the issue yourself?"
|
||||
|
||||
Log:
|
||||
TicketDataLog: "CHANNEL_ID"
|
||||
TickeDataTitle: "Ticket Data"
|
||||
TicketBlacklistLog: "CHANNEL_ID"
|
||||
|
||||
AIMode:
|
||||
Enabled: true
|
||||
ThreadName: "Chat with SupportBot AI"
|
||||
WelcomeMessage: "Hi! I’m SupportBot AI. You can chat with me here about your issue."
|
||||
|
||||
# -------------------------
|
||||
# Ticket Departments
|
||||
# -------------------------
|
||||
|
||||
DepartmentSystem:
|
||||
Enabled: false
|
||||
Placeholder: "Choose a ticket department"
|
||||
DefaultDepartment: "general"
|
||||
|
||||
Departments:
|
||||
general:
|
||||
Name: "General Support"
|
||||
Description: "Open a general support ticket"
|
||||
Emoji: "🎫"
|
||||
Category: "CATEGORY_ID"
|
||||
Role: "ROLE_ID"
|
||||
ChannelPrefix: "support-"
|
||||
DefaultPriority: "normal"
|
||||
Questions:
|
||||
- "What do you need help with?"
|
||||
- "When did the issue start?"
|
||||
|
||||
purchase:
|
||||
Name: "Purchase Support"
|
||||
Description: "Get help with store purchases"
|
||||
Emoji: "💳"
|
||||
Category: "CATEGORY_ID"
|
||||
Role: "ROLE_ID"
|
||||
ChannelPrefix: "purchase-"
|
||||
DefaultPriority: "high"
|
||||
Questions:
|
||||
- "What is your order ID?"
|
||||
- "What product did you purchase?"
|
||||
- "What payment issue are you having?"
|
||||
|
||||
reports:
|
||||
Name: "Player Report"
|
||||
Description: "Report a player for breaking the rules"
|
||||
Emoji: "🚨"
|
||||
Category: "CATEGORY_ID"
|
||||
Role: "ROLE_ID"
|
||||
ChannelPrefix: "report-"
|
||||
DefaultPriority: "normal"
|
||||
Questions:
|
||||
- "Who are you reporting?"
|
||||
- "What happened?"
|
||||
- "Do you have evidence?"
|
||||
|
||||
appeals:
|
||||
Name: "Ban Appeals"
|
||||
Description: "Appeal a server punishment"
|
||||
Emoji: "⚖️"
|
||||
Category: "CATEGORY_ID"
|
||||
Role: "ROLE_ID"
|
||||
ChannelPrefix: "appeals-"
|
||||
DefaultPriority: "low"
|
||||
Questions:
|
||||
- "What punishment are you appealing?"
|
||||
- "Why should it be reviewed?"
|
||||
- "Anything else staff should know?"
|
||||
|
||||
PrioritySystem:
|
||||
Enabled: false
|
||||
DefaultPriority: "Normal"
|
||||
AllowUsersToChooseOnOpen: true
|
||||
Priorities:
|
||||
low:
|
||||
Name: "Low"
|
||||
Emoji: "🟢"
|
||||
normal:
|
||||
Name: "Normal"
|
||||
Emoji: "🟡"
|
||||
high:
|
||||
Name: "High"
|
||||
Emoji: "🟠"
|
||||
urgent:
|
||||
Name: "Urgent"
|
||||
Emoji: "🔴"
|
||||
|
||||
# -------------------------
|
||||
# Buttons
|
||||
# -------------------------
|
||||
Buttons:
|
||||
General:
|
||||
Delete: "🗑️"
|
||||
DeleteStyle: "4"
|
||||
|
||||
Voice:
|
||||
TicketDeleteText: "Delete Voice Channel"
|
||||
|
||||
Tickets:
|
||||
ClaimEmoji: "🧑💻"
|
||||
ClaimStyle: "1"
|
||||
|
||||
Unarchive: "Unarchive Ticket"
|
||||
Unarchive_Emoji: "♻️"
|
||||
Unarchive_Style: "2"
|
||||
|
||||
Unlock: "Unlock Ticket"
|
||||
Unlock_Emoji: "🔓"
|
||||
Unlock_Style: "2"
|
||||
# Available Button Styles
|
||||
# 1 - Blurple
|
||||
# 2 - Grey
|
||||
# 3 - Green
|
||||
# 4 - Red
|
||||
|
||||
# -------------------------
|
||||
# Select Menus
|
||||
# -------------------------
|
||||
|
||||
SelectMenus:
|
||||
Tickets:
|
||||
PanelEmoji: "⚙️"
|
||||
|
||||
CloseEmoji: "🗑️"
|
||||
ArchiveEmoji: "♻️"
|
||||
LockEmoji: "🔐"
|
||||
RenameEmoji: "🏷️"
|
||||
VCEmoji: "🔊"
|
||||
|
||||
EnableInvitesEmoji: "📤"
|
||||
DisableInvitesEmoji: "📥"
|
||||
|
||||
# -------------------------
|
||||
# Logging
|
||||
# -------------------------
|
||||
|
||||
MessageDelete:
|
||||
Channel: "CHANNEL_ID"
|
||||
Colour: "#d92525"
|
||||
|
||||
MessageUpdate:
|
||||
Channel: "CHANNEL_ID"
|
||||
Colour: "#f2a024"
|
||||
|
||||
Translate:
|
||||
TranslateLog: "CHANNEL_ID"
|
||||
@@ -0,0 +1,67 @@
|
||||
# ___ _ ___ _
|
||||
# / __> _ _ ___ ___ ___ _ _ _| |_ | . > ___ _| |_
|
||||
# \__ \| | || . \| . \/ . \| '_> | | | . \/ . \ | |
|
||||
# <___/`___|| _/| _/\___/|_| |_| |___/\___/ |_|
|
||||
# |_| |_|
|
||||
#
|
||||
# SupportBot created by Emerald Services
|
||||
# Installed with MIT License
|
||||
#
|
||||
# Discord Support: https://discord.gg/emeraldsrv
|
||||
# Official Documentation: https://docs.emeraldsrv.com
|
||||
# Addons Marketplace: https://community.emeraldsrv.com
|
||||
#
|
||||
# Ticket Creation Panel Configuration (ticket-panel.yml)
|
||||
|
||||
# -------------------------------
|
||||
# Ticket Panel Configuration
|
||||
# -------------------------------
|
||||
|
||||
Panel: true
|
||||
|
||||
Style:
|
||||
Title: "## 📬 Open Ticket"
|
||||
Description: "Tap the button to open a support ticket, Our team will respond as
|
||||
soon as possible."
|
||||
Image: "https://cdn.1tzemerald.space/Emerald%20Services/SBPoster.png"
|
||||
Color: "#45b463"
|
||||
Layout: "3"
|
||||
|
||||
# Available Layout Styles
|
||||
# "1" - Style 1 - Title & Text, Button at the top -- Image at the bottom
|
||||
# "2" - Style 2 - Title & Text, Button at the bottom -- Image at the top
|
||||
# "3" - Style 3 - The classic look - Title & Text at the top -- Image in the middle -- Button at the bottom
|
||||
# "4" - Style 4 - Classic Embed (Title & Text, Image on right, Button at bottom)
|
||||
|
||||
Button:
|
||||
Text: "Open"
|
||||
Emoji: "🛟"
|
||||
Color: "2"
|
||||
CustomId: "createticket"
|
||||
|
||||
# Available Button Styles
|
||||
# 1 - Blurple
|
||||
# 2 - Grey
|
||||
# 3 - Red
|
||||
# 4 - Green
|
||||
|
||||
DepartmentMenu:
|
||||
CustomId: "ticketdepartmentselect"
|
||||
Placeholder: "Choose a ticket department"
|
||||
DefaultDescription: "Open a support ticket"
|
||||
DefaultEmoji: "🎫"
|
||||
|
||||
Settings:
|
||||
Image: true
|
||||
TopTitle: true
|
||||
TopDivider: true
|
||||
BottomTitle: false
|
||||
BottomDivider: false
|
||||
|
||||
Messages:
|
||||
NotConfigured: "Ticket Panel is not set up. Please configure it in
|
||||
`/Configs/ticket-panel.yml`."
|
||||
InvalidChannel: "TicketHome channel not found. Please check your config file."
|
||||
PanelSent: "Ticket panel message has been sent!"
|
||||
PanelSentWithDepartments: "Ticket panel message with department selection has been sent!"
|
||||
SendError: "An error occurred while sending the ticket panel message."
|
||||
@@ -0,0 +1,33 @@
|
||||
Theme:
|
||||
Background: '#0e0e0e'
|
||||
Panel: '#1d1d1d'
|
||||
PanelSecondary: '#222222'
|
||||
PanelTertiary: '#1d2938'
|
||||
Border: '#2e2e2e'
|
||||
Text: '#e6edf3'
|
||||
Muted: '#606060'
|
||||
Accent: '#396ff8'
|
||||
AccentSoft: rgba(34, 197, 94, 0.12)
|
||||
AccentBorder: rgba(34, 197, 94, 0.25)
|
||||
AccentText: '#8df0ad'
|
||||
Link: '#396ff8'
|
||||
AttachmentLink: '#396ff8'
|
||||
CodeBackground: '#0e0e0e'
|
||||
Typography:
|
||||
FontFamily: Poppins
|
||||
Header:
|
||||
Title: Ticket Transcript
|
||||
Subtitle: Exported conversation log for support ticket **{{ticketName}}**
|
||||
Badge: Closed Transcript
|
||||
ShowMetaCards: true
|
||||
Meta:
|
||||
ChannelLabel: Ticket Name
|
||||
TicketIdLabel: Ticket ID
|
||||
MessageCountLabel: Message Count
|
||||
CloseReasonLabel: Close Reason
|
||||
DefaultCloseReason: No Reason Provided.
|
||||
Messages:
|
||||
SectionTitle: Messages
|
||||
EmptyState: No messages were found in this ticket.
|
||||
Advanced:
|
||||
CustomCss: ''
|
||||
@@ -0,0 +1,82 @@
|
||||
// Emerald Services | SupportBot
|
||||
// Claude integration for SupportBot AI
|
||||
|
||||
const fs = require("fs");
|
||||
const yaml = require("js-yaml");
|
||||
const axios = require("axios");
|
||||
|
||||
const supportbotai = require("../../Structures/ConfigStore").supportbotAi;
|
||||
|
||||
function extractTextFromMessage(message) {
|
||||
if (!message) return "";
|
||||
|
||||
if (typeof message === "string") {
|
||||
return message.trim();
|
||||
}
|
||||
|
||||
if (message.text) {
|
||||
return message.text.trim();
|
||||
}
|
||||
|
||||
if (Array.isArray(message)) {
|
||||
return message
|
||||
.map((part) => {
|
||||
if (typeof part === "string") return part;
|
||||
if (part?.type === "text" && typeof part.text === "string") {
|
||||
return part.text;
|
||||
}
|
||||
return "";
|
||||
})
|
||||
.join("")
|
||||
.trim();
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
async function chat({ messages, temperature, maxTokens }) {
|
||||
const model = supportbotai.General.Model;
|
||||
const apiKey = supportbotai.General.Model_API_Key;
|
||||
|
||||
const systemMessages = messages.filter((m) => m.role === "system");
|
||||
const systemPrompt = systemMessages.map((m) => m.content).join("\n\n");
|
||||
|
||||
const userAssistantMessages = messages.filter((m) => m.role !== "system");
|
||||
|
||||
const response = await axios.post(
|
||||
"https://api.anthropic.com/v1/messages",
|
||||
{
|
||||
model,
|
||||
system: systemPrompt || undefined,
|
||||
messages: userAssistantMessages,
|
||||
temperature: temperature || supportbotai.General.Temperature || 0.4,
|
||||
max_tokens: maxTokens || supportbotai.General.Tokens || 800,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"x-api-key": apiKey,
|
||||
"anthropic-version": "2023-06-01",
|
||||
"content-type": "application/json",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const data = response.data;
|
||||
const content = extractTextFromMessage(data.content);
|
||||
|
||||
if (!content) {
|
||||
console.error("Claude raw response:", JSON.stringify(data, null, 2));
|
||||
throw new Error(
|
||||
`Claude returned empty response. stop_reason=${data?.stop_reason || "unknown"}`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
content,
|
||||
raw: data,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
chat,
|
||||
};
|
||||
@@ -0,0 +1,70 @@
|
||||
// Emerald Services | SupportBot
|
||||
// Groq integration for SupportBot AI
|
||||
|
||||
const fs = require("fs");
|
||||
const yaml = require("js-yaml");
|
||||
const OpenAI = require("openai");
|
||||
|
||||
const supportbotai = require("../../Structures/ConfigStore").supportbotAi;
|
||||
|
||||
const client = new OpenAI({
|
||||
apiKey: supportbotai.General.Model_API_Key,
|
||||
baseURL: "https://api.groq.com/openai/v1",
|
||||
});
|
||||
|
||||
function extractTextFromMessage(message) {
|
||||
if (!message) return "";
|
||||
|
||||
if (typeof message.content === "string") {
|
||||
return message.content.trim();
|
||||
}
|
||||
|
||||
if (Array.isArray(message.content)) {
|
||||
return message.content
|
||||
.map((part) => {
|
||||
if (typeof part === "string") return part;
|
||||
if (part?.type === "text" && typeof part.text === "string") {
|
||||
return part.text;
|
||||
}
|
||||
if (typeof part?.text?.value === "string") {
|
||||
return part.text.value;
|
||||
}
|
||||
return "";
|
||||
})
|
||||
.join("")
|
||||
.trim();
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
async function chat({ messages, temperature, maxTokens }) {
|
||||
const model = supportbotai.General.Model;
|
||||
|
||||
const response = await client.chat.completions.create({
|
||||
model,
|
||||
messages,
|
||||
temperature: temperature || supportbotai.General.Temperature || 0.4,
|
||||
max_tokens: maxTokens || supportbotai.General.Tokens || 800,
|
||||
});
|
||||
|
||||
const choice = response?.choices?.[0];
|
||||
const message = choice?.message;
|
||||
const content = extractTextFromMessage(message);
|
||||
|
||||
if (!content) {
|
||||
console.error("Groq raw response:", JSON.stringify(response, null, 2));
|
||||
throw new Error(
|
||||
`Groq returned empty response. finish_reason=${choice?.finish_reason || "unknown"}`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
content,
|
||||
raw: response,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
chat,
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
// Emerald Services | SupportBot
|
||||
// Open AI integration for SupportBot AI
|
||||
|
||||
const fs = require("fs");
|
||||
const yaml = require("js-yaml");
|
||||
const OpenAI = require("openai");
|
||||
|
||||
const supportbotai = require("../../Structures/ConfigStore").supportbotAi;
|
||||
|
||||
const client = new OpenAI({
|
||||
apiKey: supportbotai.General.Model_API_Key,
|
||||
});
|
||||
|
||||
function extractTextFromMessage(message) {
|
||||
if (!message) return "";
|
||||
|
||||
if (typeof message.content === "string") {
|
||||
return message.content.trim();
|
||||
}
|
||||
|
||||
if (Array.isArray(message.content)) {
|
||||
return message.content
|
||||
.map((part) => {
|
||||
if (typeof part === "string") return part;
|
||||
if (part?.type === "text" && typeof part.text === "string")
|
||||
return part.text;
|
||||
if (typeof part?.text?.value === "string") return part.text.value;
|
||||
return "";
|
||||
})
|
||||
.join("")
|
||||
.trim();
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
async function chat({ messages, temperature, maxTokens }) {
|
||||
const model = supportbotai.General.Model;
|
||||
|
||||
const response = await client.chat.completions.create({
|
||||
model,
|
||||
messages,
|
||||
temperature: temperature || supportbotai.General.Temperature || 0.4,
|
||||
max_completion_tokens: maxTokens || supportbotai.General.Tokens || 800,
|
||||
});
|
||||
|
||||
const choice = response?.choices?.[0];
|
||||
const message = choice?.message;
|
||||
const content = extractTextFromMessage(message);
|
||||
|
||||
if (!content) {
|
||||
console.error("OpenAI raw response:", JSON.stringify(response, null, 2));
|
||||
throw new Error(
|
||||
`OpenAI returned empty response. finish_reason=${choice?.finish_reason || "unknown"}`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
content,
|
||||
raw: response,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
chat,
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
const Event = require("../Structures/Event.js");
|
||||
const {
|
||||
getConfiguredGuildId,
|
||||
syncGuildHealth,
|
||||
logGuildWarning,
|
||||
} = require("../Structures/GuildManager.js");
|
||||
|
||||
module.exports = new Event("guildCreate", async (client, guild) => {
|
||||
const configuredId = getConfiguredGuildId();
|
||||
|
||||
if (!configuredId) {
|
||||
logGuildWarning(
|
||||
client,
|
||||
`Joined "${guild.name}" (${guild.id}). Set General.GuildId in config to lock the bot to one server.`,
|
||||
);
|
||||
syncGuildHealth(client);
|
||||
return;
|
||||
}
|
||||
|
||||
if (guild.id === configuredId) {
|
||||
syncGuildHealth(client);
|
||||
return;
|
||||
}
|
||||
|
||||
logGuildWarning(
|
||||
client,
|
||||
`Joined unauthorized server "${guild.name}" (${guild.id}). Leave it from the dashboard or remove the invite.`,
|
||||
);
|
||||
|
||||
syncGuildHealth(client);
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
const Event = require("../Structures/Event.js");
|
||||
const {
|
||||
getConfiguredGuildId,
|
||||
syncGuildHealth,
|
||||
logGuildWarning,
|
||||
} = require("../Structures/GuildManager.js");
|
||||
|
||||
module.exports = new Event("guildDelete", async (client, guild) => {
|
||||
const configuredId = getConfiguredGuildId();
|
||||
|
||||
if (configuredId && guild.id === configuredId) {
|
||||
logGuildWarning(
|
||||
client,
|
||||
`Left the configured server "${guild.name}" (${guild.id}). Re-invite the bot or update General.GuildId.`,
|
||||
);
|
||||
} else if (!configuredId) {
|
||||
logGuildWarning(client, `Left server "${guild.name}" (${guild.id}).`);
|
||||
} else {
|
||||
logGuildWarning(client, `Left server "${guild.name}" (${guild.id}).`);
|
||||
}
|
||||
|
||||
syncGuildHealth(client);
|
||||
});
|
||||
@@ -0,0 +1,151 @@
|
||||
const {
|
||||
EmbedBuilder,
|
||||
ActionRowBuilder,
|
||||
ButtonBuilder,
|
||||
ButtonStyle,
|
||||
MessageFlags,
|
||||
ContainerBuilder,
|
||||
TextDisplayBuilder,
|
||||
SectionBuilder,
|
||||
ThumbnailBuilder,
|
||||
MediaGalleryBuilder
|
||||
} = require("discord.js");
|
||||
const Event = require("../Structures/Event.js");
|
||||
|
||||
const supportbot = require("../Structures/ConfigStore").supportbot;
|
||||
const msgconfig = require("../Structures/ConfigStore").messages;
|
||||
|
||||
function getWelcomeThumbnail(client, member, iconCfg) {
|
||||
switch ((iconCfg?.Mode || "").toUpperCase()) {
|
||||
case "USER":
|
||||
return member.user.displayAvatarURL();
|
||||
case "BOT":
|
||||
return client.user.displayAvatarURL();
|
||||
case "CUSTOM":
|
||||
return iconCfg.CustomURL;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function buildModernComponents(client, member) {
|
||||
const container = new ContainerBuilder();
|
||||
|
||||
if (msgconfig.Welcome.Modern.Colour) {
|
||||
container.setAccentColor(parseInt(msgconfig.Welcome.Modern.Colour.replace("#", ""), 16));
|
||||
}
|
||||
|
||||
const section = new SectionBuilder().addTextDisplayComponents(
|
||||
new TextDisplayBuilder().setContent(
|
||||
msgconfig.Welcome.Modern.Title.replace(/%joined_user%/g, member.user)
|
||||
),
|
||||
new TextDisplayBuilder().setContent(
|
||||
msgconfig.Welcome.Modern.Body.replace(/%joined_user%/g, member.user)
|
||||
)
|
||||
);
|
||||
|
||||
const thumbUrl = getWelcomeThumbnail(client, member, msgconfig.Welcome.Modern.Icon);
|
||||
if (thumbUrl) {
|
||||
section.setThumbnailAccessory(
|
||||
new ThumbnailBuilder()
|
||||
.setURL(thumbUrl)
|
||||
);
|
||||
}
|
||||
|
||||
container.addSectionComponents(section);
|
||||
|
||||
// FIXED: Using MediaGalleryBuilder instead of TextDisplayBuilder for images
|
||||
if (msgconfig.Welcome.Modern.Image?.Enabled && msgconfig.Welcome.Modern.Image.URL) {
|
||||
container.addMediaGalleryComponents(
|
||||
new MediaGalleryBuilder().addItems([
|
||||
{
|
||||
media: {
|
||||
url: msgconfig.Welcome.Modern.Image.URL
|
||||
}
|
||||
}
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
if (Array.isArray(msgconfig.Welcome.Modern.Buttons)) {
|
||||
const row = new ActionRowBuilder();
|
||||
msgconfig.Welcome.Modern.Buttons.slice(0, 5).forEach(b => {
|
||||
const btn = new ButtonBuilder()
|
||||
.setLabel(b.label)
|
||||
.setStyle(ButtonStyle[b.style])
|
||||
.setEmoji(b.emoji || null);
|
||||
if (b.style === "Link") btn.setURL(b.url);
|
||||
else if (b.customId) btn.setCustomId(b.customId);
|
||||
row.addComponents(btn);
|
||||
});
|
||||
|
||||
container.addActionRowComponents(row);
|
||||
}
|
||||
|
||||
return [container];
|
||||
}
|
||||
|
||||
module.exports = new Event("guildMemberAdd", async (client, member) => {
|
||||
if (!supportbot.Welcome.Enabled) return;
|
||||
|
||||
const WelcomeChannel =
|
||||
member.guild.channels.cache.get(supportbot.Welcome.Channel) ||
|
||||
member.guild.channels.cache.find(c => c.name === supportbot.Welcome.Channel);
|
||||
if (!WelcomeChannel) return;
|
||||
|
||||
const style = (msgconfig.Welcome.LayoutStyle || "classic").toLowerCase();
|
||||
|
||||
if (style === "classic") {
|
||||
const WelcomeEmbed = new EmbedBuilder()
|
||||
.setColor(msgconfig.Welcome.Embed.Colour)
|
||||
.setTitle(msgconfig.Welcome.Embed.Title || null)
|
||||
.setDescription(
|
||||
msgconfig.Welcome.Embed.Message.replace(/%joined_user%/g, member.user)
|
||||
)
|
||||
.setTimestamp();
|
||||
|
||||
const thumb = getWelcomeThumbnail(client, member, msgconfig.Welcome.Embed.Icon);
|
||||
if (thumb) WelcomeEmbed.setThumbnail(thumb);
|
||||
|
||||
if (msgconfig.Welcome.Embed.Image?.Enabled && msgconfig.Welcome.Embed.Image.URL) {
|
||||
WelcomeEmbed.setImage(msgconfig.Welcome.Embed.Image.URL);
|
||||
}
|
||||
|
||||
const components = [];
|
||||
if (
|
||||
msgconfig.Welcome.Embed.Buttons?.Enabled &&
|
||||
Array.isArray(msgconfig.Welcome.Embed.Buttons.List)
|
||||
) {
|
||||
const row = new ActionRowBuilder();
|
||||
msgconfig.Welcome.Embed.Buttons.List.slice(0, 5).forEach(b => {
|
||||
const btn = new ButtonBuilder()
|
||||
.setLabel(b.label)
|
||||
.setStyle(ButtonStyle[b.style])
|
||||
.setEmoji(b.emoji || null);
|
||||
if (b.style === "Link") btn.setURL(b.url);
|
||||
else if (b.customId) btn.setCustomId(b.customId);
|
||||
row.addComponents(btn);
|
||||
});
|
||||
components.push(row);
|
||||
}
|
||||
|
||||
await WelcomeChannel.send({ embeds: [WelcomeEmbed], components });
|
||||
} else if (style === "modern") {
|
||||
const components = buildModernComponents(client, member);
|
||||
if (components.length > 0) {
|
||||
await WelcomeChannel.send({
|
||||
components,
|
||||
flags: MessageFlags.IsComponentsV2
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (supportbot.Roles.AutoRole.Enabled) {
|
||||
const role =
|
||||
member.guild.roles.cache.get(supportbot.Roles.AutoRole.Role) ||
|
||||
member.guild.roles.cache.find(r => r.name === supportbot.Roles.AutoRole.Role);
|
||||
if (role) await member.roles.add(role);
|
||||
}
|
||||
|
||||
console.log("\u001b[32m", "[+]", "\u001b[33m", `${member.user.username} joined the server!`);
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
const { EmbedBuilder } = require("discord.js");
|
||||
const fs = require("fs");
|
||||
const yaml = require("js-yaml");
|
||||
const Event = require("../Structures/Event.js");
|
||||
|
||||
const supportbot = require("../Structures/ConfigStore").supportbot;
|
||||
const msgconfig = require("../Structures/ConfigStore").messages;
|
||||
|
||||
module.exports = new Event("guildMemberRemove", async (client, member) => {
|
||||
|
||||
if (!supportbot.Leave || !supportbot.Leave.Enabled) return;
|
||||
|
||||
const leaveChannel =
|
||||
member.guild.channels.cache.get(supportbot.Leave.Channel) ||
|
||||
member.guild.channels.cache.find(c => c.name === supportbot.Leave.Channel);
|
||||
|
||||
if (!leaveChannel) return;
|
||||
|
||||
const leaveEmbed = new EmbedBuilder()
|
||||
.setColor(msgconfig.Leave.Embed.Colour)
|
||||
.setTitle(msgconfig.Leave.Embed.Title)
|
||||
.setDescription(
|
||||
msgconfig.Leave.Embed.Message.replace(/%left_user%/g, member.user.toString())
|
||||
)
|
||||
.setTimestamp();
|
||||
|
||||
if (msgconfig.Leave.Embed.Thumbnail === "BOT") {
|
||||
leaveEmbed.setThumbnail(client.user.displayAvatarURL());
|
||||
} else if (msgconfig.Leave.Embed.Thumbnail === "USER") {
|
||||
leaveEmbed.setThumbnail(member.user.displayAvatarURL());
|
||||
}
|
||||
|
||||
if (msgconfig.Leave.Embed.ImageEnabled) {
|
||||
leaveEmbed.setImage(msgconfig.Leave.Embed.ImageURL);
|
||||
}
|
||||
|
||||
await leaveChannel.send({ embeds: [leaveEmbed] });
|
||||
|
||||
console.log(`[-] ${member.user.tag} left the server`);
|
||||
});
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,664 @@
|
||||
const Discord = require("discord.js");
|
||||
const axios = require("axios");
|
||||
|
||||
const supportbotai = require("../Structures/ConfigStore").supportbotAi;
|
||||
const supportbot = require("../Structures/ConfigStore").supportbot;
|
||||
|
||||
const Event = require("../Structures/Event.js");
|
||||
const AIDatabase = require("../Structures/AIDatabase.js");
|
||||
const db = require("../Structures/Database.js");
|
||||
|
||||
const openai = require("./Models/openai.js");
|
||||
const groq = require("./Models/groq.js");
|
||||
const claude = require("./Models/claude.js");
|
||||
|
||||
const PASTEBIN_API_KEY = supportbotai.General.PastebinAPI_Key;
|
||||
const PASTEBIN_API_URL = supportbotai.General.PastebinAPI_URL;
|
||||
|
||||
const CHANNELS = [supportbotai.Channels.AIChannel].filter(Boolean);
|
||||
|
||||
async function createPaste(content) {
|
||||
try {
|
||||
if (!PASTEBIN_API_KEY || !PASTEBIN_API_URL) return null;
|
||||
|
||||
const response = await axios.post(
|
||||
PASTEBIN_API_URL,
|
||||
new URLSearchParams({
|
||||
api_dev_key: PASTEBIN_API_KEY,
|
||||
api_option: "paste",
|
||||
api_paste_code: content,
|
||||
api_paste_private: 1,
|
||||
api_paste_expire_date: "1D",
|
||||
api_paste_format: "text",
|
||||
}),
|
||||
);
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error("Pastebin Error:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getProvider() {
|
||||
const provider = String(
|
||||
supportbotai.General.Provider || "openai",
|
||||
).toLowerCase();
|
||||
|
||||
switch (provider) {
|
||||
case "openai":
|
||||
return openai;
|
||||
|
||||
case "groq":
|
||||
return groq;
|
||||
|
||||
case "claude":
|
||||
return claude;
|
||||
|
||||
default:
|
||||
throw new Error(`Unsupported AI provider: ${provider}`);
|
||||
}
|
||||
}
|
||||
|
||||
function formatTicketQuestionAnswers(questionAnswers) {
|
||||
if (!Array.isArray(questionAnswers) || questionAnswers.length === 0) {
|
||||
return "No ticket question answers were saved.";
|
||||
}
|
||||
|
||||
return questionAnswers
|
||||
.map((item, index) => {
|
||||
const question = item?.question || `Question ${index + 1}`;
|
||||
const answer = item?.answer || "No answer provided";
|
||||
return `${index + 1}. ${question}\nAnswer: ${answer}`;
|
||||
})
|
||||
.join("\n\n");
|
||||
}
|
||||
|
||||
function buildTicketContext(ticket, message) {
|
||||
if (!ticket) return "";
|
||||
|
||||
return `
|
||||
Ticket Information:
|
||||
Ticket ID: ${ticket.ticket_id || ticket.id || "Unknown"}
|
||||
Subject: ${ticket.subject || ticket.reason || "No subject provided"}
|
||||
Department: ${ticket.department || "Not set"}
|
||||
Priority: ${ticket.priority || "Not set"}
|
||||
Created By: ${ticket.user_id || ticket.user || "Unknown"}
|
||||
Current User: ${message?.author?.tag || "Unknown"} (${message?.author?.id || "Unknown"})
|
||||
|
||||
Ticket Question Answers:
|
||||
${formatTicketQuestionAnswers(ticket.questionAnswers)}
|
||||
|
||||
Use this information when helping the user with their ticket.
|
||||
Do not invent missing ticket details.
|
||||
`;
|
||||
}
|
||||
|
||||
function buildSystemPrompt(message) {
|
||||
const sections = [];
|
||||
|
||||
if (Array.isArray(supportbotai.CustomInstructions)) {
|
||||
sections.push(...supportbotai.CustomInstructions);
|
||||
}
|
||||
|
||||
sections.push(
|
||||
`I am currently powered by the ${supportbotai.General.Provider || "Unknown"} provider, running the ${supportbotai.General.Model || "Unknown"} model.`,
|
||||
);
|
||||
sections.push(
|
||||
`The current guild is: ${message.guild?.name || "Unknown Guild"}`,
|
||||
);
|
||||
sections.push(
|
||||
`The current channel is: #${message.channel?.name || "unknown-channel"}`,
|
||||
);
|
||||
|
||||
return sections.join("\n- ").replace(/^/, "- ");
|
||||
}
|
||||
|
||||
function buildConversationFromMemory(currentMessage) {
|
||||
const memoryConfig = supportbotai.Memory || {};
|
||||
const memoryEnabled = memoryConfig.Enabled !== false;
|
||||
const maxRecentMessages = memoryConfig.MaxRecentMessages || 15;
|
||||
const memoryScope = String(memoryConfig.Scope || "guild").toLowerCase();
|
||||
|
||||
const conversation = [
|
||||
{
|
||||
role: "system",
|
||||
content: buildSystemPrompt(currentMessage),
|
||||
},
|
||||
];
|
||||
|
||||
if (!memoryEnabled) return conversation;
|
||||
|
||||
const recentMessages =
|
||||
typeof AIDatabase.getRecentMessagesByScope === "function"
|
||||
? AIDatabase.getRecentMessagesByScope({
|
||||
guildId: currentMessage.guild?.id || null,
|
||||
channelId: currentMessage.channel?.id || null,
|
||||
limit: maxRecentMessages,
|
||||
scope: memoryScope,
|
||||
})
|
||||
: AIDatabase.getRecentMessages(
|
||||
currentMessage.channel?.id,
|
||||
maxRecentMessages,
|
||||
);
|
||||
|
||||
const latestSummary =
|
||||
memoryConfig.StoreSummaries !== false
|
||||
? typeof AIDatabase.getLatestSummaryByScope === "function"
|
||||
? AIDatabase.getLatestSummaryByScope({
|
||||
guildId: currentMessage.guild?.id || null,
|
||||
channelId: currentMessage.channel?.id || null,
|
||||
scope: memoryScope,
|
||||
})
|
||||
: AIDatabase.getLatestSummary(currentMessage.channel?.id)
|
||||
: null;
|
||||
|
||||
const facts =
|
||||
memoryConfig.StoreFacts !== false
|
||||
? typeof AIDatabase.getFactsByScope === "function"
|
||||
? AIDatabase.getFactsByScope({
|
||||
guildId: currentMessage.guild?.id || null,
|
||||
channelId: currentMessage.channel?.id || null,
|
||||
scope: memoryScope,
|
||||
})
|
||||
: AIDatabase.getFacts(currentMessage.channel?.id)
|
||||
: [];
|
||||
|
||||
const userFacts =
|
||||
memoryConfig.StoreFacts !== false &&
|
||||
typeof AIDatabase.getUserFactsByScope === "function"
|
||||
? AIDatabase.getUserFactsByScope({
|
||||
guildId: currentMessage.guild?.id || null,
|
||||
userId: currentMessage.author?.id || null,
|
||||
scope: memoryScope,
|
||||
})
|
||||
: memoryConfig.StoreFacts !== false &&
|
||||
typeof AIDatabase.getUserFacts === "function"
|
||||
? AIDatabase.getUserFacts(
|
||||
currentMessage.guild?.id,
|
||||
currentMessage.author?.id,
|
||||
)
|
||||
: [];
|
||||
|
||||
const knowledgeBase = typeof AIDatabase.getKnowledge === "function"
|
||||
? AIDatabase.getKnowledge(25)
|
||||
: [];
|
||||
|
||||
if (latestSummary?.summary) {
|
||||
conversation.push({
|
||||
role: "system",
|
||||
content: `Shared memory summary (${memoryScope} scope):\n${latestSummary.summary}`,
|
||||
});
|
||||
}
|
||||
|
||||
if (facts.length > 0) {
|
||||
const factText = facts
|
||||
.map(
|
||||
(fact) => `- [${fact.fact_type}] ${fact.fact_key}: ${fact.fact_value}`,
|
||||
)
|
||||
.join("\n");
|
||||
|
||||
conversation.push({
|
||||
role: "system",
|
||||
content: `Known shared facts (${memoryScope} scope):\n${factText}`,
|
||||
});
|
||||
}
|
||||
|
||||
if (userFacts.length > 0) {
|
||||
const userFactText = userFacts
|
||||
.map(
|
||||
(fact) => `- [${fact.fact_type}] ${fact.fact_key}: ${fact.fact_value}`,
|
||||
)
|
||||
.join("\n");
|
||||
|
||||
conversation.push({
|
||||
role: "system",
|
||||
content: `Known facts about this user across chats:\n${userFactText}`,
|
||||
});
|
||||
}
|
||||
|
||||
if (knowledgeBase.length > 0) {
|
||||
const knowledgeText = knowledgeBase
|
||||
.map((k) => `- ${k.content}`)
|
||||
.join("\n");
|
||||
|
||||
conversation.push({
|
||||
role: "system",
|
||||
content: `Server Knowledge Base (use this to answer questions):\n${knowledgeText}`,
|
||||
});
|
||||
}
|
||||
|
||||
for (const msg of recentMessages) {
|
||||
conversation.push({
|
||||
role: msg.role === "assistant" ? "assistant" : "user",
|
||||
content: msg.content,
|
||||
});
|
||||
}
|
||||
|
||||
return conversation;
|
||||
}
|
||||
|
||||
function getTicketContextForMessage(message) {
|
||||
if (!message?.channel) return null;
|
||||
|
||||
if (typeof db.getTicketByAIChannel === "function") {
|
||||
const aiTicket = db.getTicketByAIChannel(message.channel.id);
|
||||
if (aiTicket) return aiTicket;
|
||||
}
|
||||
|
||||
if (typeof db.getTicket === "function") {
|
||||
const directTicket = db.getTicket(message.channel.id);
|
||||
if (directTicket) return directTicket;
|
||||
}
|
||||
|
||||
if (message.channel.isThread() && typeof db.getTicket === "function") {
|
||||
const parentTicket = db.getTicket(message.channel.parentId);
|
||||
if (parentTicket) return parentTicket;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function getAIMessageType() {
|
||||
return String(supportbotai.Style?.MessageType || "embed").toLowerCase();
|
||||
}
|
||||
|
||||
function shouldSuggestTicket(message, ticket) {
|
||||
if (supportbotai.Features?.SuggestTickets !== true) return false;
|
||||
if (ticket) return false;
|
||||
if (message.channel.isThread()) return false;
|
||||
return CHANNELS.includes(message.channel.id);
|
||||
}
|
||||
|
||||
function getSuggestTicketText() {
|
||||
return (
|
||||
supportbotai.Messages?.SuggestTicketText ||
|
||||
"-# Need more help? React with 🎫 to open a ticket."
|
||||
);
|
||||
}
|
||||
|
||||
async function sendAIResponse({
|
||||
client,
|
||||
message,
|
||||
replyContent,
|
||||
suggestTicket = false,
|
||||
}) {
|
||||
const messageType = getAIMessageType();
|
||||
const ticketHint = suggestTicket ? `\n\n${getSuggestTicketText()}` : "";
|
||||
|
||||
if (messageType === "text") {
|
||||
return message.reply({
|
||||
content: `${replyContent}${ticketHint}`,
|
||||
});
|
||||
}
|
||||
|
||||
if (messageType === "components_v2") {
|
||||
const {
|
||||
ContainerBuilder,
|
||||
TextDisplayBuilder,
|
||||
SeparatorBuilder,
|
||||
MessageFlags,
|
||||
} = Discord;
|
||||
|
||||
const container = new ContainerBuilder();
|
||||
|
||||
if (supportbot.Embed?.Colours?.General) {
|
||||
container.setAccentColor(
|
||||
parseInt(String(supportbot.Embed.Colours.General).replace("#", ""), 16),
|
||||
);
|
||||
}
|
||||
|
||||
const title = new TextDisplayBuilder().setContent(
|
||||
`**${supportbotai.General?.Name || "SupportBot AI"}**`,
|
||||
);
|
||||
|
||||
const body = new TextDisplayBuilder().setContent(replyContent);
|
||||
|
||||
container.addTextDisplayComponents(title);
|
||||
container.addSeparatorComponents(new SeparatorBuilder().setDivider(true));
|
||||
container.addTextDisplayComponents(body);
|
||||
|
||||
if (suggestTicket) {
|
||||
container.addSeparatorComponents(new SeparatorBuilder().setDivider(true));
|
||||
container.addTextDisplayComponents(
|
||||
new TextDisplayBuilder().setContent(getSuggestTicketText()),
|
||||
);
|
||||
}
|
||||
|
||||
return message.reply({
|
||||
flags: MessageFlags.IsComponentsV2,
|
||||
components: [container],
|
||||
});
|
||||
}
|
||||
|
||||
const embed = new Discord.EmbedBuilder()
|
||||
.setAuthor({
|
||||
name: supportbotai.General.Name,
|
||||
iconURL: client.user.displayAvatarURL(),
|
||||
})
|
||||
.setDescription(`${replyContent}${ticketHint}`)
|
||||
.setColor(supportbotai.Embed.Color)
|
||||
.setFooter({
|
||||
text: supportbotai.Embed.Footer,
|
||||
iconURL: message.author.displayAvatarURL(),
|
||||
})
|
||||
.setTimestamp();
|
||||
|
||||
return message.reply({ embeds: [embed] });
|
||||
}
|
||||
|
||||
function rememberUserFacts(message) {
|
||||
if (!message?.guild || !message?.author || !message?.content) return;
|
||||
|
||||
const content = message.content.trim();
|
||||
|
||||
const nameMatch = content.match(/^my name is\s+(.+)$/i);
|
||||
if (nameMatch && typeof AIDatabase.saveUserFact === "function") {
|
||||
AIDatabase.saveUserFact({
|
||||
guildId: message.guild.id,
|
||||
userId: message.author.id,
|
||||
factType: "profile",
|
||||
factKey: "name",
|
||||
factValue: nameMatch[1].trim(),
|
||||
});
|
||||
}
|
||||
|
||||
const nicknameMatch = content.match(/^i(?:'| a)?m\s+(.+)$/i);
|
||||
if (
|
||||
nicknameMatch &&
|
||||
nicknameMatch[1] &&
|
||||
nicknameMatch[1].length <= 32 &&
|
||||
typeof AIDatabase.saveUserFact === "function"
|
||||
) {
|
||||
AIDatabase.saveUserFact({
|
||||
guildId: message.guild.id,
|
||||
userId: message.author.id,
|
||||
factType: "profile",
|
||||
factKey: "display_name",
|
||||
factValue: nicknameMatch[1].trim(),
|
||||
});
|
||||
}
|
||||
|
||||
const usesMatch = content.match(/^i use\s+(.+)$/i);
|
||||
if (usesMatch && typeof AIDatabase.saveUserFact === "function") {
|
||||
AIDatabase.saveUserFact({
|
||||
guildId: message.guild.id,
|
||||
userId: message.author.id,
|
||||
factType: "preference",
|
||||
factKey: "uses",
|
||||
factValue: usesMatch[1].trim(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new Event("messageCreate", async (client, message) => {
|
||||
if (message.guild && typeof db.getTicket === "function") {
|
||||
const activeTicket = db.getTicket(message.channel.id);
|
||||
if (activeTicket && global.apiServer) {
|
||||
global.apiServer.broadcast("ticket_message", {
|
||||
ticketId: message.channel.id,
|
||||
message: {
|
||||
id: message.id,
|
||||
author: {
|
||||
id: message.author.id,
|
||||
username: message.author.username,
|
||||
globalName: message.author.globalName || message.author.username,
|
||||
avatar: message.author.displayAvatarURL({ extension: "png", size: 64 }),
|
||||
bot: message.author.bot
|
||||
},
|
||||
content: (() => {
|
||||
if (message.content && message.content.trim()) return message.content;
|
||||
if (Array.isArray(message.components) && message.components.length > 0) {
|
||||
const texts = [];
|
||||
for (const comp of message.components) {
|
||||
const data = typeof comp.toJSON === "function" ? comp.toJSON() : comp;
|
||||
if (data.content) texts.push(data.content);
|
||||
if (Array.isArray(data.components)) {
|
||||
for (const child of data.components) {
|
||||
const childData = typeof child.toJSON === "function" ? child.toJSON() : child;
|
||||
if (childData.content) texts.push(childData.content);
|
||||
if (childData.label && (childData.type === 10 || childData.type === "TEXT_DISPLAY")) {
|
||||
texts.push(childData.label);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (texts.length > 0) return texts.join("\n");
|
||||
}
|
||||
return message.content || "";
|
||||
})(),
|
||||
containerColor: (() => {
|
||||
if (Array.isArray(message.components)) {
|
||||
for (const comp of message.components) {
|
||||
const data = typeof comp.toJSON === "function" ? comp.toJSON() : comp;
|
||||
if (data.accent_color) {
|
||||
return `#${data.accent_color.toString(16).padStart(6, "0")}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
})(),
|
||||
embeds: (message.embeds || []).map(e => ({
|
||||
title: e.title || null,
|
||||
description: e.description || null,
|
||||
color: e.color ? `#${e.color.toString(16).padStart(6, "0")}` : "#5865F2",
|
||||
author: e.author ? { name: e.author.name, iconURL: e.author.iconURL } : null,
|
||||
fields: (e.fields || []).map(f => ({ name: f.name, value: f.value, inline: f.inline })),
|
||||
footer: e.footer ? { text: e.footer.text, iconURL: e.footer.iconURL } : null,
|
||||
thumbnail: e.thumbnail?.url ? { url: e.thumbnail.url } : null,
|
||||
image: e.image?.url ? { url: e.image.url } : null,
|
||||
timestamp: e.timestamp || null
|
||||
})),
|
||||
components: (message.components || []).map(c => {
|
||||
const data = typeof c.toJSON === "function" ? c.toJSON() : c;
|
||||
return {
|
||||
type: data.type,
|
||||
components: (data.components || []).map(sub => {
|
||||
const subData = typeof sub.toJSON === "function" ? sub.toJSON() : sub;
|
||||
return {
|
||||
type: subData.type,
|
||||
label: subData.label || subData.content || null,
|
||||
style: subData.style || null,
|
||||
url: subData.url || null,
|
||||
customId: subData.custom_id || subData.customId || null,
|
||||
emoji: subData.emoji ? (subData.emoji.name || subData.emoji.id) : null
|
||||
};
|
||||
})
|
||||
};
|
||||
}),
|
||||
attachments: Array.from((message.attachments || []).values()).map(a => ({
|
||||
id: a.id,
|
||||
name: a.name,
|
||||
url: a.url,
|
||||
contentType: a.contentType
|
||||
})),
|
||||
createdAt: message.createdTimestamp
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (message.author.bot || !message.guild) return;
|
||||
|
||||
let shouldRespond =
|
||||
CHANNELS.includes(message.channelId) ||
|
||||
message.mentions.users.has(client.user.id);
|
||||
|
||||
if (!shouldRespond && message.channel.isThread()) {
|
||||
let ticket = null;
|
||||
|
||||
if (typeof db.getTicketByAIChannel === "function") {
|
||||
ticket = db.getTicketByAIChannel(message.channel.id);
|
||||
}
|
||||
|
||||
if (!ticket && typeof db.getTicket === "function") {
|
||||
ticket = db.getTicket(message.channel.id);
|
||||
}
|
||||
|
||||
if (
|
||||
!ticket &&
|
||||
message.channel.parentId &&
|
||||
typeof db.getTicket === "function"
|
||||
) {
|
||||
ticket = db.getTicket(message.channel.parentId);
|
||||
}
|
||||
|
||||
if (
|
||||
ticket &&
|
||||
ticket.aiModeEnabled &&
|
||||
ticket.aiChannelId === message.channel.id
|
||||
) {
|
||||
shouldRespond = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!shouldRespond) return;
|
||||
|
||||
if (!supportbotai.Enabled) {
|
||||
const embed = new Discord.EmbedBuilder()
|
||||
.setAuthor({
|
||||
name: supportbotai.General.Name,
|
||||
iconURL: client.user.displayAvatarURL(),
|
||||
})
|
||||
.setDescription(
|
||||
supportbotai.Messages?.AIDisabled || "AI is currently disabled.",
|
||||
)
|
||||
.setColor(supportbot.Embed.Colours.Error)
|
||||
.setFooter({
|
||||
text: supportbotai.Embed.Footer,
|
||||
iconURL: message.author.displayAvatarURL(),
|
||||
});
|
||||
|
||||
return message.reply({ embeds: [embed] });
|
||||
}
|
||||
|
||||
const typingInterval = setInterval(() => {
|
||||
message.channel.sendTyping().catch(() => { });
|
||||
}, 5000);
|
||||
|
||||
try {
|
||||
const memoryConfig = supportbotai.Memory || {};
|
||||
const memoryEnabled = memoryConfig.Enabled !== false;
|
||||
|
||||
if (memoryEnabled) {
|
||||
AIDatabase.addMessage({
|
||||
guildId: message.guild.id,
|
||||
channelId: message.channel.id,
|
||||
userId: message.author.id,
|
||||
role: "user",
|
||||
content: message.content,
|
||||
});
|
||||
|
||||
rememberUserFacts(message);
|
||||
}
|
||||
|
||||
const ticket = getTicketContextForMessage(message);
|
||||
|
||||
const conversation = buildConversationFromMemory(message);
|
||||
|
||||
if (ticket) {
|
||||
conversation.push({
|
||||
role: "system",
|
||||
content: buildTicketContext(ticket, message),
|
||||
});
|
||||
}
|
||||
|
||||
const provider = getProvider();
|
||||
|
||||
const response = await provider.chat({
|
||||
messages: conversation,
|
||||
temperature: supportbotai.General.Temperature || 0.4,
|
||||
maxTokens: supportbotai.General.Tokens || 800,
|
||||
});
|
||||
|
||||
clearInterval(typingInterval);
|
||||
|
||||
if (!response?.content) {
|
||||
const embed = new Discord.EmbedBuilder()
|
||||
.setDescription(
|
||||
supportbotai.Messages?.ErrorResponse ||
|
||||
"I couldn't generate a response.",
|
||||
)
|
||||
.setColor(supportbotai.Embed.Color);
|
||||
|
||||
return message.reply({ embeds: [embed] });
|
||||
}
|
||||
|
||||
const replyContent = response.content;
|
||||
|
||||
if (memoryEnabled) {
|
||||
AIDatabase.addMessage({
|
||||
guildId: message.guild.id,
|
||||
channelId: message.channel.id,
|
||||
userId: client.user.id,
|
||||
role: "assistant",
|
||||
content: replyContent,
|
||||
});
|
||||
}
|
||||
|
||||
if (replyContent.length > 4000) {
|
||||
const pasteLink = await createPaste(replyContent);
|
||||
|
||||
if (pasteLink) {
|
||||
const embed = new Discord.EmbedBuilder()
|
||||
.setDescription(
|
||||
(
|
||||
supportbotai.Messages?.MessageTooLong ||
|
||||
"My response was too long, so I uploaded it here: {pastebin}"
|
||||
).replace("{pastebin}", pasteLink),
|
||||
)
|
||||
.setColor(supportbotai.Embed.Color);
|
||||
|
||||
return message.reply({ embeds: [embed] });
|
||||
}
|
||||
|
||||
return message.reply({
|
||||
content: replyContent.slice(0, 1900),
|
||||
});
|
||||
}
|
||||
|
||||
const suggestTicket = shouldSuggestTicket(message, ticket);
|
||||
|
||||
const sentMessage = await sendAIResponse({
|
||||
client,
|
||||
message,
|
||||
replyContent,
|
||||
suggestTicket: false,
|
||||
});
|
||||
|
||||
if (
|
||||
shouldSuggestTicket(message, ticket) &&
|
||||
supportbotai.Features?.SuggestTickets === true
|
||||
) {
|
||||
const hintMessage = await message.channel.send(
|
||||
supportbotai.Messages?.SuggestTicketText ||
|
||||
"Need more help? React with 🎫 to open a ticket.",
|
||||
);
|
||||
|
||||
await hintMessage
|
||||
.react(supportbotai.Messages?.SuggestTicketReaction || "🎫")
|
||||
.catch(() => { });
|
||||
|
||||
setTimeout(
|
||||
() => {
|
||||
hintMessage.delete().catch(() => { });
|
||||
},
|
||||
5 * 60 * 1000,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
clearInterval(typingInterval);
|
||||
|
||||
console.error("AI Error:", error);
|
||||
|
||||
const embed = new Discord.EmbedBuilder()
|
||||
.setDescription(
|
||||
supportbotai.Messages?.AIError ||
|
||||
supportbotai.Messages?.ErrorResponse ||
|
||||
"There was an error contacting the AI provider.",
|
||||
)
|
||||
.setColor(supportbot.Embed.Colours.Error);
|
||||
|
||||
message.reply({ embeds: [embed] }).catch(() => { });
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
const { EmbedBuilder } = require('discord.js');
|
||||
const fs = require("fs");
|
||||
const yaml = require("js-yaml");
|
||||
const Event = require("../Structures/Event.js");
|
||||
|
||||
const supportbot = require("../Structures/ConfigStore").supportbot;
|
||||
const msgconfig = require("../Structures/ConfigStore").messages;
|
||||
|
||||
module.exports = new Event("messageDelete", async (client, message) => {
|
||||
if (!message.guild) return;
|
||||
|
||||
const author = message.author || (message.partial ? await message.fetch().then(m => m.author).catch(() => null) : null);
|
||||
if (!author || author.bot) return;
|
||||
|
||||
const content = message.content || (message.attachments.size > 0 ? "Contains attachment(s)" : "*No content*");
|
||||
|
||||
const deleteLogChannel = message.guild.channels.cache.get(supportbot.MessageDelete.Channel) ||
|
||||
message.guild.channels.cache.find(c => c.name === supportbot.MessageDelete.Channel);
|
||||
|
||||
const deletedMessageEmbed = new EmbedBuilder()
|
||||
.setColor(supportbot.MessageDelete.Colour)
|
||||
.setTitle("Message Deleted")
|
||||
.setDescription(`> **Channel:** <#${message.channel.id}>\n> **Message ID:** ${message.id}\n> **Message author:** ${author.tag} (${author.id})\n> **Message Created:** <t:${Math.floor(message.createdTimestamp / 1000)}:F>`)
|
||||
.addFields({ name: "Deleted Content", value: content, inline: false })
|
||||
.setThumbnail(author.displayAvatarURL())
|
||||
.setTimestamp();
|
||||
|
||||
if (message.attachments.size > 0) {
|
||||
const attachment = message.attachments.first();
|
||||
deletedMessageEmbed.addFields({ name: "Attachment", value: `[${attachment.name}](${attachment.proxyURL})` });
|
||||
}
|
||||
|
||||
if (deleteLogChannel) {
|
||||
deleteLogChannel.send({ embeds: [deletedMessageEmbed] });
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
const supportbot = require("../Structures/ConfigStore").supportbot;
|
||||
const supportbotai = require("../Structures/ConfigStore").supportbotAi;
|
||||
const cmdconfig = require("../Structures/ConfigStore").commands;
|
||||
|
||||
const Event = require("../Structures/Event.js");
|
||||
|
||||
module.exports = new Event(
|
||||
"messageReactionAdd",
|
||||
async (client, reaction, user) => {
|
||||
try {
|
||||
if (user.bot) return;
|
||||
|
||||
if (reaction.partial) {
|
||||
await reaction.fetch().catch(() => null);
|
||||
}
|
||||
|
||||
const message = reaction.message;
|
||||
if (!message || !message.guild) return;
|
||||
|
||||
const reactionEmoji = reaction.emoji?.name || reaction.emoji?.id || null;
|
||||
const expectedReaction =
|
||||
supportbotai.Messages?.SuggestTicketReaction || "🎫";
|
||||
|
||||
if (reactionEmoji !== expectedReaction) return;
|
||||
if (supportbotai.Features?.SuggestTickets !== true) return;
|
||||
if (message.author?.id !== client.user.id) return;
|
||||
|
||||
const expectedText =
|
||||
supportbotai.Messages?.SuggestTicketText ||
|
||||
"Need more help? React with 🎫 to open a ticket.";
|
||||
|
||||
if ((message.content || "").trim() !== expectedText.trim()) return;
|
||||
|
||||
await message.delete().catch(() => {});
|
||||
|
||||
const cmd = client.commands.get(cmdconfig.OpenTicket.Command);
|
||||
if (!cmd) {
|
||||
console.error("Open ticket command not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
const member = await message.guild.members
|
||||
.fetch(user.id)
|
||||
.catch(() => null);
|
||||
|
||||
if (!member) return;
|
||||
|
||||
const fakeInteraction = {
|
||||
user,
|
||||
member,
|
||||
guild: message.guild,
|
||||
channel: message.channel,
|
||||
client,
|
||||
replied: true,
|
||||
deferred: false,
|
||||
reason: `Opened from AI suggestion in #${message.channel.name}`,
|
||||
options: {
|
||||
getString: () => null,
|
||||
},
|
||||
reply: async () => null,
|
||||
};
|
||||
|
||||
await cmd.run(fakeInteraction);
|
||||
} catch (error) {
|
||||
console.error("Error handling AI ticket reaction:", error);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,57 @@
|
||||
const { EmbedBuilder } = require('discord.js');
|
||||
const fs = require("fs");
|
||||
const yaml = require("js-yaml");
|
||||
const Event = require("../Structures/Event.js");
|
||||
|
||||
// Load the configuration files
|
||||
const supportbot = require("../Structures/ConfigStore").supportbot;
|
||||
const msgconfig = require("../Structures/ConfigStore").messages;
|
||||
|
||||
module.exports = new Event("messageUpdate", async (client, oldMessage, newMessage) => {
|
||||
|
||||
// Check if the message is from a guild and not from a bot
|
||||
if (!newMessage.guild || newMessage.author.bot) return;
|
||||
|
||||
// Fetch old message if it's not cached (important for message content)
|
||||
if (!oldMessage.content && oldMessage.partial) {
|
||||
try {
|
||||
oldMessage = await oldMessage.fetch();
|
||||
} catch (err) {
|
||||
console.error('Error fetching old message:', err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback if content is missing
|
||||
const oldMessageContent = oldMessage.content || (oldMessage.attachments.size > 0 ? 'Contains attachment(s)' : '*No content*');
|
||||
const newMessageContent = newMessage.content || (newMessage.attachments.size > 0 ? 'Contains attachment(s)' : '*No content*');
|
||||
|
||||
// Get the update log channel either by ID or by name
|
||||
const updateLogChannel = newMessage.guild.channels.cache.get(supportbot.MessageUpdate.Channel) ||
|
||||
newMessage.guild.channels.cache.find(channel => channel.name === supportbot.MessageUpdate.Channel);
|
||||
|
||||
// Create the embed for the updated message
|
||||
const updatedMessageEmbed = new EmbedBuilder()
|
||||
.setColor(supportbot.MessageUpdate.Colour)
|
||||
.setTitle("Message Updated")
|
||||
.setDescription(`> **Channel:** <#${newMessage.channel.id}>\n> **Message ID:** [${newMessage.id}](https://discord.com/channels/${newMessage.guild.id}/${newMessage.channel.id}/${newMessage.id})\n> **Message author:** ${newMessage.author.tag} (${newMessage.author.id})\n> **Message Created:** <t:${Math.floor(oldMessage.createdTimestamp / 1000)}:F>`)
|
||||
.addFields(
|
||||
{ name: 'Old Message', value: oldMessageContent, inline: false },
|
||||
{ name: 'New Message', value: newMessageContent, inline: false }
|
||||
)
|
||||
.setThumbnail(newMessage.author.displayAvatarURL())
|
||||
.setTimestamp();
|
||||
|
||||
// If the new message contains an attachment, add it to the embed
|
||||
if (newMessage.attachments.size > 0) {
|
||||
const attachment = newMessage.attachments.first();
|
||||
updatedMessageEmbed.addFields({ name: 'Attachment', value: `[${attachment.name}](${attachment.proxyURL})` });
|
||||
}
|
||||
|
||||
// Send the embed to the update log channel if the channel is found
|
||||
if (updateLogChannel) {
|
||||
updateLogChannel.send({
|
||||
embeds: [updatedMessageEmbed]
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
const Event = require("../Structures/Event.js");
|
||||
const { runBotReady } = require("../Structures/BotStartup.js");
|
||||
|
||||
module.exports = new Event("clientReady", async (client) => {
|
||||
// restartDiscordBot runs startup manually — clientReady often does not re-fire anyway
|
||||
if (client.__explicitStartup) return;
|
||||
await runBotReady(client, { clearConsole: true, logCommands: false });
|
||||
});
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Emerald Services
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,95 +1,93 @@
|
||||
<h1 align="center">SupportBot v3.4</h1>
|
||||
<h4 align="center">A Self Hosted discord application</h4>
|
||||
<h1 align="center">
|
||||
<br>
|
||||
<a href="https://github.com/Emerald-Services/SupportBot"><img src="https://cdn.1tzemerald.space/Emerald%20Services/SBThreadHeader.png"></a>
|
||||
<br>
|
||||
SupportBot
|
||||
<br>
|
||||
</h1>
|
||||
|
||||
<div align="center">
|
||||
|
||||
<a href="https://opensource.org/licenses/MIT">
|
||||
<img src="https://img.shields.io/badge/license-MIT-green?style=for-the-badge" alt="shield.png">
|
||||
</a>
|
||||
|
||||
<a href="https://github.com/discordjs">
|
||||
<img src="https://img.shields.io/badge/DISCORD.JS%20-%20v14-blue?style=for-the-badge" alt="shield.png">
|
||||
</a>
|
||||
|
||||
<a href="https://github.com/nodejs/node">
|
||||
<img src="https://img.shields.io/badge/NODE.JS%20-%20v20.x-green?style=for-the-badge" alt="shield.png">
|
||||
</a>
|
||||
|
||||
<a href="https://github.com/Emerald-Services/SupportBot/releases">
|
||||
<img src="https://img.shields.io/github/downloads/Emerald-Services/SupportBot/total?style=for-the-badge" alt="shield.png">
|
||||
</a>
|
||||
|
||||
## Supported Versions
|
||||
|
||||
| SupportBot | Node.JS | Discord.JS | Supported |
|
||||
|------------|---------|------------|------------|
|
||||
| 26.x | 22.x | 14.x. | ✅ Yes
|
||||
| 8.x | 22.x | 14.x. | ❌ Yes
|
||||
| 7.7.3 | 16.x | 13.x. | ❌ No
|
||||
| 7.x | 16.x | 13.x. | ❌ No
|
||||
| 6.x | 14.x | 13.x. | ❌ No
|
||||
| 5.x | 12.x | 13.x. | ❌ No
|
||||
| 4.x | 11.x | 13.x. | ❌ No
|
||||
| Older | 10.x | 13.x. | ❌ No
|
||||
|
||||
</div>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://discord.gg/hmWgxdF">
|
||||
<img src="https://discordapp.com/api/guilds/516345301985132552/widget.png?style=shield" alt="Discord Server">
|
||||
</a>
|
||||
<img src="https://img.shields.io/badge/Made%20with-NodeJS-blue.svg" alt="Made with NodeJS">
|
||||
<a href="https://emeraldsrv.dev/">Website</a>
|
||||
•
|
||||
<a href="https://github.com/Emerald-Services/SupportBot/wiki">Wiki</a>
|
||||
<br>
|
||||
<a href="#about">About</a>
|
||||
•
|
||||
<a href="#support">Support</a>
|
||||
•
|
||||
<a href="#license">License</a>
|
||||
•
|
||||
<a href="https://github.com/Emerald-Services/SupportBot/wiki/Third-Party-Developer">Third-Party</a>
|
||||
•
|
||||
<a href="#contributors">Contributors</a>
|
||||
</p>
|
||||
|
||||
SupportBot is a free & open source discord bot for your server that has the capability to control & manage your servers support.
|
||||
## About
|
||||
|
||||
| Version | Supported | Type |
|
||||
| ------- | ------------------ |--------------------|
|
||||
| 1.0.x | ❌ | None |
|
||||
| 2.0.x | ⚠ | Community |
|
||||
| 3.0.x | ✅ | Priority |
|
||||
| 4.0.x | ❌ | N/A |
|
||||
| Premium | ❌ | N/A |
|
||||
SupportBot is an Open-Source discord bot built with [discord.js](https://github.com/discordjs/discord.js). It's primarily focused to help servers organise and manage their support system in a unique way with full customisation to match your brand.
|
||||
|
||||
# Getting Started
|
||||
Firstly you will need to download the resource, Once you have downloaded the resource open a new tab in your browser and go to https://discordapp.com/developers/applications/. Create a new application and then open your app and select bot and then create your bot. Copy the bot token and paste it where it says ``"token": "BOT TOKEN HERE",`` in the ``settings.json`` file.
|
||||
## Support
|
||||
|
||||
# Invite the bot
|
||||
Head back to discord developers portal where your application is and head into the ``OAuth`` tab and select bot and then below select ``Administrator`` then copy the link and paste into your browser and it should as your to invite the bot to a server.
|
||||
<a href="https://emeraldsrv.dev/discord">
|
||||
<img alt="Discord" src="https://img.shields.io/discord/597779408295821323?logo=discord&logoColor=white&style=for-the-badge">
|
||||
</a>
|
||||
|
||||
# Configurating SupportBot
|
||||
Once you have setup your discord application such as invited the bot and added your bot token to the ``settings.json`` we can begin configurating your bot. Firstly open up the ``settings.json`` file and you will see loads of different things to config. You will see the following below:
|
||||
|
||||
```
|
||||
"prefix": "!",
|
||||
"botname": "SupportBot",
|
||||
"colour": "#42f477",
|
||||
"footer": "Support Bot, By Emerald",
|
||||
## License
|
||||
|
||||
"BotStatus": "Support Bot",.
|
||||
Released under the [MIT](https://opensource.org/licenses/MIT) license.
|
||||
|
||||
"staff": "Staff",
|
||||
"support": "Support Team",
|
||||
"everyone": "@everyone",
|
||||
"Auto_Role": "Member",
|
||||
|
||||
"category": "Tickets",
|
||||
"Ticket_Message": "Your support ticket has been opened successfully\nPlease allow us some time to reach out to you.",
|
||||
"Ticket_Logs": "ticket-logs",
|
||||
## Third-Party
|
||||
|
||||
"Announcement_Title": "SupportBot Announcement",
|
||||
"Announcement_Channel": "announcements",
|
||||
- [Verified Addons](https://emeraldsrv.dev)
|
||||
- [BuiltByBit]([https://builtbybit.com/supportbot](https://builtbybit.com/resources/supportbot-advanced-ticket-bot.8940/))
|
||||
- [Voxel](https://polymart.org/resource/supportbot-1-discord-ticket-bot.518)
|
||||
|
||||
"Welcome_Message": "Welcome to the server!",
|
||||
"Welcome_Channel": "welcome",
|
||||
|
||||
"Leave_Message": "Thank you for visiting us!",
|
||||
"Leave_Channel": "welcome",
|
||||
## Contributors
|
||||
|
||||
"Poll_Channel": "polls",
|
||||
"Poll_Title": "📊 Polls",
|
||||
"Reaction_Poll_1": "✅",
|
||||
"Reaction_Poll_2": "❎",
|
||||
<a href="https://github.com/Emerald-Services/SupportBot/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=Emerald-Services/SupportBot">
|
||||
</a>
|
||||
|
||||
"Suggestion_Title": "SupportBot Suggestions",
|
||||
"Suggestion_Channel": "suggestions",
|
||||
"suggestyes": "✅",
|
||||
"suggestno": "❎",
|
||||
## Hosting
|
||||
|
||||
"LINK_1_NAME": "Discord",
|
||||
"LINK_2_NAME": "Twitter",
|
||||
"LINK_3_NAME": "Website",
|
||||
|
||||
"LINK_1": "https://dicord.gg/changemeinconfig",
|
||||
"LINK_2": "https://twitter.com/changemeinconfig",
|
||||
"LINK_3": "https://www.example.com/",
|
||||
|
||||
"LINKS_TITLE": "SupportBot Links",
|
||||
|
||||
"Ticket_Command": "ticket",
|
||||
"Suggest_Command": "suggest",
|
||||
"Say_Command": "say",
|
||||
"Remove_Command": "remove",
|
||||
"Add_Command": "add",
|
||||
"Ping_Command": "ping",
|
||||
"Close_Command": "close",
|
||||
"Forceclose_Command": "forceclose",
|
||||
"Rename_Command": "rename",
|
||||
"Help_Command": "help",
|
||||
"Link_Command": "links",
|
||||
"Lockchat_Command": "lock",
|
||||
"UnLockchat_Command": "unlock",
|
||||
"Poll_Command": "poll"
|
||||
```
|
||||
|
||||
# Contributors List
|
||||
|
||||
# Donators List
|
||||
You can use our [Pterodactyl egg](https://github.com/Emerald-Services/Pterodactyl-Egg) to host it on your own panel. We currently don't offer hosting for you, altough we might add this in the future.
|
||||
If you don't know how to install pterodactyl, you could always host it on replit:
|
||||
|
||||
[]([https://repl.it/github/ItsNyoty/iki](https://repl.it/github/Emerald-Services/SupportBot
|
||||
)
|
||||
@@ -0,0 +1,167 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const DB_PATH = path.join(__dirname, '../Data/ai-memory.json');
|
||||
|
||||
function load() {
|
||||
try {
|
||||
const raw = fs.readFileSync(DB_PATH, 'utf8');
|
||||
return JSON.parse(raw);
|
||||
} catch (e) {
|
||||
return {
|
||||
ai_conversations: [],
|
||||
ai_summaries: [],
|
||||
ai_facts: [],
|
||||
ai_knowledge: [],
|
||||
ai_sessions: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function save(state) {
|
||||
try {
|
||||
fs.mkdirSync(path.dirname(DB_PATH), { recursive: true });
|
||||
fs.writeFileSync(DB_PATH, JSON.stringify(state, null, 2), 'utf8');
|
||||
} catch (e) {
|
||||
console.error('Failed to persist ai-memory:', e);
|
||||
}
|
||||
}
|
||||
|
||||
const state = load();
|
||||
|
||||
function nextId(arr) {
|
||||
return arr.length === 0 ? 1 : Math.max(...arr.map(a => a.id || 0)) + 1;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
saveUserFact({ guildId, userId, factType, factKey, factValue }) {
|
||||
const existing = state.ai_facts.find(f => f.guild_id === (guildId || null) && f.user_id === (userId || null) && f.fact_type === factType && f.fact_key === factKey);
|
||||
if (existing) {
|
||||
existing.fact_value = factValue;
|
||||
existing.updated_at = Date.now();
|
||||
save(state);
|
||||
return;
|
||||
}
|
||||
|
||||
const id = nextId(state.ai_facts);
|
||||
state.ai_facts.push({ id, guild_id: guildId || null, channel_id: null, user_id: userId || null, fact_type: factType, fact_key: factKey, fact_value: factValue, created_at: Date.now(), updated_at: Date.now() });
|
||||
save(state);
|
||||
},
|
||||
|
||||
getUserFacts(guildId, userId) {
|
||||
return state.ai_facts.filter(f => f.guild_id === (guildId || null) && f.user_id === (userId || null)).sort((a,b) => b.updated_at - a.updated_at);
|
||||
},
|
||||
|
||||
getUserFactsByScope({ guildId = null, userId = null, scope = "guild" }) {
|
||||
if (!userId) return [];
|
||||
if (scope === "global") {
|
||||
return state.ai_facts.filter(f => f.user_id === (userId || null)).sort((a,b) => b.updated_at - a.updated_at);
|
||||
}
|
||||
|
||||
return state.ai_facts.filter(f => f.guild_id === (guildId || null) && f.user_id === (userId || null)).sort((a,b) => b.updated_at - a.updated_at);
|
||||
},
|
||||
|
||||
addMessage({ guildId, channelId, userId, role, content }) {
|
||||
const id = nextId(state.ai_conversations);
|
||||
state.ai_conversations.push({ id, guild_id: guildId || null, channel_id: channelId || null, user_id: userId || null, role, content, created_at: Date.now() });
|
||||
save(state);
|
||||
},
|
||||
|
||||
getRecentMessages(channelId, limit = 15) {
|
||||
const msgs = state.ai_conversations.filter(m => m.channel_id === channelId).sort((a,b) => b.created_at - a.created_at).slice(0, limit).reverse();
|
||||
return msgs;
|
||||
},
|
||||
|
||||
getRecentMessagesByScope({ guildId = null, channelId = null, limit = 15, scope = "guild" }) {
|
||||
if (scope === "channel") {
|
||||
return state.ai_conversations.filter(m => m.channel_id === (channelId || null)).sort((a,b) => b.created_at - a.created_at).slice(0, limit).reverse();
|
||||
}
|
||||
|
||||
if (scope === "global") {
|
||||
return state.ai_conversations.sort((a,b) => b.created_at - a.created_at).slice(0, limit).reverse();
|
||||
}
|
||||
|
||||
return state.ai_conversations.filter(m => m.guild_id === (guildId || null)).sort((a,b) => b.created_at - a.created_at).slice(0, limit).reverse();
|
||||
},
|
||||
|
||||
saveSummary({ guildId, channelId, summary }) {
|
||||
const existing = state.ai_summaries.find(s => s.guild_id === (guildId || null) && s.channel_id === (channelId || null));
|
||||
if (existing) {
|
||||
existing.summary = summary;
|
||||
existing.updated_at = Date.now();
|
||||
save(state);
|
||||
return;
|
||||
}
|
||||
const id = nextId(state.ai_summaries);
|
||||
state.ai_summaries.push({ id, guild_id: guildId || null, channel_id: channelId || null, summary, created_at: Date.now(), updated_at: Date.now() });
|
||||
save(state);
|
||||
},
|
||||
|
||||
getLatestSummary(channelId) {
|
||||
const arr = state.ai_summaries.filter(s => s.channel_id === channelId).sort((a,b) => b.updated_at - a.updated_at);
|
||||
return arr.length ? arr[0] : null;
|
||||
},
|
||||
|
||||
getLatestSummaryByScope({ guildId = null, channelId = null, scope = "guild" }) {
|
||||
if (scope === "channel") {
|
||||
const arr = state.ai_summaries.filter(s => s.channel_id === (channelId || null)).sort((a,b) => b.updated_at - a.updated_at);
|
||||
return arr.length ? arr[0] : null;
|
||||
}
|
||||
if (scope === "global") {
|
||||
const arr = state.ai_summaries.sort((a,b) => b.updated_at - a.updated_at);
|
||||
return arr.length ? arr[0] : null;
|
||||
}
|
||||
|
||||
const arr = state.ai_summaries.filter(s => s.guild_id === (guildId || null)).sort((a,b) => b.updated_at - a.updated_at);
|
||||
return arr.length ? arr[0] : null;
|
||||
},
|
||||
|
||||
saveFact({ guildId, channelId, factType, factKey, factValue }) {
|
||||
const existing = state.ai_facts.find(f => f.guild_id === (guildId || null) && f.channel_id === (channelId || null) && f.fact_type === factType && f.fact_key === factKey);
|
||||
if (existing) {
|
||||
existing.fact_value = factValue;
|
||||
existing.updated_at = Date.now();
|
||||
save(state);
|
||||
return;
|
||||
}
|
||||
const id = nextId(state.ai_facts);
|
||||
state.ai_facts.push({ id, guild_id: guildId || null, channel_id: channelId || null, user_id: null, fact_type: factType, fact_key: factKey, fact_value: factValue, created_at: Date.now(), updated_at: Date.now() });
|
||||
save(state);
|
||||
},
|
||||
|
||||
getFacts(channelId) {
|
||||
return state.ai_facts.filter(f => f.channel_id === channelId).sort((a,b) => b.updated_at - a.updated_at);
|
||||
},
|
||||
|
||||
getFactsByScope({ guildId = null, channelId = null, scope = "guild" }) {
|
||||
if (scope === "channel") {
|
||||
return state.ai_facts.filter(f => f.channel_id === (channelId || null) && f.user_id == null).sort((a,b) => b.updated_at - a.updated_at);
|
||||
}
|
||||
if (scope === "global") {
|
||||
return state.ai_facts.filter(f => f.user_id == null).sort((a,b) => b.updated_at - a.updated_at);
|
||||
}
|
||||
|
||||
return state.ai_facts.filter(f => f.guild_id === (guildId || null) && f.user_id == null).sort((a,b) => b.updated_at - a.updated_at);
|
||||
},
|
||||
|
||||
addKnowledge({ sourceType, sourceName, title, content }) {
|
||||
const id = nextId(state.ai_knowledge);
|
||||
state.ai_knowledge.push({ id, source_type: sourceType, source_name: sourceName, title, content, created_at: Date.now(), updated_at: Date.now() });
|
||||
save(state);
|
||||
},
|
||||
|
||||
getKnowledge(limit = 25) {
|
||||
return state.ai_knowledge.sort((a,b) => b.updated_at - a.updated_at).slice(0, limit);
|
||||
},
|
||||
|
||||
clearKnowledge() {
|
||||
state.ai_knowledge = [];
|
||||
save(state);
|
||||
},
|
||||
|
||||
clearConversations(channelId) {
|
||||
if (!channelId) return;
|
||||
state.ai_conversations = state.ai_conversations.filter(c => c.channel_id !== channelId);
|
||||
save(state);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
|
||||
// SupportBot | Emerald Services
|
||||
// Addon Structure
|
||||
class Command {
|
||||
constructor(options) {
|
||||
this.name = options.name;
|
||||
this.description = options.description;
|
||||
this.permissions = options.permissions;
|
||||
this.options = options.options || [];
|
||||
this.run = options.run;
|
||||
this.defaultPermission = true;
|
||||
}
|
||||
|
||||
// Method to dynamically update options
|
||||
setOptions(options) {
|
||||
this.options = options;
|
||||
}
|
||||
}
|
||||
|
||||
class Event {
|
||||
constructor(event, run) {
|
||||
this.event = event;
|
||||
this.run = run;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { Command, Event };
|
||||
@@ -0,0 +1,185 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const axios = require("axios");
|
||||
const { withTimeout } = require("./asyncTimeout.js");
|
||||
const AdmZip = require("adm-zip");
|
||||
const yaml = require("js-yaml");
|
||||
|
||||
const EMERALD_API = "https://emeraldsrv.dev/api/v1";
|
||||
|
||||
function getApiKey() {
|
||||
try {
|
||||
if (fs.existsSync("./Configs/api.yml")) {
|
||||
const config = yaml.load(fs.readFileSync("./Configs/api.yml", "utf8"));
|
||||
return config?.API?.EmeraldAPIKey || "";
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to read api.yml:", e);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function ensureAddonDirs() {
|
||||
if (!fs.existsSync("./Addons")) {
|
||||
fs.mkdirSync("./Addons", { recursive: true });
|
||||
}
|
||||
if (!fs.existsSync("./Addons/Configs")) {
|
||||
fs.mkdirSync("./Addons/Configs", { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
async function listCatalogAddons() {
|
||||
const key = getApiKey();
|
||||
if (!key) throw new Error("EmeraldAPIKey not configured in api.yml");
|
||||
|
||||
const res = await withTimeout(
|
||||
axios.get(`${EMERALD_API}/resources`, {
|
||||
headers: { Authorization: `Bearer ${key}` },
|
||||
timeout: 15_000,
|
||||
}),
|
||||
18_000,
|
||||
"Emerald API request timed out",
|
||||
);
|
||||
|
||||
if (res.data.status !== "success") {
|
||||
throw new Error(res.data.message || "Failed to fetch catalog");
|
||||
}
|
||||
|
||||
return res.data.data.map(item => ({
|
||||
id: item.id.toString(),
|
||||
name: item.title,
|
||||
slug: item.slug,
|
||||
description: `Emerald Services Resource - ${item.slug}`,
|
||||
price: item.price,
|
||||
is_external: item.is_external,
|
||||
external_store: item.external_store,
|
||||
repositoryUrl: item.external_url || `https://emeraldsrv.dev`,
|
||||
jsFiles: [],
|
||||
configFiles: []
|
||||
})).sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
|
||||
async function downloadFile(id) {
|
||||
const key = getApiKey();
|
||||
const res = await withTimeout(
|
||||
axios.get(`${EMERALD_API}/download?id=${id}`, {
|
||||
headers: { Authorization: `Bearer ${key}` },
|
||||
responseType: "arraybuffer",
|
||||
timeout: 30_000
|
||||
}),
|
||||
35_000,
|
||||
"Addon file download timed out",
|
||||
);
|
||||
return Buffer.from(res.data);
|
||||
}
|
||||
|
||||
function listLocalAddonJs() {
|
||||
ensureAddonDirs();
|
||||
return fs
|
||||
.readdirSync("./Addons")
|
||||
.filter((f) => f.endsWith(".js") && f !== "addon.js");
|
||||
}
|
||||
|
||||
function listLocalAddonConfigs() {
|
||||
ensureAddonDirs();
|
||||
const dir = "./Addons/Configs";
|
||||
if (!fs.existsSync(dir)) return [];
|
||||
return fs
|
||||
.readdirSync(dir)
|
||||
.filter((f) => f.endsWith(".yml") || f.endsWith(".yaml") || f.endsWith(".json"));
|
||||
}
|
||||
|
||||
function isAddonInstalled(catalogEntry) {
|
||||
const localJs = listLocalAddonJs();
|
||||
return localJs.some(f => f.includes(catalogEntry.slug) || f.includes(catalogEntry.id.toString()) || f.includes(catalogEntry.name.replace(/ /g, '')));
|
||||
}
|
||||
|
||||
async function listInstalledAddons(catalog) {
|
||||
const localJs = listLocalAddonJs();
|
||||
const installed = [];
|
||||
|
||||
for (const file of localJs) {
|
||||
const name = file.replace(/\.js$/, "");
|
||||
const match = catalog.find(c => c.slug === name || c.id.toString() === name || c.name.replace(/ /g, '') === name);
|
||||
|
||||
installed.push({
|
||||
id: match ? match.id : name,
|
||||
name: match ? match.name : name,
|
||||
jsFiles: [file],
|
||||
configFiles: listLocalAddonConfigs().filter(f => f.startsWith(name)),
|
||||
custom: !match
|
||||
});
|
||||
}
|
||||
return installed;
|
||||
}
|
||||
|
||||
async function installAddonFromCatalog(addonId) {
|
||||
const catalog = await listCatalogAddons();
|
||||
const addon = catalog.find(c => c.id === addonId.toString());
|
||||
|
||||
if (!addon) {
|
||||
throw new Error(`Addon not found in catalog`);
|
||||
}
|
||||
|
||||
ensureAddonDirs();
|
||||
const content = await downloadFile(addon.id);
|
||||
|
||||
const zip = new AdmZip(content);
|
||||
zip.extractAllTo("./Addons", true);
|
||||
|
||||
return {
|
||||
addon: addon,
|
||||
filesWritten: [],
|
||||
};
|
||||
}
|
||||
|
||||
function readAddonConfig(filename) {
|
||||
const safe = path.basename(filename);
|
||||
if (!/^[a-zA-Z0-9_.-]+\.(yml|yaml|json)$/.test(safe)) {
|
||||
throw new Error("Invalid config filename");
|
||||
}
|
||||
|
||||
const filePath = path.join("./Addons/Configs", safe);
|
||||
if (!fs.existsSync(filePath)) {
|
||||
throw new Error("Addon config not found");
|
||||
}
|
||||
|
||||
const raw = fs.readFileSync(filePath, "utf8");
|
||||
if (safe.endsWith(".json")) {
|
||||
return { filename: safe, format: "json", data: JSON.parse(raw), raw };
|
||||
}
|
||||
|
||||
const yaml = require("js-yaml");
|
||||
return { filename: safe, format: "yaml", data: yaml.load(raw) || {}, raw };
|
||||
}
|
||||
|
||||
function writeAddonConfig(filename, data) {
|
||||
const safe = path.basename(filename);
|
||||
if (!/^[a-zA-Z0-9_.-]+\.(yml|yaml|json)$/.test(safe)) {
|
||||
throw new Error("Invalid config filename");
|
||||
}
|
||||
|
||||
ensureAddonDirs();
|
||||
const filePath = path.join("./Addons/Configs", safe);
|
||||
let content;
|
||||
|
||||
if (safe.endsWith(".json")) {
|
||||
content = `${JSON.stringify(data, null, 2)}\n`;
|
||||
} else {
|
||||
const yaml = require("js-yaml");
|
||||
content = yaml.dump(data, { lineWidth: 120, noRefs: true });
|
||||
}
|
||||
|
||||
fs.writeFileSync(filePath, content);
|
||||
return { filename: safe, path: filePath.replace(/\\/g, "/") };
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
listCatalogAddons,
|
||||
listInstalledAddons,
|
||||
isAddonInstalled,
|
||||
installAddonFromCatalog,
|
||||
listLocalAddonConfigs,
|
||||
readAddonConfig,
|
||||
writeAddonConfig,
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
function getBotInviteUrl(clientId) {
|
||||
if (!clientId) return null;
|
||||
return `https://discord.com/api/oauth2/authorize?client_id=${clientId}&permissions=8&scope=bot%20applications.commands`;
|
||||
}
|
||||
|
||||
module.exports = { getBotInviteUrl };
|
||||
@@ -0,0 +1,150 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const configStore = require("./ConfigStore.js");
|
||||
const { getPrimaryGuild } = require("./GuildManager.js");
|
||||
const {
|
||||
applyPresence,
|
||||
runBotReady,
|
||||
logSlashCommandsRegistered,
|
||||
} = require("./BotStartup.js");
|
||||
|
||||
function walk(dir, ext = ".js", fileList = []) {
|
||||
if (!fs.existsSync(dir)) return fileList;
|
||||
for (const file of fs.readdirSync(dir)) {
|
||||
const full = path.join(dir, file);
|
||||
if (fs.statSync(full).isDirectory()) {
|
||||
walk(full, ext, fileList);
|
||||
} else if (file.endsWith(ext)) {
|
||||
fileList.push(full.replace(/\\/g, "/"));
|
||||
}
|
||||
}
|
||||
return fileList;
|
||||
}
|
||||
|
||||
async function refreshSlashCommands(client) {
|
||||
if (!client?.application) return { count: 0 };
|
||||
|
||||
const cmdconfig = configStore.commands;
|
||||
const commandFiles = walk("./Commands");
|
||||
client.commands.clear();
|
||||
|
||||
for (const file of commandFiles) {
|
||||
const resolved = path.resolve(file);
|
||||
if (require.cache[resolved]) {
|
||||
delete require.cache[resolved];
|
||||
}
|
||||
}
|
||||
|
||||
let count = 0;
|
||||
for (const file of commandFiles) {
|
||||
const command = require(path.resolve(file));
|
||||
const matchingConfig = Object.values(cmdconfig).find(
|
||||
(entry) =>
|
||||
entry &&
|
||||
typeof entry === "object" &&
|
||||
entry.Command &&
|
||||
String(entry.Command).toLowerCase() ===
|
||||
String(command.name).toLowerCase(),
|
||||
);
|
||||
|
||||
if (matchingConfig && matchingConfig.Enabled === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
client.commands.set(command.name, command);
|
||||
count++;
|
||||
}
|
||||
|
||||
const payload = client.commands.map((cmd) => cmd.data ?? cmd);
|
||||
for (const guild of client.guilds.cache.values()) {
|
||||
await guild.commands.set(payload);
|
||||
}
|
||||
|
||||
return { count };
|
||||
}
|
||||
|
||||
async function reloadBot(client, options = {}) {
|
||||
const { fileKey, refreshCommands = true } = options;
|
||||
|
||||
let reloaded;
|
||||
if (fileKey) {
|
||||
configStore.reloadFile(fileKey);
|
||||
reloaded = [fileKey];
|
||||
} else {
|
||||
reloaded = configStore.reloadBotConfigs();
|
||||
}
|
||||
|
||||
applyPresence(client);
|
||||
|
||||
let commands = null;
|
||||
if (refreshCommands && (!fileKey || fileKey === "commands")) {
|
||||
commands = await refreshSlashCommands(client);
|
||||
}
|
||||
|
||||
return {
|
||||
reloaded,
|
||||
commands,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Reload configs from disk, reconnect the Discord session, and refresh presence/commands.
|
||||
* API and dashboard keep running on the same process.
|
||||
*/
|
||||
async function restartDiscordBot(client) {
|
||||
if (!client) {
|
||||
const err = new Error("Bot client not available");
|
||||
err.code = "NO_CLIENT";
|
||||
throw err;
|
||||
}
|
||||
|
||||
const reloaded = configStore.reloadBotConfigs();
|
||||
const token = configStore.supportbot?.General?.Token;
|
||||
|
||||
if (!token || String(token).trim() === "" || token === "BOT_TOKEN") {
|
||||
const err = new Error("Invalid or missing bot token in supportbot.yml");
|
||||
err.code = "INVALID_TOKEN";
|
||||
throw err;
|
||||
}
|
||||
|
||||
console.log("[BotReload] Restarting Discord bot after config change…");
|
||||
client.__restarting = true;
|
||||
|
||||
if (client.user) {
|
||||
try {
|
||||
await client.destroy();
|
||||
} catch (destroyErr) {
|
||||
console.warn("[BotReload] destroy:", destroyErr.message);
|
||||
}
|
||||
}
|
||||
|
||||
client.__explicitStartup = true;
|
||||
try {
|
||||
try {
|
||||
await client.login(token);
|
||||
} catch (loginErr) {
|
||||
console.error("[BotReload] login failed:", loginErr.message);
|
||||
const err = new Error(`Could not start bot: ${loginErr.message}`);
|
||||
err.code = "LOGIN_FAILED";
|
||||
throw err;
|
||||
}
|
||||
|
||||
// clientReady does not fire again after destroy()+login() — run startup explicitly
|
||||
await runBotReady(client, { clearConsole: true, logCommands: false });
|
||||
const commands = await refreshSlashCommands(client);
|
||||
logSlashCommandsRegistered(client);
|
||||
|
||||
console.log("[BotReload] Bot restarted successfully");
|
||||
return { restarted: true, reloaded, commands };
|
||||
} finally {
|
||||
client.__explicitStartup = false;
|
||||
client.__restarting = false;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
applyPresence,
|
||||
refreshSlashCommands,
|
||||
reloadBot,
|
||||
restartDiscordBot,
|
||||
};
|
||||
@@ -0,0 +1,200 @@
|
||||
const Discord = require("discord.js");
|
||||
const configStore = require("./ConfigStore.js");
|
||||
const {
|
||||
getPrimaryGuild,
|
||||
syncGuildHealth,
|
||||
} = require("./GuildManager.js");
|
||||
|
||||
const ACTIVITY_TYPES = {
|
||||
PLAYING: Discord.ActivityType.Playing,
|
||||
WATCHING: Discord.ActivityType.Watching,
|
||||
LISTENING: Discord.ActivityType.Listening,
|
||||
COMPETING: Discord.ActivityType.Competing,
|
||||
STREAMING: Discord.ActivityType.Streaming,
|
||||
};
|
||||
|
||||
function applyPresence(client) {
|
||||
if (!client?.user) return;
|
||||
|
||||
const activity = configStore.supportbot?.Activity;
|
||||
const typeKey = activity?.Type?.toUpperCase();
|
||||
const type = ACTIVITY_TYPES[typeKey] ?? Discord.ActivityType.Playing;
|
||||
const name = activity?.Status || "Online";
|
||||
|
||||
const activityPayload = { name, type };
|
||||
|
||||
if (typeKey === "STREAMING" && activity?.StreamingURL) {
|
||||
activityPayload.url = activity.StreamingURL;
|
||||
}
|
||||
|
||||
client.user.setPresence({
|
||||
activities: [activityPayload],
|
||||
status: "online",
|
||||
});
|
||||
}
|
||||
|
||||
const c = {
|
||||
reset: "\x1b[0m",
|
||||
gray: "\x1b[90m",
|
||||
green: "\x1b[32m",
|
||||
yellow: "\x1b[33m",
|
||||
bold: "\x1b[1m",
|
||||
white: "\x1b[37m",
|
||||
};
|
||||
|
||||
function line() {
|
||||
console.log(
|
||||
`${c.gray}────────────────────────────────────────────────────────${c.reset}`,
|
||||
);
|
||||
}
|
||||
|
||||
function section(title) {
|
||||
console.log(`\n${c.green}${title}${c.reset}`);
|
||||
console.log(`${c.gray}${"-".repeat(title.length)}${c.reset}`);
|
||||
}
|
||||
|
||||
function info(label, value) {
|
||||
console.log(`${c.gray}${label.padEnd(14)}${c.reset} ${value}`);
|
||||
}
|
||||
|
||||
function status(label, ok, details = "") {
|
||||
const icon = ok ? `${c.green}✓${c.reset}` : `${c.yellow}!${c.reset}`;
|
||||
console.log(` ${icon} ${label}${details ? ` (${details})` : ""}`);
|
||||
}
|
||||
|
||||
async function logStartup(client, options = {}) {
|
||||
const { clearConsole = true } = options;
|
||||
const supportbot = configStore.supportbot;
|
||||
|
||||
if (clearConsole) {
|
||||
console.clear();
|
||||
}
|
||||
|
||||
const botName = supportbot?.General?.Name || "SupportBot";
|
||||
const botVersion = supportbot?.SupportBot_Version || "Unknown";
|
||||
const { getBotInviteUrl } = require("./BotInvite.js");
|
||||
const inviteUrl = getBotInviteUrl(client.user.id);
|
||||
|
||||
line();
|
||||
console.log(`${c.bold}${c.green}SupportBot${c.reset}`);
|
||||
console.log(`${c.gray}Emerald Services${c.reset}`);
|
||||
line();
|
||||
|
||||
section("System");
|
||||
info("Bot Name", botName);
|
||||
info("Version", botVersion);
|
||||
info("Node.js", process.version);
|
||||
info("Discord.js", `v${Discord.version}`);
|
||||
const db = require("./Database.js");
|
||||
const dbInfo = db.getDatabaseInfo ? db.getDatabaseInfo() : null;
|
||||
if (dbInfo) {
|
||||
const dbDisplay = dbInfo.type === "mysql"
|
||||
? `${dbInfo.driver} (${dbInfo.host}:${dbInfo.port}/${dbInfo.database})`
|
||||
: `${dbInfo.driver} (${dbInfo.file})`;
|
||||
info("Database", dbDisplay);
|
||||
}
|
||||
info("Logged in as", client.user.tag);
|
||||
info("Guilds", `${client.guilds.cache.size}`);
|
||||
|
||||
section("Links");
|
||||
info("Docs", "https://emerald-services.gitbook.io/");
|
||||
info("Discord", "https://dsc.gg/emerald-dev");
|
||||
|
||||
if (configStore.api?.API?.Enabled) {
|
||||
const port = configStore.api?.API?.Port || 3000;
|
||||
info("Dashboard", `http://localhost:${port}`);
|
||||
}
|
||||
|
||||
info("Invite", inviteUrl);
|
||||
|
||||
console.log("");
|
||||
}
|
||||
|
||||
function checkSetup() {
|
||||
const { isSetupComplete } = require("./DashboardSetup.js");
|
||||
if (!isSetupComplete()) {
|
||||
const port = configStore.api?.API?.Port || 25575;
|
||||
section("Setup Required");
|
||||
status("Onboarding setup incomplete", false, `http://localhost:${port}/setup`);
|
||||
console.log("");
|
||||
}
|
||||
}
|
||||
|
||||
async function checkConfigs(client) {
|
||||
const supportbot = configStore.supportbot;
|
||||
const health = syncGuildHealth(client);
|
||||
|
||||
if (health.status !== "ok") {
|
||||
section("Guild");
|
||||
status("Guild setup", false, health.message || health.status);
|
||||
console.log("");
|
||||
}
|
||||
|
||||
const guild = getPrimaryGuild(client);
|
||||
if (!guild) return;
|
||||
|
||||
section("Config Check");
|
||||
|
||||
const roles = [
|
||||
supportbot?.Roles?.StaffMember?.Admin,
|
||||
supportbot?.Roles?.StaffMember?.Staff,
|
||||
supportbot?.Roles?.AutoRole?.Role,
|
||||
].filter(Boolean);
|
||||
|
||||
const channels = [supportbot?.Ticket?.TicketHome].filter(Boolean);
|
||||
|
||||
const [roleResults, channelResults] = await Promise.all([
|
||||
Promise.all(roles.map((role) => client.getRole(role, guild))),
|
||||
Promise.all(channels.map((channel) => client.getChannel(channel, guild))),
|
||||
]);
|
||||
|
||||
const missingRoles = roles.filter((_, i) => !roleResults[i]);
|
||||
const missingChannels = channels.filter((_, i) => !channelResults[i]);
|
||||
|
||||
if (!missingRoles.length && !missingChannels.length) {
|
||||
status("All configurations validated successfully", true);
|
||||
} else {
|
||||
if (missingRoles.length) {
|
||||
status("Missing roles", false, missingRoles.join(", "));
|
||||
}
|
||||
if (missingChannels.length) {
|
||||
status("Missing channels", false, missingChannels.join(", "));
|
||||
}
|
||||
}
|
||||
|
||||
console.log("");
|
||||
}
|
||||
|
||||
function logSlashCommandsRegistered(client) {
|
||||
const guild = getPrimaryGuild(client);
|
||||
if (!guild) return;
|
||||
console.log(
|
||||
`\n${c.green}✓${c.reset} ${c.white}Slash Commands Registered${c.reset} ${c.gray}for ${guild.name}${c.reset}`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Same work as Events/ready.js — safe to call after config reload reconnects.
|
||||
*/
|
||||
async function runBotReady(client, options = {}) {
|
||||
const { clearConsole = true, logCommands = false } = options;
|
||||
|
||||
syncGuildHealth(client);
|
||||
applyPresence(client);
|
||||
await logStartup(client, { clearConsole });
|
||||
await checkConfigs(client);
|
||||
|
||||
if (logCommands) {
|
||||
logSlashCommandsRegistered(client);
|
||||
}
|
||||
|
||||
checkSetup();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
applyPresence,
|
||||
logStartup,
|
||||
checkConfigs,
|
||||
logSlashCommandsRegistered,
|
||||
runBotReady,
|
||||
};
|
||||
@@ -0,0 +1,193 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const Discord = require("discord.js");
|
||||
const { GatewayIntentBits, Partials } = require("discord.js");
|
||||
const { Command, Event } = require("./Addon.js");
|
||||
const configStore = require("./ConfigStore.js");
|
||||
const { getPrimaryGuild } = require("./GuildManager.js");
|
||||
|
||||
const supportbot = configStore.supportbot;
|
||||
const cmdconfig = configStore.commands;
|
||||
|
||||
const c = {
|
||||
reset: "\x1b[0m",
|
||||
gray: "\x1b[90m",
|
||||
green: "\x1b[32m",
|
||||
yellow: "\x1b[33m",
|
||||
cyan: "\x1b[36m",
|
||||
red: "\x1b[31m",
|
||||
white: "\x1b[37m",
|
||||
bold: "\x1b[1m",
|
||||
};
|
||||
|
||||
function line() {
|
||||
console.log(`${c.gray}────────────────────────────────────────────────────────${c.reset}`);
|
||||
}
|
||||
|
||||
function section(title) {
|
||||
console.log(`\n${c.green}${title}${c.reset}`);
|
||||
console.log(`${c.gray}${"-".repeat(title.length)}${c.reset}`);
|
||||
}
|
||||
|
||||
function walk(dir, ext = ".js", fileList = []) {
|
||||
const files = fs.readdirSync(dir);
|
||||
for (const file of files) {
|
||||
const full = `${dir}/${file}`;
|
||||
const stat = fs.statSync(full);
|
||||
if (stat.isDirectory()) {
|
||||
walk(full, ext, fileList);
|
||||
} else if (file.endsWith(ext)) {
|
||||
fileList.push(full);
|
||||
}
|
||||
}
|
||||
return fileList;
|
||||
}
|
||||
|
||||
class Client extends Discord.Client {
|
||||
constructor() {
|
||||
super({
|
||||
intents: [
|
||||
GatewayIntentBits.Guilds,
|
||||
GatewayIntentBits.GuildMembers,
|
||||
GatewayIntentBits.GuildMessages,
|
||||
GatewayIntentBits.GuildMessageReactions,
|
||||
GatewayIntentBits.GuildEmojisAndStickers,
|
||||
GatewayIntentBits.MessageContent,
|
||||
],
|
||||
partials: [Partials.Message, Partials.Channel, Partials.Reaction],
|
||||
});
|
||||
|
||||
this.commands = new Discord.Collection();
|
||||
}
|
||||
|
||||
async getChannel(channel, guild) {
|
||||
if (!channel) return null;
|
||||
const channelStr = String(channel).toLowerCase();
|
||||
return guild.channels.cache.find(
|
||||
(c) =>
|
||||
(c.type === Discord.ChannelType.GuildText || c.type === Discord.ChannelType.GuildNews) &&
|
||||
(c.id === channel || (c.name && c.name.toLowerCase() === channelStr)),
|
||||
);
|
||||
}
|
||||
|
||||
async getRole(role, guild) {
|
||||
if (!role) return null;
|
||||
const roleStr = String(role).toLowerCase();
|
||||
return guild.roles.cache.find(
|
||||
(r) => r.id === role || (r.name && r.name.toLowerCase() === roleStr),
|
||||
);
|
||||
}
|
||||
|
||||
async getCategory(category, guild) {
|
||||
if (!category) return null;
|
||||
const catStr = String(category).toLowerCase();
|
||||
return guild.channels.cache.find(
|
||||
(c) =>
|
||||
c.type === Discord.ChannelType.GuildCategory &&
|
||||
(c.id === category || (c.name && c.name.toLowerCase() === catStr)),
|
||||
);
|
||||
}
|
||||
|
||||
async start(token) {
|
||||
const commandFiles = walk("./Commands");
|
||||
|
||||
const allCommands = commandFiles.map((file) => ({
|
||||
file,
|
||||
command: require(`../${file}`),
|
||||
}));
|
||||
|
||||
const enabledCommands = allCommands.filter(({ command }) => {
|
||||
const matchingConfig = Object.values(cmdconfig).find(
|
||||
(entry) =>
|
||||
entry &&
|
||||
typeof entry === "object" &&
|
||||
entry.Command &&
|
||||
String(entry.Command).toLowerCase() === String(command.name).toLowerCase(),
|
||||
);
|
||||
|
||||
if (!matchingConfig) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return matchingConfig.Enabled !== false;
|
||||
});
|
||||
|
||||
section("Commands");
|
||||
|
||||
enabledCommands.forEach(({ command, file }) => {
|
||||
console.log(
|
||||
`${c.green}✓${c.reset} ${c.white}${command.name}${c.reset} ${c.gray}(${path.basename(file)})${c.reset}`,
|
||||
);
|
||||
this.commands.set(command.name, command);
|
||||
});
|
||||
|
||||
if (supportbot.General.Addons.Enabled) {
|
||||
const addonFiles = fs.readdirSync("./Addons").filter((file) => file.endsWith(".js"));
|
||||
|
||||
section("Addons");
|
||||
|
||||
addonFiles.forEach((file) => {
|
||||
let addon = require(`../Addons/${file}`);
|
||||
|
||||
// Handle single or multiple addons per file
|
||||
const addonArray = Array.isArray(addon) ? addon : [addon];
|
||||
|
||||
addonArray.forEach((item, index) => {
|
||||
// If name is missing, use filename (handling index for arrays)
|
||||
if (!item.name) {
|
||||
item.name = index === 0 ? file.split(".")[0] : `${file.split(".")[0]}_${index}`;
|
||||
}
|
||||
|
||||
console.log(`${c.green}✓${c.reset} ${c.white}${item.name}${c.reset}`);
|
||||
|
||||
if (item instanceof Command) {
|
||||
this.commands.set(item.name, item);
|
||||
}
|
||||
|
||||
if (item.events && Array.isArray(item.events)) {
|
||||
item.events.forEach((event) => {
|
||||
if (event instanceof Event) {
|
||||
this.on(event.event, (...args) => event.run(this, ...args));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// If it's a direct Event instance
|
||||
if (item instanceof Event) {
|
||||
this.on(item.event, (...args) => item.run(this, ...args));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
this.on("clientReady", async () => {
|
||||
const guild = getPrimaryGuild(this);
|
||||
if (!guild) return;
|
||||
await guild.commands.set(this.commands);
|
||||
const { logSlashCommandsRegistered } = require("./BotStartup.js");
|
||||
logSlashCommandsRegistered(this);
|
||||
});
|
||||
|
||||
section("Events");
|
||||
|
||||
fs.readdirSync("./Events")
|
||||
.filter((file) => file.endsWith(".js"))
|
||||
.forEach((file) => {
|
||||
const event = require(`../Events/${file}`);
|
||||
console.log(
|
||||
`${c.green}✓${c.reset} ${c.white}${event.event}${c.reset} ${c.gray}(${file})${c.reset}`,
|
||||
);
|
||||
this.on(event.event, (...args) => event.run(this, ...args));
|
||||
});
|
||||
|
||||
console.log("");
|
||||
|
||||
if (process.argv[2] !== "test") {
|
||||
await this.login(token);
|
||||
} else {
|
||||
console.log(`${c.red}RUNNING IN TEST MODE, IF NOT INTENDED, PLEASE USE npm start${c.reset}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Client;
|
||||
@@ -0,0 +1,14 @@
|
||||
// SupportBot | Emerald Services
|
||||
// Command Structure
|
||||
|
||||
class Command {
|
||||
constructor(options) {
|
||||
this.name = options.name;
|
||||
this.description = options.description;
|
||||
this.permissions = options.permissions || [];
|
||||
this.options = options.options || [];
|
||||
this.run = options.run;
|
||||
this.defaultPermission = true;
|
||||
}
|
||||
}
|
||||
module.exports = Command;
|
||||
@@ -0,0 +1,89 @@
|
||||
// Live config objects — reload mutates in place so existing references stay valid.
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const yaml = require("js-yaml");
|
||||
|
||||
const CONFIG_DIR = path.join(__dirname, "../Configs");
|
||||
|
||||
const FILE_KEYS = [
|
||||
"supportbot",
|
||||
"commands",
|
||||
"messages",
|
||||
"ticket-panel",
|
||||
"supportbot-ai",
|
||||
"api",
|
||||
];
|
||||
|
||||
function readConfig(name) {
|
||||
return yaml.load(fs.readFileSync(path.join(CONFIG_DIR, `${name}.yml`), "utf8"));
|
||||
}
|
||||
|
||||
function deepAssign(target, source) {
|
||||
if (!target || typeof target !== "object" || Array.isArray(target)) {
|
||||
return source;
|
||||
}
|
||||
if (!source || typeof source !== "object" || Array.isArray(source)) {
|
||||
return source;
|
||||
}
|
||||
|
||||
for (const key of Object.keys(source)) {
|
||||
const next = source[key];
|
||||
if (
|
||||
next &&
|
||||
typeof next === "object" &&
|
||||
!Array.isArray(next) &&
|
||||
target[key] &&
|
||||
typeof target[key] === "object" &&
|
||||
!Array.isArray(target[key])
|
||||
) {
|
||||
deepAssign(target[key], next);
|
||||
} else {
|
||||
target[key] = next;
|
||||
}
|
||||
}
|
||||
|
||||
for (const key of Object.keys(target)) {
|
||||
if (!(key in source)) {
|
||||
delete target[key];
|
||||
}
|
||||
}
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
const store = {};
|
||||
for (const name of FILE_KEYS) {
|
||||
store[name] = readConfig(name);
|
||||
}
|
||||
|
||||
function reloadFile(fileKey) {
|
||||
if (!FILE_KEYS.includes(fileKey)) {
|
||||
throw new Error(`Unknown config: ${fileKey}`);
|
||||
}
|
||||
const fresh = readConfig(fileKey);
|
||||
deepAssign(store[fileKey], fresh);
|
||||
return store[fileKey];
|
||||
}
|
||||
|
||||
function reloadBotConfigs() {
|
||||
const reloaded = [];
|
||||
for (const name of FILE_KEYS) {
|
||||
if (name === "api") continue;
|
||||
reloadFile(name);
|
||||
reloaded.push(name);
|
||||
}
|
||||
return reloaded;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
supportbot: store.supportbot,
|
||||
commands: store.commands,
|
||||
messages: store.messages,
|
||||
ticketPanel: store["ticket-panel"],
|
||||
supportbotAi: store["supportbot-ai"],
|
||||
api: store.api,
|
||||
store,
|
||||
reloadFile,
|
||||
reloadBotConfigs,
|
||||
readConfig,
|
||||
};
|
||||
@@ -0,0 +1,229 @@
|
||||
/**
|
||||
* Dashboard alert registry — server-side providers for actionable banners.
|
||||
*
|
||||
* Register a provider to surface new alerts anywhere in the dashboard:
|
||||
*
|
||||
* const { dashboardAlerts } = require("./DashboardAlerts.js");
|
||||
* dashboardAlerts.register("my-addon", (ctx) => {
|
||||
* if (!ctx.configStore.myAddon?.Configured) {
|
||||
* return [{
|
||||
* id: "my-addon:setup",
|
||||
* severity: "warning",
|
||||
* category: "my-addon",
|
||||
* title: "My addon needs setup",
|
||||
* message: "Finish configuration to enable this feature.",
|
||||
* action: { label: "Configure", href: "/configs?file=my-addon" },
|
||||
* }];
|
||||
* }
|
||||
* return [];
|
||||
* });
|
||||
*/
|
||||
const { evaluateGuildHealth } = require("./GuildManager.js");
|
||||
const { evaluateAllModules } = require("./ModuleStatus.js");
|
||||
|
||||
const SEVERITY_ORDER = { error: 0, warning: 1, info: 2 };
|
||||
|
||||
const MODULE_LINKS = {
|
||||
tickets: {
|
||||
title: "Ticket system",
|
||||
href: "/configs?file=ticket-panel",
|
||||
},
|
||||
supportbot: {
|
||||
title: "Core bot settings",
|
||||
href: "/configs?file=supportbot",
|
||||
},
|
||||
transcripts: {
|
||||
title: "Ticket transcripts",
|
||||
href: "/transcripts",
|
||||
},
|
||||
suggestions: {
|
||||
title: "Suggestions",
|
||||
href: "/configs?file=messages",
|
||||
},
|
||||
ai: {
|
||||
title: "AI assistant",
|
||||
href: "/configs?file=supportbot-ai",
|
||||
},
|
||||
commands: {
|
||||
title: "Commands",
|
||||
href: "/configs?file=commands",
|
||||
},
|
||||
};
|
||||
|
||||
class DashboardAlertRegistry {
|
||||
constructor() {
|
||||
/** @type {Map<string, (ctx: DashboardAlertContext) => DashboardAlert[] | Promise<DashboardAlert[]>>} */
|
||||
this.providers = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} id Unique provider id (used for debugging / overrides)
|
||||
* @param {(ctx: DashboardAlertContext) => DashboardAlert[] | Promise<DashboardAlert[]>} fn
|
||||
*/
|
||||
register(id, fn) {
|
||||
this.providers.set(id, fn);
|
||||
return this;
|
||||
}
|
||||
|
||||
unregister(id) {
|
||||
this.providers.delete(id);
|
||||
}
|
||||
|
||||
/** @param {DashboardAlertContext} ctx */
|
||||
async evaluate(ctx) {
|
||||
const alerts = [];
|
||||
for (const fn of this.providers.values()) {
|
||||
const result = await fn(ctx);
|
||||
if (Array.isArray(result) && result.length) {
|
||||
alerts.push(...result);
|
||||
}
|
||||
}
|
||||
return sortAlerts(alerts);
|
||||
}
|
||||
}
|
||||
|
||||
function sortAlerts(alerts) {
|
||||
return [...alerts].sort((a, b) => {
|
||||
const sa = SEVERITY_ORDER[a.severity] ?? 9;
|
||||
const sb = SEVERITY_ORDER[b.severity] ?? 9;
|
||||
if (sa !== sb) return sa - sb;
|
||||
return a.title.localeCompare(b.title);
|
||||
});
|
||||
}
|
||||
|
||||
function guildSeverity(status) {
|
||||
if (status === "offline") return "error";
|
||||
return "warning";
|
||||
}
|
||||
|
||||
function shouldSkipModuleAlert(moduleId, info) {
|
||||
if (info.status === "done") return true;
|
||||
if (moduleId === "ai" && info.subtitle === "AI is disabled in config") return true;
|
||||
// Transcripts are automatic — empty archive is normal, not a setup step.
|
||||
if (moduleId === "transcripts") return true;
|
||||
// Partially configured modules are tracked on Overview — only banner when nothing is done yet.
|
||||
if (info.status === "progress" && (info.passed ?? 0) > 0) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function moduleSeverity(moduleId, info) {
|
||||
if (info.status === "todo") {
|
||||
return moduleId === "tickets" || moduleId === "supportbot" ? "warning" : "info";
|
||||
}
|
||||
return "info";
|
||||
}
|
||||
|
||||
function evaluateGuildAlerts(ctx) {
|
||||
const { client } = ctx;
|
||||
if (!client?.user) {
|
||||
return [
|
||||
{
|
||||
id: "guild:offline",
|
||||
severity: "error",
|
||||
category: "guild",
|
||||
title: "Bot offline",
|
||||
message: "The bot is not connected to Discord. Check your token and restart the server.",
|
||||
action: { label: "Bot config", href: "/configs?file=supportbot" },
|
||||
meta: { status: "offline" },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const health = evaluateGuildHealth(client);
|
||||
if (health.status === "ok") return [];
|
||||
|
||||
return [
|
||||
{
|
||||
id: "guild:health",
|
||||
severity: guildSeverity(health.status),
|
||||
category: "guild",
|
||||
title: "Server setup required",
|
||||
message: health.message,
|
||||
action: { label: "Bot config", href: "/configs?file=supportbot" },
|
||||
meta: {
|
||||
status: health.status,
|
||||
configuredGuildId: health.configuredGuildId,
|
||||
configuredGuild: health.configuredGuild,
|
||||
extraGuilds: health.extraGuilds,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function evaluateModuleAlerts(ctx) {
|
||||
const { configStore, transcriptCount = 0 } = ctx;
|
||||
const modules = evaluateAllModules(configStore, { transcriptCount });
|
||||
const alerts = [];
|
||||
|
||||
for (const [moduleId, info] of Object.entries(modules)) {
|
||||
if (shouldSkipModuleAlert(moduleId, info)) continue;
|
||||
|
||||
const link = MODULE_LINKS[moduleId] || {
|
||||
title: moduleId,
|
||||
href: "/configs",
|
||||
};
|
||||
|
||||
const progress =
|
||||
info.total > 0 ? `${info.passed ?? 0} of ${info.total} checks complete` : null;
|
||||
|
||||
alerts.push({
|
||||
id: `module:${moduleId}`,
|
||||
severity: moduleSeverity(moduleId, info),
|
||||
category: "modules",
|
||||
title:
|
||||
info.status === "todo"
|
||||
? `${link.title} — setup required`
|
||||
: `${link.title} — in progress`,
|
||||
message: info.subtitle,
|
||||
action: { label: "Open module", href: link.href },
|
||||
meta: {
|
||||
moduleId,
|
||||
status: info.status,
|
||||
passed: info.passed,
|
||||
total: info.total,
|
||||
progress,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return alerts;
|
||||
}
|
||||
|
||||
const dashboardAlerts = new DashboardAlertRegistry();
|
||||
dashboardAlerts.register("guild", evaluateGuildAlerts);
|
||||
dashboardAlerts.register("modules", evaluateModuleAlerts);
|
||||
|
||||
/**
|
||||
* @typedef {Object} DashboardAlertAction
|
||||
* @property {string} label
|
||||
* @property {string} href
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} DashboardAlert
|
||||
* @property {string} id
|
||||
* @property {"error"|"warning"|"info"} severity
|
||||
* @property {string} category
|
||||
* @property {string} title
|
||||
* @property {string} message
|
||||
* @property {DashboardAlertAction} [action]
|
||||
* @property {Record<string, unknown>} [meta]
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} DashboardAlertContext
|
||||
* @property {import("discord.js").Client} [client]
|
||||
* @property {import("./ConfigStore.js")} configStore
|
||||
* @property {number} [transcriptCount]
|
||||
*/
|
||||
|
||||
async function evaluateDashboardAlerts(ctx) {
|
||||
return dashboardAlerts.evaluate(ctx);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
dashboardAlerts,
|
||||
evaluateDashboardAlerts,
|
||||
sortAlerts,
|
||||
MODULE_LINKS,
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
const { fullPermissions, hasPermission, canAccessConfig } = require("./DashboardPermissions.js");
|
||||
|
||||
function grantServiceAccess(req) {
|
||||
req.dashboardService = true;
|
||||
req.dashboardPermissions = fullPermissions();
|
||||
req.dashboardRole = "service";
|
||||
req.dashboardIsOwner = true;
|
||||
}
|
||||
|
||||
function forbid(res, message = "Forbidden") {
|
||||
return res.status(403).json({ success: false, error: message });
|
||||
}
|
||||
|
||||
function createPermissionMiddleware(getPermissions) {
|
||||
return function requirePermission(permission) {
|
||||
return (req, res, next) => {
|
||||
if (req.dashboardService) return next();
|
||||
const perms = getPermissions(req) || req.dashboardPermissions;
|
||||
if (hasPermission(perms, permission)) return next();
|
||||
return forbid(res);
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
function createConfigMiddleware(getPermissions) {
|
||||
return function requireConfigAccess(mode = "view") {
|
||||
return (req, res, next) => {
|
||||
if (req.dashboardService) return next();
|
||||
const perms = getPermissions(req) || req.dashboardPermissions;
|
||||
const file = req.params?.file || req.body?.filename;
|
||||
if (!file) return next();
|
||||
if (!canAccessConfig(perms, file, mode)) return forbid(res);
|
||||
return next();
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
function assertConfigFilesEditable(permissions, files, res) {
|
||||
if (!permissions) return forbid(res);
|
||||
for (const file of files) {
|
||||
if (!canAccessConfig(permissions, file, "edit")) {
|
||||
return forbid(res, `No permission to edit ${file}.yml`);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
grantServiceAccess,
|
||||
forbid,
|
||||
createPermissionMiddleware,
|
||||
createConfigMiddleware,
|
||||
assertConfigFilesEditable,
|
||||
fullPermissions,
|
||||
hasPermission,
|
||||
canAccessConfig,
|
||||
};
|
||||
@@ -0,0 +1,168 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const BRANDING_DIR = path.join(__dirname, '../Data/branding');
|
||||
const BRANDING_FILE = path.join(BRANDING_DIR, 'branding.json');
|
||||
const FAVICON_BASENAME = 'favicon';
|
||||
|
||||
const DEFAULTS = {
|
||||
title: 'SupportBot',
|
||||
pageTitle: 'SupportBot Dashboard',
|
||||
faviconFile: null,
|
||||
faviconMime: null,
|
||||
updatedAt: null,
|
||||
};
|
||||
|
||||
const ALLOWED_MIME = new Set([
|
||||
'image/png',
|
||||
'image/jpeg',
|
||||
'image/x-icon',
|
||||
'image/vnd.microsoft.icon',
|
||||
'image/svg+xml',
|
||||
'image/webp',
|
||||
]);
|
||||
|
||||
const MIME_EXT = {
|
||||
'image/png': '.png',
|
||||
'image/jpeg': '.jpg',
|
||||
'image/x-icon': '.ico',
|
||||
'image/vnd.microsoft.icon': '.ico',
|
||||
'image/svg+xml': '.svg',
|
||||
'image/webp': '.webp',
|
||||
};
|
||||
|
||||
function ensureDir() {
|
||||
if (!fs.existsSync(BRANDING_DIR)) {
|
||||
fs.mkdirSync(BRANDING_DIR, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
function readBranding() {
|
||||
ensureDir();
|
||||
if (!fs.existsSync(BRANDING_FILE)) {
|
||||
return { ...DEFAULTS };
|
||||
}
|
||||
try {
|
||||
const raw = JSON.parse(fs.readFileSync(BRANDING_FILE, 'utf8'));
|
||||
return {
|
||||
title: String(raw.title || DEFAULTS.title).slice(0, 64),
|
||||
pageTitle: String(raw.pageTitle || DEFAULTS.pageTitle).slice(0, 80),
|
||||
faviconFile: raw.faviconFile || null,
|
||||
faviconMime: raw.faviconMime || null,
|
||||
updatedAt: raw.updatedAt || null,
|
||||
};
|
||||
} catch {
|
||||
return { ...DEFAULTS };
|
||||
}
|
||||
}
|
||||
|
||||
function writeBranding(data) {
|
||||
ensureDir();
|
||||
const next = {
|
||||
title: String(data.title || DEFAULTS.title).slice(0, 64),
|
||||
pageTitle: String(data.pageTitle || DEFAULTS.pageTitle).slice(0, 80),
|
||||
faviconFile: data.faviconFile ?? null,
|
||||
faviconMime: data.faviconMime ?? null,
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
fs.writeFileSync(BRANDING_FILE, JSON.stringify(next, null, 2), 'utf8');
|
||||
return next;
|
||||
}
|
||||
|
||||
function publicPayload(branding) {
|
||||
const hasFavicon = Boolean(branding.faviconFile);
|
||||
return {
|
||||
title: branding.title,
|
||||
pageTitle: branding.pageTitle,
|
||||
hasFavicon,
|
||||
faviconUrl: hasFavicon ? '/api/branding/favicon' : null,
|
||||
updatedAt: branding.updatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
function updateBranding({ title, pageTitle }) {
|
||||
const current = readBranding();
|
||||
return writeBranding({
|
||||
...current,
|
||||
title: title !== undefined ? title : current.title,
|
||||
pageTitle: pageTitle !== undefined ? pageTitle : current.pageTitle,
|
||||
});
|
||||
}
|
||||
|
||||
function removeExistingFavicons() {
|
||||
if (!fs.existsSync(BRANDING_DIR)) return;
|
||||
for (const file of fs.readdirSync(BRANDING_DIR)) {
|
||||
if (file.startsWith(`${FAVICON_BASENAME}.`)) {
|
||||
fs.unlinkSync(path.join(BRANDING_DIR, file));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function saveFaviconFromBase64(dataUrl) {
|
||||
const match = String(dataUrl || '').match(
|
||||
/^data:([\w/+.-]+);base64,([A-Za-z0-9+/=]+)$/,
|
||||
);
|
||||
if (!match) {
|
||||
throw new Error('Invalid image data. Upload a PNG, ICO, SVG, or WebP file.');
|
||||
}
|
||||
|
||||
const mime = match[1];
|
||||
if (!ALLOWED_MIME.has(mime)) {
|
||||
throw new Error('Unsupported image type. Use PNG, ICO, SVG, JPEG, or WebP.');
|
||||
}
|
||||
|
||||
const buffer = Buffer.from(match[2], 'base64');
|
||||
if (buffer.length > 512 * 1024) {
|
||||
throw new Error('Favicon must be 512 KB or smaller.');
|
||||
}
|
||||
|
||||
ensureDir();
|
||||
removeExistingFavicons();
|
||||
|
||||
const ext = MIME_EXT[mime] || '.png';
|
||||
const filename = `${FAVICON_BASENAME}${ext}`;
|
||||
const filePath = path.join(BRANDING_DIR, filename);
|
||||
fs.writeFileSync(filePath, buffer);
|
||||
|
||||
const current = readBranding();
|
||||
return writeBranding({
|
||||
...current,
|
||||
faviconFile: filename,
|
||||
faviconMime: mime,
|
||||
});
|
||||
}
|
||||
|
||||
function clearFavicon() {
|
||||
removeExistingFavicons();
|
||||
const current = readBranding();
|
||||
return writeBranding({
|
||||
...current,
|
||||
faviconFile: null,
|
||||
faviconMime: null,
|
||||
});
|
||||
}
|
||||
|
||||
function getFaviconPath() {
|
||||
const branding = readBranding();
|
||||
if (!branding.faviconFile) return null;
|
||||
const filePath = path.join(BRANDING_DIR, branding.faviconFile);
|
||||
if (!fs.existsSync(filePath)) return null;
|
||||
return { filePath, mime: branding.faviconMime || 'image/png' };
|
||||
}
|
||||
|
||||
function resetBranding() {
|
||||
clearFavicon();
|
||||
return writeBranding({ ...DEFAULTS });
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
readBranding,
|
||||
writeBranding,
|
||||
publicPayload,
|
||||
updateBranding,
|
||||
saveFaviconFromBase64,
|
||||
clearFavicon,
|
||||
getFaviconPath,
|
||||
resetBranding,
|
||||
DEFAULTS,
|
||||
};
|
||||
@@ -0,0 +1,202 @@
|
||||
// SupportBot | Emerald Services
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { fullPermissions, normalizePermissions } = require("./DashboardPermissions.js");
|
||||
|
||||
const GROUPS_FILE = path.join(__dirname, "../Data/dashboard-groups.json");
|
||||
|
||||
const DEFAULT_GROUPS = [
|
||||
{
|
||||
id: "owner",
|
||||
name: "Owner",
|
||||
description: "Full system owner access to all features and configs",
|
||||
color: "#10B981", // Emerald Green
|
||||
discordRoleId: null,
|
||||
isSystem: true,
|
||||
permissions: fullPermissions(),
|
||||
},
|
||||
{
|
||||
id: "admin",
|
||||
name: "Admin",
|
||||
description: "Full administrative access including user and group management",
|
||||
color: "#3B82F6", // Blue
|
||||
discordRoleId: null,
|
||||
isSystem: true,
|
||||
permissions: fullPermissions(false, false),
|
||||
},
|
||||
{
|
||||
id: "support",
|
||||
name: "Support Staff",
|
||||
description: "Access to Overview, Live Tickets, and Transcripts",
|
||||
color: "#8B5CF6", // Purple
|
||||
discordRoleId: null,
|
||||
isSystem: false,
|
||||
permissions: {
|
||||
overview: true,
|
||||
tickets: true,
|
||||
logs: false,
|
||||
transcripts: true,
|
||||
settings: { view: false, update: false },
|
||||
configs: {
|
||||
supportbot: { view: false, edit: false },
|
||||
"ticket-panel": { view: false, edit: false },
|
||||
commands: { view: false, edit: false },
|
||||
messages: { view: false, edit: false },
|
||||
"supportbot-ai": { view: false, edit: false },
|
||||
api: { view: false, edit: false },
|
||||
},
|
||||
users: { view: false, manage: false },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "viewer",
|
||||
name: "Viewer",
|
||||
description: "Read-only access to dashboard and logs",
|
||||
color: "#6B7280", // Gray
|
||||
discordRoleId: null,
|
||||
isSystem: false,
|
||||
permissions: {
|
||||
overview: true,
|
||||
tickets: true,
|
||||
logs: true,
|
||||
transcripts: true,
|
||||
settings: { view: true, update: false },
|
||||
configs: {
|
||||
supportbot: { view: true, edit: false },
|
||||
"ticket-panel": { view: true, edit: false },
|
||||
commands: { view: true, edit: false },
|
||||
messages: { view: true, edit: false },
|
||||
"supportbot-ai": { view: true, edit: false },
|
||||
api: { view: true, edit: false },
|
||||
},
|
||||
users: { view: false, manage: false },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
function ensureStorage() {
|
||||
const dir = path.dirname(GROUPS_FILE);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
if (!fs.existsSync(GROUPS_FILE)) {
|
||||
const initialData = {
|
||||
version: 1,
|
||||
groups: DEFAULT_GROUPS,
|
||||
};
|
||||
fs.writeFileSync(GROUPS_FILE, JSON.stringify(initialData, null, 2), "utf8");
|
||||
}
|
||||
}
|
||||
|
||||
function loadStore() {
|
||||
ensureStorage();
|
||||
try {
|
||||
const raw = fs.readFileSync(GROUPS_FILE, "utf8");
|
||||
const parsed = JSON.parse(raw);
|
||||
if (parsed && Array.isArray(parsed.groups)) {
|
||||
return parsed.groups;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("[GroupStore] Error loading groups file:", err.message);
|
||||
}
|
||||
return DEFAULT_GROUPS;
|
||||
}
|
||||
|
||||
function saveStore(groups) {
|
||||
ensureStorage();
|
||||
const payload = {
|
||||
version: 1,
|
||||
groups,
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
fs.writeFileSync(GROUPS_FILE, JSON.stringify(payload, null, 2), "utf8");
|
||||
}
|
||||
|
||||
function listGroups() {
|
||||
return loadStore();
|
||||
}
|
||||
|
||||
function getGroup(id) {
|
||||
const groups = loadStore();
|
||||
return groups.find((g) => g.id === id) || null;
|
||||
}
|
||||
|
||||
function createGroup(payload = {}) {
|
||||
const groups = loadStore();
|
||||
const name = (payload.name || "New Group").trim();
|
||||
const slug = name.toLowerCase().replace(/[^a-z0-9_-]+/g, "_");
|
||||
let newId = slug || `group_${Date.now()}`;
|
||||
|
||||
let counter = 1;
|
||||
while (groups.some((g) => g.id === newId)) {
|
||||
newId = `${slug}_${counter++}`;
|
||||
}
|
||||
|
||||
const newGroup = {
|
||||
id: newId,
|
||||
name,
|
||||
description: (payload.description || "").trim(),
|
||||
color: payload.color || "#10B981",
|
||||
discordRoleId: payload.discordRoleId ? String(payload.discordRoleId).trim() : null,
|
||||
isSystem: false,
|
||||
permissions: normalizePermissions(payload.permissions),
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
|
||||
groups.push(newGroup);
|
||||
saveStore(groups);
|
||||
return newGroup;
|
||||
}
|
||||
|
||||
function updateGroup(id, payload = {}) {
|
||||
if (id === "owner") {
|
||||
throw new Error('System group "Owner" cannot be modified.');
|
||||
}
|
||||
const groups = loadStore();
|
||||
const index = groups.findIndex((g) => g.id === id);
|
||||
if (index === -1) {
|
||||
throw new Error(`Group with ID "${id}" not found.`);
|
||||
}
|
||||
|
||||
const existing = groups[index];
|
||||
const updatedGroup = {
|
||||
...existing,
|
||||
name: payload.name !== undefined ? payload.name.trim() : existing.name,
|
||||
description: payload.description !== undefined ? payload.description.trim() : existing.description,
|
||||
color: payload.color || existing.color,
|
||||
discordRoleId: payload.discordRoleId !== undefined ? (payload.discordRoleId ? String(payload.discordRoleId).trim() : null) : existing.discordRoleId,
|
||||
permissions: payload.permissions ? normalizePermissions(payload.permissions) : existing.permissions,
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
|
||||
groups[index] = updatedGroup;
|
||||
saveStore(groups);
|
||||
return updatedGroup;
|
||||
}
|
||||
|
||||
function deleteGroup(id) {
|
||||
const groups = loadStore();
|
||||
const group = groups.find((g) => g.id === id);
|
||||
if (!group) {
|
||||
throw new Error(`Group with ID "${id}" not found.`);
|
||||
}
|
||||
|
||||
if (group.isSystem) {
|
||||
throw new Error(`System group "${group.name}" cannot be deleted.`);
|
||||
}
|
||||
|
||||
const filtered = groups.filter((g) => g.id !== id);
|
||||
saveStore(filtered);
|
||||
return true;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
listGroups,
|
||||
getGroup,
|
||||
createGroup,
|
||||
updateGroup,
|
||||
deleteGroup,
|
||||
DEFAULT_GROUPS,
|
||||
};
|
||||
@@ -0,0 +1,150 @@
|
||||
const CONFIG_FILES = [
|
||||
"supportbot",
|
||||
"ticket-panel",
|
||||
"commands",
|
||||
"messages",
|
||||
"supportbot-ai",
|
||||
"api",
|
||||
];
|
||||
|
||||
const ROLES = ["owner", "admin", "moderator", "editor", "viewer", "custom"];
|
||||
|
||||
function configMap(view, edit) {
|
||||
const configs = {};
|
||||
for (const file of CONFIG_FILES) {
|
||||
configs[file] = { view: Boolean(view), edit: Boolean(edit) };
|
||||
}
|
||||
return configs;
|
||||
}
|
||||
|
||||
function fullPermissions(rawYaml = true, userManage = true) {
|
||||
return {
|
||||
overview: true,
|
||||
tickets: true,
|
||||
logs: true,
|
||||
transcripts: true,
|
||||
rawYaml: Boolean(rawYaml),
|
||||
settings: { view: true, update: true },
|
||||
configs: configMap(true, true),
|
||||
users: { view: true, manage: Boolean(userManage) },
|
||||
};
|
||||
}
|
||||
|
||||
function permissionsFromRole(role, customPermissions) {
|
||||
switch (role) {
|
||||
case "owner":
|
||||
return fullPermissions(true, true);
|
||||
case "admin":
|
||||
return fullPermissions(false, false);
|
||||
case "moderator":
|
||||
return {
|
||||
overview: true,
|
||||
tickets: true,
|
||||
logs: true,
|
||||
transcripts: true,
|
||||
rawYaml: false,
|
||||
settings: { view: true, update: false },
|
||||
configs: configMap(true, false),
|
||||
users: { view: false, manage: false },
|
||||
};
|
||||
case "editor":
|
||||
return {
|
||||
overview: true,
|
||||
tickets: true,
|
||||
logs: true,
|
||||
transcripts: false,
|
||||
rawYaml: false,
|
||||
settings: { view: true, update: false },
|
||||
configs: configMap(true, true),
|
||||
users: { view: false, manage: false },
|
||||
};
|
||||
case "viewer":
|
||||
return {
|
||||
overview: true,
|
||||
tickets: true,
|
||||
logs: true,
|
||||
transcripts: true,
|
||||
rawYaml: false,
|
||||
settings: { view: true, update: false },
|
||||
configs: configMap(true, false),
|
||||
users: { view: false, manage: false },
|
||||
};
|
||||
case "custom":
|
||||
return normalizePermissions(customPermissions);
|
||||
default:
|
||||
return permissionsFromRole("viewer");
|
||||
}
|
||||
}
|
||||
|
||||
function normalizePermissions(input) {
|
||||
const base = permissionsFromRole("viewer");
|
||||
if (!input || typeof input !== "object") return base;
|
||||
|
||||
return {
|
||||
overview: input.overview ?? base.overview,
|
||||
tickets: input.tickets ?? input.overview ?? base.tickets,
|
||||
logs: input.logs ?? base.logs,
|
||||
transcripts: input.transcripts ?? base.transcripts,
|
||||
rawYaml: Boolean(input.rawYaml ?? input.configs?.rawYaml ?? base.rawYaml),
|
||||
settings: {
|
||||
view: input.settings?.view ?? base.settings.view,
|
||||
update: input.settings?.update ?? base.settings.update,
|
||||
},
|
||||
configs: CONFIG_FILES.reduce((acc, file) => {
|
||||
acc[file] = {
|
||||
view: input.configs?.[file]?.view ?? base.configs[file].view,
|
||||
edit: input.configs?.[file]?.edit ?? base.configs[file].edit,
|
||||
};
|
||||
return acc;
|
||||
}, {}),
|
||||
users: {
|
||||
view: input.users?.view ?? base.users.view,
|
||||
manage: input.users?.manage ?? base.users.manage,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function hasPermission(permissions, check) {
|
||||
if (!permissions) return false;
|
||||
|
||||
if (check === "overview") return Boolean(permissions.overview);
|
||||
if (check === "tickets") return Boolean(permissions.tickets ?? permissions.overview);
|
||||
if (check === "logs") return Boolean(permissions.logs);
|
||||
if (check === "transcripts") return Boolean(permissions.transcripts);
|
||||
if (check === "configs.raw_yaml" || check === "configs.rawYaml" || check === "rawYaml") {
|
||||
return Boolean(permissions.rawYaml ?? permissions.configs?.rawYaml ?? false);
|
||||
}
|
||||
if (check === "settings.view") return Boolean(permissions.settings?.view);
|
||||
if (check === "settings.update") return Boolean(permissions.settings?.update);
|
||||
if (check === "users.view") return Boolean(permissions.users?.view);
|
||||
if (check === "users.manage") return Boolean(permissions.users?.manage);
|
||||
|
||||
const configView = check.match(/^configs\.([a-z0-9-]+)\.view$/);
|
||||
if (configView) {
|
||||
const file = configView[1];
|
||||
return Boolean(permissions.configs?.[file]?.view);
|
||||
}
|
||||
|
||||
const configEdit = check.match(/^configs\.([a-z0-9-]+)\.edit$/);
|
||||
if (configEdit) {
|
||||
const file = configEdit[1];
|
||||
return Boolean(permissions.configs?.[file]?.edit);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function canAccessConfig(permissions, file, mode = "view") {
|
||||
if (!CONFIG_FILES.includes(file)) return false;
|
||||
return hasPermission(permissions, `configs.${file}.${mode}`);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
CONFIG_FILES,
|
||||
ROLES,
|
||||
fullPermissions,
|
||||
permissionsFromRole,
|
||||
normalizePermissions,
|
||||
hasPermission,
|
||||
canAccessConfig,
|
||||
};
|
||||
@@ -0,0 +1,75 @@
|
||||
const crypto = require("crypto");
|
||||
|
||||
const SESSION_COOKIE = "sb_dashboard";
|
||||
const SESSION_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
||||
|
||||
function signSession(payload, secret) {
|
||||
const body = Buffer.from(JSON.stringify(payload)).toString("base64url");
|
||||
const sig = crypto
|
||||
.createHmac("sha256", secret)
|
||||
.update(body)
|
||||
.digest("base64url");
|
||||
return `${body}.${sig}`;
|
||||
}
|
||||
|
||||
function verifySession(token, secret) {
|
||||
if (!token || typeof token !== "string") return null;
|
||||
const dot = token.lastIndexOf(".");
|
||||
if (dot <= 0) return null;
|
||||
|
||||
const body = token.slice(0, dot);
|
||||
const sig = token.slice(dot + 1);
|
||||
const expected = crypto
|
||||
.createHmac("sha256", secret)
|
||||
.update(body)
|
||||
.digest("base64url");
|
||||
|
||||
if (sig.length !== expected.length) return null;
|
||||
if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = JSON.parse(
|
||||
Buffer.from(body, "base64url").toString("utf8"),
|
||||
);
|
||||
if (!payload?.userId || !payload?.exp) return null;
|
||||
if (Date.now() > payload.exp) return null;
|
||||
return payload;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function createSessionPayload(user) {
|
||||
return {
|
||||
userId: user.id,
|
||||
username: user.username,
|
||||
globalName: user.global_name || null,
|
||||
avatar: user.avatar || null,
|
||||
exp: Date.now() + SESSION_MAX_AGE_MS,
|
||||
};
|
||||
}
|
||||
|
||||
function sessionCookieOptions(maxAgeMs = SESSION_MAX_AGE_MS, opts = {}) {
|
||||
const secure =
|
||||
opts.secure !== undefined
|
||||
? Boolean(opts.secure)
|
||||
: process.env.NODE_ENV === "production";
|
||||
return {
|
||||
httpOnly: true,
|
||||
secure,
|
||||
sameSite: "lax",
|
||||
maxAge: maxAgeMs,
|
||||
path: "/",
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
SESSION_COOKIE,
|
||||
SESSION_MAX_AGE_MS,
|
||||
signSession,
|
||||
verifySession,
|
||||
createSessionPayload,
|
||||
sessionCookieOptions,
|
||||
};
|
||||
@@ -0,0 +1,510 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const axios = require("axios");
|
||||
const YAML = require("yaml");
|
||||
const configStore = require("./ConfigStore.js");
|
||||
const { CALLBACK_PATH } = require("./DiscordOAuth.js");
|
||||
|
||||
const DISCORD_API = "https://discord.com/api/v10";
|
||||
const CONFIG_DIR = path.join(__dirname, "../Configs");
|
||||
const API_FILE = path.join(CONFIG_DIR, "api.yml");
|
||||
const SUPPORTBOT_FILE = path.join(CONFIG_DIR, "supportbot.yml");
|
||||
|
||||
const PLACEHOLDER_PATTERNS = [
|
||||
/DISCORD_CLIENT_ID/i,
|
||||
/DISCORD_CLIENT_SECRET/i,
|
||||
/YOUR_DISCORD_USER_ID/i,
|
||||
/CHANGE_ME/i,
|
||||
/BOT_TOKEN/i,
|
||||
/PUT_RANDOM_STRING/i,
|
||||
];
|
||||
|
||||
function isEmpty(value) {
|
||||
if (value == null) return true;
|
||||
if (typeof value === "boolean") return false;
|
||||
if (typeof value === "number") return false;
|
||||
return String(value).trim() === "";
|
||||
}
|
||||
|
||||
function isPlaceholder(value) {
|
||||
if (isEmpty(value)) return true;
|
||||
const s = String(value).trim();
|
||||
return PLACEHOLDER_PATTERNS.some((p) => p.test(s));
|
||||
}
|
||||
|
||||
function isDiscordSnowflake(value) {
|
||||
return /^\d{17,20}$/.test(String(value).trim());
|
||||
}
|
||||
|
||||
function isValidBotToken(value) {
|
||||
if (isPlaceholder(value)) return false;
|
||||
const s = String(value).trim();
|
||||
return s.length >= 50 && s.includes(".");
|
||||
}
|
||||
|
||||
function isValidSecretKey(value) {
|
||||
if (isPlaceholder(value)) return false;
|
||||
return String(value).trim().length >= 16;
|
||||
}
|
||||
|
||||
function readApiRoot() {
|
||||
if (!fs.existsSync(API_FILE)) return null;
|
||||
try {
|
||||
const raw = YAML.parse(fs.readFileSync(API_FILE, "utf8"));
|
||||
return raw?.API ?? raw ?? null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function readSupportbotRoot() {
|
||||
if (!fs.existsSync(SUPPORTBOT_FILE)) return {};
|
||||
try {
|
||||
return YAML.parse(fs.readFileSync(SUPPORTBOT_FILE, "utf8")) || {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function getOwnerIds(oauth) {
|
||||
const ids = [];
|
||||
for (const id of oauth?.OwnerUserIds || []) {
|
||||
const s = String(id).trim();
|
||||
if (s) ids.push(s);
|
||||
}
|
||||
if (ids.length === 0) {
|
||||
for (const id of oauth?.AllowedUserIds || []) {
|
||||
const s = String(id).trim();
|
||||
if (s) ids.push(s);
|
||||
}
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
function buildSteps(apiRoot, supportbot) {
|
||||
const oauth = apiRoot?.OAuth || {};
|
||||
const token = supportbot?.General?.Token;
|
||||
const dbConfig = supportbot?.Database || {};
|
||||
|
||||
return [
|
||||
{
|
||||
id: "botToken",
|
||||
title: "Bot token",
|
||||
description: "From the Discord Developer Portal → your application → Bot → Reset Token.",
|
||||
complete: isValidBotToken(token),
|
||||
},
|
||||
{
|
||||
id: "database",
|
||||
title: "Database storage",
|
||||
description: "Choose SQLite or MySQL database engine for storing bot data.",
|
||||
complete: Boolean(dbConfig.Driver),
|
||||
},
|
||||
{
|
||||
id: "secretKey",
|
||||
title: "Dashboard session secret",
|
||||
description: "Random string used to sign login cookies. Keep it private.",
|
||||
complete: isValidSecretKey(apiRoot?.SecretKey),
|
||||
},
|
||||
{
|
||||
id: "oauth",
|
||||
title: "Discord OAuth app",
|
||||
description: "Same application as your bot — Client ID and Client Secret from OAuth2.",
|
||||
complete:
|
||||
!isPlaceholder(oauth.ClientId) &&
|
||||
!isPlaceholder(oauth.ClientSecret) &&
|
||||
isDiscordSnowflake(oauth.ClientId),
|
||||
},
|
||||
{
|
||||
id: "redirectUri",
|
||||
title: "OAuth redirect URL",
|
||||
description: `Add this exact URL under OAuth2 → Redirects in the Discord portal.`,
|
||||
complete: isValidRedirectUriConfigured(oauth.RedirectUri),
|
||||
},
|
||||
{
|
||||
id: "owners",
|
||||
title: "Dashboard owners",
|
||||
description: "Discord user IDs that can sign in and manage the dashboard (at least one).",
|
||||
complete: getOwnerIds(oauth).length > 0 && getOwnerIds(oauth).every(isDiscordSnowflake),
|
||||
},
|
||||
{
|
||||
id: "emeraldApi",
|
||||
title: "Emerald API",
|
||||
description: "Optional. Used for the one-click addon installer.",
|
||||
complete: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function isValidRedirectUriConfigured(uri) {
|
||||
if (isPlaceholder(uri)) return false;
|
||||
try {
|
||||
const u = new URL(String(uri).trim());
|
||||
if (!["http:", "https:"].includes(u.protocol)) return false;
|
||||
return u.pathname.replace(/\/$/, "") === CALLBACK_PATH.replace(/\/$/, "");
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isSetupComplete() {
|
||||
const apiRoot = readApiRoot();
|
||||
if (!apiRoot?.Enabled) return false;
|
||||
const oauth = apiRoot.OAuth || {};
|
||||
if (!oauth.Enabled) return false;
|
||||
const steps = buildSteps(apiRoot, readSupportbotRoot());
|
||||
return steps.every((s) => s.complete);
|
||||
}
|
||||
|
||||
function getSetupStatus(req) {
|
||||
const apiRoot = readApiRoot() || {};
|
||||
const steps = buildSteps(apiRoot, readSupportbotRoot());
|
||||
const complete = steps.every((s) => s.complete);
|
||||
|
||||
let suggestedRedirectUri = null;
|
||||
if (req) {
|
||||
const proto =
|
||||
req.get("x-forwarded-proto")?.split(",")[0]?.trim() ||
|
||||
req.protocol ||
|
||||
"http";
|
||||
const host = req.get("x-forwarded-host")?.split(",")[0]?.trim() || req.get("host");
|
||||
if (host) {
|
||||
suggestedRedirectUri = `${proto}://${host}${CALLBACK_PATH}`;
|
||||
}
|
||||
}
|
||||
if (!suggestedRedirectUri) {
|
||||
suggestedRedirectUri = `http://localhost:${apiRoot.Port || 3000}${CALLBACK_PATH}`;
|
||||
}
|
||||
|
||||
return {
|
||||
complete,
|
||||
steps,
|
||||
suggestedRedirectUri,
|
||||
oauthCallbackPath: CALLBACK_PATH,
|
||||
};
|
||||
}
|
||||
|
||||
async function validateBotToken(token) {
|
||||
const trimmed = String(token || "").trim();
|
||||
if (!isValidBotToken(trimmed)) {
|
||||
return { ok: false, error: "Enter a valid bot token from the Discord Developer Portal." };
|
||||
}
|
||||
|
||||
try {
|
||||
const [userRes, appRes] = await Promise.all([
|
||||
axios.get(`${DISCORD_API}/users/@me`, {
|
||||
headers: { Authorization: `Bot ${trimmed}` },
|
||||
timeout: 12_000,
|
||||
}),
|
||||
axios.get(`${DISCORD_API}/oauth2/applications/@me`, {
|
||||
headers: { Authorization: `Bot ${trimmed}` },
|
||||
timeout: 12_000,
|
||||
}),
|
||||
]);
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
bot: {
|
||||
id: userRes.data.id,
|
||||
username: userRes.data.username,
|
||||
discriminator: userRes.data.discriminator,
|
||||
},
|
||||
applicationId: String(appRes.data.id),
|
||||
};
|
||||
} catch (err) {
|
||||
const status = err.response?.status;
|
||||
if (status === 401) {
|
||||
return { ok: false, error: "Discord rejected this bot token (401 Unauthorized)." };
|
||||
}
|
||||
return {
|
||||
ok: false,
|
||||
error: err.response?.data?.message || err.message || "Could not verify bot token.",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function validateOAuthCredentials(clientId, clientSecret, botToken) {
|
||||
const id = String(clientId || "").trim();
|
||||
const secret = String(clientSecret || "").trim();
|
||||
|
||||
if (!isDiscordSnowflake(id)) {
|
||||
return { ok: false, error: "Client ID must be a numeric Discord application ID." };
|
||||
}
|
||||
if (isPlaceholder(secret) || secret.length < 20) {
|
||||
return { ok: false, error: "Enter the OAuth Client Secret from the Discord portal." };
|
||||
}
|
||||
|
||||
if (botToken) {
|
||||
const botCheck = await validateBotToken(botToken);
|
||||
if (botCheck.ok && botCheck.applicationId && botCheck.applicationId !== id) {
|
||||
return {
|
||||
ok: false,
|
||||
error: `Client ID does not match the bot application (${botCheck.applicationId}). Use the same app's credentials.`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await axios.post(
|
||||
`${DISCORD_API}/oauth2/token`,
|
||||
new URLSearchParams({ grant_type: "client_credentials", scope: "identify" }),
|
||||
{
|
||||
auth: { username: id, password: secret },
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
timeout: 12_000,
|
||||
},
|
||||
);
|
||||
return { ok: true, clientId: id };
|
||||
} catch (err) {
|
||||
const status = err.response?.status;
|
||||
if (botToken) {
|
||||
const botCheck = await validateBotToken(botToken);
|
||||
if (botCheck.ok && botCheck.applicationId === id) {
|
||||
return {
|
||||
ok: true,
|
||||
clientId: id,
|
||||
warning:
|
||||
"Could not verify the client secret with Discord (token endpoint). ID matches your bot — double-check the secret in the portal.",
|
||||
};
|
||||
}
|
||||
}
|
||||
if (status === 401 || status === 400) {
|
||||
return { ok: false, error: "Invalid Client ID or Client Secret." };
|
||||
}
|
||||
return {
|
||||
ok: false,
|
||||
error: err.response?.data?.error_description || err.message || "OAuth validation failed.",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function validateRedirectUri(uri) {
|
||||
const trimmed = String(uri || "").trim();
|
||||
if (!isValidRedirectUriConfigured(trimmed)) {
|
||||
return {
|
||||
ok: false,
|
||||
error: `Redirect URL must end with ${CALLBACK_PATH} and use http or https.`,
|
||||
};
|
||||
}
|
||||
return { ok: true, redirectUri: trimmed };
|
||||
}
|
||||
|
||||
async function validateOwnerUserIds(ownerIds, botToken) {
|
||||
const ids = (Array.isArray(ownerIds) ? ownerIds : [ownerIds])
|
||||
.map((id) => String(id).trim())
|
||||
.filter(Boolean);
|
||||
|
||||
if (ids.length === 0) {
|
||||
return { ok: false, error: "Add at least one Discord user ID." };
|
||||
}
|
||||
|
||||
const invalid = ids.filter((id) => !isDiscordSnowflake(id));
|
||||
if (invalid.length) {
|
||||
return { ok: false, error: `Invalid user ID: ${invalid[0]}` };
|
||||
}
|
||||
|
||||
if (!botToken || !isValidBotToken(botToken)) {
|
||||
return { ok: true, ownerUserIds: ids, warning: "User IDs format OK (bot token not verified yet)." };
|
||||
}
|
||||
|
||||
const users = [];
|
||||
try {
|
||||
for (const id of ids) {
|
||||
const res = await axios.get(`${DISCORD_API}/users/${id}`, {
|
||||
headers: { Authorization: `Bot ${String(botToken).trim()}` },
|
||||
timeout: 12_000,
|
||||
});
|
||||
users.push({
|
||||
id: res.data.id,
|
||||
username: res.data.username,
|
||||
globalName: res.data.global_name || null,
|
||||
});
|
||||
}
|
||||
return { ok: true, ownerUserIds: ids, users };
|
||||
} catch (err) {
|
||||
if (err.response?.status === 404) {
|
||||
return { ok: false, error: "One or more user IDs were not found. Check Developer Mode → Copy User ID." };
|
||||
}
|
||||
if (err.response?.status === 403) {
|
||||
return {
|
||||
ok: false,
|
||||
error: "Bot could not look up users. Ensure the bot is in a server with those users, or verify IDs manually.",
|
||||
};
|
||||
}
|
||||
return { ok: false, error: err.message || "Failed to verify owner user IDs." };
|
||||
}
|
||||
}
|
||||
|
||||
function validateSecretKey(secretKey) {
|
||||
if (!isValidSecretKey(secretKey)) {
|
||||
return { ok: false, error: "Use at least 16 characters (not CHANGE_ME)." };
|
||||
}
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
async function validateDatabase(dbDriver, dbHost, dbPort, dbUser, dbPassword, dbDatabase) {
|
||||
const driver = String(dbDriver || "sqlite").toLowerCase();
|
||||
if (driver === "mysql" || driver === "mariadb") {
|
||||
const host = String(dbHost || "").trim();
|
||||
const port = Number(dbPort) || 3306;
|
||||
const user = String(dbUser || "").trim();
|
||||
const password = String(dbPassword || "");
|
||||
const database = String(dbDatabase || "").trim();
|
||||
|
||||
if (isEmpty(host)) return { ok: false, error: "MySQL Host is required." };
|
||||
if (isEmpty(user)) return { ok: false, error: "MySQL User is required." };
|
||||
if (isEmpty(database)) return { ok: false, error: "MySQL Database Name is required." };
|
||||
|
||||
try {
|
||||
const mysql = require("mysql2/promise");
|
||||
const connection = await mysql.createConnection({
|
||||
host,
|
||||
port,
|
||||
user,
|
||||
password,
|
||||
database,
|
||||
connectTimeout: 5000,
|
||||
});
|
||||
await connection.end();
|
||||
return { ok: true, driver: "mysql" };
|
||||
} catch (err) {
|
||||
return {
|
||||
ok: false,
|
||||
error: `MySQL Connection Failed: ${err.message || err.code || "Could not connect to database host."}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { ok: true, driver: "sqlite" };
|
||||
}
|
||||
|
||||
async function validateSetupPayload(payload) {
|
||||
const results = {};
|
||||
|
||||
if (payload.botToken != null) {
|
||||
results.botToken = await validateBotToken(payload.botToken);
|
||||
}
|
||||
if (payload.dbDriver != null) {
|
||||
results.database = await validateDatabase(
|
||||
payload.dbDriver,
|
||||
payload.dbHost,
|
||||
payload.dbPort,
|
||||
payload.dbUser,
|
||||
payload.dbPassword,
|
||||
payload.dbDatabase,
|
||||
);
|
||||
}
|
||||
if (payload.secretKey != null) {
|
||||
results.secretKey = validateSecretKey(payload.secretKey);
|
||||
}
|
||||
if (payload.clientId != null && payload.clientSecret != null) {
|
||||
results.oauth = await validateOAuthCredentials(
|
||||
payload.clientId,
|
||||
payload.clientSecret,
|
||||
payload.botToken,
|
||||
);
|
||||
}
|
||||
if (payload.redirectUri != null) {
|
||||
results.redirectUri = validateRedirectUri(payload.redirectUri);
|
||||
}
|
||||
if (payload.ownerUserIds != null) {
|
||||
results.owners = await validateOwnerUserIds(payload.ownerUserIds, payload.botToken);
|
||||
}
|
||||
if (payload.emeraldApiKey != null) {
|
||||
const key = String(payload.emeraldApiKey).trim();
|
||||
if (key === "") {
|
||||
results.emeraldApi = { ok: true };
|
||||
} else if (key.length < 10) {
|
||||
results.emeraldApi = { ok: false, error: "Invalid API key." };
|
||||
} else {
|
||||
results.emeraldApi = { ok: true };
|
||||
}
|
||||
}
|
||||
|
||||
const allOk = Object.values(results).every((r) => r?.ok);
|
||||
return { ok: allOk, results };
|
||||
}
|
||||
|
||||
function ensureApiFile() {
|
||||
if (fs.existsSync(API_FILE)) return;
|
||||
const example = path.join(CONFIG_DIR, "api.yml.example");
|
||||
if (fs.existsSync(example)) {
|
||||
fs.copyFileSync(example, API_FILE);
|
||||
configStore.reloadFile("api");
|
||||
} else {
|
||||
throw new Error("Configs/api.yml is missing. Copy Configs/api.yml.example to api.yml.");
|
||||
}
|
||||
}
|
||||
|
||||
function applySetup(payload) {
|
||||
ensureApiFile();
|
||||
|
||||
const ownerIds = (Array.isArray(payload.ownerUserIds)
|
||||
? payload.ownerUserIds
|
||||
: String(payload.ownerUserIds || "").split(/[\s,]+/)
|
||||
)
|
||||
.map((id) => String(id).trim())
|
||||
.filter(Boolean);
|
||||
|
||||
const apiDoc = YAML.parseDocument(fs.readFileSync(API_FILE, "utf8"));
|
||||
if (!apiDoc.get("API")) apiDoc.set("API", {});
|
||||
|
||||
apiDoc.setIn(["API", "Enabled"], true);
|
||||
apiDoc.setIn(["API", "SecretKey"], String(payload.secretKey).trim());
|
||||
if (payload.emeraldApiKey != null) {
|
||||
apiDoc.setIn(["API", "EmeraldAPIKey"], String(payload.emeraldApiKey).trim());
|
||||
}
|
||||
if (payload.port != null && payload.port !== "") {
|
||||
apiDoc.setIn(["API", "Port"], Number(payload.port) || 3000);
|
||||
}
|
||||
if (!apiDoc.getIn(["API", "OAuth"])) apiDoc.setIn(["API", "OAuth"], {});
|
||||
|
||||
apiDoc.setIn(["API", "OAuth", "Enabled"], true);
|
||||
apiDoc.setIn(["API", "OAuth", "ClientId"], String(payload.clientId).trim());
|
||||
apiDoc.setIn(["API", "OAuth", "ClientSecret"], String(payload.clientSecret).trim());
|
||||
apiDoc.setIn(["API", "OAuth", "RedirectUri"], String(payload.redirectUri).trim());
|
||||
apiDoc.setIn(["API", "OAuth", "OwnerUserIds"], ownerIds);
|
||||
apiDoc.setIn(["API", "OAuth", "AllowedUserIds"], []);
|
||||
|
||||
fs.writeFileSync(API_FILE, apiDoc.toString());
|
||||
|
||||
const sbDoc = YAML.parseDocument(fs.readFileSync(SUPPORTBOT_FILE, "utf8"));
|
||||
if (payload.botToken != null && String(payload.botToken).trim() !== "") {
|
||||
sbDoc.setIn(["General", "Token"], String(payload.botToken).trim());
|
||||
}
|
||||
|
||||
if (payload.dbDriver != null) {
|
||||
const driver = String(payload.dbDriver).toLowerCase() === "mysql" ? "mysql" : "sqlite";
|
||||
sbDoc.setIn(["Database", "Driver"], driver);
|
||||
if (driver === "mysql") {
|
||||
sbDoc.setIn(["Database", "MySQL", "Host"], String(payload.dbHost || "127.0.0.1").trim());
|
||||
sbDoc.setIn(["Database", "MySQL", "Port"], Number(payload.dbPort) || 3306);
|
||||
sbDoc.setIn(["Database", "MySQL", "User"], String(payload.dbUser || "").trim());
|
||||
sbDoc.setIn(["Database", "MySQL", "Password"], String(payload.dbPassword || ""));
|
||||
sbDoc.setIn(["Database", "MySQL", "Database"], String(payload.dbDatabase || "").trim());
|
||||
} else {
|
||||
sbDoc.setIn(["Database", "SQLite", "File"], "./Data/supportbot.db");
|
||||
}
|
||||
}
|
||||
|
||||
fs.writeFileSync(SUPPORTBOT_FILE, sbDoc.toString());
|
||||
|
||||
configStore.reloadFile("api");
|
||||
configStore.reloadFile("supportbot");
|
||||
|
||||
return {
|
||||
ownerUserIds: ownerIds,
|
||||
api: configStore.api?.API || configStore.api,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
isSetupComplete,
|
||||
getSetupStatus,
|
||||
validateBotToken,
|
||||
validateOAuthCredentials,
|
||||
validateRedirectUri,
|
||||
validateOwnerUserIds,
|
||||
validateSecretKey,
|
||||
validateSetupPayload,
|
||||
applySetup,
|
||||
};
|
||||
@@ -0,0 +1,311 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const {
|
||||
CONFIG_FILES,
|
||||
ROLES,
|
||||
fullPermissions,
|
||||
permissionsFromRole,
|
||||
normalizePermissions,
|
||||
hasPermission,
|
||||
canAccessConfig,
|
||||
} = require("./DashboardPermissions.js");
|
||||
const { discordAvatarUrl } = require("./DiscordOAuth.js");
|
||||
|
||||
const STORE_PATH = path.join(__dirname, "../Data/dashboard-users.json");
|
||||
|
||||
function ensureDataDir() {
|
||||
const dir = path.dirname(STORE_PATH);
|
||||
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
function emptyStore() {
|
||||
return { version: 1, users: {} };
|
||||
}
|
||||
|
||||
function readStore() {
|
||||
ensureDataDir();
|
||||
if (!fs.existsSync(STORE_PATH)) return emptyStore();
|
||||
try {
|
||||
const raw = JSON.parse(fs.readFileSync(STORE_PATH, "utf8"));
|
||||
if (!raw.users || typeof raw.users !== "object") return emptyStore();
|
||||
return raw;
|
||||
} catch {
|
||||
return emptyStore();
|
||||
}
|
||||
}
|
||||
|
||||
function writeStore(store) {
|
||||
ensureDataDir();
|
||||
fs.writeFileSync(STORE_PATH, JSON.stringify(store, null, 2), "utf8");
|
||||
}
|
||||
|
||||
function getOwnerIds(oauth) {
|
||||
const owners = new Set();
|
||||
for (const id of oauth?.OwnerUserIds || []) owners.add(String(id));
|
||||
for (const id of oauth?.AllowedUserIds || []) owners.add(String(id));
|
||||
return owners;
|
||||
}
|
||||
|
||||
function isYamlOwner(userId, oauth) {
|
||||
return getOwnerIds(oauth).has(String(userId));
|
||||
}
|
||||
|
||||
function bootstrapFromOAuth(oauth) {
|
||||
const store = readStore();
|
||||
const owners = getOwnerIds(oauth);
|
||||
let changed = false;
|
||||
|
||||
for (const id of owners) {
|
||||
if (!store.users[id]) {
|
||||
store.users[id] = {
|
||||
id,
|
||||
username: null,
|
||||
globalName: null,
|
||||
avatar: null,
|
||||
role: "owner",
|
||||
permissions: null,
|
||||
enabled: true,
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
createdBy: "system",
|
||||
};
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (changed) writeStore(store);
|
||||
return store;
|
||||
}
|
||||
|
||||
function syncDiscordProfile(userId, profile) {
|
||||
const store = readStore();
|
||||
const entry = store.users[userId];
|
||||
if (!entry) return;
|
||||
|
||||
entry.username = profile.username ?? entry.username;
|
||||
entry.globalName = profile.globalName ?? entry.globalName;
|
||||
entry.avatar = profile.avatar ?? entry.avatar;
|
||||
entry.updatedAt = Date.now();
|
||||
writeStore(store);
|
||||
}
|
||||
|
||||
const { getGroup } = require("./DashboardGroupStore.js");
|
||||
|
||||
function resolvePermissions(entry, oauth, userId) {
|
||||
if (isYamlOwner(userId, oauth)) {
|
||||
return { permissions: fullPermissions(), role: "owner", isOwner: true, groupId: "owner" };
|
||||
}
|
||||
if (!entry?.enabled) return null;
|
||||
|
||||
const groupId = entry.groupId || entry.role || "support";
|
||||
const group = getGroup(groupId);
|
||||
|
||||
if (entry.role === "custom" && entry.permissions) {
|
||||
return { permissions: normalizePermissions(entry.permissions), role: "custom", isOwner: false, groupId };
|
||||
}
|
||||
|
||||
const permissions = group ? group.permissions : permissionsFromRole(entry.role || "viewer");
|
||||
return { permissions, role: entry.role || groupId, isOwner: false, groupId };
|
||||
}
|
||||
|
||||
function getAccess(userId, oauth) {
|
||||
bootstrapFromOAuth(oauth);
|
||||
const store = readStore();
|
||||
const id = String(userId);
|
||||
|
||||
if (isYamlOwner(id, oauth)) {
|
||||
const entry = store.users[id];
|
||||
return {
|
||||
entry: entry || { id, role: "owner" },
|
||||
permissions: fullPermissions(),
|
||||
role: "owner",
|
||||
isOwner: true,
|
||||
yamlOwner: true,
|
||||
};
|
||||
}
|
||||
|
||||
const entry = store.users[id];
|
||||
const resolved = resolvePermissions(entry, oauth, id);
|
||||
if (!resolved) return null;
|
||||
|
||||
return {
|
||||
entry,
|
||||
permissions: resolved.permissions,
|
||||
role: resolved.role,
|
||||
isOwner: false,
|
||||
yamlOwner: false,
|
||||
};
|
||||
}
|
||||
|
||||
function canLogin(userId, oauth) {
|
||||
return Boolean(getAccess(userId, oauth));
|
||||
}
|
||||
|
||||
function formatPublicUser(entry, access, discordProfile) {
|
||||
const id = String(entry?.id || discordProfile?.id);
|
||||
const avatarHash = entry?.avatar ?? discordProfile?.avatar ?? null;
|
||||
|
||||
return {
|
||||
id,
|
||||
username: entry?.username || discordProfile?.username || "Unknown",
|
||||
globalName: entry?.globalName ?? discordProfile?.global_name ?? null,
|
||||
avatar: avatarHash
|
||||
? discordAvatarUrl({ id, avatar: avatarHash })
|
||||
: discordProfile
|
||||
? discordAvatarUrl(discordProfile)
|
||||
: discordAvatarUrl({ id, avatar: null }),
|
||||
role: access.role,
|
||||
isOwner: access.isOwner,
|
||||
permissions: access.permissions,
|
||||
};
|
||||
}
|
||||
|
||||
function listUsers(oauth, discordClient) {
|
||||
bootstrapFromOAuth(oauth);
|
||||
const store = readStore();
|
||||
const yamlOwners = getOwnerIds(oauth);
|
||||
|
||||
let updated = false;
|
||||
|
||||
const usersList = Object.values(store.users).map((entry) => {
|
||||
if (discordClient?.users?.cache) {
|
||||
const cached = discordClient.users.cache.get(String(entry.id));
|
||||
if (cached && (!entry.username || !entry.avatar)) {
|
||||
entry.username = cached.username || entry.username;
|
||||
entry.globalName = cached.globalName || cached.username || entry.globalName;
|
||||
entry.avatar = cached.avatar || entry.avatar;
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
|
||||
const access = resolvePermissions(entry, oauth, entry.id) || {
|
||||
permissions: permissionsFromRole("viewer"),
|
||||
role: entry.role,
|
||||
isOwner: false,
|
||||
};
|
||||
|
||||
return {
|
||||
id: entry.id,
|
||||
username: entry.username || (yamlOwners.has(String(entry.id)) ? "Owner" : "User"),
|
||||
globalName: entry.globalName || entry.username || null,
|
||||
avatar: entry.avatar
|
||||
? discordAvatarUrl({ id: entry.id, avatar: entry.avatar })
|
||||
: discordAvatarUrl({ id: entry.id, avatar: null }),
|
||||
role: yamlOwners.has(String(entry.id)) ? "owner" : entry.role || entry.groupId || "support",
|
||||
groupId: entry.groupId || entry.role || "support",
|
||||
permissions:
|
||||
entry.role === "custom"
|
||||
? normalizePermissions(entry.permissions)
|
||||
: access.permissions,
|
||||
enabled: entry.enabled !== false,
|
||||
isOwner: yamlOwners.has(String(entry.id)),
|
||||
yamlOwner: yamlOwners.has(String(entry.id)),
|
||||
createdAt: entry.createdAt,
|
||||
updatedAt: entry.updatedAt,
|
||||
};
|
||||
});
|
||||
|
||||
if (updated) {
|
||||
writeStore(store);
|
||||
}
|
||||
|
||||
return usersList;
|
||||
}
|
||||
|
||||
function addUser(payload, actorId, oauth) {
|
||||
const id = String(payload.id || "").trim();
|
||||
if (!/^\d{17,20}$/.test(id)) {
|
||||
throw new Error("Invalid Discord user ID");
|
||||
}
|
||||
|
||||
const role = payload.role || payload.groupId || "support";
|
||||
const groupId = payload.groupId || payload.role || "support";
|
||||
|
||||
const store = readStore();
|
||||
if (store.users[id]) throw new Error("User already exists");
|
||||
|
||||
store.users[id] = {
|
||||
id,
|
||||
username: payload.username || null,
|
||||
globalName: payload.globalName || null,
|
||||
avatar: null,
|
||||
role,
|
||||
groupId,
|
||||
permissions:
|
||||
role === "custom" ? normalizePermissions(payload.permissions) : null,
|
||||
enabled: payload.enabled !== false,
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
createdBy: String(actorId),
|
||||
};
|
||||
|
||||
writeStore(store);
|
||||
return listUsers(oauth).find((u) => u.id === id);
|
||||
}
|
||||
|
||||
function updateUser(id, payload, actorId, oauth) {
|
||||
const store = readStore();
|
||||
const entry = store.users[String(id)];
|
||||
if (!entry) throw new Error("User not found");
|
||||
|
||||
if (isYamlOwner(id, oauth) && payload.enabled === false) {
|
||||
throw new Error("Cannot disable an owner defined in api.yml");
|
||||
}
|
||||
|
||||
if (payload.groupId) {
|
||||
entry.groupId = payload.groupId;
|
||||
if (!payload.role) entry.role = payload.groupId;
|
||||
}
|
||||
|
||||
if (payload.role) {
|
||||
entry.role = payload.role;
|
||||
if (!payload.groupId) entry.groupId = payload.role;
|
||||
}
|
||||
|
||||
if (payload.enabled !== undefined) entry.enabled = Boolean(payload.enabled);
|
||||
if (payload.username !== undefined) entry.username = payload.username;
|
||||
if (payload.globalName !== undefined) entry.globalName = payload.globalName;
|
||||
|
||||
if (payload.permissions !== undefined) {
|
||||
entry.permissions = normalizePermissions(payload.permissions);
|
||||
if (entry.role !== "custom") entry.role = "custom";
|
||||
}
|
||||
|
||||
if (entry.role === "custom" && payload.permissions === undefined && payload.role !== "custom") {
|
||||
entry.permissions = null;
|
||||
}
|
||||
|
||||
entry.updatedAt = Date.now();
|
||||
writeStore(store);
|
||||
return listUsers(oauth).find((u) => u.id === String(id));
|
||||
}
|
||||
|
||||
function removeUser(id, oauth) {
|
||||
if (isYamlOwner(id, oauth)) {
|
||||
throw new Error("Remove this user from OwnerUserIds in api.yml instead");
|
||||
}
|
||||
|
||||
const store = readStore();
|
||||
if (!store.users[String(id)]) throw new Error("User not found");
|
||||
delete store.users[String(id)];
|
||||
writeStore(store);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
CONFIG_FILES,
|
||||
STORE_PATH,
|
||||
getOwnerIds,
|
||||
isYamlOwner,
|
||||
bootstrapFromOAuth,
|
||||
syncDiscordProfile,
|
||||
getAccess,
|
||||
canLogin,
|
||||
formatPublicUser,
|
||||
listUsers,
|
||||
addUser,
|
||||
updateUser,
|
||||
removeUser,
|
||||
hasPermission,
|
||||
canAccessConfig,
|
||||
ROLES,
|
||||
};
|
||||
@@ -0,0 +1,901 @@
|
||||
// SupportBot | Unified SQL Database Adapter (MySQL/MariaDB & SQLite)
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const yaml = require("js-yaml");
|
||||
|
||||
const CONFIG_PATH = path.join(__dirname, "../Configs/supportbot.yml");
|
||||
const LEGACY_JSON_PATH = path.join(__dirname, "../Data/supportbot.json");
|
||||
const SQLITE_DB_PATH = path.join(__dirname, "../Data/supportbot.db");
|
||||
|
||||
// Load config
|
||||
let dbConfig = { Driver: "sqlite", MySQL: {}, SQLite: { File: "./Data/supportbot.db" } };
|
||||
try {
|
||||
if (fs.existsSync(CONFIG_PATH)) {
|
||||
const rawYaml = fs.readFileSync(CONFIG_PATH, "utf8");
|
||||
const parsed = yaml.load(rawYaml);
|
||||
if (parsed && parsed.Database) {
|
||||
dbConfig = parsed.Database;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("[Database] Warning: Failed to parse Database section in supportbot.yml, defaulting to SQLite.");
|
||||
}
|
||||
|
||||
const driver = String(dbConfig.Driver || "sqlite").toLowerCase();
|
||||
const isMySQL = driver === "mysql" || driver === "mariadb";
|
||||
|
||||
let sqliteDb = null;
|
||||
let mysqlPool = null;
|
||||
let mysqlReady = false;
|
||||
const mysqlWriteQueue = [];
|
||||
|
||||
// In-memory state cache for synchronous, high-performance reads
|
||||
const state = {
|
||||
tickets: {},
|
||||
ticket_panel: [],
|
||||
ticket_users: {},
|
||||
profiles: {},
|
||||
settings: {},
|
||||
blacklisted_users: {},
|
||||
};
|
||||
|
||||
// ----------------------------------------------------
|
||||
// Database Initialization
|
||||
// ----------------------------------------------------
|
||||
function initDatabase() {
|
||||
if (isMySQL) {
|
||||
initMySQL();
|
||||
} else {
|
||||
initSQLite();
|
||||
}
|
||||
migrateLegacyJSON();
|
||||
}
|
||||
|
||||
function initSQLite() {
|
||||
const Database = require("better-sqlite3");
|
||||
const dbDir = path.dirname(SQLITE_DB_PATH);
|
||||
if (!fs.existsSync(dbDir)) {
|
||||
fs.mkdirSync(dbDir, { recursive: true });
|
||||
}
|
||||
|
||||
sqliteDb = new Database(SQLITE_DB_PATH);
|
||||
sqliteDb.pragma("journal_mode = WAL");
|
||||
|
||||
sqliteDb.exec(`
|
||||
CREATE TABLE IF NOT EXISTS tickets (
|
||||
ticket_id TEXT PRIMARY KEY,
|
||||
user_id TEXT,
|
||||
subject TEXT,
|
||||
description TEXT,
|
||||
department TEXT,
|
||||
priority TEXT,
|
||||
status TEXT,
|
||||
ai_mode_enabled INTEGER DEFAULT 0,
|
||||
ai_channel_id TEXT,
|
||||
ai_type TEXT,
|
||||
ai_enabled_by TEXT,
|
||||
ai_enabled_at INTEGER,
|
||||
question_answers TEXT,
|
||||
ticket_name TEXT,
|
||||
ticket_number TEXT,
|
||||
voice_channel_id TEXT,
|
||||
created_at INTEGER,
|
||||
updated_at INTEGER
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ticket_panels (
|
||||
id INTEGER PRIMARY KEY,
|
||||
message_id TEXT,
|
||||
channel_id TEXT,
|
||||
created_at INTEGER
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ticket_users (
|
||||
ticket_id TEXT,
|
||||
user_id TEXT,
|
||||
PRIMARY KEY (ticket_id, user_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS profiles (
|
||||
user_id TEXT PRIMARY KEY,
|
||||
bio TEXT DEFAULT '',
|
||||
timezone TEXT DEFAULT '',
|
||||
clocked_in INTEGER DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
setting_key TEXT PRIMARY KEY,
|
||||
setting_value TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS blacklisted_users (
|
||||
user_id TEXT PRIMARY KEY,
|
||||
reason TEXT,
|
||||
added_by TEXT,
|
||||
created_at INTEGER
|
||||
);
|
||||
`);
|
||||
|
||||
loadSQLiteState();
|
||||
const clr = { reset: "\x1b[0m", gray: "\x1b[90m", green: "\x1b[32m", white: "\x1b[37m" };
|
||||
console.log(`${clr.green}✓${clr.reset} ${clr.white}Database Connected${clr.reset} ${clr.gray}to SQLite (${SQLITE_DB_PATH})${clr.reset}`);
|
||||
}
|
||||
|
||||
function loadSQLiteState() {
|
||||
const tickets = sqliteDb.prepare("SELECT * FROM tickets").all();
|
||||
for (const r of tickets) {
|
||||
state.tickets[r.ticket_id] = {
|
||||
ticket_id: r.ticket_id,
|
||||
user_id: r.user_id,
|
||||
subject: r.subject || "",
|
||||
description: r.description || "",
|
||||
department: r.department || "general",
|
||||
priority: r.priority || "medium",
|
||||
status: r.status || "open",
|
||||
aiModeEnabled: r.ai_mode_enabled,
|
||||
aiChannelId: r.ai_channel_id,
|
||||
aiType: r.ai_type,
|
||||
aiEnabledBy: r.ai_enabled_by,
|
||||
aiEnabledAt: r.ai_enabled_at,
|
||||
questionAnswers: r.question_answers || "[]",
|
||||
ticketName: r.ticket_name,
|
||||
ticketNumber: r.ticket_number,
|
||||
voiceChannelId: r.voice_channel_id,
|
||||
created_at: r.created_at,
|
||||
updated_at: r.updated_at,
|
||||
};
|
||||
}
|
||||
|
||||
const panels = sqliteDb.prepare("SELECT * FROM ticket_panels ORDER BY id ASC").all();
|
||||
state.ticket_panel = panels.map((p) => ({
|
||||
id: p.id,
|
||||
message_id: p.message_id,
|
||||
channel_id: p.channel_id,
|
||||
created_at: p.created_at,
|
||||
}));
|
||||
|
||||
const ticketUsers = sqliteDb.prepare("SELECT * FROM ticket_users").all();
|
||||
for (const tu of ticketUsers) {
|
||||
state.ticket_users[tu.ticket_id] = state.ticket_users[tu.ticket_id] || [];
|
||||
if (!state.ticket_users[tu.ticket_id].includes(tu.user_id)) {
|
||||
state.ticket_users[tu.ticket_id].push(tu.user_id);
|
||||
}
|
||||
}
|
||||
|
||||
const profiles = sqliteDb.prepare("SELECT * FROM profiles").all();
|
||||
for (const p of profiles) {
|
||||
state.profiles[p.user_id] = {
|
||||
user_id: p.user_id,
|
||||
bio: p.bio || "",
|
||||
timezone: p.timezone || "",
|
||||
clockedIn: p.clocked_in,
|
||||
};
|
||||
}
|
||||
|
||||
const settings = sqliteDb.prepare("SELECT * FROM settings").all();
|
||||
for (const s of settings) {
|
||||
try {
|
||||
state.settings[s.setting_key] = JSON.parse(s.setting_value);
|
||||
} catch {
|
||||
state.settings[s.setting_key] = s.setting_value;
|
||||
}
|
||||
}
|
||||
|
||||
const blacklisted = sqliteDb.prepare("SELECT * FROM blacklisted_users").all();
|
||||
for (const b of blacklisted) {
|
||||
state.blacklisted_users[b.user_id] = {
|
||||
user_id: b.user_id,
|
||||
reason: b.reason || "",
|
||||
added_by: b.added_by || "",
|
||||
created_at: b.created_at,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function initMySQL() {
|
||||
const mysql = require("mysql2");
|
||||
const mysqlOpts = dbConfig.MySQL || {};
|
||||
|
||||
mysqlPool = mysql.createPool({
|
||||
host: mysqlOpts.Host || "127.0.0.1",
|
||||
port: mysqlOpts.Port || 3306,
|
||||
user: mysqlOpts.User || "supportbot",
|
||||
password: mysqlOpts.Password || "",
|
||||
database: mysqlOpts.Database || "supportbot",
|
||||
connectionLimit: mysqlOpts.ConnectionLimit || 10,
|
||||
waitForConnections: true,
|
||||
});
|
||||
|
||||
const poolPromise = mysqlPool.promise();
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
await poolPromise.query(`
|
||||
CREATE TABLE IF NOT EXISTS tickets (
|
||||
ticket_id VARCHAR(255) PRIMARY KEY,
|
||||
user_id VARCHAR(255),
|
||||
subject TEXT,
|
||||
description TEXT,
|
||||
department VARCHAR(255),
|
||||
priority VARCHAR(255),
|
||||
status VARCHAR(255),
|
||||
ai_mode_enabled INT DEFAULT 0,
|
||||
ai_channel_id VARCHAR(255),
|
||||
ai_type VARCHAR(255),
|
||||
ai_enabled_by VARCHAR(255),
|
||||
ai_enabled_at BIGINT,
|
||||
question_answers TEXT,
|
||||
ticket_name VARCHAR(255),
|
||||
ticket_number VARCHAR(255),
|
||||
voice_channel_id VARCHAR(255),
|
||||
created_at BIGINT,
|
||||
updated_at BIGINT
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
`);
|
||||
|
||||
await poolPromise.query(`
|
||||
CREATE TABLE IF NOT EXISTS ticket_panels (
|
||||
id BIGINT PRIMARY KEY,
|
||||
message_id VARCHAR(255),
|
||||
channel_id VARCHAR(255),
|
||||
created_at BIGINT
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
`);
|
||||
|
||||
await poolPromise.query(`
|
||||
CREATE TABLE IF NOT EXISTS ticket_users (
|
||||
ticket_id VARCHAR(255),
|
||||
user_id VARCHAR(255),
|
||||
PRIMARY KEY (ticket_id, user_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
`);
|
||||
|
||||
await poolPromise.query(`
|
||||
CREATE TABLE IF NOT EXISTS profiles (
|
||||
user_id VARCHAR(255) PRIMARY KEY,
|
||||
bio TEXT,
|
||||
timezone VARCHAR(255),
|
||||
clocked_in INT DEFAULT 0
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
`);
|
||||
|
||||
await poolPromise.query(`
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
setting_key VARCHAR(255) PRIMARY KEY,
|
||||
setting_value LONGTEXT
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
`);
|
||||
|
||||
await poolPromise.query(`
|
||||
CREATE TABLE IF NOT EXISTS blacklisted_users (
|
||||
user_id VARCHAR(255) PRIMARY KEY,
|
||||
reason TEXT,
|
||||
added_by VARCHAR(255),
|
||||
created_at BIGINT
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
`);
|
||||
|
||||
// Load initial state from MySQL
|
||||
const [tickets] = await poolPromise.query("SELECT * FROM tickets");
|
||||
for (const r of tickets) {
|
||||
state.tickets[r.ticket_id] = {
|
||||
ticket_id: r.ticket_id,
|
||||
user_id: r.user_id,
|
||||
subject: r.subject || "",
|
||||
description: r.description || "",
|
||||
department: r.department || "general",
|
||||
priority: r.priority || "medium",
|
||||
status: r.status || "open",
|
||||
aiModeEnabled: r.ai_mode_enabled,
|
||||
aiChannelId: r.ai_channel_id,
|
||||
aiType: r.ai_type,
|
||||
aiEnabledBy: r.ai_enabled_by,
|
||||
aiEnabledAt: r.ai_enabled_at,
|
||||
questionAnswers: r.question_answers || "[]",
|
||||
ticketName: r.ticket_name,
|
||||
ticketNumber: r.ticket_number,
|
||||
voiceChannelId: r.voice_channel_id,
|
||||
created_at: Number(r.created_at),
|
||||
updated_at: Number(r.updated_at),
|
||||
};
|
||||
}
|
||||
|
||||
const [panels] = await poolPromise.query("SELECT * FROM ticket_panels ORDER BY id ASC");
|
||||
state.ticket_panel = panels.map((p) => ({
|
||||
id: Number(p.id),
|
||||
message_id: p.message_id,
|
||||
channel_id: p.channel_id,
|
||||
created_at: Number(p.created_at),
|
||||
}));
|
||||
|
||||
const [ticketUsers] = await poolPromise.query("SELECT * FROM ticket_users");
|
||||
for (const tu of ticketUsers) {
|
||||
state.ticket_users[tu.ticket_id] = state.ticket_users[tu.ticket_id] || [];
|
||||
if (!state.ticket_users[tu.ticket_id].includes(tu.user_id)) {
|
||||
state.ticket_users[tu.ticket_id].push(tu.user_id);
|
||||
}
|
||||
}
|
||||
|
||||
const [profiles] = await poolPromise.query("SELECT * FROM profiles");
|
||||
for (const p of profiles) {
|
||||
state.profiles[p.user_id] = {
|
||||
user_id: p.user_id,
|
||||
bio: p.bio || "",
|
||||
timezone: p.timezone || "",
|
||||
clockedIn: p.clocked_in,
|
||||
};
|
||||
}
|
||||
|
||||
const [settings] = await poolPromise.query("SELECT * FROM settings");
|
||||
for (const s of settings) {
|
||||
try {
|
||||
state.settings[s.setting_key] = JSON.parse(s.setting_value);
|
||||
} catch {
|
||||
state.settings[s.setting_key] = s.setting_value;
|
||||
}
|
||||
}
|
||||
|
||||
const [blacklisted] = await poolPromise.query("SELECT * FROM blacklisted_users");
|
||||
for (const b of blacklisted) {
|
||||
state.blacklisted_users[b.user_id] = {
|
||||
user_id: b.user_id,
|
||||
reason: b.reason || "",
|
||||
added_by: b.added_by || "",
|
||||
created_at: Number(b.created_at),
|
||||
};
|
||||
}
|
||||
|
||||
mysqlReady = true;
|
||||
const clr = { reset: "\x1b[0m", gray: "\x1b[90m", green: "\x1b[32m", white: "\x1b[37m" };
|
||||
console.log(`${clr.green}✓${clr.reset} ${clr.white}Database Connected${clr.reset} ${clr.gray}to MySQL (${mysqlOpts.Host}:${mysqlOpts.Port}/${mysqlOpts.Database})${clr.reset}`);
|
||||
|
||||
while (mysqlWriteQueue.length > 0) {
|
||||
const fn = mysqlWriteQueue.shift();
|
||||
try { fn(); } catch (e) {}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("[Database] Error initializing MySQL tables or state:", err.message);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
// ----------------------------------------------------
|
||||
// Legacy JSON Migration Utility
|
||||
// ----------------------------------------------------
|
||||
function migrateLegacyJSON() {
|
||||
if (!fs.existsSync(LEGACY_JSON_PATH)) return;
|
||||
|
||||
try {
|
||||
const raw = fs.readFileSync(LEGACY_JSON_PATH, "utf8");
|
||||
const jsonState = JSON.parse(raw);
|
||||
console.log("[Database] Legacy supportbot.json detected. Migrating records to SQL...");
|
||||
|
||||
if (jsonState.tickets) {
|
||||
for (const tId of Object.keys(jsonState.tickets)) {
|
||||
const t = jsonState.tickets[tId];
|
||||
if (t && !state.tickets[tId]) {
|
||||
const ticketObj = {
|
||||
id: t.ticket_id || tId,
|
||||
user: t.user_id,
|
||||
reason: t.subject,
|
||||
description: t.description,
|
||||
department: t.department,
|
||||
priority: t.priority,
|
||||
aiModeEnabled: t.aiModeEnabled,
|
||||
aiChannelId: t.aiChannelId,
|
||||
aiType: t.aiType,
|
||||
aiEnabledBy: t.aiEnabledBy,
|
||||
aiEnabledAt: t.aiEnabledAt,
|
||||
questionAnswers: typeof t.questionAnswers === "string" ? JSON.parse(t.questionAnswers) : t.questionAnswers || [],
|
||||
name: t.ticketName,
|
||||
number: t.ticketNumber,
|
||||
};
|
||||
exportsModule.addTicket(ticketObj);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (jsonState.ticket_panel && Array.isArray(jsonState.ticket_panel)) {
|
||||
for (const p of jsonState.ticket_panel) {
|
||||
exportsModule.saveTicketPanel(p.message_id, p.channel_id);
|
||||
}
|
||||
}
|
||||
|
||||
if (jsonState.profiles) {
|
||||
for (const uId of Object.keys(jsonState.profiles)) {
|
||||
const prof = jsonState.profiles[uId];
|
||||
if (prof) {
|
||||
state.profiles[uId] = prof;
|
||||
persistProfile(uId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (jsonState.settings) {
|
||||
exportsModule.saveSettings(jsonState.settings);
|
||||
}
|
||||
|
||||
// Archive legacy json file
|
||||
const backupPath = LEGACY_JSON_PATH + ".bak";
|
||||
fs.renameSync(LEGACY_JSON_PATH, backupPath);
|
||||
console.log(`[Database] Legacy supportbot.json successfully migrated and archived to ${backupPath}`);
|
||||
} catch (e) {
|
||||
console.error("[Database] Failed to migrate legacy supportbot.json:", e);
|
||||
}
|
||||
|
||||
// Migrate legacy BlacklistedUsers.json if present
|
||||
const legacyBlacklistPath = path.join(__dirname, "../Data/BlacklistedUsers.json");
|
||||
if (fs.existsSync(legacyBlacklistPath)) {
|
||||
try {
|
||||
const raw = fs.readFileSync(legacyBlacklistPath, "utf8");
|
||||
const data = JSON.parse(raw);
|
||||
if (data && Array.isArray(data.blacklistedUsers)) {
|
||||
for (const uId of data.blacklistedUsers) {
|
||||
exportsModule.addBlacklistedUser(uId, "Migrated from BlacklistedUsers.json", "System");
|
||||
}
|
||||
}
|
||||
fs.renameSync(legacyBlacklistPath, legacyBlacklistPath + ".bak");
|
||||
console.log(`[Database] Legacy BlacklistedUsers.json successfully migrated and archived.`);
|
||||
} catch (e) {
|
||||
console.error("[Database] Failed to migrate BlacklistedUsers.json:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------
|
||||
// SQL Persistence Helpers
|
||||
// ----------------------------------------------------
|
||||
function persistTicket(ticketId) {
|
||||
const t = state.tickets[ticketId];
|
||||
if (!t) return;
|
||||
|
||||
if (isMySQL && mysqlPool) {
|
||||
const sql = `
|
||||
INSERT INTO tickets (
|
||||
ticket_id, user_id, subject, description, department, priority, status,
|
||||
ai_mode_enabled, ai_channel_id, ai_type, ai_enabled_by, ai_enabled_at,
|
||||
question_answers, ticket_name, ticket_number, voice_channel_id, created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
user_id=VALUES(user_id), subject=VALUES(subject), description=VALUES(description),
|
||||
department=VALUES(department), priority=VALUES(priority), status=VALUES(status),
|
||||
ai_mode_enabled=VALUES(ai_mode_enabled), ai_channel_id=VALUES(ai_channel_id),
|
||||
ai_type=VALUES(ai_type), ai_enabled_by=VALUES(ai_enabled_by), ai_enabled_at=VALUES(ai_enabled_at),
|
||||
question_answers=VALUES(question_answers), ticket_name=VALUES(ticket_name),
|
||||
ticket_number=VALUES(ticket_number), voice_channel_id=VALUES(voice_channel_id), updated_at=VALUES(updated_at);
|
||||
`;
|
||||
const params = [
|
||||
t.ticket_id,
|
||||
t.user_id || "",
|
||||
t.subject || "",
|
||||
t.description || "",
|
||||
t.department || "general",
|
||||
t.priority || "medium",
|
||||
t.status || "open",
|
||||
t.aiModeEnabled ? 1 : 0,
|
||||
t.aiChannelId || null,
|
||||
t.aiType || null,
|
||||
t.aiEnabledBy || null,
|
||||
t.aiEnabledAt || null,
|
||||
typeof t.questionAnswers === "string" ? t.questionAnswers : JSON.stringify(t.questionAnswers || []),
|
||||
t.ticketName || null,
|
||||
t.ticketNumber || null,
|
||||
t.voiceChannelId || null,
|
||||
t.created_at || Date.now(),
|
||||
t.updated_at || Date.now(),
|
||||
];
|
||||
mysqlPool.query(sql, params, (err) => {
|
||||
if (err) console.error("[Database] MySQL persistTicket error:", err.message);
|
||||
});
|
||||
} else if (sqliteDb) {
|
||||
const stmt = sqliteDb.prepare(`
|
||||
INSERT INTO tickets (
|
||||
ticket_id, user_id, subject, description, department, priority, status,
|
||||
ai_mode_enabled, ai_channel_id, ai_type, ai_enabled_by, ai_enabled_at,
|
||||
question_answers, ticket_name, ticket_number, voice_channel_id, created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(ticket_id) DO UPDATE SET
|
||||
user_id=excluded.user_id, subject=excluded.subject, description=excluded.description,
|
||||
department=excluded.department, priority=excluded.priority, status=excluded.status,
|
||||
ai_mode_enabled=excluded.ai_mode_enabled, ai_channel_id=excluded.ai_channel_id,
|
||||
ai_type=excluded.ai_type, ai_enabled_by=excluded.ai_enabled_by, ai_enabled_at=excluded.ai_enabled_at,
|
||||
question_answers=excluded.question_answers, ticket_name=excluded.ticket_name,
|
||||
ticket_number=excluded.ticket_number, voice_channel_id=excluded.voice_channel_id, updated_at=excluded.updated_at;
|
||||
`);
|
||||
stmt.run(
|
||||
t.ticket_id,
|
||||
t.user_id || "",
|
||||
t.subject || "",
|
||||
t.description || "",
|
||||
t.department || "general",
|
||||
t.priority || "medium",
|
||||
t.status || "open",
|
||||
t.aiModeEnabled ? 1 : 0,
|
||||
t.aiChannelId || null,
|
||||
t.aiType || null,
|
||||
t.aiEnabledBy || null,
|
||||
t.aiEnabledAt || null,
|
||||
typeof t.questionAnswers === "string" ? t.questionAnswers : JSON.stringify(t.questionAnswers || []),
|
||||
t.ticketName || null,
|
||||
t.ticketNumber || null,
|
||||
t.voiceChannelId || null,
|
||||
t.created_at || Date.now(),
|
||||
t.updated_at || Date.now()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function persistPanel(panelRow) {
|
||||
if (isMySQL && mysqlPool) {
|
||||
mysqlPool.query(
|
||||
"INSERT INTO ticket_panels (id, message_id, channel_id, created_at) VALUES (?, ?, ?, ?)",
|
||||
[panelRow.id, panelRow.message_id, panelRow.channel_id, panelRow.created_at],
|
||||
(err) => {
|
||||
if (err) console.error("[Database] MySQL persistPanel error:", err.message);
|
||||
}
|
||||
);
|
||||
} else if (sqliteDb) {
|
||||
const stmt = sqliteDb.prepare("INSERT INTO ticket_panels (id, message_id, channel_id, created_at) VALUES (?, ?, ?, ?)");
|
||||
stmt.run(panelRow.id, panelRow.message_id, panelRow.channel_id, panelRow.created_at);
|
||||
}
|
||||
}
|
||||
|
||||
function persistTicketUser(ticketId, userId, action = "add") {
|
||||
if (action === "add") {
|
||||
if (isMySQL && mysqlPool) {
|
||||
mysqlPool.query("INSERT IGNORE INTO ticket_users (ticket_id, user_id) VALUES (?, ?)", [ticketId, userId], (err) => {
|
||||
if (err) console.error("[Database] MySQL persistTicketUser error:", err.message);
|
||||
});
|
||||
} else if (sqliteDb) {
|
||||
sqliteDb.prepare("INSERT OR IGNORE INTO ticket_users (ticket_id, user_id) VALUES (?, ?)").run(ticketId, userId);
|
||||
}
|
||||
} else {
|
||||
if (isMySQL && mysqlPool) {
|
||||
mysqlPool.query("DELETE FROM ticket_users WHERE ticket_id = ? AND user_id = ?", [ticketId, userId], (err) => {
|
||||
if (err) console.error("[Database] MySQL deleteTicketUser error:", err.message);
|
||||
});
|
||||
} else if (sqliteDb) {
|
||||
sqliteDb.prepare("DELETE FROM ticket_users WHERE ticket_id = ? AND user_id = ?").run(ticketId, userId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function persistProfile(userId) {
|
||||
const p = state.profiles[userId];
|
||||
if (!p) return;
|
||||
|
||||
if (isMySQL && mysqlPool) {
|
||||
const sql = `
|
||||
INSERT INTO profiles (user_id, bio, timezone, clocked_in) VALUES (?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE bio=VALUES(bio), timezone=VALUES(timezone), clocked_in=VALUES(clocked_in);
|
||||
`;
|
||||
mysqlPool.query(sql, [p.user_id, p.bio || "", p.timezone || "", p.clockedIn ? 1 : 0], (err) => {
|
||||
if (err) console.error("[Database] MySQL persistProfile error:", err.message);
|
||||
});
|
||||
} else if (sqliteDb) {
|
||||
const stmt = sqliteDb.prepare(`
|
||||
INSERT INTO profiles (user_id, bio, timezone, clocked_in) VALUES (?, ?, ?, ?)
|
||||
ON CONFLICT(user_id) DO UPDATE SET bio=excluded.bio, timezone=excluded.timezone, clocked_in=excluded.clocked_in;
|
||||
`);
|
||||
stmt.run(p.user_id, p.bio || "", p.timezone || "", p.clockedIn ? 1 : 0);
|
||||
}
|
||||
}
|
||||
|
||||
function persistSettings() {
|
||||
const settingsObj = state.settings || {};
|
||||
for (const k of Object.keys(settingsObj)) {
|
||||
const val = JSON.stringify(settingsObj[k]);
|
||||
if (isMySQL && mysqlPool) {
|
||||
mysqlPool.query(
|
||||
"INSERT INTO settings (setting_key, setting_value) VALUES (?, ?) ON DUPLICATE KEY UPDATE setting_value=VALUES(setting_value)",
|
||||
[k, val],
|
||||
(err) => {
|
||||
if (err) console.error("[Database] MySQL persistSettings error:", err.message);
|
||||
}
|
||||
);
|
||||
} else if (sqliteDb) {
|
||||
sqliteDb.prepare("INSERT INTO settings (setting_key, setting_value) VALUES (?, ?) ON CONFLICT(setting_key) DO UPDATE SET setting_value=excluded.setting_value").run(k, val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function persistBlacklistedUser(userId, action = "add") {
|
||||
if (isMySQL && !mysqlReady) {
|
||||
mysqlWriteQueue.push(() => persistBlacklistedUser(userId, action));
|
||||
return;
|
||||
}
|
||||
const b = state.blacklisted_users[userId];
|
||||
if (action === "add" && b) {
|
||||
if (isMySQL && mysqlPool) {
|
||||
const sql = `
|
||||
INSERT INTO blacklisted_users (user_id, reason, added_by, created_at) VALUES (?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE reason=VALUES(reason), added_by=VALUES(added_by), created_at=VALUES(created_at);
|
||||
`;
|
||||
mysqlPool.query(sql, [b.user_id, b.reason || "", b.added_by || "", b.created_at || Date.now()], (err) => {
|
||||
if (err) console.error("[Database] MySQL persistBlacklistedUser error:", err.message);
|
||||
});
|
||||
} else if (sqliteDb) {
|
||||
const stmt = sqliteDb.prepare(`
|
||||
INSERT INTO blacklisted_users (user_id, reason, added_by, created_at) VALUES (?, ?, ?, ?)
|
||||
ON CONFLICT(user_id) DO UPDATE SET reason=excluded.reason, added_by=excluded.added_by, created_at=excluded.created_at;
|
||||
`);
|
||||
stmt.run(b.user_id, b.reason || "", b.added_by || "", b.created_at || Date.now());
|
||||
}
|
||||
} else {
|
||||
if (isMySQL && mysqlPool) {
|
||||
mysqlPool.query("DELETE FROM blacklisted_users WHERE user_id = ?", [userId], (err) => {
|
||||
if (err) console.error("[Database] MySQL removeBlacklistedUser error:", err.message);
|
||||
});
|
||||
} else if (sqliteDb) {
|
||||
sqliteDb.prepare("DELETE FROM blacklisted_users WHERE user_id = ?").run(userId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function ensureProfile(userId) {
|
||||
if (!state.profiles[userId]) {
|
||||
state.profiles[userId] = { user_id: userId, bio: "", timezone: "", clockedIn: 0 };
|
||||
persistProfile(userId);
|
||||
}
|
||||
}
|
||||
|
||||
function normaliseTicketRow(row) {
|
||||
if (!row) return null;
|
||||
let questionAnswers = [];
|
||||
if (row.questionAnswers) {
|
||||
try {
|
||||
questionAnswers = typeof row.questionAnswers === "string" ? JSON.parse(row.questionAnswers) : row.questionAnswers;
|
||||
} catch {
|
||||
questionAnswers = [];
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...row,
|
||||
aiModeEnabled: Boolean(row.aiModeEnabled),
|
||||
clockedIn: row.clockedIn !== undefined ? Boolean(row.clockedIn) : row.clockedIn,
|
||||
questionAnswers,
|
||||
};
|
||||
}
|
||||
|
||||
const exportsModule = {
|
||||
addTicket(ticket) {
|
||||
const now = Date.now();
|
||||
const id = ticket.id;
|
||||
state.tickets[id] = state.tickets[id] || {};
|
||||
const existing = state.tickets[id];
|
||||
|
||||
state.tickets[id] = {
|
||||
ticket_id: id,
|
||||
user_id: ticket.user,
|
||||
subject: ticket.reason || "",
|
||||
description: ticket.description || "",
|
||||
created_at: existing.created_at || now,
|
||||
updated_at: now,
|
||||
department: ticket.department || "general",
|
||||
priority: ticket.priority || "medium",
|
||||
aiModeEnabled: ticket.aiModeEnabled ? 1 : 0,
|
||||
aiChannelId: ticket.aiChannelId || null,
|
||||
aiType: ticket.aiType || null,
|
||||
aiEnabledBy: ticket.aiEnabledBy || null,
|
||||
aiEnabledAt: ticket.aiEnabledAt || null,
|
||||
questionAnswers: JSON.stringify(ticket.questionAnswers || []),
|
||||
ticketName: ticket.name || null,
|
||||
ticketNumber: ticket.number || null,
|
||||
status: existing.status || "open",
|
||||
};
|
||||
|
||||
persistTicket(id);
|
||||
|
||||
if (global.apiServer) {
|
||||
global.apiServer.broadcast("ticket_created", {
|
||||
ticket_id: id,
|
||||
user_id: ticket.user,
|
||||
subject: ticket.reason || "",
|
||||
description: ticket.description || "",
|
||||
department: ticket.department || "general",
|
||||
priority: ticket.priority || "medium",
|
||||
created_at: now,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
updateTicketQuestionAnswers(ticketId, questionAnswers = []) {
|
||||
const t = state.tickets[ticketId];
|
||||
if (!t) return;
|
||||
t.questionAnswers = JSON.stringify(questionAnswers);
|
||||
t.updated_at = Date.now();
|
||||
persistTicket(ticketId);
|
||||
},
|
||||
|
||||
updateTicketVoice(ticketId, voiceChannelId) {
|
||||
const t = state.tickets[ticketId];
|
||||
if (!t) return;
|
||||
t.voiceChannelId = voiceChannelId;
|
||||
t.updated_at = Date.now();
|
||||
persistTicket(ticketId);
|
||||
},
|
||||
|
||||
updateTicketStatus(ticketId, status) {
|
||||
const t = state.tickets[ticketId];
|
||||
if (!t) return;
|
||||
t.status = status;
|
||||
t.updated_at = Date.now();
|
||||
persistTicket(ticketId);
|
||||
|
||||
if (global.apiServer && status === "closed") {
|
||||
global.apiServer.broadcast("ticket_closed", { ticket_id: ticketId });
|
||||
}
|
||||
},
|
||||
|
||||
updateTicketDepartment(ticketId, department) {
|
||||
const t = state.tickets[ticketId];
|
||||
if (!t) return;
|
||||
t.department = department;
|
||||
t.updated_at = Date.now();
|
||||
persistTicket(ticketId);
|
||||
},
|
||||
|
||||
updateTicketPriority(ticketId, priority) {
|
||||
const t = state.tickets[ticketId];
|
||||
if (!t) return;
|
||||
t.priority = priority;
|
||||
t.updated_at = Date.now();
|
||||
persistTicket(ticketId);
|
||||
},
|
||||
|
||||
setTicketAIState(ticketId, aiState = {}) {
|
||||
const t = state.tickets[ticketId];
|
||||
if (!t) return;
|
||||
t.aiModeEnabled = aiState.enabled ? 1 : 0;
|
||||
t.aiChannelId = aiState.aiChannelId || null;
|
||||
t.aiType = aiState.aiType || null;
|
||||
t.aiEnabledBy = aiState.enabledBy || null;
|
||||
t.aiEnabledAt = aiState.enabledAt ? new Date(aiState.enabledAt).getTime() : aiState.enabled ? Date.now() : null;
|
||||
t.updated_at = Date.now();
|
||||
persistTicket(ticketId);
|
||||
},
|
||||
|
||||
enableTicketAI(ticketId, aiChannelId = null, aiType = null, enabledBy = null) {
|
||||
const t = state.tickets[ticketId];
|
||||
if (!t) return;
|
||||
t.aiModeEnabled = 1;
|
||||
t.aiChannelId = aiChannelId;
|
||||
t.aiType = aiType;
|
||||
t.aiEnabledBy = enabledBy;
|
||||
t.aiEnabledAt = Date.now();
|
||||
t.updated_at = Date.now();
|
||||
persistTicket(ticketId);
|
||||
},
|
||||
|
||||
disableTicketAI(ticketId) {
|
||||
const t = state.tickets[ticketId];
|
||||
if (!t) return;
|
||||
t.aiModeEnabled = 0;
|
||||
t.updated_at = Date.now();
|
||||
persistTicket(ticketId);
|
||||
},
|
||||
|
||||
getTicket(ticketId) {
|
||||
return normaliseTicketRow(state.tickets[ticketId]);
|
||||
},
|
||||
|
||||
getTicketByAIChannel(aiChannelId) {
|
||||
const keys = Object.keys(state.tickets);
|
||||
for (const k of keys) {
|
||||
const t = state.tickets[k];
|
||||
if (t && t.aiChannelId === aiChannelId) return normaliseTicketRow(t);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
getAllTickets() {
|
||||
return Object.values(state.tickets).map(normaliseTicketRow);
|
||||
},
|
||||
|
||||
saveTicketPanel(messageId, channelId) {
|
||||
const row = { id: Date.now(), message_id: messageId, channel_id: channelId, created_at: Date.now() };
|
||||
state.ticket_panel.push(row);
|
||||
persistPanel(row);
|
||||
},
|
||||
|
||||
getTicketPanel() {
|
||||
const arr = state.ticket_panel;
|
||||
if (!arr || arr.length === 0) return null;
|
||||
return arr[arr.length - 1];
|
||||
},
|
||||
|
||||
addUserToTicket(ticketId, userId) {
|
||||
state.ticket_users[ticketId] = state.ticket_users[ticketId] || [];
|
||||
if (!state.ticket_users[ticketId].includes(userId)) {
|
||||
state.ticket_users[ticketId].push(userId);
|
||||
persistTicketUser(ticketId, userId, "add");
|
||||
}
|
||||
},
|
||||
|
||||
removeUserFromTicket(ticketId, userId) {
|
||||
if (state.ticket_users[ticketId]) {
|
||||
state.ticket_users[ticketId] = state.ticket_users[ticketId].filter((u) => u !== userId);
|
||||
persistTicketUser(ticketId, userId, "remove");
|
||||
}
|
||||
},
|
||||
|
||||
getTicketUsers(ticketId) {
|
||||
return (state.ticket_users[ticketId] || []).slice();
|
||||
},
|
||||
|
||||
getSettings() {
|
||||
return state.settings || {};
|
||||
},
|
||||
|
||||
getProfile(userId) {
|
||||
ensureProfile(userId);
|
||||
const row = state.profiles[userId];
|
||||
if (!row) return { user_id: userId, bio: "", timezone: "", clockedIn: false };
|
||||
return { ...row, clockedIn: Boolean(row.clockedIn) };
|
||||
},
|
||||
|
||||
setClockedIn(userId, clockedIn) {
|
||||
ensureProfile(userId);
|
||||
state.profiles[userId].clockedIn = clockedIn ? 1 : 0;
|
||||
persistProfile(userId);
|
||||
},
|
||||
|
||||
saveSettings(obj) {
|
||||
state.settings = obj || {};
|
||||
persistSettings();
|
||||
},
|
||||
|
||||
addBlacklistedUser(userId, reason = "", addedBy = "") {
|
||||
state.blacklisted_users[userId] = {
|
||||
user_id: userId,
|
||||
reason,
|
||||
added_by: addedBy,
|
||||
created_at: Date.now(),
|
||||
};
|
||||
persistBlacklistedUser(userId, "add");
|
||||
},
|
||||
|
||||
removeBlacklistedUser(userId) {
|
||||
if (state.blacklisted_users[userId]) {
|
||||
delete state.blacklisted_users[userId];
|
||||
persistBlacklistedUser(userId, "remove");
|
||||
}
|
||||
},
|
||||
|
||||
isUserBlacklisted(userId) {
|
||||
return Boolean(state.blacklisted_users[userId]);
|
||||
},
|
||||
|
||||
getBlacklistedUsers() {
|
||||
return Object.keys(state.blacklisted_users);
|
||||
},
|
||||
|
||||
getBlacklistedUserDetails() {
|
||||
return Object.values(state.blacklisted_users);
|
||||
},
|
||||
|
||||
getDatabaseInfo() {
|
||||
if (isMySQL) {
|
||||
const mysqlOpts = dbConfig.MySQL || {};
|
||||
return {
|
||||
driver: "MySQL",
|
||||
host: mysqlOpts.Host || "127.0.0.1",
|
||||
port: mysqlOpts.Port || 3306,
|
||||
database: mysqlOpts.Database || "supportbot",
|
||||
type: "mysql"
|
||||
};
|
||||
}
|
||||
const sqliteOpts = dbConfig.SQLite || {};
|
||||
return {
|
||||
driver: "SQLite",
|
||||
file: sqliteOpts.File || "./Data/supportbot.db",
|
||||
type: "sqlite"
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
// Initialize database on require
|
||||
initDatabase();
|
||||
|
||||
module.exports = exportsModule;
|
||||
@@ -0,0 +1,233 @@
|
||||
const crypto = require("crypto");
|
||||
const axios = require("axios");
|
||||
|
||||
const DISCORD_API = "https://discord.com/api";
|
||||
const OAUTH_STATE_COOKIE = "sb_oauth_state";
|
||||
const OAUTH_REDIRECT_COOKIE = "sb_oauth_redirect";
|
||||
const OAUTH_STATE_MAX_AGE_MS = 10 * 60 * 1000;
|
||||
const CALLBACK_PATH = "/api/auth/discord/callback";
|
||||
|
||||
function discordAvatarUrl(user) {
|
||||
if (user.avatar) {
|
||||
const ext = user.avatar.startsWith("a_") ? "gif" : "png";
|
||||
return `https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}.${ext}?size=128`;
|
||||
}
|
||||
const index = Number((BigInt(user.id) >> 22n) % 6n);
|
||||
return `https://cdn.discordapp.com/embed/avatars/${index}.png`;
|
||||
}
|
||||
|
||||
function getTrustProxySetting(apiConfig) {
|
||||
if (apiConfig?.TrustProxy === false) return false;
|
||||
if (typeof apiConfig?.TrustProxy === "number") return apiConfig.TrustProxy;
|
||||
if (apiConfig?.TrustProxy === true) return 1;
|
||||
return false;
|
||||
}
|
||||
|
||||
function getRequestProto(req, apiConfig) {
|
||||
if (apiConfig?.TrustProxy !== false) {
|
||||
const forwarded = req.get("x-forwarded-proto");
|
||||
if (forwarded) {
|
||||
return forwarded.split(",")[0].trim().toLowerCase();
|
||||
}
|
||||
}
|
||||
return (req.protocol || "http").toLowerCase();
|
||||
}
|
||||
|
||||
function getRequestHost(req) {
|
||||
const forwarded = req.get("x-forwarded-host");
|
||||
if (forwarded) {
|
||||
return forwarded.split(",")[0].trim();
|
||||
}
|
||||
return req.get("host") || "";
|
||||
}
|
||||
|
||||
function getRequestOrigin(req, apiConfig) {
|
||||
const proto = getRequestProto(req, apiConfig);
|
||||
const host = getRequestHost(req);
|
||||
if (!host) return null;
|
||||
return `${proto}://${host}`;
|
||||
}
|
||||
|
||||
function normalizeRedirectUri(uri) {
|
||||
try {
|
||||
const u = new URL(uri);
|
||||
u.hash = "";
|
||||
u.search = "";
|
||||
return u.toString();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function redirectUriMatchesHostPath(a, b) {
|
||||
try {
|
||||
const ua = new URL(a);
|
||||
const ub = new URL(b);
|
||||
return ua.host === ub.host && ua.pathname === ub.pathname;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function collectConfiguredRedirectUris(oauth) {
|
||||
const list = [];
|
||||
if (oauth?.RedirectUri) {
|
||||
const normalized = normalizeRedirectUri(oauth.RedirectUri);
|
||||
if (normalized) list.push(normalized);
|
||||
}
|
||||
if (Array.isArray(oauth?.RedirectUris)) {
|
||||
for (const entry of oauth.RedirectUris) {
|
||||
const normalized = normalizeRedirectUri(entry);
|
||||
if (normalized && !list.includes(normalized)) {
|
||||
list.push(normalized);
|
||||
}
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pick the OAuth redirect_uri for this request. When UseRequestOrigin is enabled
|
||||
* (default), uses the same host/path as RedirectUri but the protocol the user
|
||||
* actually used (http vs https). Register every variant in the Discord portal.
|
||||
*/
|
||||
function resolveOAuthRedirectUri(req, oauth, apiConfig) {
|
||||
const configured = collectConfiguredRedirectUris(oauth);
|
||||
const fallback = configured[0] || oauth?.RedirectUri || "";
|
||||
|
||||
const useRequestOrigin = oauth?.UseRequestOrigin !== false;
|
||||
if (!useRequestOrigin || !req) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
const origin = getRequestOrigin(req, apiConfig);
|
||||
if (!origin) return fallback;
|
||||
|
||||
const dynamic = `${origin}${CALLBACK_PATH}`;
|
||||
const normalizedDynamic = normalizeRedirectUri(dynamic);
|
||||
if (!normalizedDynamic) return fallback;
|
||||
|
||||
if (configured.includes(normalizedDynamic)) {
|
||||
return normalizedDynamic;
|
||||
}
|
||||
|
||||
const matchesConfigured = configured.some((uri) =>
|
||||
redirectUriMatchesHostPath(uri, normalizedDynamic),
|
||||
);
|
||||
if (matchesConfigured) {
|
||||
return normalizedDynamic;
|
||||
}
|
||||
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function isSecureRequest(req, oauth, apiConfig) {
|
||||
if (oauth?.CookieSecure === true) return true;
|
||||
if (oauth?.CookieSecure === false) return false;
|
||||
if (req) {
|
||||
if (typeof req.secure === "boolean") return req.secure;
|
||||
return getRequestProto(req, apiConfig) === "https";
|
||||
}
|
||||
return process.env.NODE_ENV === "production";
|
||||
}
|
||||
|
||||
function buildAuthorizeUrl(oauth, state, redirectUri) {
|
||||
const params = new URLSearchParams({
|
||||
client_id: oauth.ClientId,
|
||||
redirect_uri: redirectUri,
|
||||
response_type: "code",
|
||||
scope: "identify",
|
||||
state,
|
||||
prompt: "none",
|
||||
});
|
||||
return `${DISCORD_API}/oauth2/authorize?${params.toString()}`;
|
||||
}
|
||||
|
||||
async function exchangeCode(oauth, code, redirectUri) {
|
||||
const body = new URLSearchParams({
|
||||
client_id: oauth.ClientId,
|
||||
client_secret: oauth.ClientSecret,
|
||||
grant_type: "authorization_code",
|
||||
code,
|
||||
redirect_uri: redirectUri,
|
||||
});
|
||||
|
||||
const { data } = await axios.post(`${DISCORD_API}/oauth2/token`, body, {
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
timeout: 15000,
|
||||
});
|
||||
|
||||
return data.access_token;
|
||||
}
|
||||
|
||||
async function fetchDiscordUser(accessToken) {
|
||||
const { data } = await axios.get(`${DISCORD_API}/users/@me`, {
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
timeout: 15000,
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
function createOAuthState() {
|
||||
return crypto.randomBytes(24).toString("hex");
|
||||
}
|
||||
|
||||
function oauthStateCookieOptions(req, oauth, apiConfig) {
|
||||
return {
|
||||
httpOnly: true,
|
||||
secure: isSecureRequest(req, oauth, apiConfig),
|
||||
sameSite: "lax",
|
||||
maxAge: OAUTH_STATE_MAX_AGE_MS,
|
||||
path: "/",
|
||||
};
|
||||
}
|
||||
|
||||
function oauthRedirectCookieOptions(req, oauth, apiConfig) {
|
||||
return oauthStateCookieOptions(req, oauth, apiConfig);
|
||||
}
|
||||
|
||||
function isOAuthConfigured(oauth) {
|
||||
return Boolean(
|
||||
oauth?.Enabled &&
|
||||
oauth.ClientId &&
|
||||
oauth.ClientSecret &&
|
||||
oauth.RedirectUri,
|
||||
);
|
||||
}
|
||||
|
||||
function isUserAllowed(userId, oauth) {
|
||||
const allowed = oauth?.AllowedUserIds;
|
||||
if (!Array.isArray(allowed) || allowed.length === 0) {
|
||||
return false;
|
||||
}
|
||||
return allowed.map(String).includes(String(userId));
|
||||
}
|
||||
|
||||
function formatPublicUser(user) {
|
||||
return {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
globalName: user.global_name || null,
|
||||
avatar: discordAvatarUrl(user),
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
OAUTH_STATE_COOKIE,
|
||||
OAUTH_REDIRECT_COOKIE,
|
||||
OAUTH_STATE_MAX_AGE_MS,
|
||||
CALLBACK_PATH,
|
||||
getTrustProxySetting,
|
||||
resolveOAuthRedirectUri,
|
||||
isSecureRequest,
|
||||
buildAuthorizeUrl,
|
||||
exchangeCode,
|
||||
fetchDiscordUser,
|
||||
createOAuthState,
|
||||
oauthStateCookieOptions,
|
||||
oauthRedirectCookieOptions,
|
||||
isOAuthConfigured,
|
||||
isUserAllowed,
|
||||
formatPublicUser,
|
||||
discordAvatarUrl,
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
// SupportBot | Emerald Services
|
||||
// Event Structure
|
||||
|
||||
class Event {
|
||||
constructor(event, run) {
|
||||
this.event = event;
|
||||
this.run = run;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Event;
|
||||
@@ -0,0 +1,277 @@
|
||||
const { ChannelType } = require("discord.js");
|
||||
const configStore = require("./ConfigStore.js");
|
||||
const { withTimeout } = require("./asyncTimeout.js");
|
||||
|
||||
const RESOURCES_CACHE_MS = 90 * 1000;
|
||||
/** Bump when resource payload shape changes (e.g. added emojis). */
|
||||
const RESOURCES_CACHE_VERSION = 2;
|
||||
let resourcesCache = { guildId: null, data: null, at: 0, version: 0 };
|
||||
let resourcesInflight = null;
|
||||
|
||||
function getConfiguredGuildId() {
|
||||
const id = configStore.supportbot?.General?.GuildId;
|
||||
if (!id || id === "GUILD_ID" || String(id).trim() === "") return null;
|
||||
return String(id).trim();
|
||||
}
|
||||
|
||||
function setConfiguredGuildId(guildId) {
|
||||
if (!configStore.supportbot.General) {
|
||||
configStore.supportbot.General = {};
|
||||
}
|
||||
configStore.supportbot.General.GuildId = guildId ? String(guildId) : "";
|
||||
}
|
||||
|
||||
function getPrimaryGuild(client) {
|
||||
if (!client) return null;
|
||||
|
||||
const configuredId = getConfiguredGuildId();
|
||||
if (configuredId) {
|
||||
const match = client.guilds.cache.get(configuredId);
|
||||
if (match) return match;
|
||||
}
|
||||
|
||||
return client.guilds.cache.first() || null;
|
||||
}
|
||||
|
||||
function evaluateGuildHealth(client) {
|
||||
const configuredId = getConfiguredGuildId();
|
||||
const guilds = [...client.guilds.cache.values()];
|
||||
|
||||
if (!configuredId) {
|
||||
return {
|
||||
status: "missing_config",
|
||||
configuredGuildId: null,
|
||||
message:
|
||||
"No guild ID is set. Add your Discord server ID under Bot config → General.",
|
||||
configuredGuild: null,
|
||||
extraGuilds: guilds.map(formatGuildSummary),
|
||||
};
|
||||
}
|
||||
|
||||
const configuredGuild = client.guilds.cache.get(configuredId) || null;
|
||||
const extraGuilds = guilds
|
||||
.filter((g) => g.id !== configuredId)
|
||||
.map(formatGuildSummary);
|
||||
|
||||
if (!configuredGuild) {
|
||||
return {
|
||||
status: "not_in_guild",
|
||||
configuredGuildId: configuredId,
|
||||
message: `The bot is not in the configured server (${configuredId}). Invite it or update the guild ID.`,
|
||||
configuredGuild: null,
|
||||
extraGuilds: guilds.map(formatGuildSummary),
|
||||
};
|
||||
}
|
||||
|
||||
if (extraGuilds.length > 0) {
|
||||
return {
|
||||
status: "extra_guilds",
|
||||
configuredGuildId: configuredId,
|
||||
message: `The bot is in ${extraGuilds.length} extra server(s). Leave them so only your configured server remains.`,
|
||||
configuredGuild: formatGuildSummary(configuredGuild),
|
||||
extraGuilds,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: "ok",
|
||||
configuredGuildId: configuredId,
|
||||
message: null,
|
||||
configuredGuild: formatGuildSummary(configuredGuild),
|
||||
extraGuilds: [],
|
||||
};
|
||||
}
|
||||
|
||||
function formatGuildSummary(guild) {
|
||||
return {
|
||||
id: guild.id,
|
||||
name: guild.name,
|
||||
memberCount: guild.memberCount ?? null,
|
||||
icon: guild.iconURL?.({ size: 128 }) ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
function syncGuildHealth(client) {
|
||||
if (!client?.user) {
|
||||
client.guildHealth = {
|
||||
status: "offline",
|
||||
configuredGuildId: getConfiguredGuildId(),
|
||||
message: "Bot is not connected to Discord.",
|
||||
configuredGuild: null,
|
||||
extraGuilds: [],
|
||||
};
|
||||
return client.guildHealth;
|
||||
}
|
||||
|
||||
client.guildHealth = evaluateGuildHealth(client);
|
||||
return client.guildHealth;
|
||||
}
|
||||
|
||||
function logGuildWarning(client, message) {
|
||||
const name = client?.user?.username || "Bot";
|
||||
console.warn(`\x1b[33m[Guild]\x1b[0m ${name}: ${message}`);
|
||||
}
|
||||
|
||||
async function leaveGuild(client, guildId) {
|
||||
const guild = client.guilds.cache.get(String(guildId));
|
||||
if (!guild) {
|
||||
throw new Error("Bot is not in that server.");
|
||||
}
|
||||
|
||||
const configuredId = getConfiguredGuildId();
|
||||
if (configuredId && guild.id === configuredId) {
|
||||
throw new Error("Cannot leave the configured server. Change the guild ID first.");
|
||||
}
|
||||
|
||||
await guild.leave();
|
||||
syncGuildHealth(client);
|
||||
return syncGuildHealth(client);
|
||||
}
|
||||
|
||||
function buildResourcesFromGuild(guild) {
|
||||
const roles = [...guild.roles.cache.values()]
|
||||
.filter((r) => r.id !== guild.id)
|
||||
.sort((a, b) => b.position - a.position)
|
||||
.map((r) => ({
|
||||
id: r.id,
|
||||
name: r.name,
|
||||
color: r.hexColor || null,
|
||||
}));
|
||||
|
||||
const channels = [...guild.channels.cache.values()]
|
||||
.filter(
|
||||
(c) =>
|
||||
c.type === ChannelType.GuildText ||
|
||||
c.type === ChannelType.GuildVoice ||
|
||||
c.type === ChannelType.GuildAnnouncement ||
|
||||
c.type === ChannelType.GuildStageVoice ||
|
||||
c.type === ChannelType.GuildForum,
|
||||
)
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map((c) => ({
|
||||
id: c.id,
|
||||
name: c.name,
|
||||
type: c.type,
|
||||
}));
|
||||
|
||||
const categories = [...guild.channels.cache.values()]
|
||||
.filter((c) => c.type === ChannelType.GuildCategory)
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map((c) => ({
|
||||
id: c.id,
|
||||
name: c.name,
|
||||
}));
|
||||
|
||||
const emojis = [...guild.emojis.cache.values()]
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map((e) => {
|
||||
const animated = Boolean(e.animated);
|
||||
const ext = animated ? "gif" : "png";
|
||||
return {
|
||||
id: e.id,
|
||||
name: e.name,
|
||||
animated,
|
||||
url:
|
||||
e.imageURL({ extension: ext, size: 64 }) ||
|
||||
`https://cdn.discordapp.com/emojis/${e.id}.${ext}?size=64`,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
guildId: guild.id,
|
||||
guildName: guild.name,
|
||||
guildIcon: guild.iconURL({ extension: "png", size: 64 }) || null,
|
||||
roles,
|
||||
channels,
|
||||
categories,
|
||||
emojis,
|
||||
};
|
||||
}
|
||||
|
||||
async function fetchGuildResourcesFromDiscord(client, guildId) {
|
||||
const id = guildId || getConfiguredGuildId();
|
||||
if (!id) return null;
|
||||
|
||||
const guild = client.guilds.cache.get(id);
|
||||
if (!guild) return null;
|
||||
|
||||
try {
|
||||
await withTimeout(
|
||||
Promise.all([guild.channels.fetch(), guild.roles.fetch()]),
|
||||
12_000,
|
||||
"Discord channel/role fetch timed out",
|
||||
);
|
||||
} catch (err) {
|
||||
console.warn("[Guild] Failed to fetch channels/roles:", err.message);
|
||||
if (guild.roles.cache.size === 0 && guild.channels.cache.size === 0) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await withTimeout(guild.emojis.fetch(), 12_000, "Discord emoji fetch timed out");
|
||||
} catch (err) {
|
||||
console.warn("[Guild] Failed to fetch guild emojis:", err.message);
|
||||
}
|
||||
|
||||
return buildResourcesFromGuild(guild);
|
||||
}
|
||||
|
||||
async function fetchGuildResources(client, guildId) {
|
||||
const id = guildId || getConfiguredGuildId();
|
||||
if (!id || !client?.user) return null;
|
||||
|
||||
const now = Date.now();
|
||||
if (
|
||||
resourcesCache.data &&
|
||||
resourcesCache.version === RESOURCES_CACHE_VERSION &&
|
||||
resourcesCache.guildId === id &&
|
||||
now - resourcesCache.at < RESOURCES_CACHE_MS
|
||||
) {
|
||||
return resourcesCache.data;
|
||||
}
|
||||
|
||||
if (resourcesInflight) {
|
||||
try {
|
||||
return await resourcesInflight;
|
||||
} catch {
|
||||
/* fall through to retry */
|
||||
}
|
||||
}
|
||||
|
||||
resourcesInflight = (async () => {
|
||||
const data = await fetchGuildResourcesFromDiscord(client, id);
|
||||
if (data) {
|
||||
resourcesCache = {
|
||||
guildId: id,
|
||||
data,
|
||||
at: Date.now(),
|
||||
version: RESOURCES_CACHE_VERSION,
|
||||
};
|
||||
}
|
||||
return data;
|
||||
})();
|
||||
|
||||
try {
|
||||
return await resourcesInflight;
|
||||
} finally {
|
||||
resourcesInflight = null;
|
||||
}
|
||||
}
|
||||
|
||||
function clearGuildResourcesCache() {
|
||||
resourcesCache = { guildId: null, data: null, at: 0, version: 0 };
|
||||
resourcesInflight = null;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getConfiguredGuildId,
|
||||
setConfiguredGuildId,
|
||||
getPrimaryGuild,
|
||||
evaluateGuildHealth,
|
||||
syncGuildHealth,
|
||||
logGuildWarning,
|
||||
leaveGuild,
|
||||
fetchGuildResources,
|
||||
clearGuildResourcesCache,
|
||||
};
|
||||
@@ -0,0 +1,181 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const LOG_ROOT = path.resolve(__dirname, "../Logs");
|
||||
const LOG_TYPES = ["Output", "Warn", "Error"];
|
||||
|
||||
function todayDate() {
|
||||
return new Date().toISOString().split("T")[0];
|
||||
}
|
||||
|
||||
function getLogPath(type, date = todayDate()) {
|
||||
return path.join(LOG_ROOT, type, `${type}-${date}.log`);
|
||||
}
|
||||
|
||||
/** Prefer today's file; fall back to the newest log in the folder. */
|
||||
function resolveLogPath(type) {
|
||||
const todayPath = getLogPath(type);
|
||||
if (fs.existsSync(todayPath)) return todayPath;
|
||||
|
||||
const dir = path.join(LOG_ROOT, type);
|
||||
if (!fs.existsSync(dir)) return todayPath;
|
||||
|
||||
const files = fs
|
||||
.readdirSync(dir)
|
||||
.filter((f) => f.endsWith(".log"))
|
||||
.sort()
|
||||
.reverse();
|
||||
|
||||
if (!files.length) return todayPath;
|
||||
return path.join(dir, files[0]);
|
||||
}
|
||||
|
||||
function stripAnsi(text) {
|
||||
return String(text).replace(/\x1b\[[0-9;]*m/g, "");
|
||||
}
|
||||
|
||||
function parseLogLine(line, type) {
|
||||
const trimmed = line.trimEnd();
|
||||
if (!trimmed) return null;
|
||||
|
||||
const match = trimmed.match(/^\[([^\]]+)\]\s?(.*)$/);
|
||||
if (match) {
|
||||
return {
|
||||
type,
|
||||
timestamp: match[1],
|
||||
message: stripAnsi(match[2]),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
type,
|
||||
timestamp: null,
|
||||
message: stripAnsi(trimmed),
|
||||
};
|
||||
}
|
||||
|
||||
function readFromByte(filePath, fromByte) {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return { lines: [], position: 0 };
|
||||
}
|
||||
|
||||
const stat = fs.statSync(filePath);
|
||||
if (stat.size === 0) {
|
||||
return { lines: [], position: 0 };
|
||||
}
|
||||
|
||||
const start = Math.max(0, Math.min(fromByte, stat.size));
|
||||
if (start >= stat.size) {
|
||||
return { lines: [], position: stat.size };
|
||||
}
|
||||
|
||||
const fd = fs.openSync(filePath, "r");
|
||||
const buffer = Buffer.alloc(stat.size - start);
|
||||
fs.readSync(fd, buffer, 0, buffer.length, start);
|
||||
fs.closeSync(fd);
|
||||
|
||||
const text = buffer.toString("utf8");
|
||||
const lines = text.split(/\r?\n/).filter((l) => l.length > 0);
|
||||
|
||||
return { lines, position: stat.size };
|
||||
}
|
||||
|
||||
function readLastLines(filePath, maxLines) {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return { lines: [], position: 0 };
|
||||
}
|
||||
|
||||
const stat = fs.statSync(filePath);
|
||||
if (stat.size === 0) {
|
||||
return { lines: [], position: 0 };
|
||||
}
|
||||
|
||||
const chunkSize = Math.min(stat.size, 512 * 1024);
|
||||
const start = Math.max(0, stat.size - chunkSize);
|
||||
const fd = fs.openSync(filePath, "r");
|
||||
const buffer = Buffer.alloc(chunkSize);
|
||||
fs.readSync(fd, buffer, 0, chunkSize, start);
|
||||
fs.closeSync(fd);
|
||||
|
||||
const lines = buffer
|
||||
.toString("utf8")
|
||||
.split(/\r?\n/)
|
||||
.filter((l) => l.length > 0);
|
||||
const slice = lines.slice(-maxLines);
|
||||
|
||||
return { lines: slice, position: stat.size };
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string[]} types
|
||||
* @param {Record<string, number>} cursor byte offsets per type
|
||||
* @param {number} initialLines lines to load when cursor is empty
|
||||
*/
|
||||
function fetchLogs(types, cursor = {}, initialLines = 300) {
|
||||
const validTypes = types.filter((t) => LOG_TYPES.includes(t));
|
||||
const entries = [];
|
||||
const nextCursor = { ...cursor };
|
||||
|
||||
for (const type of validTypes) {
|
||||
const filePath = resolveLogPath(type);
|
||||
const prevPos = cursor[type] ?? 0;
|
||||
let lines = [];
|
||||
let position = 0;
|
||||
|
||||
if (!fs.existsSync(filePath)) {
|
||||
nextCursor[type] = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (prevPos === 0 && initialLines > 0) {
|
||||
const tail = readLastLines(filePath, initialLines);
|
||||
lines = tail.lines;
|
||||
position = tail.position;
|
||||
} else if (prevPos > 0) {
|
||||
const chunk = readFromByte(filePath, prevPos);
|
||||
lines = chunk.lines;
|
||||
position = chunk.position;
|
||||
} else {
|
||||
// No tail requested and no cursor — skip content, anchor at EOF
|
||||
position = fs.statSync(filePath).size;
|
||||
}
|
||||
|
||||
nextCursor[type] = position;
|
||||
|
||||
for (const line of lines) {
|
||||
const entry = parseLogLine(line, type);
|
||||
if (entry) entries.push(entry);
|
||||
}
|
||||
}
|
||||
|
||||
entries.sort((a, b) => {
|
||||
const ta = a.timestamp ? Date.parse(a.timestamp) : 0;
|
||||
const tb = b.timestamp ? Date.parse(b.timestamp) : 0;
|
||||
return ta - tb;
|
||||
});
|
||||
|
||||
return { entries, cursor: nextCursor };
|
||||
}
|
||||
|
||||
function getLogMeta() {
|
||||
const date = todayDate();
|
||||
return LOG_TYPES.map((type) => {
|
||||
const filePath = resolveLogPath(type);
|
||||
const exists = fs.existsSync(filePath) && fs.statSync(filePath).size > 0;
|
||||
return {
|
||||
type,
|
||||
date,
|
||||
file: exists ? path.basename(filePath) : null,
|
||||
size: exists ? fs.statSync(filePath).size : 0,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
LOG_ROOT,
|
||||
LOG_TYPES,
|
||||
fetchLogs,
|
||||
getLogMeta,
|
||||
getLogPath,
|
||||
resolveLogPath,
|
||||
};
|
||||
@@ -0,0 +1,270 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const PLACEHOLDER_PATTERNS = [
|
||||
/ROLE_ID/i,
|
||||
/CHANNEL_ID/i,
|
||||
/CATEGORY_ID/i,
|
||||
/TAG_ID/i,
|
||||
/FORUM_CHANNEL_ID/i,
|
||||
/BOT_TOKEN/i,
|
||||
/MODEL_API_KEY/i,
|
||||
/PASTEBIN_API_KEY/i,
|
||||
/YOUR_/i,
|
||||
/PUT_RANDOM_STRING/i,
|
||||
/example\.com/i,
|
||||
];
|
||||
|
||||
function isEmpty(value) {
|
||||
if (value == null) return true;
|
||||
if (typeof value === "boolean") return false;
|
||||
if (typeof value === "number") return false;
|
||||
return String(value).trim() === "";
|
||||
}
|
||||
|
||||
function isPlaceholder(value) {
|
||||
if (isEmpty(value)) return true;
|
||||
const s = String(value).trim();
|
||||
return PLACEHOLDER_PATTERNS.some((p) => p.test(s));
|
||||
}
|
||||
|
||||
function isDiscordId(value) {
|
||||
return /^\d{17,20}$/.test(String(value).trim());
|
||||
}
|
||||
|
||||
function isConfiguredId(value) {
|
||||
return isDiscordId(value) && !isPlaceholder(value);
|
||||
}
|
||||
|
||||
function isValidToken(value) {
|
||||
if (isPlaceholder(value)) return false;
|
||||
const s = String(value).trim();
|
||||
return s.length >= 50 && s.includes(".");
|
||||
}
|
||||
|
||||
function scoreChecks(checks) {
|
||||
const total = checks.length;
|
||||
const passed = checks.filter((c) => c.ok).length;
|
||||
|
||||
if (total === 0) {
|
||||
return { status: "done", subtitle: "Ready", passed: 0, total: 0 };
|
||||
}
|
||||
|
||||
if (passed === 0) {
|
||||
const hint = checks.find((c) => !c.ok)?.hint ?? "Not configured yet";
|
||||
return { status: "todo", subtitle: hint, passed, total };
|
||||
}
|
||||
|
||||
if (passed === total) {
|
||||
return { status: "done", subtitle: "Configured and ready", passed, total };
|
||||
}
|
||||
|
||||
const next = checks.find((c) => !c.ok);
|
||||
return {
|
||||
status: "progress",
|
||||
subtitle: next?.hint ?? `${passed} of ${total} steps complete`,
|
||||
passed,
|
||||
total,
|
||||
};
|
||||
}
|
||||
|
||||
function evaluateTickets(supportbot, ticketPanel) {
|
||||
const ticket = supportbot?.Ticket ?? {};
|
||||
const ticketType = String(ticket.TicketType || "channels").toLowerCase();
|
||||
|
||||
return scoreChecks([
|
||||
{
|
||||
ok: ticketPanel?.Panel === true,
|
||||
hint: "Enable the ticket panel",
|
||||
},
|
||||
{
|
||||
ok: isConfiguredId(ticket.TicketHome),
|
||||
hint: "Set TicketHome channel ID",
|
||||
},
|
||||
{
|
||||
ok:
|
||||
ticketType !== "channels" ||
|
||||
isConfiguredId(ticket.TicketChannelsCategory),
|
||||
hint: "Set ticket category ID",
|
||||
},
|
||||
{
|
||||
ok: !isPlaceholder(ticketPanel?.Button?.Text),
|
||||
hint: "Configure panel button text",
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
function evaluateSupportbot(supportbot) {
|
||||
const roles = supportbot?.Roles?.StaffMember ?? {};
|
||||
const welcome = supportbot?.Welcome ?? {};
|
||||
|
||||
return scoreChecks([
|
||||
{ ok: isValidToken(supportbot?.General?.Token), hint: "Add your bot token" },
|
||||
{
|
||||
ok:
|
||||
isConfiguredId(roles.Staff) ||
|
||||
isConfiguredId(roles.Admin) ||
|
||||
isConfiguredId(roles.Moderator),
|
||||
hint: "Set staff role IDs",
|
||||
},
|
||||
{
|
||||
ok: !welcome.Enabled || isConfiguredId(welcome.Channel),
|
||||
hint: "Set welcome channel ID",
|
||||
},
|
||||
{
|
||||
ok: !isPlaceholder(supportbot?.Activity?.Status),
|
||||
hint: "Set bot activity status",
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
function evaluateTranscripts(supportbot, transcriptCount) {
|
||||
const ticketReady = isConfiguredId(supportbot?.Ticket?.TicketHome);
|
||||
|
||||
if (!ticketReady) {
|
||||
// Ticket setup is tracked by the tickets module — avoid a misleading transcripts alert.
|
||||
return {
|
||||
status: "done",
|
||||
subtitle: "Available after ticket channels are configured",
|
||||
passed: 0,
|
||||
total: 0,
|
||||
};
|
||||
}
|
||||
|
||||
if (transcriptCount > 0) {
|
||||
return {
|
||||
status: "done",
|
||||
subtitle: `${transcriptCount} transcript${transcriptCount === 1 ? "" : "s"} saved`,
|
||||
passed: 1,
|
||||
total: 1,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: "done",
|
||||
subtitle: "No transcripts yet — they are created when tickets close",
|
||||
passed: 1,
|
||||
total: 1,
|
||||
};
|
||||
}
|
||||
|
||||
function evaluateSuggestions(supportbot) {
|
||||
const sug = supportbot?.Suggestions ?? {};
|
||||
const mode = String(sug.Mode || "Channel").toLowerCase();
|
||||
|
||||
const channelOk =
|
||||
mode === "forum"
|
||||
? isConfiguredId(sug.ForumChannel)
|
||||
: isConfiguredId(sug.Channel);
|
||||
|
||||
return scoreChecks([
|
||||
{
|
||||
ok: channelOk,
|
||||
hint:
|
||||
mode === "forum"
|
||||
? "Set suggestions forum channel"
|
||||
: "Set suggestions channel ID",
|
||||
},
|
||||
{
|
||||
ok: !isPlaceholder(sug.UpvoteEmoji) && !isPlaceholder(sug.DownvoteEmoji),
|
||||
hint: "Configure voting emojis",
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
function evaluateAi(supportbotAi) {
|
||||
if (supportbotAi?.Enabled !== true) {
|
||||
return {
|
||||
status: "progress",
|
||||
subtitle: "AI is disabled in config",
|
||||
passed: 0,
|
||||
total: 3,
|
||||
};
|
||||
}
|
||||
|
||||
return scoreChecks([
|
||||
{
|
||||
ok: !isPlaceholder(supportbotAi?.General?.Model_API_Key),
|
||||
hint: "Add your model API key",
|
||||
},
|
||||
{
|
||||
ok: isConfiguredId(supportbotAi?.Channels?.AIChannel),
|
||||
hint: "Set AI channel ID",
|
||||
},
|
||||
{
|
||||
ok: !isPlaceholder(supportbotAi?.General?.Model),
|
||||
hint: "Choose an AI model",
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
function evaluateCommands(commands) {
|
||||
const entries = Object.entries(commands || {}).filter(
|
||||
([, v]) => v && typeof v === "object" && "Enabled" in v,
|
||||
);
|
||||
|
||||
if (!entries.length) {
|
||||
return { status: "todo", subtitle: "No commands configured", passed: 0, total: 0 };
|
||||
}
|
||||
|
||||
const enabled = entries.filter(([, v]) => v.Enabled !== false).length;
|
||||
const total = entries.length;
|
||||
const ratio = enabled / total;
|
||||
|
||||
if (ratio >= 0.85) {
|
||||
return {
|
||||
status: "done",
|
||||
subtitle: `${enabled} of ${total} commands enabled`,
|
||||
passed: enabled,
|
||||
total,
|
||||
};
|
||||
}
|
||||
|
||||
if (enabled === 0) {
|
||||
return {
|
||||
status: "todo",
|
||||
subtitle: "Enable slash commands",
|
||||
passed: 0,
|
||||
total,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: "progress",
|
||||
subtitle: `${enabled} of ${total} commands enabled`,
|
||||
passed: enabled,
|
||||
total,
|
||||
};
|
||||
}
|
||||
|
||||
function countTranscripts() {
|
||||
const dir = path.resolve(__dirname, "../Data/Transcripts");
|
||||
if (!fs.existsSync(dir)) return 0;
|
||||
return fs
|
||||
.readdirSync(dir)
|
||||
.filter((f) => f.endsWith("-transcript.html")).length;
|
||||
}
|
||||
|
||||
function evaluateAllModules(configStore, options = {}) {
|
||||
const supportbot = configStore.supportbot;
|
||||
const ticketPanel = configStore.ticketPanel;
|
||||
const supportbotAi = configStore.supportbotAi;
|
||||
const commands = configStore.commands;
|
||||
const transcriptCount =
|
||||
options.transcriptCount ?? countTranscripts();
|
||||
|
||||
return {
|
||||
tickets: evaluateTickets(supportbot, ticketPanel),
|
||||
supportbot: evaluateSupportbot(supportbot),
|
||||
transcripts: evaluateTranscripts(supportbot, transcriptCount),
|
||||
suggestions: evaluateSuggestions(supportbot),
|
||||
ai: evaluateAi(supportbotAi),
|
||||
commands: evaluateCommands(commands),
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
evaluateAllModules,
|
||||
isPlaceholder,
|
||||
isConfiguredId,
|
||||
};
|
||||
@@ -0,0 +1,63 @@
|
||||
const notificationStore = require("./NotificationStore.js");
|
||||
const { fetchUpdateInfo } = require("./UpdateChecker.js");
|
||||
|
||||
function notifyBotRestart(success, errorMessage) {
|
||||
if (success) {
|
||||
return notificationStore.add({
|
||||
type: "bot",
|
||||
title: "Bot restarted",
|
||||
message:
|
||||
"Discord bot reconnected with your latest config. API and dashboard stayed online.",
|
||||
});
|
||||
}
|
||||
|
||||
return notificationStore.add({
|
||||
type: "error",
|
||||
title: "Bot failed to restart",
|
||||
message: errorMessage || "Check the logs for details.",
|
||||
href: "/logs",
|
||||
});
|
||||
}
|
||||
|
||||
function notifyUpdateAvailable(info) {
|
||||
return notificationStore.add({
|
||||
id: "update-available",
|
||||
type: "update",
|
||||
title: "Update available",
|
||||
message: `Version ${info.latest} is ready (you are on ${info.current}).`,
|
||||
href: "/settings",
|
||||
});
|
||||
}
|
||||
|
||||
function notifyUpdateInstalled(message) {
|
||||
return notificationStore.add({
|
||||
type: "update",
|
||||
title: "Update installed",
|
||||
message:
|
||||
message ||
|
||||
"Files were updated. Restart your server (npm start) to finish applying changes.",
|
||||
href: "/settings",
|
||||
});
|
||||
}
|
||||
|
||||
function clearUpdateAvailable() {
|
||||
notificationStore.dismiss("update-available");
|
||||
}
|
||||
|
||||
async function runUpdateCheck() {
|
||||
const info = await fetchUpdateInfo();
|
||||
if (info.updateAvailable) {
|
||||
notifyUpdateAvailable(info);
|
||||
} else {
|
||||
clearUpdateAvailable();
|
||||
}
|
||||
return info;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
notifyBotRestart,
|
||||
notifyUpdateAvailable,
|
||||
notifyUpdateInstalled,
|
||||
clearUpdateAvailable,
|
||||
runUpdateCheck,
|
||||
};
|
||||
@@ -0,0 +1,86 @@
|
||||
const MAX_NOTIFICATIONS = 50;
|
||||
|
||||
/** @type {Map<string, object>} */
|
||||
const byId = new Map();
|
||||
/** @type {string[]} */
|
||||
let order = [];
|
||||
|
||||
/**
|
||||
* @param {object} input
|
||||
* @param {string} [input.id] Stable id replaces existing (e.g. update-available)
|
||||
*/
|
||||
function add(input) {
|
||||
const id =
|
||||
input.id ||
|
||||
`n-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
|
||||
const item = {
|
||||
id,
|
||||
type: input.type || "info",
|
||||
title: input.title || "Notification",
|
||||
message: input.message || "",
|
||||
href: input.href || null,
|
||||
createdAt: input.createdAt || Date.now(),
|
||||
read: false,
|
||||
};
|
||||
|
||||
if (byId.has(id)) {
|
||||
order = order.filter((x) => x !== id);
|
||||
}
|
||||
|
||||
byId.set(id, item);
|
||||
order.unshift(id);
|
||||
|
||||
while (order.length > MAX_NOTIFICATIONS) {
|
||||
const removed = order.pop();
|
||||
if (removed) byId.delete(removed);
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
function list() {
|
||||
return order.map((id) => byId.get(id)).filter(Boolean);
|
||||
}
|
||||
|
||||
function unreadCount() {
|
||||
let n = 0;
|
||||
for (const item of byId.values()) {
|
||||
if (!item.read) n++;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
function markRead(ids) {
|
||||
if (ids === "all") {
|
||||
for (const item of byId.values()) {
|
||||
item.read = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
for (const id of ids) {
|
||||
const item = byId.get(id);
|
||||
if (item) item.read = true;
|
||||
}
|
||||
}
|
||||
|
||||
function dismiss(id) {
|
||||
if (!byId.has(id)) return false;
|
||||
byId.delete(id);
|
||||
order = order.filter((x) => x !== id);
|
||||
return true;
|
||||
}
|
||||
|
||||
function clear() {
|
||||
byId.clear();
|
||||
order = [];
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
add,
|
||||
list,
|
||||
unreadCount,
|
||||
markRead,
|
||||
dismiss,
|
||||
clear,
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
// SupportBot | Emerald Services
|
||||
// Ticket ID Structure
|
||||
|
||||
const db = require("./Database");
|
||||
|
||||
class TicketNumberID {
|
||||
static async pad() {
|
||||
let num = await this.get();
|
||||
let n = num + "";
|
||||
await this.set(num + 1);
|
||||
return n.length >= 4 ? n : new Array(4 - n.length + 1).join("0") + n;
|
||||
}
|
||||
|
||||
static async get() {
|
||||
const settings = db.getSettings ? db.getSettings() : {};
|
||||
if (typeof settings.ticket_counter === "number") {
|
||||
return settings.ticket_counter;
|
||||
}
|
||||
|
||||
const row = db.getTicketPanel ? db.getTicketPanel() : null;
|
||||
if (row && typeof row.message_id === "string" && row.message_id.startsWith("panel-")) {
|
||||
const parsed = parseInt(row.message_id.replace("panel-", ""), 10);
|
||||
if (!isNaN(parsed)) return parsed;
|
||||
}
|
||||
|
||||
if (row && typeof row.id === "number" && row.id < 1000000) {
|
||||
return row.id;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static async set(value) {
|
||||
const settings = db.getSettings ? db.getSettings() : {};
|
||||
settings.ticket_counter = value;
|
||||
if (db.saveSettings) db.saveSettings(settings);
|
||||
if (db.saveTicketPanel) db.saveTicketPanel(`panel-${value}`, "none");
|
||||
return value;
|
||||
}
|
||||
|
||||
static async reset() {
|
||||
return await this.set(0);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TicketNumberID;
|
||||
@@ -0,0 +1,293 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const Discord = require("discord.js");
|
||||
const yaml = require("js-yaml");
|
||||
const db = require("./Database.js");
|
||||
|
||||
const supportbot = require("./ConfigStore").supportbot;
|
||||
const msgconfig = require("./ConfigStore").messages;
|
||||
const { createTranscriptHTML } = require("./TranscriptTemplate.js");
|
||||
|
||||
function escapeHtml(str = "") {
|
||||
return String(str)
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """);
|
||||
}
|
||||
|
||||
function parseMarkdown(content = "") {
|
||||
let safe = escapeHtml(content);
|
||||
|
||||
safe = safe.replace(/```([\s\S]*?)```/g, "<pre><code>$1</code></pre>");
|
||||
safe = safe.replace(/`(.*?)`/g, "<code>$1</code>");
|
||||
safe = safe.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>");
|
||||
safe = safe.replace(/\*(.*?)\*/g, "<em>$1</em>");
|
||||
safe = safe.replace(/_(.*?)_/g, "<em>$1</em>");
|
||||
safe = safe.replace(
|
||||
/\[(.*?)\]\((.*?)\)/g,
|
||||
'<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>',
|
||||
);
|
||||
|
||||
return safe.replace(/\n/g, "<br>");
|
||||
}
|
||||
|
||||
function formatDate(iso) {
|
||||
try {
|
||||
return new Date(iso).toLocaleString("en-GB", {
|
||||
dateStyle: "medium",
|
||||
timeStyle: "short",
|
||||
});
|
||||
} catch {
|
||||
return iso;
|
||||
}
|
||||
}
|
||||
|
||||
function getFileTypeInfo(att) {
|
||||
const fileName = String(att.name || "attachment").toLowerCase();
|
||||
|
||||
if (/\.(png|jpg|jpeg|gif|webp|bmp|svg)$/i.test(fileName)) {
|
||||
return "image";
|
||||
}
|
||||
|
||||
if (/\.(mp4|webm|mov|m4v)$/i.test(fileName)) {
|
||||
return "video";
|
||||
}
|
||||
|
||||
if (/\.(mp3|wav|ogg|m4a|flac)$/i.test(fileName)) {
|
||||
return "audio";
|
||||
}
|
||||
|
||||
return "file";
|
||||
}
|
||||
|
||||
function renderAttachment(att) {
|
||||
const url = escapeHtml(att.url || "");
|
||||
const name = escapeHtml(att.name || "attachment");
|
||||
const type = getFileTypeInfo(att);
|
||||
|
||||
if (type === "image") {
|
||||
return `
|
||||
<div class="attachment">
|
||||
<a href="${url}" target="_blank" rel="noopener noreferrer" class="attachment-file">📎 ${name}</a>
|
||||
<div class="attachment-preview attachment-image">
|
||||
<img src="${url}" alt="${name}" loading="lazy">
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (type === "video") {
|
||||
return `
|
||||
<div class="attachment">
|
||||
<a href="${url}" target="_blank" rel="noopener noreferrer" class="attachment-file">📎 ${name}</a>
|
||||
<div class="attachment-preview">
|
||||
<video controls preload="metadata">
|
||||
<source src="${url}">
|
||||
</video>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (type === "audio") {
|
||||
return `
|
||||
<div class="attachment">
|
||||
<a href="${url}" target="_blank" rel="noopener noreferrer" class="attachment-file">📎 ${name}</a>
|
||||
<div class="attachment-preview">
|
||||
<audio controls preload="metadata">
|
||||
<source src="${url}">
|
||||
</audio>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="attachment">
|
||||
<a href="${url}" target="_blank" rel="noopener noreferrer" class="attachment-file">📎 ${name}</a>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderEmbeds(embeds = []) {
|
||||
return embeds
|
||||
.map((embed) => {
|
||||
const fields = (embed.fields || [])
|
||||
.map(
|
||||
(field) => `
|
||||
<div class="embed-field">
|
||||
<div class="embed-field-name">${escapeHtml(field.name || "Field")}</div>
|
||||
<div class="embed-field-value">${parseMarkdown(field.value || "")}</div>
|
||||
</div>
|
||||
`,
|
||||
)
|
||||
.join("");
|
||||
|
||||
return `
|
||||
<div class="discord-embed">
|
||||
${embed.title ? `<div class="discord-embed-title">${escapeHtml(embed.title)}</div>` : ""}
|
||||
${embed.description ? `<div class="discord-embed-description">${parseMarkdown(embed.description)}</div>` : ""}
|
||||
${fields}
|
||||
</div>
|
||||
`;
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
|
||||
async function createTranscript(interaction, ticketInput, reasonInput) {
|
||||
let ticketRecord = typeof ticketInput === "object" && ticketInput !== null ? ticketInput : null;
|
||||
let closeReason = typeof ticketInput === "string" ? ticketInput : reasonInput;
|
||||
|
||||
if (!ticketRecord && typeof db.getTicket === "function") {
|
||||
ticketRecord = db.getTicket(interaction.channel.id) || {};
|
||||
}
|
||||
|
||||
const { getChannel } = interaction.client;
|
||||
|
||||
const transcriptChannel = await getChannel(
|
||||
supportbot.Ticket.Log.TicketDataLog,
|
||||
interaction.guild,
|
||||
);
|
||||
|
||||
if (!transcriptChannel) {
|
||||
throw new Error("Transcript log channel missing or inaccessible.");
|
||||
}
|
||||
|
||||
let allMessages = [];
|
||||
let lastId = null;
|
||||
|
||||
while (true) {
|
||||
const options = { limit: 100 };
|
||||
if (lastId) options.before = lastId;
|
||||
|
||||
const messages = await interaction.channel.messages.fetch(options);
|
||||
if (messages.size === 0) break;
|
||||
|
||||
allMessages = [...allMessages, ...messages.values()];
|
||||
lastId = messages.last().id;
|
||||
|
||||
if (messages.size < 100) break;
|
||||
}
|
||||
|
||||
allMessages.sort((a, b) => a.createdTimestamp - b.createdTimestamp);
|
||||
|
||||
const transcriptData = allMessages.map((msg) => ({
|
||||
content: msg.content || "",
|
||||
username: msg.author.username,
|
||||
userId: msg.author.id,
|
||||
avatar: msg.author.displayAvatarURL({ extension: "png", size: 128 }),
|
||||
timestamp: msg.createdAt.toISOString(),
|
||||
attachments: Array.from(msg.attachments.values()).map((att) => ({
|
||||
url: att.url,
|
||||
name: att.name || "attachment",
|
||||
contentType: att.contentType || null,
|
||||
size: att.size || 0,
|
||||
})),
|
||||
embeds: msg.embeds.map((embed) => ({
|
||||
title: embed.title || "",
|
||||
description: embed.description || "",
|
||||
fields: embed.fields || [],
|
||||
})),
|
||||
}));
|
||||
|
||||
if (typeof db.updateTicketStatus === "function") {
|
||||
db.updateTicketStatus(interaction.channel.id, "closed");
|
||||
}
|
||||
|
||||
let ticketUserId = ticketRecord?.user_id || ticketRecord?.user || ticketRecord?.author_id;
|
||||
|
||||
if (!ticketUserId && interaction.channel.permissionOverwrites) {
|
||||
const memberOverwrite = interaction.channel.permissionOverwrites.cache.find(
|
||||
(pow) => pow.type === 1 && pow.id !== interaction.client.user.id
|
||||
);
|
||||
if (memberOverwrite) {
|
||||
ticketUserId = memberOverwrite.id;
|
||||
}
|
||||
}
|
||||
|
||||
if (!ticketUserId && allMessages.length > 0) {
|
||||
const firstNonBotMsg = allMessages.find((m) => !m.author.bot);
|
||||
if (firstNonBotMsg) {
|
||||
ticketUserId = firstNonBotMsg.author.id;
|
||||
}
|
||||
}
|
||||
|
||||
const tUser =
|
||||
(ticketUserId ? interaction.client.users.cache.get(ticketUserId) : null) ||
|
||||
(ticketUserId ? await interaction.client.users.fetch(ticketUserId).catch(() => null) : null);
|
||||
|
||||
const transcriptEmbed = new Discord.EmbedBuilder()
|
||||
.setTitle(msgconfig.TicketLog.Title)
|
||||
.setColor(msgconfig.TicketLog.Colour)
|
||||
.setFooter({
|
||||
text: supportbot.Embed.Footer,
|
||||
iconURL: interaction.user.displayAvatarURL(),
|
||||
})
|
||||
.setDescription(
|
||||
`> **Ticket:** ${interaction.channel.name} (\`${interaction.channel.id}\`)\n` +
|
||||
`> **User:** ${tUser ? `<@${tUser.id}> (\`${tUser.tag}\`)` : "Unknown User"}\n` +
|
||||
`> **Closed by:** <@${interaction.user.id}>\n` +
|
||||
`> **Message Count:** ${transcriptData.length}`,
|
||||
)
|
||||
.addFields({
|
||||
name: "Reason",
|
||||
value: `\`\`\`${closeReason || "No Reason Provided."}\`\`\``,
|
||||
inline: false,
|
||||
});
|
||||
|
||||
try {
|
||||
const configStore = require("./ConfigStore");
|
||||
const transcriptStore = require("./TranscriptStore");
|
||||
const apiConfig = configStore.api?.API;
|
||||
const tSettings = transcriptStore.getSettings();
|
||||
|
||||
if (apiConfig?.Enabled !== false) {
|
||||
const port = apiConfig?.Port || 3000;
|
||||
const isPublic = Boolean(tSettings?.publicAccess?.enabled);
|
||||
const transcriptUrl = `http://localhost:${port}/transcripts/${interaction.channel.id}`;
|
||||
|
||||
transcriptEmbed.addFields({
|
||||
name: isPublic ? "Public Web Transcript" : "Dashboard Transcript",
|
||||
value: `[${isPublic ? "Click here to view public transcript" : "Click here to view in Dashboard"}](${transcriptUrl})`,
|
||||
inline: false,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn("[TicketManager] Could not add web transcript URL:", err.message);
|
||||
}
|
||||
|
||||
const transcriptDir = path.join(process.cwd(), "./Data/Transcripts");
|
||||
if (!fs.existsSync(transcriptDir)) {
|
||||
fs.mkdirSync(transcriptDir, { recursive: true });
|
||||
}
|
||||
|
||||
const htmlFileName = `${interaction.channel.id}-transcript.html`;
|
||||
const htmlPath = path.join(transcriptDir, htmlFileName);
|
||||
|
||||
const html = createTranscriptHTML(
|
||||
{
|
||||
id: interaction.channel.id,
|
||||
name: interaction.channel.name,
|
||||
messages: transcriptData,
|
||||
},
|
||||
closeReason,
|
||||
);
|
||||
|
||||
fs.writeFileSync(htmlPath, html, "utf8");
|
||||
|
||||
await transcriptChannel.send({
|
||||
embeds: [transcriptEmbed],
|
||||
files: [
|
||||
new Discord.AttachmentBuilder(htmlPath, {
|
||||
name: `SPOILER_${htmlFileName}`,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
return { htmlPath };
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createTranscript,
|
||||
};
|
||||
@@ -0,0 +1,223 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const TRANSCRIPT_DIR = path.join(__dirname, '../Data/Transcripts');
|
||||
const SETTINGS_FILE = path.join(__dirname, '../Data/transcript-settings.json');
|
||||
|
||||
const DEFAULT_SETTINGS = {
|
||||
autoDelete: {
|
||||
enabled: false,
|
||||
afterDays: 30,
|
||||
},
|
||||
publicAccess: {
|
||||
enabled: false,
|
||||
},
|
||||
};
|
||||
|
||||
let purgeTimer = null;
|
||||
|
||||
function ensureTranscriptDir() {
|
||||
if (!fs.existsSync(TRANSCRIPT_DIR)) {
|
||||
fs.mkdirSync(TRANSCRIPT_DIR, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
function getSettings() {
|
||||
try {
|
||||
if (!fs.existsSync(SETTINGS_FILE)) {
|
||||
return JSON.parse(JSON.stringify(DEFAULT_SETTINGS));
|
||||
}
|
||||
const raw = JSON.parse(fs.readFileSync(SETTINGS_FILE, 'utf8'));
|
||||
const days = Number(raw?.autoDelete?.afterDays);
|
||||
return {
|
||||
autoDelete: {
|
||||
enabled: Boolean(raw?.autoDelete?.enabled),
|
||||
afterDays:
|
||||
Number.isFinite(days) && days >= 1
|
||||
? Math.min(Math.floor(days), 3650)
|
||||
: DEFAULT_SETTINGS.autoDelete.afterDays,
|
||||
},
|
||||
publicAccess: {
|
||||
enabled: Boolean(raw?.publicAccess?.enabled),
|
||||
},
|
||||
};
|
||||
} catch {
|
||||
return JSON.parse(JSON.stringify(DEFAULT_SETTINGS));
|
||||
}
|
||||
}
|
||||
|
||||
function saveSettings(patch) {
|
||||
const current = getSettings();
|
||||
const next = {
|
||||
autoDelete: {
|
||||
enabled:
|
||||
patch?.autoDelete?.enabled !== undefined
|
||||
? Boolean(patch.autoDelete.enabled)
|
||||
: current.autoDelete.enabled,
|
||||
afterDays:
|
||||
patch?.autoDelete?.afterDays !== undefined
|
||||
? Math.min(
|
||||
Math.max(Math.floor(Number(patch.autoDelete.afterDays) || 1), 1),
|
||||
3650,
|
||||
)
|
||||
: current.autoDelete.afterDays,
|
||||
},
|
||||
publicAccess: {
|
||||
enabled:
|
||||
patch?.publicAccess?.enabled !== undefined
|
||||
? Boolean(patch.publicAccess.enabled)
|
||||
: current.publicAccess.enabled,
|
||||
},
|
||||
};
|
||||
|
||||
const dir = path.dirname(SETTINGS_FILE);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
fs.writeFileSync(SETTINGS_FILE, JSON.stringify(next, null, 2), 'utf8');
|
||||
return next;
|
||||
}
|
||||
|
||||
function normalizeId(rawId) {
|
||||
return String(rawId).replace(/[^0-9]/g, '');
|
||||
}
|
||||
|
||||
function readTranscriptFile(rawId) {
|
||||
const id = normalizeId(rawId);
|
||||
if (!id) return null;
|
||||
ensureTranscriptDir();
|
||||
const filePath = path.join(TRANSCRIPT_DIR, `${id}-transcript.html`);
|
||||
if (!fs.existsSync(filePath)) return null;
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
const titleMatch = content.match(/<title>\s*Transcript\s*-\s*([^<]+)\s*<\/title>/i);
|
||||
return {
|
||||
id,
|
||||
filePath,
|
||||
filename: `${id}-transcript.html`,
|
||||
content,
|
||||
ticketName: titleMatch ? titleMatch[1].trim() : null,
|
||||
};
|
||||
}
|
||||
|
||||
function listTranscripts() {
|
||||
ensureTranscriptDir();
|
||||
if (!fs.existsSync(TRANSCRIPT_DIR)) {
|
||||
return [];
|
||||
}
|
||||
return fs
|
||||
.readdirSync(TRANSCRIPT_DIR)
|
||||
.filter((f) => f.endsWith('-transcript.html'))
|
||||
.map((f) => {
|
||||
const id = f.replace('-transcript.html', '');
|
||||
const stats = fs.statSync(path.join(TRANSCRIPT_DIR, f));
|
||||
const parsed = readTranscriptFile(id);
|
||||
return {
|
||||
id,
|
||||
filename: f,
|
||||
createdAt: stats.mtime.toISOString(),
|
||||
size: stats.size,
|
||||
ticketName: parsed?.ticketName ?? null,
|
||||
};
|
||||
})
|
||||
.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
|
||||
}
|
||||
|
||||
function deleteTranscript(rawId) {
|
||||
const id = normalizeId(rawId);
|
||||
if (!id) return false;
|
||||
const filePath = path.join(TRANSCRIPT_DIR, `${id}-transcript.html`);
|
||||
if (!fs.existsSync(filePath)) return false;
|
||||
fs.unlinkSync(filePath);
|
||||
return true;
|
||||
}
|
||||
|
||||
function purgeExpiredTranscripts() {
|
||||
const settings = getSettings();
|
||||
if (!settings.autoDelete.enabled) {
|
||||
return { deleted: 0, skipped: true };
|
||||
}
|
||||
|
||||
ensureTranscriptDir();
|
||||
const maxAgeMs = settings.autoDelete.afterDays * 24 * 60 * 60 * 1000;
|
||||
const cutoff = Date.now() - maxAgeMs;
|
||||
let deleted = 0;
|
||||
|
||||
for (const f of fs.readdirSync(TRANSCRIPT_DIR)) {
|
||||
if (!f.endsWith('-transcript.html')) continue;
|
||||
const filePath = path.join(TRANSCRIPT_DIR, f);
|
||||
const stats = fs.statSync(filePath);
|
||||
if (stats.mtime.getTime() < cutoff) {
|
||||
fs.unlinkSync(filePath);
|
||||
deleted += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return { deleted, skipped: false };
|
||||
}
|
||||
|
||||
function notFoundHtml(message = 'Transcript not found') {
|
||||
return `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Transcript</title>
|
||||
<style>body{font-family:system-ui,sans-serif;padding:2rem;background:#0b0f14;color:#e6edf3;margin:0}</style></head>
|
||||
<body><h1>${message}</h1></body></html>`;
|
||||
}
|
||||
|
||||
function servePublicTranscript(req, res) {
|
||||
const settings = getSettings();
|
||||
if (!settings.publicAccess.enabled) {
|
||||
return res.status(404).type('html').send(notFoundHtml('Transcript not found'));
|
||||
}
|
||||
|
||||
const transcript = readTranscriptFile(req.params.id);
|
||||
if (!transcript) {
|
||||
return res.status(404).type('html').send(notFoundHtml('Transcript not found'));
|
||||
}
|
||||
|
||||
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
||||
res.setHeader('Cache-Control', 'public, max-age=120');
|
||||
return res.send(transcript.content);
|
||||
}
|
||||
|
||||
function startAutoDeleteScheduler(intervalMs = 60 * 60 * 1000) {
|
||||
if (purgeTimer) return;
|
||||
|
||||
const run = () => {
|
||||
try {
|
||||
const result = purgeExpiredTranscripts();
|
||||
if (result.deleted > 0) {
|
||||
console.log(
|
||||
`[Transcripts] Auto-deleted ${result.deleted} transcript(s) older than ${getSettings().autoDelete.afterDays} day(s)`,
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[Transcripts] Auto-delete failed:', err.message);
|
||||
}
|
||||
};
|
||||
|
||||
run();
|
||||
purgeTimer = setInterval(run, intervalMs);
|
||||
if (typeof purgeTimer.unref === 'function') {
|
||||
purgeTimer.unref();
|
||||
}
|
||||
}
|
||||
|
||||
function getPublicUrl(req, id) {
|
||||
const settings = getSettings();
|
||||
if (!settings.publicAccess.enabled) return null;
|
||||
const host = req.get('host');
|
||||
const proto = req.get('x-forwarded-proto') || req.protocol || 'http';
|
||||
return `${proto}://${host}/transcripts/${id}`;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
TRANSCRIPT_DIR,
|
||||
DEFAULT_SETTINGS,
|
||||
getSettings,
|
||||
saveSettings,
|
||||
readTranscriptFile,
|
||||
listTranscripts,
|
||||
deleteTranscript,
|
||||
purgeExpiredTranscripts,
|
||||
servePublicTranscript,
|
||||
startAutoDeleteScheduler,
|
||||
getPublicUrl,
|
||||
};
|
||||
@@ -0,0 +1,721 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const yaml = require("js-yaml");
|
||||
const {
|
||||
normalizeFontFamily,
|
||||
resolveTypography,
|
||||
} = require("./googleFonts.js");
|
||||
|
||||
const TEMPLATE_PATH = path.join(__dirname, "../Configs/transcript-template.yml");
|
||||
|
||||
let cachedTemplate = null;
|
||||
|
||||
function escapeHtml(str = "") {
|
||||
return String(str)
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """);
|
||||
}
|
||||
|
||||
function parseMarkdown(content = "") {
|
||||
let safe = escapeHtml(content);
|
||||
|
||||
safe = safe.replace(/```([\s\S]*?)```/g, "<pre><code>$1</code></pre>");
|
||||
safe = safe.replace(/`(.*?)`/g, "<code>$1</code>");
|
||||
safe = safe.replace(/\|\|(.*?)\|\|/g, '<span class="spoiler">$1</span>');
|
||||
safe = safe.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>");
|
||||
safe = safe.replace(/\*(.*?)\*/g, "<em>$1</em>");
|
||||
safe = safe.replace(/_(.*?)_/g, "<em>$1</em>");
|
||||
safe = safe.replace(/~~(.*?)~~/g, "<del>$1</del>");
|
||||
safe = safe.replace(/__(.*?)__/g, "<u>$1</u>");
|
||||
safe = safe.replace(/<@!?(\d+)>/g, '<span class="mention">@User</span>');
|
||||
safe = safe.replace(/<@&(\d+)>/g, '<span class="mention">@Role</span>');
|
||||
safe = safe.replace(
|
||||
/\[(.*?)\]\((.*?)\)/g,
|
||||
'<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>',
|
||||
);
|
||||
|
||||
return safe.replace(/\n/g, "<br>");
|
||||
}
|
||||
|
||||
function formatDate(iso) {
|
||||
try {
|
||||
return new Date(iso).toLocaleString("en-GB", {
|
||||
dateStyle: "medium",
|
||||
timeStyle: "short",
|
||||
});
|
||||
} catch {
|
||||
return iso;
|
||||
}
|
||||
}
|
||||
|
||||
function getFileTypeInfo(att) {
|
||||
const fileName = String(att.name || "attachment").toLowerCase();
|
||||
|
||||
if (/\.(png|jpg|jpeg|gif|webp|bmp|svg)$/i.test(fileName)) {
|
||||
return "image";
|
||||
}
|
||||
if (/\.(mp4|webm|mov|m4v)$/i.test(fileName)) {
|
||||
return "video";
|
||||
}
|
||||
if (/\.(mp3|wav|ogg|m4a|flac)$/i.test(fileName)) {
|
||||
return "audio";
|
||||
}
|
||||
return "file";
|
||||
}
|
||||
|
||||
function renderAttachment(att) {
|
||||
const url = escapeHtml(att.url || "");
|
||||
const name = escapeHtml(att.name || "attachment");
|
||||
const type = getFileTypeInfo(att);
|
||||
|
||||
if (type === "image") {
|
||||
return `
|
||||
<div class="attachment">
|
||||
<a href="${url}" target="_blank" rel="noopener noreferrer" class="attachment-file">📎 ${name}</a>
|
||||
<div class="attachment-preview attachment-image">
|
||||
<img src="${url}" alt="${name}" loading="lazy">
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (type === "video") {
|
||||
return `
|
||||
<div class="attachment">
|
||||
<a href="${url}" target="_blank" rel="noopener noreferrer" class="attachment-file">📎 ${name}</a>
|
||||
<div class="attachment-preview">
|
||||
<video controls preload="metadata">
|
||||
<source src="${url}">
|
||||
</video>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (type === "audio") {
|
||||
return `
|
||||
<div class="attachment">
|
||||
<a href="${url}" target="_blank" rel="noopener noreferrer" class="attachment-file">📎 ${name}</a>
|
||||
<div class="attachment-preview">
|
||||
<audio controls preload="metadata">
|
||||
<source src="${url}">
|
||||
</audio>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="attachment">
|
||||
<a href="${url}" target="_blank" rel="noopener noreferrer" class="attachment-file">📎 ${name}</a>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderEmbeds(embeds = []) {
|
||||
return embeds
|
||||
.map((embed) => {
|
||||
const color = embed.color || "#5865F2";
|
||||
const fields = (embed.fields || [])
|
||||
.map(
|
||||
(field) => `
|
||||
<div class="embed-field ${field.inline ? 'inline-field' : ''}">
|
||||
<div class="embed-field-name">${escapeHtml(field.name || "Field")}</div>
|
||||
<div class="embed-field-value">${parseMarkdown(field.value || "")}</div>
|
||||
</div>
|
||||
`,
|
||||
)
|
||||
.join("");
|
||||
|
||||
const author = embed.author ? `
|
||||
<div class="embed-author">
|
||||
${embed.author.iconURL ? `<img src="${escapeHtml(embed.author.iconURL)}" class="embed-author-icon" />` : ''}
|
||||
<span>${escapeHtml(embed.author.name || '')}</span>
|
||||
</div>
|
||||
` : '';
|
||||
|
||||
const thumbnail = embed.thumbnail?.url ? `
|
||||
<img src="${escapeHtml(embed.thumbnail.url)}" class="embed-thumbnail" />
|
||||
` : '';
|
||||
|
||||
const image = embed.image?.url ? `
|
||||
<div class="embed-image"><img src="${escapeHtml(embed.image.url)}" /></div>
|
||||
` : '';
|
||||
|
||||
const footer = (embed.footer || embed.timestamp) ? `
|
||||
<div class="embed-footer">
|
||||
${embed.footer?.iconURL ? `<img src="${escapeHtml(embed.footer.iconURL)}" class="embed-footer-icon" />` : ''}
|
||||
${embed.footer?.text ? `<span>${escapeHtml(embed.footer.text)}</span>` : ''}
|
||||
</div>
|
||||
` : '';
|
||||
|
||||
return `
|
||||
<div class="discord-embed" style="border-left-color: ${escapeHtml(color)}; border-left-width: 4px; border-left-style: solid; position: relative;">
|
||||
${author}
|
||||
${thumbnail}
|
||||
${embed.title ? `<div class="discord-embed-title">${parseMarkdown(embed.title)}</div>` : ""}
|
||||
${embed.description ? `<div class="discord-embed-description">${parseMarkdown(embed.description)}</div>` : ""}
|
||||
${fields ? `<div class="embed-fields-grid">${fields}</div>` : ""}
|
||||
${image}
|
||||
${footer}
|
||||
</div>
|
||||
`;
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
|
||||
function deepMerge(base, patch) {
|
||||
const out = structuredClone(base);
|
||||
for (const [key, val] of Object.entries(patch || {})) {
|
||||
if (
|
||||
val &&
|
||||
typeof val === "object" &&
|
||||
!Array.isArray(val) &&
|
||||
out[key] &&
|
||||
typeof out[key] === "object" &&
|
||||
!Array.isArray(out[key])
|
||||
) {
|
||||
out[key] = deepMerge(out[key], val);
|
||||
} else {
|
||||
out[key] = val;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function getDefaultTemplate() {
|
||||
return {
|
||||
Theme: {
|
||||
Background: "#0b0f14",
|
||||
Panel: "#111821",
|
||||
PanelSecondary: "#182230",
|
||||
PanelTertiary: "#1d2938",
|
||||
Border: "#283548",
|
||||
Text: "#e6edf3",
|
||||
Muted: "#9fb0c3",
|
||||
Accent: "#22c55e",
|
||||
AccentSoft: "rgba(34, 197, 94, 0.12)",
|
||||
AccentBorder: "rgba(34, 197, 94, 0.25)",
|
||||
AccentText: "#8df0ad",
|
||||
Link: "#8fd9ff",
|
||||
AttachmentLink: "#8fd9a8",
|
||||
CodeBackground: "#0f1720",
|
||||
},
|
||||
Typography: {
|
||||
FontFamily: "Inter",
|
||||
},
|
||||
Header: {
|
||||
Title: "Ticket Transcript",
|
||||
Subtitle:
|
||||
"Exported conversation log for support ticket **{{ticketName}}**",
|
||||
Badge: "Closed Transcript",
|
||||
ShowMetaCards: true,
|
||||
},
|
||||
Meta: {
|
||||
ChannelLabel: "Channel",
|
||||
TicketIdLabel: "Ticket ID",
|
||||
MessageCountLabel: "Message Count",
|
||||
CloseReasonLabel: "Close Reason",
|
||||
DefaultCloseReason: "No Reason Provided.",
|
||||
},
|
||||
Messages: {
|
||||
SectionTitle: "Messages",
|
||||
EmptyState: "No messages were found in this ticket.",
|
||||
},
|
||||
Advanced: {
|
||||
CustomCss: "",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeTemplate(raw) {
|
||||
const merged = deepMerge(getDefaultTemplate(), raw || {});
|
||||
if (merged.Typography) {
|
||||
merged.Typography.FontFamily = normalizeFontFamily(
|
||||
merged.Typography.FontFamily,
|
||||
);
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
function loadTemplateFromDisk() {
|
||||
try {
|
||||
if (!fs.existsSync(TEMPLATE_PATH)) {
|
||||
const defaults = getDefaultTemplate();
|
||||
fs.mkdirSync(path.dirname(TEMPLATE_PATH), { recursive: true });
|
||||
fs.writeFileSync(TEMPLATE_PATH, yaml.dump(defaults), "utf8");
|
||||
return defaults;
|
||||
}
|
||||
const parsed = yaml.load(fs.readFileSync(TEMPLATE_PATH, "utf8"));
|
||||
return normalizeTemplate(parsed);
|
||||
} catch (err) {
|
||||
console.error("[TranscriptTemplate] Failed to load template:", err.message);
|
||||
return getDefaultTemplate();
|
||||
}
|
||||
}
|
||||
|
||||
function getTemplate() {
|
||||
if (!cachedTemplate) {
|
||||
cachedTemplate = loadTemplateFromDisk();
|
||||
}
|
||||
return structuredClone(cachedTemplate);
|
||||
}
|
||||
|
||||
function reloadTemplate() {
|
||||
cachedTemplate = loadTemplateFromDisk();
|
||||
return getTemplate();
|
||||
}
|
||||
|
||||
function saveTemplate(data) {
|
||||
const merged = normalizeTemplate(data);
|
||||
fs.mkdirSync(path.dirname(TEMPLATE_PATH), { recursive: true });
|
||||
fs.writeFileSync(TEMPLATE_PATH, yaml.dump(merged), "utf8");
|
||||
cachedTemplate = merged;
|
||||
return merged;
|
||||
}
|
||||
|
||||
function applyPlaceholders(text, ctx) {
|
||||
return String(text || "")
|
||||
.replace(/\{\{ticketName\}\}/g, ctx.name || "")
|
||||
.replace(/\{\{ticketId\}\}/g, ctx.id || "")
|
||||
.replace(/\{\{messageCount\}\}/g, String(ctx.messageCount ?? 0))
|
||||
.replace(/\{\{reason\}\}/g, ctx.reason || "");
|
||||
}
|
||||
|
||||
function formatHeaderText(text, ctx) {
|
||||
const withPlaceholders = applyPlaceholders(text, ctx);
|
||||
const escaped = escapeHtml(withPlaceholders);
|
||||
return escaped.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>");
|
||||
}
|
||||
|
||||
function buildStyles(template) {
|
||||
const t = template.Theme;
|
||||
const { fontFamilyCss } = resolveTypography(template.Typography?.FontFamily);
|
||||
const custom = String(template.Advanced?.CustomCss || "").trim();
|
||||
|
||||
return `
|
||||
:root {
|
||||
--bg: ${t.Background};
|
||||
--panel: ${t.Panel};
|
||||
--panel-2: ${t.PanelSecondary};
|
||||
--panel-3: ${t.PanelTertiary};
|
||||
--border: ${t.Border};
|
||||
--text: ${t.Text};
|
||||
--muted: ${t.Muted};
|
||||
--accent: ${t.Accent};
|
||||
--accent-soft: ${t.AccentSoft};
|
||||
--accent-border: ${t.AccentBorder};
|
||||
--accent-text: ${t.AccentText};
|
||||
--link: ${t.Link};
|
||||
--attachment-link: ${t.AttachmentLink};
|
||||
--code: ${t.CodeBackground};
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-family: ${fontFamilyCss};
|
||||
}
|
||||
|
||||
body { padding: 32px 20px; }
|
||||
|
||||
.page { max-width: 1100px; margin: 0 auto; }
|
||||
|
||||
.header {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 16px;
|
||||
padding: 24px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.header-top {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.title-wrap h1 {
|
||||
margin: 0;
|
||||
font-size: 1.8rem;
|
||||
line-height: 1.2;
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.title-wrap p {
|
||||
margin: 8px 0 0;
|
||||
color: var(--muted);
|
||||
font-size: 0.98rem;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background: var(--accent-soft);
|
||||
color: var(--accent-text);
|
||||
border: 1px solid var(--accent-border);
|
||||
padding: 8px 12px;
|
||||
border-radius: 999px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.meta-grid {
|
||||
margin-top: 18px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.meta-card {
|
||||
background: var(--panel-2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.meta-label {
|
||||
color: var(--muted);
|
||||
font-size: 0.82rem;
|
||||
margin-bottom: 6px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.meta-value {
|
||||
font-size: 0.98rem;
|
||||
font-weight: 600;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.messages-wrap {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.messages-header {
|
||||
padding: 18px 20px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: var(--panel-2);
|
||||
font-weight: 800;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.message {
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
padding: 18px 20px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.message:last-child { border-bottom: none; }
|
||||
|
||||
.avatar {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
min-width: 42px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
background: var(--panel-3);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.message-body { min-width: 0; flex: 1; }
|
||||
|
||||
.message-header {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.username {
|
||||
font-weight: 800;
|
||||
color: var(--text);
|
||||
font-size: 0.98rem;
|
||||
}
|
||||
|
||||
.userid {
|
||||
color: var(--muted);
|
||||
font-size: 0.8rem;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
}
|
||||
|
||||
.timestamp {
|
||||
color: var(--muted);
|
||||
font-size: 0.82rem;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.content {
|
||||
color: var(--text);
|
||||
font-size: 0.96rem;
|
||||
line-height: 1.6;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.content:empty { display: none; }
|
||||
|
||||
.discord-embed {
|
||||
margin-top: 12px;
|
||||
background: var(--panel-2);
|
||||
border: 1px solid var(--border);
|
||||
border-left: 4px solid var(--accent);
|
||||
border-radius: 10px;
|
||||
padding: 12px 14px;
|
||||
}
|
||||
|
||||
.discord-embed-title { font-weight: 800; margin-bottom: 6px; }
|
||||
.discord-embed-description { color: var(--text); line-height: 1.5; }
|
||||
|
||||
.embed-author { display: flex; align-items: center; gap: 8px; font-weight: 700; font-size: 0.9em; margin-bottom: 6px; }
|
||||
.embed-author-icon { width: 18px; height: 18px; border-radius: 50%; }
|
||||
.embed-fields-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 10px; margin-top: 10px; padding-top: 10px; border-top: 1px solid rgba(255,255,255,0.06); }
|
||||
.embed-thumbnail { position: absolute; top: 12px; right: 12px; width: 44px; height: 44px; border-radius: 6px; object-fit: cover; }
|
||||
.embed-image img { width: 100%; max-height: 300px; object-fit: cover; border-radius: 8px; margin-top: 10px; }
|
||||
.embed-footer { display: flex; align-items: center; gap: 6px; font-size: 0.8em; color: var(--muted); margin-top: 8px; padding-top: 6px; border-top: 1px solid rgba(255,255,255,0.04); }
|
||||
.embed-footer-icon { width: 14px; height: 14px; border-radius: 50%; }
|
||||
.mention { background: rgba(88, 101, 242, 0.2); color: #5865F2; padding: 2px 6px; border-radius: 4px; font-weight: 600; font-size: 0.9em; }
|
||||
.spoiler { background: #202225; color: transparent; border-radius: 4px; padding: 0 4px; cursor: pointer; }
|
||||
.spoiler:hover { color: inherit; background: rgba(255,255,255,0.1); }
|
||||
|
||||
.embed-field {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.embed-field-name { font-weight: 700; margin-bottom: 4px; font-size: 0.85em; text-transform: uppercase; color: var(--muted); }
|
||||
.embed-field-value { color: var(--text); line-height: 1.5; }
|
||||
|
||||
.attachment {
|
||||
margin-top: 12px;
|
||||
background: var(--panel-2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.attachment-file {
|
||||
display: inline-block;
|
||||
color: var(--attachment-link);
|
||||
text-decoration: none;
|
||||
font-weight: 700;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.attachment-file:hover { text-decoration: underline; }
|
||||
|
||||
.attachment-preview { margin-top: 10px; }
|
||||
|
||||
.attachment-preview img,
|
||||
.attachment-preview video {
|
||||
width: 100%;
|
||||
max-width: 560px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--border);
|
||||
display: block;
|
||||
background: #0a0f15;
|
||||
}
|
||||
|
||||
.attachment-preview audio {
|
||||
width: 100%;
|
||||
max-width: 560px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
code {
|
||||
background: var(--code);
|
||||
border: 1px solid var(--border);
|
||||
padding: 2px 6px;
|
||||
border-radius: 6px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: var(--code);
|
||||
border: 1px solid var(--border);
|
||||
padding: 12px;
|
||||
border-radius: 10px;
|
||||
overflow-x: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
pre code { background: transparent; border: none; padding: 0; }
|
||||
|
||||
a { color: var(--link); }
|
||||
|
||||
.empty-state { padding: 24px 20px; color: var(--muted); }
|
||||
|
||||
@media (max-width: 700px) {
|
||||
body { padding: 18px 12px; }
|
||||
.header, .messages-wrap { border-radius: 12px; }
|
||||
.message { padding: 14px; }
|
||||
.timestamp { width: 100%; margin-left: 0; }
|
||||
}
|
||||
|
||||
${custom}
|
||||
`;
|
||||
}
|
||||
|
||||
function getSampleTicket() {
|
||||
return {
|
||||
id: "123456789012345678",
|
||||
name: "ticket-support-demo",
|
||||
messages: [
|
||||
{
|
||||
username: "SupportBot",
|
||||
userId: "100000000000000001",
|
||||
avatar: "https://cdn.discordapp.com/embed/avatars/0.png",
|
||||
timestamp: new Date().toISOString(),
|
||||
content: "Hello! Thanks for opening a ticket. How can we help?",
|
||||
embeds: [],
|
||||
attachments: [],
|
||||
},
|
||||
{
|
||||
username: "Member",
|
||||
userId: "100000000000000002",
|
||||
avatar: "https://cdn.discordapp.com/embed/avatars/1.png",
|
||||
timestamp: new Date(Date.now() - 60000).toISOString(),
|
||||
content: "I need help with my purchase — order **#48291**.",
|
||||
embeds: [
|
||||
{
|
||||
title: "Order details",
|
||||
description: "Payment received · awaiting delivery",
|
||||
fields: [{ name: "Status", value: "Pending" }],
|
||||
},
|
||||
],
|
||||
attachments: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function createTranscriptHTML(ticket, reason, templateInput) {
|
||||
const template = templateInput
|
||||
? normalizeTemplate(templateInput)
|
||||
: getTemplate();
|
||||
const header = template.Header;
|
||||
const meta = template.Meta;
|
||||
const messagesCfg = template.Messages;
|
||||
const closeReason = reason || meta.DefaultCloseReason;
|
||||
const ctx = {
|
||||
name: ticket.name,
|
||||
id: ticket.id,
|
||||
messageCount: ticket.messages.length,
|
||||
reason: closeReason,
|
||||
};
|
||||
|
||||
const { headLinks } = resolveTypography(template.Typography?.FontFamily);
|
||||
|
||||
const metaSection = header.ShowMetaCards
|
||||
? `
|
||||
<div class="meta-grid">
|
||||
<div class="meta-card">
|
||||
<div class="meta-label">${escapeHtml(meta.ChannelLabel)}</div>
|
||||
<div class="meta-value">${escapeHtml(ticket.name)}</div>
|
||||
</div>
|
||||
<div class="meta-card">
|
||||
<div class="meta-label">${escapeHtml(meta.TicketIdLabel)}</div>
|
||||
<div class="meta-value">${escapeHtml(ticket.id)}</div>
|
||||
</div>
|
||||
<div class="meta-card">
|
||||
<div class="meta-label">${escapeHtml(meta.MessageCountLabel)}</div>
|
||||
<div class="meta-value">${ticket.messages.length}</div>
|
||||
</div>
|
||||
<div class="meta-card">
|
||||
<div class="meta-label">${escapeHtml(meta.CloseReasonLabel)}</div>
|
||||
<div class="meta-value">${escapeHtml(closeReason)}</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: "";
|
||||
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Transcript - ${escapeHtml(ticket.name)}</title>
|
||||
${headLinks}
|
||||
<style>${buildStyles(template)}</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<section class="header">
|
||||
<div class="header-top">
|
||||
<div class="title-wrap">
|
||||
<h1>${escapeHtml(header.Title)}</h1>
|
||||
<p>${formatHeaderText(header.Subtitle, ctx)}</p>
|
||||
</div>
|
||||
<div class="badge">${escapeHtml(header.Badge)}</div>
|
||||
</div>
|
||||
${metaSection}
|
||||
</section>
|
||||
|
||||
<section class="messages-wrap">
|
||||
<div class="messages-header">${escapeHtml(messagesCfg.SectionTitle)}</div>
|
||||
${
|
||||
ticket.messages.length
|
||||
? ticket.messages
|
||||
.map(
|
||||
(msg) => `
|
||||
<article class="message">
|
||||
<img src="${escapeHtml(msg.avatar || "")}" alt="Avatar" class="avatar">
|
||||
<div class="message-body">
|
||||
<div class="message-header">
|
||||
<span class="username">${escapeHtml(msg.username || "Unknown User")}</span>
|
||||
<span class="userid">${escapeHtml(msg.userId || "Unknown ID")}</span>
|
||||
<span class="timestamp">${formatDate(msg.timestamp)}</span>
|
||||
</div>
|
||||
<div class="content">${parseMarkdown(msg.content || "")}</div>
|
||||
${renderEmbeds(msg.embeds || [])}
|
||||
${(msg.attachments || []).map(renderAttachment).join("")}
|
||||
</div>
|
||||
</article>
|
||||
`,
|
||||
)
|
||||
.join("")
|
||||
: `<div class="empty-state">${escapeHtml(messagesCfg.EmptyState)}</div>`
|
||||
}
|
||||
</section>
|
||||
</div>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
function renderPreview(templateInput) {
|
||||
const sample = getSampleTicket();
|
||||
return createTranscriptHTML(
|
||||
sample,
|
||||
"Resolved — thank you for contacting support.",
|
||||
templateInput,
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
TEMPLATE_PATH,
|
||||
getDefaultTemplate,
|
||||
getTemplate,
|
||||
reloadTemplate,
|
||||
saveTemplate,
|
||||
normalizeTemplate,
|
||||
getSampleTicket,
|
||||
createTranscriptHTML,
|
||||
renderPreview,
|
||||
};
|
||||
@@ -0,0 +1,73 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const REPOSITORIES = [
|
||||
{ id: "bot", name: "SupportBot", repo: "Emerald-Services/SupportBot", branch: "release" },
|
||||
{ id: "dashboard", name: "SupportBot Dashboard", repo: "Emerald-Services/SupportBot-Dashboard", branch: "release" }
|
||||
];
|
||||
|
||||
function compareVersions(a, b) {
|
||||
const pa = String(a)
|
||||
.replace(/^v/i, "")
|
||||
.split(".")
|
||||
.map((n) => parseInt(n, 10) || 0);
|
||||
const pb = String(b)
|
||||
.replace(/^v/i, "")
|
||||
.split(".")
|
||||
.map((n) => parseInt(n, 10) || 0);
|
||||
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
||||
if ((pa[i] || 0) > (pb[i] || 0)) return 1;
|
||||
if ((pa[i] || 0) < (pb[i] || 0)) return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
async function fetchUpdateInfo() {
|
||||
const axios = require("axios");
|
||||
const pkgPath = path.join(__dirname, "../package.json");
|
||||
const currentPkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
||||
const currentVersion = currentPkg.version;
|
||||
|
||||
const checks = [];
|
||||
|
||||
for (const target of REPOSITORIES) {
|
||||
let installedVersion = target.id === "bot" ? currentPkg.version : (currentPkg.dashboardVersion || "1.0.0");
|
||||
let latest = installedVersion;
|
||||
try {
|
||||
const pkgUrl = target.id === 'dashboard'
|
||||
? `https://raw.githubusercontent.com/${target.repo}/${target.branch}/dashboard/package.json`
|
||||
: `https://raw.githubusercontent.com/${target.repo}/${target.branch}/package.json`;
|
||||
|
||||
const remoteRes = await axios.get(pkgUrl, {
|
||||
timeout: 15000,
|
||||
headers: { "User-Agent": "SupportBot-Updater" },
|
||||
});
|
||||
if (remoteRes.data && remoteRes.data.version) {
|
||||
latest = remoteRes.data.version;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(`[Updater] Failed to check ${target.repo} for updates.`);
|
||||
}
|
||||
|
||||
const updateAvailable = compareVersions(latest, installedVersion) > 0;
|
||||
|
||||
checks.push({
|
||||
id: target.id,
|
||||
name: target.name,
|
||||
current: installedVersion,
|
||||
latest,
|
||||
updateAvailable,
|
||||
repository: `https://github.com/${target.repo}`,
|
||||
branch: target.branch,
|
||||
zipUrl: `https://github.com/${target.repo}/archive/refs/heads/${target.branch}.zip`,
|
||||
});
|
||||
}
|
||||
|
||||
return checks;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
compareVersions,
|
||||
fetchUpdateInfo,
|
||||
REPOSITORIES,
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
function withTimeout(promise, ms, message) {
|
||||
let timer;
|
||||
const timeout = new Promise((_, reject) => {
|
||||
timer = setTimeout(() => {
|
||||
reject(new Error(message || `Timed out after ${ms}ms`));
|
||||
}, ms);
|
||||
});
|
||||
|
||||
return Promise.race([promise, timeout]).finally(() => {
|
||||
clearTimeout(timer);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { withTimeout };
|
||||
@@ -0,0 +1,84 @@
|
||||
/** Curated Google Fonts for transcript HTML (keep in sync with dashboard/src/lib/google-fonts.ts). */
|
||||
|
||||
const SYSTEM_FALLBACK =
|
||||
'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif';
|
||||
|
||||
const GOOGLE_FONTS = [
|
||||
{ id: "Inter", label: "Inter" },
|
||||
{ id: "Roboto", label: "Roboto" },
|
||||
{ id: "Open Sans", label: "Open Sans" },
|
||||
{ id: "Lato", label: "Lato" },
|
||||
{ id: "Montserrat", label: "Montserrat" },
|
||||
{ id: "Poppins", label: "Poppins" },
|
||||
{ id: "Nunito", label: "Nunito" },
|
||||
{ id: "Raleway", label: "Raleway" },
|
||||
{ id: "Work Sans", label: "Work Sans" },
|
||||
{ id: "Source Sans 3", label: "Source Sans 3" },
|
||||
{ id: "DM Sans", label: "DM Sans" },
|
||||
{ id: "Manrope", label: "Manrope" },
|
||||
{ id: "Outfit", label: "Outfit" },
|
||||
{ id: "Rubik", label: "Rubik" },
|
||||
{ id: "Ubuntu", label: "Ubuntu" },
|
||||
{ id: "Fira Sans", label: "Fira Sans" },
|
||||
{ id: "PT Sans", label: "PT Sans" },
|
||||
{ id: "Noto Sans", label: "Noto Sans" },
|
||||
{ id: "Oswald", label: "Oswald" },
|
||||
{ id: "Merriweather", label: "Merriweather" },
|
||||
{ id: "Playfair Display", label: "Playfair Display" },
|
||||
{ id: "Libre Baskerville", label: "Libre Baskerville" },
|
||||
{ id: "JetBrains Mono", label: "JetBrains Mono" },
|
||||
{ id: "Fira Code", label: "Fira Code" },
|
||||
];
|
||||
|
||||
const GOOGLE_FONT_IDS = new Set(GOOGLE_FONTS.map((f) => f.id));
|
||||
|
||||
function normalizeFontFamily(fontFamily) {
|
||||
if (!fontFamily || typeof fontFamily !== "string") return "Inter";
|
||||
const trimmed = fontFamily.trim();
|
||||
if (trimmed === "system") return "system";
|
||||
if (!trimmed.includes(",")) {
|
||||
return GOOGLE_FONT_IDS.has(trimmed) ? trimmed : "Inter";
|
||||
}
|
||||
const match = trimmed.match(/^["']?([^"',]+)["']?/);
|
||||
const first = match?.[1]?.trim();
|
||||
if (first && GOOGLE_FONT_IDS.has(first)) return first;
|
||||
return "Inter";
|
||||
}
|
||||
|
||||
function googleFontsCssUrl(familyIds) {
|
||||
const families = familyIds
|
||||
.map((id) => {
|
||||
const encoded = encodeURIComponent(id).replace(/%20/g, "+");
|
||||
return `family=${encoded}:wght@400;600;700;800`;
|
||||
})
|
||||
.join("&");
|
||||
return `https://fonts.googleapis.com/css2?${families}&display=swap`;
|
||||
}
|
||||
|
||||
function resolveTypography(fontFamily) {
|
||||
const id = normalizeFontFamily(fontFamily);
|
||||
|
||||
if (id === "system") {
|
||||
return {
|
||||
fontId: "system",
|
||||
fontFamilyCss: SYSTEM_FALLBACK,
|
||||
headLinks: "",
|
||||
};
|
||||
}
|
||||
|
||||
const url = googleFontsCssUrl([id]);
|
||||
return {
|
||||
fontId: id,
|
||||
fontFamilyCss: `"${id}", ${SYSTEM_FALLBACK}`,
|
||||
headLinks: `<link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link href="${url}" rel="stylesheet">`,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
GOOGLE_FONTS,
|
||||
GOOGLE_FONT_IDS,
|
||||
SYSTEM_FALLBACK,
|
||||
normalizeFontFamily,
|
||||
googleFontsCssUrl,
|
||||
resolveTypography,
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
theme: jekyll-theme-cayman
|
||||
@@ -1,60 +0,0 @@
|
||||
// SupportBot
|
||||
// Command: Add User
|
||||
|
||||
const Discord = require("discord.js");
|
||||
const bot = new Discord.Client()
|
||||
|
||||
bot.settings = require("../settings.json");
|
||||
|
||||
module.exports.run = async (bot, message, args) => {
|
||||
message.delete()
|
||||
|
||||
console.log(`\x1b[36m`, `${message.author} has executed ${bot.settings.prefix}${bot.settings.Add_Command}`)
|
||||
|
||||
let staffGroup = message.guild.roles.find(staffRole => staffRole.name === bot.settings.support)
|
||||
|
||||
const rolemissing = new Discord.RichEmbed()
|
||||
.setDescription(`:x: Looks like this server doesn't have the role **${bot.settings.support}**`)
|
||||
.setColor(bot.settings.colour)
|
||||
if (!staffGroup) return message.reply({embed: rolemissing});
|
||||
|
||||
const donothaverole = new Discord.RichEmbed()
|
||||
.setDescription(`:x: Sorry! You cannot use this command with the role **${bot.settings.support}**`)
|
||||
.setColor(bot.settings.colour)
|
||||
if (!message.member.roles.has(staffGroup.id)) return message.reply({embed: donothaverole});
|
||||
|
||||
const outsideticket = new Discord.RichEmbed()
|
||||
.setDescription(`:x: Cannot use this command becase you are outside a ticket channel.`)
|
||||
.setColor(bot.settings.colour)
|
||||
if (!message.channel.name.startsWith(`ticket-`)) return message.channel.send({embed: outsideticket});
|
||||
|
||||
let rUser = message.guild.member(message.mentions.users.first() || message.guild.members.get(args[0]));
|
||||
|
||||
const cantfinduser = new Discord.RichEmbed()
|
||||
.setDescription(`:x: Hmm! Does that user exist? I cannot find the user.`)
|
||||
.setColor(bot.settings.colour)
|
||||
if(!rUser) return message.channel.send({embed: cantfinduser});
|
||||
|
||||
const channel = message.guild.channels.find(channel => channel.name === message.channel.name);
|
||||
|
||||
const cantfindchannel = new Discord.RichEmbed()
|
||||
.setDescription(`:x: Hmm! Does that ticket exist? I cannot find the ticket channel.`)
|
||||
.setColor(bot.settings.colour)
|
||||
if(!channel) return message.channel.send({embed: cantfindchannel});
|
||||
message.delete().catch(O_o=>{});
|
||||
|
||||
message.channel.overwritePermissions(rUser, { READ_MESSAGES: true, SEND_MESSAGES: true });
|
||||
|
||||
const useradded = new Discord.RichEmbed()
|
||||
.setColor(bot.settings.colour)
|
||||
.setDescription(`:white_check_mark: Successfully Added ${rUser} to the ticket.`)
|
||||
.setTimestamp();
|
||||
|
||||
message.channel.send({embed: useradded});
|
||||
console.log(`\x1b[36m`, `${message.author} has successfully added ${rUser} to ${message.channel}`)
|
||||
|
||||
}
|
||||
|
||||
exports.help = {
|
||||
name: bot.settings.Add_Command,
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
// SupportBot
|
||||
// Command: Close Ticket
|
||||
|
||||
const Discord = require("discord.js");
|
||||
const bot = new Discord.Client()
|
||||
|
||||
bot.settings = require("../settings.json");
|
||||
|
||||
exports.run = (bot, message, args) => {
|
||||
message.delete();
|
||||
|
||||
const outsideticket = new Discord.RichEmbed()
|
||||
.setDescription(`:x: Cannot use this command becase you are outside a ticket channel.`)
|
||||
.setColor(bot.settings.colour)
|
||||
if (!message.channel.name.startsWith(`ticket-`)) return message.channel.send({embed: outsideticket});
|
||||
const close1 = new Discord.RichEmbed()
|
||||
.setDescription(`Looks like you have come to the end of your support ticket\nPlease confirm that you want to close your ticket by saying ||**confirm**||`)
|
||||
.setFooter("Your request will be avoided in 20 seconds")
|
||||
.setColor(bot.settings.colour)
|
||||
message.channel.send({embed: close1}).then(m => {
|
||||
message.channel.awaitMessages(response => response.content === `confirm`, {
|
||||
max: 1,
|
||||
time: 10000,
|
||||
errors: ['time'],
|
||||
|
||||
}).then((collected) => {
|
||||
message.channel.delete();
|
||||
|
||||
}).catch(() => {
|
||||
m.edit('Close ticket request, timedout').then(m2 => {
|
||||
m2.delete();
|
||||
}, 3000);
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
console.log(`\x1b[36m`, `${message.author} has executed ${bot.settings.prefix}${bot.settings.Close_Command}`)
|
||||
|
||||
}
|
||||
|
||||
exports.help = {
|
||||
name: bot.settings.Close_Command,
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
// SupportBot
|
||||
// Command: Forceclose Ticket
|
||||
|
||||
const Discord = require("discord.js");
|
||||
const bot = new Discord.Client()
|
||||
|
||||
bot.settings = require("../settings.json");
|
||||
|
||||
exports.run = (bot, message, args) => {
|
||||
message.delete();
|
||||
|
||||
let staffGroup = message.guild.roles.find(staffRole => staffRole.name === `${bot.settings.support}`)
|
||||
|
||||
const rolemissing = new Discord.RichEmbed()
|
||||
.setDescription(`:x: Looks like this server doesn't have the role **${bot.settings.support}**`)
|
||||
.setColor(bot.settings.colour)
|
||||
if (!staffGroup) return message.reply({embed: rolemissing});
|
||||
|
||||
const donothaverole = new Discord.RichEmbed()
|
||||
.setDescription(`:x: Sorry! You cannot use this command with the role **${bot.settings.support}**`)
|
||||
.setColor(bot.settings.colour)
|
||||
if (!message.member.roles.has(staffGroup.id)) return message.reply({embed: donothaverole});
|
||||
|
||||
const outsideticket = new Discord.RichEmbed()
|
||||
.setDescription(`:x: Cannot use this command becase you are outside a ticket channel.`)
|
||||
.setColor(bot.settings.colour)
|
||||
if (!message.channel.name.startsWith(`ticket-`)) return message.channel.send({embed: outsideticket});
|
||||
message.channel.delete();
|
||||
|
||||
console.log(`\x1b[36m`, `${message.author} has executed ${bot.settings.prefix}${bot.settings.Forceclose_Command}`)
|
||||
|
||||
}
|
||||
|
||||
exports.help = {
|
||||
name: bot.settings.Forceclose_Command,
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
// SupportBot
|
||||
// Command: Help
|
||||
|
||||
const Discord = require("discord.js");
|
||||
const bot = new Discord.Client()
|
||||
|
||||
bot.settings = require("../settings.json");
|
||||
|
||||
exports.run = (bot, message, args) => {
|
||||
message.delete();
|
||||
|
||||
let userCommands = "";
|
||||
userCommands += `**${bot.settings.prefix}${bot.settings.Link_Command}**: Get some important links!\n`;
|
||||
userCommands += `**${bot.settings.prefix}${bot.settings.Ping_Command}**: Check the bots ping. Pong!\n`;
|
||||
userCommands += `**${bot.settings.prefix}${bot.settings.Ticket_Command} <reason>**: Create a support ticket\n`;
|
||||
userCommands += `**${bot.settings.prefix}${bot.settings.Close_Command}**: Close your support ticket\n`;
|
||||
userCommands += `**${bot.settings.prefix}${bot.settings.Suggest_Command} <suggestion>**: Create a server suggestion\n`;
|
||||
|
||||
let supportCommands = "";
|
||||
supportCommands += `**${bot.settings.prefix}${bot.settings.Add_Command} <user>**: Adds a user to a ticket\n`;
|
||||
supportCommands += `**${bot.settings.prefix}${bot.settings.Remove_Command} <user>**: Removes a user from a ticket\n`;
|
||||
supportCommands += `**${bot.settings.prefix}${bot.settings.Forceclose_Command}**: Forceclose a support ticket\n`;
|
||||
supportCommands += `**${bot.settings.prefix}${bot.settings.Rename_Command}**: Rename a support ticket\n`;
|
||||
|
||||
let staffCommands = "";
|
||||
staffCommands += `**${bot.settings.prefix}${bot.settings.Say_Command} <message>**: Create a bot announcement\n`;
|
||||
staffCommands += `**${bot.settings.prefix}${bot.settings.Lockchat_Command}**: Lock the chat channel\n`;
|
||||
staffCommands += `**${bot.settings.prefix}${bot.settings.UnLockchat_Command}**: UnLock the chat channel\n`;
|
||||
|
||||
const embed = new Discord.RichEmbed()
|
||||
.setTitle(bot.settings.botname)
|
||||
.addField(":ticket: Support Commands", userCommands)
|
||||
.addField(":pushpin: Support Commands", supportCommands, true)
|
||||
.addField(":pushpin: Staff Commands", staffCommands, true)
|
||||
.setColor(bot.settings.colour)
|
||||
.setFooter(bot.settings.footer, message.author.displayAvatarURL);
|
||||
|
||||
message.channel.send(embed)
|
||||
|
||||
console.log(`\x1b[36m`, `${message.author} has executed ${bot.settings.prefix}${bot.settings.Help_Command}`)
|
||||
|
||||
}
|
||||
|
||||
exports.help = {
|
||||
name: bot.settings.Help_Command,
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
// SupportBot
|
||||
// Command: Help
|
||||
|
||||
const Discord = require("discord.js");
|
||||
const bot = new Discord.Client()
|
||||
|
||||
bot.settings = require("../settings.json");
|
||||
|
||||
exports.run = (bot, message, args) => {
|
||||
message.delete();
|
||||
|
||||
let BotLinks = "";
|
||||
BotLinks += `[${bot.settings.LINK_1_NAME}](${bot.settings.LINK_1})\n`;
|
||||
BotLinks += `[${bot.settings.LINK_2_NAME}](${bot.settings.LINK_2})\n`;
|
||||
BotLinks += `[${bot.settings.LINK_3_NAME}](${bot.settings.LINK_3})\n`;
|
||||
|
||||
const links = new Discord.RichEmbed()
|
||||
.setTitle(`${bot.settings.LINKS_TITLE}`)
|
||||
.setDescription(BotLinks)
|
||||
.setColor(bot.settings.colour)
|
||||
.setFooter(bot.settings.footer)
|
||||
message.channel.send(links)
|
||||
|
||||
console.log(`\x1b[36m`, `${message.author} has executed ${bot.settings.prefix}${bot.settings.Link_Command}`)
|
||||
|
||||
}
|
||||
|
||||
exports.help = {
|
||||
name: bot.settings.Link_Command,
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
// SupportBot
|
||||
// Command: Lockchat
|
||||
|
||||
const Discord = require("discord.js");
|
||||
const bot = new Discord.Client()
|
||||
|
||||
bot.settings = require("../settings.json");
|
||||
|
||||
exports.run = async(bot, message, args) => {
|
||||
message.delete();
|
||||
|
||||
let staffGroup = message.guild.roles.find(staffRole => staffRole.name === `${bot.settings.staff}`)
|
||||
let everyoneRole = message.guild.roles.find(everyone => everyone.name === `${bot.settings.everyone}`)
|
||||
|
||||
const rolemissing = new Discord.RichEmbed()
|
||||
.setDescription(`:x: Looks like this server doesn't have the role **${bot.settings.staff}**`)
|
||||
.setColor(bot.settings.colour)
|
||||
if (!staffGroup) return message.reply(rolemissing).catch(err=>{console.error(err)})
|
||||
|
||||
const donothaverole = new Discord.RichEmbed()
|
||||
.setDescription(`:x: Sorry! You cannot use this command with the role **${bot.settings.staff}**`)
|
||||
.setColor(bot.settings.colour)
|
||||
if (!message.member.roles.has(staffGroup.id)) return message.reply(donothaverole)
|
||||
|
||||
message.channel.overwritePermissions(everyoneRole, { READ_MESSAGES: false, SEND_MESSAGES: false });
|
||||
|
||||
const lockmsg = new Discord.RichEmbed()
|
||||
.setDescription(":white_check_mark: **Chat Locked**")
|
||||
.setColor(bot.settings.colour)
|
||||
message.channel.send(lockmsg)
|
||||
|
||||
console.log(`\x1b[36m`, `${message.author} has executed ${bot.settings.prefix}${bot.settings.Lockchat_Command}`)
|
||||
|
||||
}
|
||||
|
||||
exports.help = {
|
||||
name: bot.settings.Lockchat_Command,
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
// SupportBot
|
||||
// Command: Ping
|
||||
|
||||
const Discord = require("discord.js");
|
||||
const bot = new Discord.Client()
|
||||
|
||||
bot.settings = require("../settings.json");
|
||||
|
||||
exports.run = (bot, message, args) => {
|
||||
message.delete();
|
||||
|
||||
const embed = new Discord.RichEmbed()
|
||||
.setDescription(`:ping_pong: **Pong!** \`${bot.ping}ms\` `)
|
||||
.setColor(bot.settings.colour)
|
||||
message.channel.send(embed).catch(console.error);
|
||||
|
||||
console.log(`\x1b[36m`, `${message.author} has executed ${bot.settings.prefix}${bot.settings.Ping_Command}`)
|
||||
|
||||
}
|
||||
|
||||
exports.help = {
|
||||
name: bot.settings.Ping_Command,
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
// SupportBot
|
||||
// Command: Poll
|
||||
|
||||
const Discord = require("discord.js");
|
||||
const bot = new Discord.Client()
|
||||
|
||||
bot.settings = require("../settings.json");
|
||||
|
||||
exports.run = (bot, message, args) => {
|
||||
message.delete();
|
||||
|
||||
let staffGroup = message.guild.roles.find(staffRole => staffRole.name === `${bot.settings.staff}`)
|
||||
|
||||
const rolemissing = new Discord.RichEmbed()
|
||||
.setDescription(`:x: Looks like this server doesn't have the role **${bot.settings.staff}**`)
|
||||
.setColor(bot.settings.colour)
|
||||
if (!staffGroup) return message.reply(rolemissing).catch(err=>{console.error(err)})
|
||||
|
||||
const donothaverole = new Discord.RichEmbed()
|
||||
.setDescription(`:x: Sorry! You cannot use this command with the role **${bot.settings.staff}**`)
|
||||
.setColor(bot.settings.colour)
|
||||
if (!message.member.roles.has(staffGroup.id)) return message.reply(donothaverole)
|
||||
|
||||
const embed = new Discord.RichEmbed()
|
||||
.setTitle(`${bot.settings.Poll_Title}`)
|
||||
.setDescription(args.join(" "))
|
||||
.setTimestamp()
|
||||
.setColor(bot.settings.colour)
|
||||
.setFooter(bot.settings.footer, message.author.displayAvatarURL)
|
||||
|
||||
let ac = message.guild.channels.find(AnnounceChannel => AnnounceChannel.name === `${bot.settings.Poll_Channel}`)
|
||||
if(!ac) return message.channel.send(`:x: Error! Could not find the logs channel **${bot.settings.Poll_Channel}**`)
|
||||
|
||||
ac.send(embed)
|
||||
|
||||
.then(async function(msg) {
|
||||
msg.react(bot.settings.Reaction_Poll_1).then(() => msg.react(bot.settings.Reaction_Poll_2));
|
||||
});
|
||||
|
||||
console.log(`\x1b[36m`, `${message.author} has executed ${bot.settings.prefix}${bot.settings.Poll_Command}`)
|
||||
|
||||
}
|
||||
|
||||
exports.help = {
|
||||
name: bot.settings.Poll_Command,
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
// SupportBot
|
||||
// Command: Remove User
|
||||
|
||||
const Discord = require("discord.js");
|
||||
const bot = new Discord.Client()
|
||||
|
||||
bot.settings = require("../settings.json");
|
||||
|
||||
module.exports.run = async (bot, message, args) => {
|
||||
message.delete()
|
||||
|
||||
console.log(`\x1b[36m`, `${message.author} has executed ${bot.settings.prefix}${bot.settings.Add_Command}`)
|
||||
|
||||
let staffGroup = message.guild.roles.find(staffRole => staffRole.name === bot.settings.support)
|
||||
|
||||
const rolemissing = new Discord.RichEmbed()
|
||||
.setDescription(`:x: Looks like this server doesn't have the role **${bot.settings.support}**`)
|
||||
.setColor(bot.settings.colour)
|
||||
if (!staffGroup) return message.reply({embed: rolemissing});
|
||||
|
||||
const donothaverole = new Discord.RichEmbed()
|
||||
.setDescription(`:x: Sorry! You cannot use this command with the role **${bot.settings.support}**`)
|
||||
.setColor(bot.settings.colour)
|
||||
if (!message.member.roles.has(staffGroup.id)) return message.reply({embed: donothaverole});
|
||||
|
||||
const outsideticket = new Discord.RichEmbed()
|
||||
.setDescription(`:x: Cannot use this command becase you are outside a ticket channel.`)
|
||||
.setColor(bot.settings.colour)
|
||||
if (!message.channel.name.startsWith(`ticket-`)) return message.channel.send({embed: outsideticket});
|
||||
|
||||
let rUser = message.guild.member(message.mentions.users.first() || message.guild.members.get(args[0]));
|
||||
|
||||
const cantfinduser = new Discord.RichEmbed()
|
||||
.setDescription(`:x: Hmm! Does that user exist? I cannot find the user.`)
|
||||
.setColor(bot.settings.colour)
|
||||
if(!rUser) return message.channel.send({embed: cantfinduser});
|
||||
|
||||
const channel = message.guild.channels.find(channel => channel.name === message.channel.name);
|
||||
|
||||
const cantfindchannel = new Discord.RichEmbed()
|
||||
.setDescription(`:x: Hmm! Does that ticket exist? I cannot find the ticket channel.`)
|
||||
.setColor(bot.settings.colour)
|
||||
if(!channel) return message.channel.send({embed: cantfindchannel});
|
||||
message.delete().catch(O_o=>{});
|
||||
|
||||
message.channel.overwritePermissions(rUser, { READ_MESSAGES: false, SEND_MESSAGES: false });
|
||||
|
||||
const useradded = new Discord.RichEmbed()
|
||||
.setColor(bot.settings.colour)
|
||||
.setDescription(`:white_check_mark: Successfully remove ${rUser} from the ticket.`)
|
||||
.setTimestamp();
|
||||
|
||||
message.channel.send({embed: useradded});
|
||||
console.log(`\x1b[36m`, `${message.author} has successfully removed ${rUser} from ${message.channel}`)
|
||||
|
||||
}
|
||||
|
||||
exports.help = {
|
||||
name: bot.settings.Remove_Command,
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
// SupportBot
|
||||
// Command: Close Ticket
|
||||
|
||||
const Discord = require("discord.js");
|
||||
const bot = new Discord.Client()
|
||||
|
||||
bot.settings = require("../settings.json");
|
||||
|
||||
exports.run = (bot, message, args) => {
|
||||
message.delete();
|
||||
|
||||
let staffGroup = message.guild.roles.find(staffRole => staffRole.name === bot.settings.support)
|
||||
|
||||
const rolemissing = new Discord.RichEmbed()
|
||||
.setDescription(`:x: Looks like this server doesn't have the role **${bot.settings.support}**`)
|
||||
.setColor(bot.settings.colour)
|
||||
if (!staffGroup) return message.reply({embed: rolemissing});
|
||||
|
||||
const donothaverole = new Discord.RichEmbed()
|
||||
.setDescription(`:x: Sorry! You cannot use this command with the role **${bot.settings.support}**`)
|
||||
.setColor(bot.settings.colour)
|
||||
if (!message.member.roles.has(staffGroup.id)) return message.reply({embed: donothaverole});
|
||||
|
||||
const outsideticket = new Discord.RichEmbed()
|
||||
.setDescription(`:x: Cannot use this command becase you are outside a ticket channel.`)
|
||||
.setColor(bot.settings.colour)
|
||||
if (!message.channel.name.startsWith(`ticket-`)) return message.channel.send({embed: outsideticket});
|
||||
const renameto = args.join(" ")
|
||||
message.channel.setName(`ticket-${renameto}-${message.author.username}`)
|
||||
|
||||
// Renamed Ticket Logistic
|
||||
const logEmbed = new Discord.RichEmbed()
|
||||
.setTitle(":ticket: Ticket Renamed")
|
||||
.setDescription(`<@${message.author.id}> has renamed a ticket to ${renameto}`)
|
||||
.setColor(bot.settings.colour)
|
||||
.setFooter(bot.settings.footer)
|
||||
|
||||
let logChannel = message.guild.channels.find(TicketChannel => TicketChannel.name === `${bot.settings.Ticket_Logs}`);
|
||||
if(!logChannel) return message.channel.send(`:x: Error! Could not find the logs channel **${bot.settings.Ticket_Logs}**`);
|
||||
|
||||
logChannel.send({embed: logEmbed})
|
||||
|
||||
console.log(`\x1b[36m`, `${message.author} has executed ${bot.settings.prefix}${bot.settings.Rename_Command}`)
|
||||
|
||||
}
|
||||
|
||||
exports.help = {
|
||||
name: bot.settings.Rename_Command,
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
// SupportBot
|
||||
// Command: Report
|
||||
|
||||
const Discord = require("discord.js");
|
||||
const bot = new Discord.Client()
|
||||
|
||||
bot.settings = require("../settings.json");
|
||||
|
||||
exports.run = (bot, message, args) => {
|
||||
message.delete();
|
||||
|
||||
let staffGroup = message.guild.roles.find(staffRole => staffRole.name === `${bot.settings.staff}`);
|
||||
|
||||
const rolemissing = new Discord.RichEmbed()
|
||||
.setDescription(`:x: Looks like this server doesn't have the role **${bot.settings.staff}**`)
|
||||
.setColor(bot.settings.colour);
|
||||
|
||||
if(!staffGroup) {
|
||||
message.reply(rolemissing).catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
} else {
|
||||
const donothaverole = new Discord.RichEmbed()
|
||||
.setDescription(`:x: Sorry! You cannot use this command with the role **${bot.settings.staff}**`)
|
||||
.setColor(bot.settings.colour)
|
||||
if(!message.member.roles.has(staffGroup.id)) {
|
||||
message.reply(donothaverole);
|
||||
} else {
|
||||
if(!args[0]) {
|
||||
message.reply(":x: Sorry! Please specify the user to report.");
|
||||
} else {
|
||||
if(!args[1]) {
|
||||
message.reply(":x: Sorry! Specify the reason of your report.");
|
||||
} else {
|
||||
let reportDesc = args.slice(1).join(" ");
|
||||
const embed = new Discord.RichEmbed()
|
||||
.setTitle(`${bot.settings.Report_Title}`)
|
||||
.setThumbnail(message.author.avatarURL)
|
||||
.addField("Report by:", `<@${message.author.id}>`, false)
|
||||
.addField("Description:", `${reportDesc}`, false)
|
||||
.addField("Reported User:", `${args[0]}`, false)
|
||||
.setTimestamp(new Date())
|
||||
.setColor(bot.settings.colour)
|
||||
.setFooter(`${message.author.username}#${message.author.discriminator}`, message.author.displayAvatarURL);
|
||||
let rc = message.guild.channels.find(ReportChannel => ReportChannel.name === `${bot.settings.Report_Channel}`)
|
||||
if(!rc) {
|
||||
message.channel.send(`:x: Error! Could not find the logs channel **${bot.settings.Report_Channel}**`);
|
||||
} else {
|
||||
rc.send(embed);
|
||||
console.log(`\x1b[36m`, `${message.author} has executed ${bot.settings.prefix}${bot.settings.Report_Command}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exports.help = {
|
||||
name: bot.settings.Report_Command
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
// SupportBot
|
||||
// Command: Say
|
||||
|
||||
const Discord = require("discord.js");
|
||||
const bot = new Discord.Client()
|
||||
|
||||
bot.settings = require("../settings.json");
|
||||
|
||||
exports.run = (bot, message, args) => {
|
||||
message.delete();
|
||||
|
||||
let staffGroup = message.guild.roles.find(staffRole => staffRole.name === `${bot.settings.staff}`)
|
||||
|
||||
const rolemissing = new Discord.RichEmbed()
|
||||
.setDescription(`:x: Looks like this server doesn't have the role **${bot.settings.staff}**`)
|
||||
.setColor(bot.settings.colour)
|
||||
if (!staffGroup) return message.reply(rolemissing).catch(err=>{console.error(err)})
|
||||
|
||||
const donothaverole = new Discord.RichEmbed()
|
||||
.setDescription(`:x: Sorry! You cannot use this command with the role **${bot.settings.staff}**`)
|
||||
.setColor(bot.settings.colour)
|
||||
if (!message.member.roles.has(staffGroup.id)) return message.reply(donothaverole)
|
||||
|
||||
const embed = new Discord.RichEmbed()
|
||||
.setTitle(`${bot.settings.Announcement_Title}`)
|
||||
.setDescription(args.join(" "))
|
||||
.setTimestamp()
|
||||
.setColor(bot.settings.colour)
|
||||
.setFooter(`${message.author.username}#${message.author.discriminator}`, message.author.displayAvatarURL)
|
||||
|
||||
let ac = message.guild.channels.find(AnnounceChannel => AnnounceChannel.name === `${bot.settings.Announcement_Channel}`)
|
||||
if(!ac) return message.channel.send(`:x: Error! Could not find the logs channel **${bot.settings.Announcement_Channel}**`)
|
||||
|
||||
ac.send(embed)
|
||||
|
||||
console.log(`\x1b[36m`, `${message.author} has executed ${bot.settings.prefix}${bot.settings.Say_Command}`)
|
||||
|
||||
}
|
||||
|
||||
exports.help = {
|
||||
name: bot.settings.Say_Command,
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
// SupportBot
|
||||
// Command: Suggest
|
||||
|
||||
const Discord = require("discord.js");
|
||||
const bot = new Discord.Client()
|
||||
|
||||
bot.settings = require("../settings.json");
|
||||
|
||||
exports.run = async(bot, message, args) => {
|
||||
message.delete();
|
||||
|
||||
const SuggestionEmbed = new Discord.RichEmbed()
|
||||
.setTitle(`${bot.settings.Suggestion_Title}`)
|
||||
.setDescription(args.join(" "))
|
||||
.setFooter(`Suggestion by ${message.author.username}`, message.author.displayAvatarURL)
|
||||
.setColor(bot.settings.colour)
|
||||
|
||||
let sc = message.guild.channels.find(SuggestionChannel => SuggestionChannel.name === `${bot.settings.Suggestion_Channel}`);
|
||||
if(!sc) return message.channel.send(`:x: Error! Could not find the suggestion channel **${bot.settings.Suggestion_Channel}**`);
|
||||
|
||||
sc.send(SuggestionEmbed)
|
||||
|
||||
.then(async function(msg) {
|
||||
msg.react(bot.settings.suggestyes).then(() => msg.react(bot.settings.suggestno));
|
||||
});
|
||||
|
||||
console.log(`\x1b[36m`, `${message.author} has executed ${bot.settings.prefix}${bot.settings.Suggest_Command}`)
|
||||
|
||||
}
|
||||
|
||||
exports.help = {
|
||||
name: bot.settings.Suggest_Command,
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
// SupportBot
|
||||
// Command: Ticket
|
||||
|
||||
const Discord = require("discord.js");
|
||||
const bot = new Discord.Client()
|
||||
|
||||
bot.settings = require("../settings.json");
|
||||
|
||||
exports.run = async(bot, message, args) => {
|
||||
message.delete();
|
||||
|
||||
const ID = Math.floor(Math.random() * 10) + 2000;
|
||||
const subject = args.join(" ") || "No Subject.";
|
||||
|
||||
const alreadyopen = new Discord.RichEmbed()
|
||||
.setDescription(`:x: Cannot create a ticket because **ticket-${message.author.username}** already exists.`)
|
||||
.setColor(bot.settings.colour)
|
||||
if (message.guild.channels.find(TicketChannel => TicketChannel.name === 'ticket-' + message.author.username)) return message.channel.send(alreadyopen);
|
||||
message.guild.createChannel(`ticket-${message.author.username}`, 'text').then(TicketChannel => {
|
||||
|
||||
// Roles
|
||||
let staff = message.guild.roles.find(staffRole => staffRole.name === `${bot.settings.support}`)
|
||||
let everyone = message.guild.roles.find(everyoneRole => everyoneRole.name === `@everyone`)
|
||||
|
||||
// Permissions
|
||||
TicketChannel.overwritePermissions(everyone, { SEND_MESSAGES: false, READ_MESSAGES: false })
|
||||
TicketChannel.overwritePermissions(staff, { SEND_MESSAGES: true, READ_MESSAGES: true })
|
||||
TicketChannel.overwritePermissions(message.author, { SEND_MESSAGES: true, READ_MESSAGES: true })
|
||||
TicketChannel.overwritePermissions(bot.user, { SEND_MESSAGES: true, READ_MESSAGES: true })
|
||||
|
||||
// Category
|
||||
let category = message.guild.channels.find(c => c.name === bot.settings.category);
|
||||
if (category) {
|
||||
TicketChannel.setParent(category.id);
|
||||
} else {
|
||||
if (message.guild.channels.get(bot.settings.category)) {
|
||||
TicketChannel.setParent(message.guild.channels.get(bot.settings.category).id);
|
||||
}
|
||||
}
|
||||
|
||||
const ticketopened = new Discord.RichEmbed()
|
||||
.setDescription(`:white_check_mark: <@${message.author.id}> You ticket has been created <#${TicketChannel.id}>`)
|
||||
.setColor(bot.settings.colour)
|
||||
message.channel.send({embed: ticketopened});
|
||||
|
||||
// Ticket Message - ( Able to edit this message via the settings.json file )
|
||||
const ticketMessage = `Hi! <@${message.author.id}>\n${bot.settings.Ticket_Message}`;
|
||||
|
||||
const TicketMessage = new Discord.RichEmbed()
|
||||
.setDescription(ticketMessage)
|
||||
.setColor(bot.settings.colour)
|
||||
.setFooter(bot.settings.footer)
|
||||
TicketChannel.send({embed: TicketMessage});
|
||||
|
||||
if (subject != 'No Subject.') {
|
||||
TicketChannel.setTopic(subject);
|
||||
}
|
||||
|
||||
// Ticket Logging
|
||||
const logEmbed = new Discord.RichEmbed()
|
||||
.setTitle(":ticket: Logistics of your Ticket")
|
||||
.addField("Ticket ID", ID, true)
|
||||
.addField("User", `<@${message.author.id}>`, true)
|
||||
.addField("Channel", `ticket#${ID}`, true)
|
||||
.setColor(bot.settings.colour)
|
||||
.setFooter(bot.settings.footer)
|
||||
|
||||
if (subject != 'No Subject.') {
|
||||
logEmbed.addField('Subject', subject, true);
|
||||
}
|
||||
|
||||
let logChannel = message.guild.channels.find(TicketChannel => TicketChannel.name === `${bot.settings.Ticket_Logs}`);
|
||||
if(!logChannel) return message.channel.send(`:x: Error! Could not find the logs channel **${bot.settings.Ticket_Logs}**`);
|
||||
|
||||
logChannel.send({embed: logEmbed})
|
||||
|
||||
}).catch(err=>{console.error(err)});
|
||||
|
||||
console.log(`\x1b[36m`, `${message.author} has executed ${bot.settings.prefix}${bot.settings.Ticket_Command}`)
|
||||
|
||||
}
|
||||
|
||||
exports.help = {
|
||||
name: bot.settings.Ticket_Command,
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
// SupportBot
|
||||
// Command: Lockchat
|
||||
|
||||
const Discord = require("discord.js");
|
||||
const bot = new Discord.Client()
|
||||
|
||||
bot.settings = require("../settings.json");
|
||||
|
||||
exports.run = async(bot, message, args) => {
|
||||
message.delete();
|
||||
|
||||
let staffGroup = message.guild.roles.find(staffRole => staffRole.name === `${bot.settings.staff}`)
|
||||
let everyoneRole = message.guild.roles.find(everyone => everyone.name === `${bot.settings.everyone}`)
|
||||
|
||||
const rolemissing = new Discord.RichEmbed()
|
||||
.setDescription(`:x: Looks like this server doesn't have the role **${bot.settings.staff}**`)
|
||||
.setColor(bot.settings.colour)
|
||||
if (!staffGroup) return message.reply(rolemissing).catch(err=>{console.error(err)})
|
||||
|
||||
const donothaverole = new Discord.RichEmbed()
|
||||
.setDescription(`:x: Sorry! You cannot use this command with the role **${bot.settings.staff}**`)
|
||||
.setColor(bot.settings.colour)
|
||||
if (!message.member.roles.has(staffGroup.id)) return message.reply(donothaverole)
|
||||
|
||||
message.channel.overwritePermissions(everyoneRole, { READ_MESSAGES: true, SEND_MESSAGES: true });
|
||||
|
||||
const lockmsg = new Discord.RichEmbed()
|
||||
.setDescription(":white_check_mark: **Chat Un-Locked**")
|
||||
.setColor(bot.settings.colour)
|
||||
message.channel.send(lockmsg)
|
||||
|
||||
console.log(`\x1b[36m`, `${message.author} has executed ${bot.settings.prefix}${bot.settings.UnLockchat_Command}`)
|
||||
|
||||
}
|
||||
|
||||
exports.help = {
|
||||
name: bot.settings.UnLockchat_Command,
|
||||
}
|
||||
@@ -1,105 +1,101 @@
|
||||
// Resource: SupportBot
|
||||
// Developed by Emerald
|
||||
// Version: 3.5
|
||||
// ___ _ ___ _
|
||||
// / __> _ _ ___ ___ ___ _ _ _| |_ | . > ___ _| |_
|
||||
// \__ \| | || . \| . \/ . \| '_> | | | . \/ . \ | |
|
||||
// <___/`___|| _/| _/\___/|_| |_| |___/\___/ |_|
|
||||
// |_| |_|
|
||||
//
|
||||
// SupportBot created by Emerald Services
|
||||
// Installed with MIT License
|
||||
//
|
||||
// Discord Support: https://emeraldsrv.dev/discord
|
||||
// Community Resources: https://community.emeraldsrv.dev
|
||||
|
||||
const Discord = require("discord.js");
|
||||
const fs = require("fs");
|
||||
const bot = new Discord.Client();
|
||||
const path = require("path");
|
||||
|
||||
bot.commands = new Discord.Collection();
|
||||
bot.settings = require("./settings.json");
|
||||
const configStore = require("./Structures/ConfigStore.js");
|
||||
|
||||
fs.readdir("./commands/", (err, files) => {
|
||||
if(err) console.log(err);
|
||||
|
||||
let jsfile = files.filter(f => f.split(".").pop() === "js")
|
||||
if(jsfile.length <= 0) return console.log(`${bot.settings.botname} No commands found. Try to Re-download the resource`);
|
||||
|
||||
jsfile.forEach((f, i) =>{
|
||||
let props = require(`./commands/${f}`);
|
||||
console.log(`[${bot.settings.botname}] > Command Loaded > ${f}`);
|
||||
bot.commands.set(props.help.name, props);
|
||||
const Client = require("./Structures/Client.js");
|
||||
const client = new Client({
|
||||
intents: ["Guilds", "GuildMembers", "GuildMessages", "MessageContent"],
|
||||
});
|
||||
|
||||
const { isSetupComplete } = require("./Structures/DashboardSetup.js");
|
||||
const APIServer = require("./API/server.js");
|
||||
|
||||
const api = new APIServer(client);
|
||||
client.apiServer = api;
|
||||
|
||||
const port = api.config?.Port || 25575;
|
||||
if (api.config?.Enabled) {
|
||||
api.start(port);
|
||||
if (!isSetupComplete()) {
|
||||
console.log(`\n\x1b[33m\x1b[1m[Setup Required] Go to http://localhost:${port}/setup to set up your bot and dashboard.\x1b[0m\n`);
|
||||
}
|
||||
} else {
|
||||
console.warn("[Dashboard] API disabled in Configs/api.yml");
|
||||
}
|
||||
|
||||
const token = configStore.supportbot?.General?.Token;
|
||||
const isPlaceholderToken = !token || token.includes("BOT_TOKEN") || token.includes("CHANGE_ME") || token.length < 30;
|
||||
|
||||
if (isPlaceholderToken) {
|
||||
console.warn(`\x1b[33m[SupportBot] Bot token is not configured yet. Complete setup at http://localhost:${port}/setup\x1b[0m`);
|
||||
} else {
|
||||
client.start(token);
|
||||
}
|
||||
|
||||
// SupportBot - New Logging System
|
||||
|
||||
const logTypes = ["Output", "Warn", "Error"];
|
||||
|
||||
if (!fs.existsSync("./Logs")) {
|
||||
fs.mkdirSync("./Logs");
|
||||
}
|
||||
logTypes.forEach((type) => {
|
||||
const dir = `./Logs/${type}`;
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir);
|
||||
}
|
||||
});
|
||||
|
||||
function logToFile(type, data) {
|
||||
const date = new Date().toISOString().split("T")[0];
|
||||
const file = path.join(`./Logs/${type}`, `${type}-${date}.log`);
|
||||
const timestamp = new Date().toISOString();
|
||||
fs.appendFileSync(file, `[${timestamp}] ${data}\n`);
|
||||
|
||||
if (global.apiServer) {
|
||||
global.apiServer.broadcast("log", {
|
||||
type,
|
||||
timestamp,
|
||||
message: String(data)
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
bot.addons = new Discord.Collection();
|
||||
const origLog = console.log;
|
||||
console.log = (...args) => {
|
||||
origLog(...args);
|
||||
logToFile("Output", args.join(" "));
|
||||
};
|
||||
|
||||
fs.readdir("./sb-addons/", (err, files) => {
|
||||
if(err) console.log(err);
|
||||
|
||||
let jsfile = files.filter(f => f.split(".").pop() === "js")
|
||||
if(jsfile.length <= 0) return console.log(`${bot.settings.botname} No addons detected nor default! Either download addons from https://itzemerald.tk/`);
|
||||
|
||||
jsfile.forEach((f, i) =>{
|
||||
let props = require(`./sb-addons/${f}`);
|
||||
console.log(`[${bot.settings.botname}] > Addons Loaded > ${f}`);
|
||||
bot.addons.set(props.help.name, props);
|
||||
});
|
||||
|
||||
});
|
||||
const origWarn = console.warn;
|
||||
console.warn = (...args) => {
|
||||
origWarn(...args);
|
||||
logToFile("Warn", args.join(" "));
|
||||
};
|
||||
|
||||
bot.on("ready", async () => {
|
||||
bot.user.setActivity(bot.settings.BotStatus, {type:"PLAYING"});
|
||||
console.log(`\x1b[36m`, `=== [SupportBot] ===\n\nSupportBot - Created by Emerald Services\nInvite ${bot.user.username} to your server\nhttps://discordapp.com/api/oauth2/authorize?client_id=${bot.user.username}&permissions=8&scope=bot\n\n=== [SupportBot] ===`)
|
||||
const origError = console.error;
|
||||
console.error = (...args) => {
|
||||
origError(...args);
|
||||
logToFile("Error", args.join(" "));
|
||||
};
|
||||
|
||||
process.on("unhandledRejection", (reason) => {
|
||||
console.error("Unhandled Rejection:", reason);
|
||||
});
|
||||
|
||||
bot.on("message", async message => {
|
||||
if(message.author.bot) return;
|
||||
if(message.channel.type === "dm") return;
|
||||
if (message.content.indexOf(bot.settings.prefix) !== 0) return;
|
||||
|
||||
let messageArray = message.content.split(" ");
|
||||
const args = message.content.slice(bot.settings.prefix.length).trim().split(/ +/g);
|
||||
const command = args.shift().toLowerCase();
|
||||
|
||||
const cmd = bot.commands.get(command);
|
||||
if(!cmd) return;
|
||||
cmd.run(bot, message, args);
|
||||
process.on("uncaughtException", (err) => {
|
||||
console.error("Uncaught Exception:", err);
|
||||
});
|
||||
|
||||
bot.on("message", async message => {
|
||||
if(message.author.bot) return;
|
||||
if(message.channel.type === "dm") return;
|
||||
if (message.content.indexOf(bot.settings.prefix) !== 0) return;
|
||||
|
||||
let messageArray = message.content.split(" ");
|
||||
const args = message.content.slice(bot.settings.prefix.length).trim().split(/ +/g);
|
||||
const addon = args.shift().toLowerCase();
|
||||
|
||||
const cmd = bot.addons.get(addon);
|
||||
if(!cmd) return;
|
||||
cmd.run(bot, message, args);
|
||||
});
|
||||
|
||||
// [SupportBot] Welcome Message && Auto Join Role
|
||||
|
||||
bot.on("guildMemberAdd", (member) => {
|
||||
let channel = member.guild.channels.find(channel => channel.name === `${bot.settings.Welcome_Channel}`)
|
||||
let autorole = member.guild.roles.find(autorole => autorole.name === `${bot.settings.Auto_Role}`)
|
||||
|
||||
const WelcomeMsg= new Discord.RichEmbed()
|
||||
.setColor(bot.settings.colour)
|
||||
.setThumbnail(member.user.avatarURL)
|
||||
.setDescription(`:wave: ${member.user.username}\n${bot.settings.Welcome_Message}`)
|
||||
|
||||
channel.send(WelcomeMsg);
|
||||
member.addRole(role).catch(console.error);
|
||||
|
||||
});
|
||||
|
||||
// [SupportBot] Leave Message
|
||||
|
||||
bot.on("guildMemberRemove", (member) => {
|
||||
let channel = member.guild.channels.find(channel => channel.name === `${bot.settings.Leave_Channel}`)
|
||||
|
||||
const LeaveMsg= new Discord.RichEmbed()
|
||||
.setColor(bot.settings.colour)
|
||||
.setThumbnail(member.user.avatarURL)
|
||||
.setDescription(`:wave: ${member.user.username}\n${bot.settings.Leave_Message}`)
|
||||
|
||||
channel.send(LeaveMsg);
|
||||
|
||||
});
|
||||
|
||||
bot.login(bot.settings.token);
|
||||
+36
-12
@@ -1,18 +1,42 @@
|
||||
{
|
||||
"name": "supportbot",
|
||||
"version": "3.4",
|
||||
"description": "SupportBot",
|
||||
"version": "26.2-pre-2",
|
||||
"description": "SupportBot is an open-sourced support system for discord. It's completely free to use with high amounts of customization to match your own brand.",
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"discord.js": "^11.4.2"
|
||||
},
|
||||
"devDependencies": {},
|
||||
"scripts": {
|
||||
"test": "!help"
|
||||
"setup": "npm i && node index.js",
|
||||
"start": "node index.js",
|
||||
"git:setup": "node scripts/setup-git-security.js"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/Emerald-Services/SupportBot.git"
|
||||
},
|
||||
"keywords": [
|
||||
"SupportBot"
|
||||
],
|
||||
"author": "Emerald Services",
|
||||
"homepage": "https://discord.gg/hmWgxdF"
|
||||
}
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/Emerald-Services/SupportBot/issues"
|
||||
},
|
||||
"homepage": "https://emeraldsrv.dev",
|
||||
"dependencies": {
|
||||
"@vitalets/google-translate-api": "^9.2.1",
|
||||
"adm-zip": "^0.6.0",
|
||||
"axios": "^1.7.2",
|
||||
"better-sqlite3": "^12.2.0",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"cors": "^2.8.6",
|
||||
"discord.js": "^14.13.0",
|
||||
"express": "^5.2.1",
|
||||
"figlet": "^1.9.3",
|
||||
"google-translate-api": "^2.3.0",
|
||||
"gradient-string": "^3.0.0",
|
||||
"js": "^0.1.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"moment": "^2.29.3",
|
||||
"ms": "^2.1.3",
|
||||
"mysql2": "^3.23.3",
|
||||
"openai": "^4.50.0",
|
||||
"systeminformation": "^5.31.5",
|
||||
"yaml": "^2.8.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:Poppins,system-ui,sans-serif;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}:root{--background: 240 10% 4%;--foreground: 0 0% 98%;--card: 240 7% 11%;--card-foreground: 0 0% 98%;--popover: 240 8% 10%;--popover-foreground: 0 0% 98%;--primary: 141 45% 49%;--primary-foreground: 0 0% 100%;--secondary: 240 6% 14%;--secondary-foreground: 0 0% 90%;--muted: 240 6% 14%;--muted-foreground: 240 5%Line truncated
|
||||
@@ -0,0 +1,180 @@
|
||||
var w3=Object.defineProperty;var xy=t=>{throw TypeError(t)};var j3=(t,n,a)=>n in t?w3(t,n,{enumerable:!0,configurable:!0,writable:!0,value:a}):t[n]=a;var vy=(t,n,a)=>j3(t,typeof n!="symbol"?n+"":n,a),by=(t,n,a)=>n.has(t)||xy("Cannot "+a);var pn=(t,n,a)=>(by(t,n,"read from private field"),a?a.call(t):n.get(t)),yy=(t,n,a)=>n.has(t)?xy("Cannot add the same private member more than once"):n instanceof WeakSet?n.add(t):n.set(t,a),dp=(t,n,a,o)=>(by(t,n,"write to private field"),o?o.call(t,a):n.set(t,a),a);function C3(t,n){for(var a=0;a<n.length;a++){const o=n[a];if(typeof o!="string"&&!Array.isArray(o)){for(const c in o)if(c!=="default"&&!(c in t)){const l=Object.getOwnPropertyDescriptor(o,c);l&&Object.defineProperty(t,c,l.get?l:{enumerable:!0,get:()=>o[c]})}}}return Object.freeze(Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}))}(function(){const n=document.createElement("link").relList;if(n&&n.supports&&n.supports("modulepreload"))return;for(const c of document.querySelectorAll('link[rel="modulepreload"]'))o(c);new MutationObserver(c=>{for(const l of c)if(l.type==="childList")for(const d of l.addedNodes)d.tagName==="LINK"&&d.rel==="modulepreload"&&o(d)}).observe(document,{childList:!0,subtree:!0});function a(c){const l={};return c.integrity&&(l.integrity=c.integrity),c.referrerPolicy&&(l.referrerPolicy=c.referrerPolicy),c.crossOrigin==="use-credentials"?l.credentials="include":c.crossOrigin==="anonymous"?l.credentials="omit":l.credentials="same-origin",l}function o(c){if(c.ep)return;c.ep=!0;const l=a(c);fetch(c.href,l)}})();function s2(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var fp={exports:{}},Sl={};/**
|
||||
* @license React
|
||||
* react-jsx-runtime.production.js
|
||||
*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/var wy;function S3(){if(wy)return Sl;wy=1;var t=Symbol.for("react.transitional.element"),n=Symbol.for("react.fragment");function a(o,c,l){var d=null;if(l!==void 0&&(d=""+l),c.key!==void 0&&(d=""+c.key),"key"in c){l={};for(var m in c)m!=="key"&&(l[m]=c[m])}else l=c;return c=l.ref,{$$typeof:t,type:o,key:d,ref:c!==void 0?c:null,props:l}}return Sl.Fragment=n,Sl.jsx=a,Sl.jsxs=a,Sl}var jy;function N3(){return jy||(jy=1,fp.exports=S3()),fp.exports}var s=N3(),mp={exports:{}},Pe={};/**
|
||||
* @license React
|
||||
* react.production.js
|
||||
*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/var Cy;function E3(){if(Cy)return Pe;Cy=1;var t=Symbol.for("react.transitional.element"),n=Symbol.for("react.portal"),a=Symbol.for("react.fragment"),o=Symbol.for("react.strict_mode"),c=Symbol.for("react.profiler"),l=Symbol.for("react.consumer"),d=Symbol.for("react.context"),m=Symbol.for("react.forward_ref"),p=Symbol.for("react.suspense"),g=Symbol.for("react.memo"),v=Symbol.for("react.lazy"),b=Symbol.for("react.activity"),w=Symbol.iterator;function y(O){return O===null||typeof O!="object"?null:(O=w&&O[w]||O["@@iterator"],typeof O=="function"?O:null)}var C={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},S=Object.assign,E={};function k(O,D,$){this.props=O,this.context=D,this.refs=E,this.updater=$||C}k.prototype.isReactComponent={},k.prototype.setState=function(O,D){if(typeof O!="object"&&typeof O!="function"&&O!=null)throw Error("takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,O,D,"setState")},k.prototype.forceUpdate=function(O){this.updater.enqueueForceUpdate(this,O,"forceUpdate")};function N(){}N.prototype=k.prototype;function T(O,D,$){this.props=O,this.context=D,this.refs=E,this.updater=$||C}var M=T.prototype=new N;M.constructor=T,S(M,k.prototype),M.isPureReactComponent=!0;var _=Array.isArray;function R(){}var L={H:null,A:null,T:null,S:null},te=Object.prototype.hasOwnProperty;function K(O,D,$){var H=$.ref;return{$$typeof:t,type:O,key:D,ref:H!==void 0?H:null,props:$}}function I(O,D){return K(O.type,D,O.props)}function G(O){return typeof O=="object"&&O!==null&&O.$$typeof===t}function Y(O){var D={"=":"=0",":":"=2"};return"$"+O.replace(/[=:]/g,function($){return D[$]})}var ce=/\/+/g;function fe(O,D){return typeof O=="object"&&O!==null&&O.key!=null?Y(""+O.key):D.toString(36)}function se(O){switch(O.status){case"fulfilled":return O.value;case"rejected":throw O.reason;default:switch(typeof O.status=="string"?O.then(R,R):(O.status="pending",O.then(function(D){O.status==="pending"&&(O.status="fulfilled",O.value=D)},function(D){O.status==="pending"&&(O.status="rejected",O.reason=D)})),O.status){case"fulfilled":return O.value;case"rejected":throw O.reason}}throw O}function P(O,D,$,H,X){var xe=typeof O;(xe==="undefined"||xe==="boolean")&&(O=null);var q=!1;if(O===null)q=!0;else switch(xe){case"bigint":case"string":case"number":q=!0;break;case"object":switch(O.$$typeof){case t:case n:q=!0;break;case v:return q=O._init,P(q(O._payload),D,$,H,X)}}if(q)return X=X(O),q=H===""?"."+fe(O,0):H,_(X)?($="",q!=null&&($=q.replace(ce,"$&/")+"/"),P(X,D,$,"",function(B){return B})):X!=null&&(G(X)&&(X=I(X,$+(X.key==null||O&&O.key===X.key?"":(""+X.key).replace(ce,"$&/")+"/")+q)),D.push(X)),1;q=0;var ge=H===""?".":H+":";if(_(O))for(var me=0;me<O.length;me++)H=O[me],xe=ge+fe(H,me),q+=P(H,D,$,xe,X);else if(me=y(O),typeof me=="function")for(O=me.call(O),me=0;!(H=O.next()).done;)H=H.value,xe=ge+fe(H,me++),q+=P(H,D,$,xe,X);else if(xe==="object"){if(typeof O.then=="function")return P(se(O),D,$,H,X);throw D=String(O),Error("Objects are not valid as a React child (found: "+(D==="[object Object]"?"object with keys {"+Object.keys(O).join(", ")+"}":D)+"). If you meant to render a collection of children, use an array instead.")}return q}function Z(O,D,$){if(O==null)return O;var H=[],X=0;return P(O,H,"","",function(xe){return D.call($,xe,X++)}),H}function ae(O){if(O._status===-1){var D=O._result;D=D(),D.then(function($){(O._status===0||O._status===-1)&&(O._status=1,O._result=$)},function($){(O._status===0||O._status===-1)&&(O._status=2,O._result=$)}),O._status===-1&&(O._status=0,O._result=D)}if(O._status===1)return O._result.default;throw O._result}var re=typeof reportError=="function"?reportError:function(O){if(typeof window=="object"&&typeof window.ErrorEvent=="function"){var D=new window.ErrorEvent("error",{bubbles:!0,cancelable:!0,message:typeof O=="object"&&O!==null&&typeof O.message=="string"?String(O.message):String(O),error:O});if(!window.dispatchEvent(D))return}else if(typeof process=="object"&&typeof process.emit=="function"){process.emit("uncaughtException",O);return}console.error(O)},V={map:Z,forEach:function(O,D,$){Z(O,function(){D.apply(this,arguments)},$)},count:function(O){var D=0;return Z(O,function(){D++}),D},toArray:function(O){return Z(O,function(D){return D})||[]},only:function(O){if(!G(O))throw Error("React.Children.only expected to receive a single React element child.");return O}};return Pe.Activity=b,Pe.Children=V,Pe.Component=k,Pe.Fragment=a,Pe.Profiler=c,Pe.PureComponent=T,Pe.StrictMode=o,Pe.Suspense=p,Pe.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE=L,Pe.__COMPILER_RUNTIME={__proto__:null,c:function(O){return L.H.useMemoCache(O)}},Pe.cache=function(O){return function(){return O.apply(null,arguments)}},Pe.cacheSignal=function(){return null},Pe.cloneElement=function(O,D,$){if(O==null)throw Error("The argument must be a React element, but
Line truncated
|
||||
* @license React
|
||||
* scheduler.production.js
|
||||
*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/var Ny;function k3(){return Ny||(Ny=1,(function(t){function n(P,Z){var ae=P.length;P.push(Z);e:for(;0<ae;){var re=ae-1>>>1,V=P[re];if(0<c(V,Z))P[re]=Z,P[ae]=V,ae=re;else break e}}function a(P){return P.length===0?null:P[0]}function o(P){if(P.length===0)return null;var Z=P[0],ae=P.pop();if(ae!==Z){P[0]=ae;e:for(var re=0,V=P.length,O=V>>>1;re<O;){var D=2*(re+1)-1,$=P[D],H=D+1,X=P[H];if(0>c($,ae))H<V&&0>c(X,$)?(P[re]=X,P[H]=ae,re=H):(P[re]=$,P[D]=ae,re=D);else if(H<V&&0>c(X,ae))P[re]=X,P[H]=ae,re=H;else break e}}return Z}function c(P,Z){var ae=P.sortIndex-Z.sortIndex;return ae!==0?ae:P.id-Z.id}if(t.unstable_now=void 0,typeof performance=="object"&&typeof performance.now=="function"){var l=performance;t.unstable_now=function(){return l.now()}}else{var d=Date,m=d.now();t.unstable_now=function(){return d.now()-m}}var p=[],g=[],v=1,b=null,w=3,y=!1,C=!1,S=!1,E=!1,k=typeof setTimeout=="function"?setTimeout:null,N=typeof clearTimeout=="function"?clearTimeout:null,T=typeof setImmediate<"u"?setImmediate:null;function M(P){for(var Z=a(g);Z!==null;){if(Z.callback===null)o(g);else if(Z.startTime<=P)o(g),Z.sortIndex=Z.expirationTime,n(p,Z);else break;Z=a(g)}}function _(P){if(S=!1,M(P),!C)if(a(p)!==null)C=!0,R||(R=!0,Y());else{var Z=a(g);Z!==null&&se(_,Z.startTime-P)}}var R=!1,L=-1,te=5,K=-1;function I(){return E?!0:!(t.unstable_now()-K<te)}function G(){if(E=!1,R){var P=t.unstable_now();K=P;var Z=!0;try{e:{C=!1,S&&(S=!1,N(L),L=-1),y=!0;var ae=w;try{t:{for(M(P),b=a(p);b!==null&&!(b.expirationTime>P&&I());){var re=b.callback;if(typeof re=="function"){b.callback=null,w=b.priorityLevel;var V=re(b.expirationTime<=P);if(P=t.unstable_now(),typeof V=="function"){b.callback=V,M(P),Z=!0;break t}b===a(p)&&o(p),M(P)}else o(p);b=a(p)}if(b!==null)Z=!0;else{var O=a(g);O!==null&&se(_,O.startTime-P),Z=!1}}break e}finally{b=null,w=ae,y=!1}Z=void 0}}finally{Z?Y():R=!1}}}var Y;if(typeof T=="function")Y=function(){T(G)};else if(typeof MessageChannel<"u"){var ce=new MessageChannel,fe=ce.port2;ce.port1.onmessage=G,Y=function(){fe.postMessage(null)}}else Y=function(){k(G,0)};function se(P,Z){L=k(function(){P(t.unstable_now())},Z)}t.unstable_IdlePriority=5,t.unstable_ImmediatePriority=1,t.unstable_LowPriority=4,t.unstable_NormalPriority=3,t.unstable_Profiling=null,t.unstable_UserBlockingPriority=2,t.unstable_cancelCallback=function(P){P.callback=null},t.unstable_forceFrameRate=function(P){0>P||125<P?console.error("forceFrameRate takes a positive int between 0 and 125, forcing frame rates higher than 125 fps is not supported"):te=0<P?Math.floor(1e3/P):5},t.unstable_getCurrentPriorityLevel=function(){return w},t.unstable_next=function(P){switch(w){case 1:case 2:case 3:var Z=3;break;default:Z=w}var ae=w;w=Z;try{return P()}finally{w=ae}},t.unstable_requestPaint=function(){E=!0},t.unstable_runWithPriority=function(P,Z){switch(P){case 1:case 2:case 3:case 4:case 5:break;default:P=3}var ae=w;w=P;try{return Z()}finally{w=ae}},t.unstable_scheduleCallback=function(P,Z,ae){var re=t.unstable_now();switch(typeof ae=="object"&&ae!==null?(ae=ae.delay,ae=typeof ae=="number"&&0<ae?re+ae:re):ae=re,P){case 1:var V=-1;break;case 2:V=250;break;case 5:V=1073741823;break;case 4:V=1e4;break;default:V=5e3}return V=ae+V,P={id:v++,callback:Z,priorityLevel:P,startTime:ae,expirationTime:V,sortIndex:-1},ae>re?(P.sortIndex=ae,n(g,P),a(p)===null&&P===a(g)&&(S?(N(L),L=-1):S=!0,se(_,ae-re))):(P.sortIndex=V,n(p,P),C||y||(C=!0,R||(R=!0,Y()))),P},t.unstable_shouldYield=I,t.unstable_wrapCallback=function(P){var Z=w;return function(){var ae=w;w=Z;try{return P.apply(this,arguments)}finally{w=ae}}}})(gp)),gp}var Ey;function T3(){return Ey||(Ey=1,hp.exports=k3()),hp.exports}var xp={exports:{}},hn={};/**
|
||||
* @license React
|
||||
* react-dom.production.js
|
||||
*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/var ky;function M3(){if(ky)return hn;ky=1;var t=Xh();function n(p){var g="https://react.dev/errors/"+p;if(1<arguments.length){g+="?args[]="+encodeURIComponent(arguments[1]);for(var v=2;v<arguments.length;v++)g+="&args[]="+encodeURIComponent(arguments[v])}return"Minified React error #"+p+"; visit "+g+" for the full message or use the non-minified dev environment for full errors and additional helpful warnings."}function a(){}var o={d:{f:a,r:function(){throw Error(n(522))},D:a,C:a,L:a,m:a,X:a,S:a,M:a},p:0,findDOMNode:null},c=Symbol.for("react.portal");function l(p,g,v){var b=3<arguments.length&&arguments[3]!==void 0?arguments[3]:null;return{$$typeof:c,key:b==null?null:""+b,children:p,containerInfo:g,implementation:v}}var d=t.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;function m(p,g){if(p==="font")return"";if(typeof g=="string")return g==="use-credentials"?g:""}return hn.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE=o,hn.createPortal=function(p,g){var v=2<arguments.length&&arguments[2]!==void 0?arguments[2]:null;if(!g||g.nodeType!==1&&g.nodeType!==9&&g.nodeType!==11)throw Error(n(299));return l(p,g,null,v)},hn.flushSync=function(p){var g=d.T,v=o.p;try{if(d.T=null,o.p=2,p)return p()}finally{d.T=g,o.p=v,o.d.f()}},hn.preconnect=function(p,g){typeof p=="string"&&(g?(g=g.crossOrigin,g=typeof g=="string"?g==="use-credentials"?g:"":void 0):g=null,o.d.C(p,g))},hn.prefetchDNS=function(p){typeof p=="string"&&o.d.D(p)},hn.preinit=function(p,g){if(typeof p=="string"&&g&&typeof g.as=="string"){var v=g.as,b=m(v,g.crossOrigin),w=typeof g.integrity=="string"?g.integrity:void 0,y=typeof g.fetchPriority=="string"?g.fetchPriority:void 0;v==="style"?o.d.S(p,typeof g.precedence=="string"?g.precedence:void 0,{crossOrigin:b,integrity:w,fetchPriority:y}):v==="script"&&o.d.X(p,{crossOrigin:b,integrity:w,fetchPriority:y,nonce:typeof g.nonce=="string"?g.nonce:void 0})}},hn.preinitModule=function(p,g){if(typeof p=="string")if(typeof g=="object"&&g!==null){if(g.as==null||g.as==="script"){var v=m(g.as,g.crossOrigin);o.d.M(p,{crossOrigin:v,integrity:typeof g.integrity=="string"?g.integrity:void 0,nonce:typeof g.nonce=="string"?g.nonce:void 0})}}else g==null&&o.d.M(p)},hn.preload=function(p,g){if(typeof p=="string"&&typeof g=="object"&&g!==null&&typeof g.as=="string"){var v=g.as,b=m(v,g.crossOrigin);o.d.L(p,v,{crossOrigin:b,integrity:typeof g.integrity=="string"?g.integrity:void 0,nonce:typeof g.nonce=="string"?g.nonce:void 0,type:typeof g.type=="string"?g.type:void 0,fetchPriority:typeof g.fetchPriority=="string"?g.fetchPriority:void 0,referrerPolicy:typeof g.referrerPolicy=="string"?g.referrerPolicy:void 0,imageSrcSet:typeof g.imageSrcSet=="string"?g.imageSrcSet:void 0,imageSizes:typeof g.imageSizes=="string"?g.imageSizes:void 0,media:typeof g.media=="string"?g.media:void 0})}},hn.preloadModule=function(p,g){if(typeof p=="string")if(g){var v=m(g.as,g.crossOrigin);o.d.m(p,{as:typeof g.as=="string"&&g.as!=="script"?g.as:void 0,crossOrigin:v,integrity:typeof g.integrity=="string"?g.integrity:void 0})}else o.d.m(p)},hn.requestFormReset=function(p){o.d.r(p)},hn.unstable_batchedUpdates=function(p,g){return p(g)},hn.useFormState=function(p,g,v){return d.H.useFormState(p,g,v)},hn.useFormStatus=function(){return d.H.useHostTransitionStatus()},hn.version="19.2.8",hn}var Ty;function o2(){if(Ty)return xp.exports;Ty=1;function t(){if(!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__>"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(t)}catch(n){console.error(n)}}return t(),xp.exports=M3(),xp.exports}/**
|
||||
* @license React
|
||||
* react-dom-client.production.js
|
||||
*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/var My;function R3(){if(My)return Nl;My=1;var t=T3(),n=Xh(),a=o2();function o(e){var r="https://react.dev/errors/"+e;if(1<arguments.length){r+="?args[]="+encodeURIComponent(arguments[1]);for(var i=2;i<arguments.length;i++)r+="&args[]="+encodeURIComponent(arguments[i])}return"Minified React error #"+e+"; visit "+r+" for the full message or use the non-minified dev environment for full errors and additional helpful warnings."}function c(e){return!(!e||e.nodeType!==1&&e.nodeType!==9&&e.nodeType!==11)}function l(e){var r=e,i=e;if(e.alternate)for(;r.return;)r=r.return;else{e=r;do r=e,(r.flags&4098)!==0&&(i=r.return),e=r.return;while(e)}return r.tag===3?i:null}function d(e){if(e.tag===13){var r=e.memoizedState;if(r===null&&(e=e.alternate,e!==null&&(r=e.memoizedState)),r!==null)return r.dehydrated}return null}function m(e){if(e.tag===31){var r=e.memoizedState;if(r===null&&(e=e.alternate,e!==null&&(r=e.memoizedState)),r!==null)return r.dehydrated}return null}function p(e){if(l(e)!==e)throw Error(o(188))}function g(e){var r=e.alternate;if(!r){if(r=l(e),r===null)throw Error(o(188));return r!==e?null:e}for(var i=e,u=r;;){var h=i.return;if(h===null)break;var x=h.alternate;if(x===null){if(u=h.return,u!==null){i=u;continue}break}if(h.child===x.child){for(x=h.child;x;){if(x===i)return p(h),e;if(x===u)return p(h),r;x=x.sibling}throw Error(o(188))}if(i.return!==u.return)i=h,u=x;else{for(var j=!1,A=h.child;A;){if(A===i){j=!0,i=h,u=x;break}if(A===u){j=!0,u=h,i=x;break}A=A.sibling}if(!j){for(A=x.child;A;){if(A===i){j=!0,i=x,u=h;break}if(A===u){j=!0,u=x,i=h;break}A=A.sibling}if(!j)throw Error(o(189))}}if(i.alternate!==u)throw Error(o(190))}if(i.tag!==3)throw Error(o(188));return i.stateNode.current===i?e:r}function v(e){var r=e.tag;if(r===5||r===26||r===27||r===6)return e;for(e=e.child;e!==null;){if(r=v(e),r!==null)return r;e=e.sibling}return null}var b=Object.assign,w=Symbol.for("react.element"),y=Symbol.for("react.transitional.element"),C=Symbol.for("react.portal"),S=Symbol.for("react.fragment"),E=Symbol.for("react.strict_mode"),k=Symbol.for("react.profiler"),N=Symbol.for("react.consumer"),T=Symbol.for("react.context"),M=Symbol.for("react.forward_ref"),_=Symbol.for("react.suspense"),R=Symbol.for("react.suspense_list"),L=Symbol.for("react.memo"),te=Symbol.for("react.lazy"),K=Symbol.for("react.activity"),I=Symbol.for("react.memo_cache_sentinel"),G=Symbol.iterator;function Y(e){return e===null||typeof e!="object"?null:(e=G&&e[G]||e["@@iterator"],typeof e=="function"?e:null)}var ce=Symbol.for("react.client.reference");function fe(e){if(e==null)return null;if(typeof e=="function")return e.$$typeof===ce?null:e.displayName||e.name||null;if(typeof e=="string")return e;switch(e){case S:return"Fragment";case k:return"Profiler";case E:return"StrictMode";case _:return"Suspense";case R:return"SuspenseList";case K:return"Activity"}if(typeof e=="object")switch(e.$$typeof){case C:return"Portal";case T:return e.displayName||"Context";case N:return(e._context.displayName||"Context")+".Consumer";case M:var r=e.render;return e=e.displayName,e||(e=r.displayName||r.name||"",e=e!==""?"ForwardRef("+e+")":"ForwardRef"),e;case L:return r=e.displayName||null,r!==null?r:fe(e.type)||"Memo";case te:r=e._payload,e=e._init;try{return fe(e(r))}catch{}}return null}var se=Array.isArray,P=n.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE,Z=a.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE,ae={pending:!1,data:null,method:null,action:null},re=[],V=-1;function O(e){return{current:e}}function D(e){0>V||(e.current=re[V],re[V]=null,V--)}function $(e,r){V++,re[V]=e.current,e.current=r}var H=O(null),X=O(null),xe=O(null),q=O(null);function ge(e,r){switch($(xe,r),$(X,e),$(H,null),r.nodeType){case 9:case 11:e=(e=r.documentElement)&&(e=e.namespaceURI)?Bb(e):0;break;default:if(e=r.tagName,r=r.namespaceURI)r=Bb(r),e=$b(r,e);else switch(e){case"svg":e=1;break;case"math":e=2;break;default:e=0}}D(H),$(H,e)}function me(){D(H),D(X),D(xe)}function B(e){e.memoizedState!==null&&$(q,e);var r=H.current,i=$b(r,e.type);r!==i&&($(X,e),$(H,i))}function ie(e){X.current===e&&(D(H),D(X)),q.current===e&&(D(q),yl._currentValue=ae)}var we,Ce;function ze(e){if(we===void 0)try{throw Error()}catch(i){var r=i.stack.trim().match(/\n( *(at )?)/);we=r&&r[1]||"",Ce=-1<i.stack.indexOf(`
|
||||
at`)?" (<anonymous>)":-1<i.stack.indexOf("@")?"@unknown:0:0":""}return`
|
||||
`+we+e+Ce}var Ke=!1;function ft(e,r){if(!e||Ke)return"";Ke=!0;var i=Error.prepareStackTrace;Error.prepareStackTrace=void 0;try{var u={DetermineComponentFrameRoot:function(){try{if(r){var he=function(){throw Error()};if(Object.defineProperty(he.prototype,"props",{set:function(){throw Error()}}),typeof Reflect=="object"&&Reflect.construct){try{Reflect.construct(he,[])}catch(ne){var ee=ne}Reflect.construct(e,[],he)}else{try{he.call()}catch(ne){ee=ne}e.call(he.prototype)}}else{try{throw Error()}catch(ne){ee=ne}(he=e())&&typeof he.catch=="function"&&he.catch(function(){})}}catch(ne){if(ne&&ee&&typeof ne.stack=="string")return[ne.stack,ee.stack]}return[null,null]}};u.DetermineComponentFrameRoot.displayName="DetermineComponentFrameRoot";var h=Object.getOwnPropertyDescriptor(u.DetermineComponentFrameRoot,"name");h&&h.configurable&&Object.defineProperty(u.DetermineComponentFrameRoot,"name",{value:"DetermineComponentFrameRoot"});var x=u.DetermineComponentFrameRoot(),j=x[0],A=x[1];if(j&&A){var z=j.split(`
|
||||
`),J=A.split(`
|
||||
`);for(h=u=0;u<z.length&&!z[u].includes("DetermineComponentFrameRoot");)u++;for(;h<J.length&&!J[h].includes("DetermineComponentFrameRoot");)h++;if(u===z.length||h===J.length)for(u=z.length-1,h=J.length-1;1<=u&&0<=h&&z[u]!==J[h];)h--;for(;1<=u&&0<=h;u--,h--)if(z[u]!==J[h]){if(u!==1||h!==1)do if(u--,h--,0>h||z[u]!==J[h]){var le=`
|
||||
`+z[u].replace(" at new "," at ");return e.displayName&&le.includes("<anonymous>")&&(le=le.replace("<anonymous>",e.displayName)),le}while(1<=u&&0<=h);break}}}finally{Ke=!1,Error.prepareStackTrace=i}return(i=e?e.displayName||e.name:"")?ze(i):""}function Pt(e,r){switch(e.tag){case 26:case 27:case 5:return ze(e.type);case 16:return ze("Lazy");case 13:return e.child!==r&&r!==null?ze("Suspense Fallback"):ze("Suspense");case 19:return ze("SuspenseList");case 0:case 15:return ft(e.type,!1);case 11:return ft(e.type.render,!1);case 1:return ft(e.type,!0);case 31:return ze("Activity");default:return""}}function gn(e){try{var r="",i=null;do r+=Pt(e,i),i=e,e=e.return;while(e);return r}catch(u){return`
|
||||
Error generating stack: `+u.message+`
|
||||
`+u.stack}}var Yr=Object.prototype.hasOwnProperty,_n=t.unstable_scheduleCallback,wn=t.unstable_cancelCallback,Xn=t.unstable_shouldYield,Zn=t.unstable_requestPaint,It=t.unstable_now,po=t.unstable_getCurrentPriorityLevel,nn=t.unstable_ImmediatePriority,Dn=t.unstable_UserBlockingPriority,Da=t.unstable_NormalPriority,ho=t.unstable_LowPriority,Qn=t.unstable_IdlePriority,Es=t.log,qr=t.unstable_setDisableYieldValue,gr=null,Tt=null;function On(e){if(typeof Es=="function"&&qr(e),Tt&&typeof Tt.setStrictMode=="function")try{Tt.setStrictMode(gr,e)}catch{}}var Mt=Math.clz32?Math.clz32:go,Oi=Math.log,Li=Math.LN2;function go(e){return e>>>=0,e===0?32:31-(Oi(e)/Li|0)|0}var Kr=256,Oa=262144,Mr=4194304;function Kt(e){var r=e&42;if(r!==0)return r;switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return e&261888;case 262144:case 524288:case 1048576:case 2097152:return e&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return e&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return e}}function La(e,r,i){var u=e.pendingLanes;if(u===0)return 0;var h=0,x=e.suspendedLanes,j=e.pingedLanes;e=e.warmLanes;var A=u&134217727;return A!==0?(u=A&~x,u!==0?h=Kt(u):(j&=A,j!==0?h=Kt(j):i||(i=A&~e,i!==0&&(h=Kt(i))))):(A=u&~x,A!==0?h=Kt(A):j!==0?h=Kt(j):i||(i=u&~e,i!==0&&(h=Kt(i)))),h===0?0:r!==0&&r!==h&&(r&x)===0&&(x=h&-h,i=r&-r,x>=i||x===32&&(i&4194048)!==0)?r:h}function Oe(e,r){return(e.pendingLanes&~(e.suspendedLanes&~e.pingedLanes)&r)===0}function tt(e,r){switch(e){case 1:case 2:case 4:case 8:case 64:return r+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return r+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function it(){var e=Mr;return Mr<<=1,(Mr&62914560)===0&&(Mr=4194304),e}function rn(e){for(var r=[],i=0;31>i;i++)r.push(e);return r}function Ft(e,r){e.pendingLanes|=r,r!==268435456&&(e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0)}function Rt(e,r,i,u,h,x){var j=e.pendingLanes;e.pendingLanes=i,e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0,e.expiredLanes&=i,e.entangledLanes&=i,e.errorRecoveryDisabledLanes&=i,e.shellSuspendCounter=0;var A=e.entanglements,z=e.expirationTimes,J=e.hiddenUpdates;for(i=j&~i;0<i;){var le=31-Mt(i),he=1<<le;A[le]=0,z[le]=-1;var ee=J[le];if(ee!==null)for(J[le]=null,le=0;le<ee.length;le++){var ne=ee[le];ne!==null&&(ne.lane&=-536870913)}i&=~he}u!==0&&Jn(e,u,0),x!==0&&h===0&&e.tag!==0&&(e.suspendedLanes|=x&~(j&~r))}function Jn(e,r,i){e.pendingLanes|=r,e.suspendedLanes&=~r;var u=31-Mt(r);e.entangledLanes|=r,e.entanglements[u]=e.entanglements[u]|1073741824|i&261930}function fn(e,r){var i=e.entangledLanes|=r;for(e=e.entanglements;i;){var u=31-Mt(i),h=1<<u;h&r|e[u]&r&&(e[u]|=r),i&=~h}}function Ln(e,r){var i=r&-r;return i=(i&42)!==0?1:xo(i),(i&(e.suspendedLanes|r))!==0?0:i}function xo(e){switch(e){case 2:e=1;break;case 8:e=4;break;case 32:e=16;break;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:case 4194304:case 8388608:case 16777216:case 33554432:e=128;break;case 268435456:e=134217728;break;default:e=0}return e}function vo(e){return e&=-e,2<e?8<e?(e&134217727)!==0?32:268435456:8:2}function W(){var e=Z.p;return e!==0?e:(e=window.event,e===void 0?32:uy(e.type))}function oe(e,r){var i=Z.p;try{return Z.p=e,r()}finally{Z.p=i}}var Se=Math.random().toString(36).slice(2),ye="__reactFiber$"+Se,Re="__reactProps$"+Se,nt="__reactContainer$"+Se,mt="__reactEvents$"+Se,bt="__reactListeners$"+Se,Rr="__reactHandles$"+Se,xn="__reactResources$"+Se,mn="__reactMarker$"+Se;function Wr(e){delete e[ye],delete e[Re],delete e[mt],delete e[bt],delete e[Rr]}function xr(e){var r=e[ye];if(r)return r;for(var i=e.parentNode;i;){if(r=i[nt]||i[ye]){if(i=r.alternate,r.child!==null||i!==null&&i.child!==null)for(e=Kb(e);e!==null;){if(i=e[ye])return i;e=Kb(e)}return r}e=i,i=e.parentNode}return null}function Xr(e){if(e=e[ye]||e[nt]){var r=e.tag;if(r===5||r===6||r===13||r===31||r===26||r===27||r===3)return e}return null}function zi(e){var r=e.tag;if(r===5||r===26||r===27||r===6)return e.stateNode;throw Error(o(33))}function bo(e){var r=e[xn];return r||(r=e[xn]={hoistableStyles:new Map,hoistableScripts:new Map}),r}function Wt(e){e[mn]=!0}var Tx=new Set,Mx={};function ks(e,r){yo(e,r),yo(e+"Capture",r)}function yo(e,r){for(Mx[e]=r,e=0;e<r.length;e++)Tx.add(r[e])}var f5=RegExp("^[:A-Z_a-z\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\Line truncated
|
||||
`).replace(G4,"")}function Ib(e,r){return r=Pb(r),Pb(e)===r}function ht(e,r,i,u,h,x){switch(i){case"children":typeof u=="string"?r==="body"||r==="textarea"&&u===""||jo(e,u):(typeof u=="number"||typeof u=="bigint")&&r!=="body"&&jo(e,""+u);break;case"className":gc(e,"class",u);break;case"tabIndex":gc(e,"tabindex",u);break;case"dir":case"role":case"viewBox":case"width":case"height":gc(e,i,u);break;case"style":Ix(e,u,x);break;case"data":if(r!=="object"){gc(e,"data",u);break}case"src":case"href":if(u===""&&(r!=="a"||i!=="href")){e.removeAttribute(i);break}if(u==null||typeof u=="function"||typeof u=="symbol"||typeof u=="boolean"){e.removeAttribute(i);break}u=vc(""+u),e.setAttribute(i,u);break;case"action":case"formAction":if(typeof u=="function"){e.setAttribute(i,"javascript:throw new Error('A React form was unexpectedly submitted. If you called form.submit() manually, consider using form.requestSubmit() instead. If you\\'re trying to use event.stopPropagation() in a submit event handler, consider also calling event.preventDefault().')");break}else typeof x=="function"&&(i==="formAction"?(r!=="input"&&ht(e,r,"name",h.name,h,null),ht(e,r,"formEncType",h.formEncType,h,null),ht(e,r,"formMethod",h.formMethod,h,null),ht(e,r,"formTarget",h.formTarget,h,null)):(ht(e,r,"encType",h.encType,h,null),ht(e,r,"method",h.method,h,null),ht(e,r,"target",h.target,h,null)));if(u==null||typeof u=="symbol"||typeof u=="boolean"){e.removeAttribute(i);break}u=vc(""+u),e.setAttribute(i,u);break;case"onClick":u!=null&&(e.onclick=Qr);break;case"onScroll":u!=null&&Ve("scroll",e);break;case"onScrollEnd":u!=null&&Ve("scrollend",e);break;case"dangerouslySetInnerHTML":if(u!=null){if(typeof u!="object"||!("__html"in u))throw Error(o(61));if(i=u.__html,i!=null){if(h.children!=null)throw Error(o(60));e.innerHTML=i}}break;case"multiple":e.multiple=u&&typeof u!="function"&&typeof u!="symbol";break;case"muted":e.muted=u&&typeof u!="function"&&typeof u!="symbol";break;case"suppressContentEditableWarning":case"suppressHydrationWarning":case"defaultValue":case"defaultChecked":case"innerHTML":case"ref":break;case"autoFocus":break;case"xlinkHref":if(u==null||typeof u=="function"||typeof u=="boolean"||typeof u=="symbol"){e.removeAttribute("xlink:href");break}i=vc(""+u),e.setAttributeNS("http://www.w3.org/1999/xlink","xlink:href",i);break;case"contentEditable":case"spellCheck":case"draggable":case"value":case"autoReverse":case"externalResourcesRequired":case"focusable":case"preserveAlpha":u!=null&&typeof u!="function"&&typeof u!="symbol"?e.setAttribute(i,""+u):e.removeAttribute(i);break;case"inert":case"allowFullScreen":case"async":case"autoPlay":case"controls":case"default":case"defer":case"disabled":case"disablePictureInPicture":case"disableRemotePlayback":case"formNoValidate":case"hidden":case"loop":case"noModule":case"noValidate":case"open":case"playsInline":case"readOnly":case"required":case"reversed":case"scoped":case"seamless":case"itemScope":u&&typeof u!="function"&&typeof u!="symbol"?e.setAttribute(i,""):e.removeAttribute(i);break;case"capture":case"download":u===!0?e.setAttribute(i,""):u!==!1&&u!=null&&typeof u!="function"&&typeof u!="symbol"?e.setAttribute(i,u):e.removeAttribute(i);break;case"cols":case"rows":case"size":case"span":u!=null&&typeof u!="function"&&typeof u!="symbol"&&!isNaN(u)&&1<=u?e.setAttribute(i,u):e.removeAttribute(i);break;case"rowSpan":case"start":u==null||typeof u=="function"||typeof u=="symbol"||isNaN(u)?e.removeAttribute(i):e.setAttribute(i,u);break;case"popover":Ve("beforetoggle",e),Ve("toggle",e),hc(e,"popover",u);break;case"xlinkActuate":Zr(e,"http://www.w3.org/1999/xlink","xlink:actuate",u);break;case"xlinkArcrole":Zr(e,"http://www.w3.org/1999/xlink","xlink:arcrole",u);break;case"xlinkRole":Zr(e,"http://www.w3.org/1999/xlink","xlink:role",u);break;case"xlinkShow":Zr(e,"http://www.w3.org/1999/xlink","xlink:show",u);break;case"xlinkTitle":Zr(e,"http://www.w3.org/1999/xlink","xlink:title",u);break;case"xlinkType":Zr(e,"http://www.w3.org/1999/xlink","xlink:type",u);break;case"xmlBase":Zr(e,"http://www.w3.org/XML/1998/namespace","xml:base",u);break;case"xmlLang":Zr(e,"http://www.w3.org/XML/1998/namespace","xml:lang",u);break;case"xmlSpace":Zr(e,"http://www.w3.org/XML/1998/namespace","xml:space",u);break;case"is":hc(e,"is",u);break;case"innerText":case"textContent":break;default:(!(2<i.length)||i[0]!=="o"&&i[0]!=="O"||i[1]!=="n"&&i[1]!=="N")&&(i=x5.get(i)||i,hc(e,i,u))}}function qm(e,r,i,u,h,x){switch(i){case"style":Ix(e,u,x);break;case"dangerouslySetInnerHTML":if(u!=null){if(typeof u!="object"||!("__html"in u))throw Error(o(61));if(i=u.__html,i!=null){if(h.children!=null)throw Error(o(60));e.innerHTML=i}}break;case"children":typeof u=="string"?jo(e,u):(typeof u=="number"||typeof u=="bigint")&&jo(e,""+u);break;case"onScroll":u!=null&&Ve("scroll",e);break;case"onScrollEnd":u!=null&&Ve("scrollend",e);break;case"onClick":u!=null&&(e.onclick=Qr);break;case"suppressContentEditableWarning":case"suppressHydrationWarning":case"inne
Line truncated
|
||||
* react-router v7.18.2
|
||||
*
|
||||
* Copyright (c) Remix Software Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE.md file in the root directory of this source tree.
|
||||
*
|
||||
* @license MIT
|
||||
*/var Zh=/^(?:[a-z][a-z0-9+.-]*:|[\\/]{2})/i,i2=/^[\\/]{2}/;function D3(t,n){return n+t.replace(/\\/g,"/")}var Ay="popstate";function _y(t){return typeof t=="object"&&t!=null&&"pathname"in t&&"search"in t&&"hash"in t&&"state"in t&&"key"in t}function O3(t={}){function n(o,c){var g;let l=(g=c.state)==null?void 0:g.masked,{pathname:d,search:m,hash:p}=l||o.location;return Qp("",{pathname:d,search:m,hash:p},c.state&&c.state.usr||null,c.state&&c.state.key||"default",l?{pathname:o.location.pathname,search:o.location.search,hash:o.location.hash}:void 0)}function a(o,c){return typeof c=="string"?c:Il(c)}return z3(n,a,null,t)}function Nt(t,n){if(t===!1||t===null||typeof t>"u")throw new Error(n)}function fr(t,n){if(!t){typeof console<"u"&&console.warn(n);try{throw new Error(n)}catch{}}}function L3(){return Math.random().toString(36).substring(2,10)}function Dy(t,n){return{usr:t.state,key:t.key,idx:n,masked:t.mask?{pathname:t.pathname,search:t.search,hash:t.hash}:void 0}}function Qp(t,n,a=null,o,c){return{pathname:typeof t=="string"?t:t.pathname,search:"",hash:"",...typeof n=="string"?vi(n):n,state:a,key:n&&n.key||o||L3(),mask:c}}function Il({pathname:t="/",search:n="",hash:a=""}){return n&&n!=="?"&&(t+=n.charAt(0)==="?"?n:"?"+n),a&&a!=="#"&&(t+=a.charAt(0)==="#"?a:"#"+a),t}function vi(t){let n={};if(t){let a=t.indexOf("#");a>=0&&(n.hash=t.substring(a),t=t.substring(0,a));let o=t.indexOf("?");o>=0&&(n.search=t.substring(o),t=t.substring(0,o)),t&&(n.pathname=t)}return n}function z3(t,n,a,o={}){let{window:c=document.defaultView,v5Compat:l=!1}=o,d=c.history,m="POP",p=null,g=v();g==null&&(g=0,d.replaceState({...d.state,idx:g},""));function v(){return(d.state||{idx:null}).idx}function b(){m="POP";let E=v(),k=E==null?null:E-g;g=E,p&&p({action:m,location:S.location,delta:k})}function w(E,k){m="PUSH";let N=_y(E)?E:Qp(S.location,E,k);g=v()+1;let T=Dy(N,g),M=S.createHref(N.mask||N);try{d.pushState(T,"",M)}catch(_){if(_ instanceof DOMException&&_.name==="DataCloneError")throw _;c.location.assign(M)}l&&p&&p({action:m,location:S.location,delta:1})}function y(E,k){m="REPLACE";let N=_y(E)?E:Qp(S.location,E,k);g=v();let T=Dy(N,g),M=S.createHref(N.mask||N);d.replaceState(T,"",M),l&&p&&p({action:m,location:S.location,delta:0})}function C(E){return P3(c,E)}let S={get action(){return m},get location(){return t(c,d)},listen(E){if(p)throw new Error("A history only accepts one active listener");return c.addEventListener(Ay,b),p=E,()=>{c.removeEventListener(Ay,b),p=null}},createHref(E){return n(c,E)},createURL:C,encodeLocation(E){let k=C(E);return{pathname:k.pathname,search:k.search,hash:k.hash}},push:w,replace:y,go(E){return d.go(E)}};return S}function P3(t,n,a=!1){let o="http://localhost";t&&(o=t.location.origin!=="null"?t.location.origin:t.location.href),Nt(o,"No window.location.(origin|href) available to create URL");let c=typeof n=="string"?n:Il(n);return c=c.replace(/ $/,"%20"),!a&&i2.test(c)&&(c=o+c),new URL(c,o)}function l2(t,n,a="/"){return I3(t,n,a,!1)}function I3(t,n,a,o,c){let l=typeof n=="string"?vi(n):n,d=Na(l.pathname||"/",a);if(d==null)return null;let m=U3(t),p=null,g=X3(d);for(let v=0;p==null&&v<m.length;++v)p=W3(m[v],g,o);return p}function U3(t){let n=c2(t);return B3(n),n}function c2(t,n=[],a=[],o="",c=!1){let l=(d,m,p=c,g)=>{let v={relativePath:g===void 0?d.path||"":g,caseSensitive:d.caseSensitive===!0,childrenIndex:m,route:d};if(v.relativePath.startsWith("/")){if(!v.relativePath.startsWith(o)&&p)return;Nt(v.relativePath.startsWith(o),`Absolute route path "${v.relativePath}" nested under path "${o}" is not valid. An absolute child route path must start with the combined path of all its parent routes.`),v.relativePath=v.relativePath.slice(o.length)}let b=Cr([o,v.relativePath]),w=a.concat(v);d.children&&d.children.length>0&&(Nt(d.index!==!0,`Index routes must not have child routes. Please remove all child routes from route path "${b}".`),c2(d.children,n,w,b,p)),!(d.path==null&&!d.index)&&n.push({path:b,score:q3(b,d.index),routesMeta:w.map((y,C)=>{let[S,E]=f2(y.relativePath,y.caseSensitive,C===w.length-1);return{...y,matcher:S,compiledParams:E}})})};return t.forEach((d,m)=>{var p;if(d.path===""||!((p=d.path)!=null&&p.includes("?")))l(d,m);else for(let g of u2(d.path))l(d,m,!0,g)}),n}function u2(t){let n=t.split("/");if(n.length===0)return[];let[a,...o]=n,c=a.endsWith("?"),l=a.replace(/\?$/,"");if(o.length===0)return c?[l,""]:[l];let d=u2(o.join("/")),m=[];return m.push(...d.map(p=>p===""?l:[l,p].join("/"))),c&&m.push(...d),m.map(p=>t.startsWith("/")&&p===""?"/":p)}function B3(t){t.sort((n,a)=>n.score!==a.score?a.score-n.score:K3(n.routesMeta.map(o=>o.childrenIndex),a.routesMeta.map(o=>o.childrenIndex)))}var $3=/^:[\w-]+$/,H3=3,G3=2,V3=1,F3=10,Y3=-2,Oy=t=>t==="*";function q3(t,n){let a=t.split("/"),o=a.length;return a.some(Oy)&&(o+=Y3),n&&(o+=G3),a.filter(c=>!Oy(c)).reduce((c,l)=>c+($3.test(l)?H3:l===""?V3:F3),o)}function K3(t,n){return t.length===n.length&&t.slice(0,-1).every((o,c)=>o===n[c])?t[t.length-1]-n[n.length-1]:0}fuLine truncated
|
||||
|
||||
Please change the parent <Route path="${k}"> to <Route path="${k==="/"?"*":`${k}/*`}">.`)}let v=Kn(),b;if(n){let k=typeof n=="string"?vi(n):n;Nt(p==="/"||((E=k.pathname)==null?void 0:E.startsWith(p)),`When overriding the location using \`<Routes location>\` or \`useRoutes(routes, location)\`, the location pathname must begin with the portion of the URL pathname that was matched by all parent routes. The current pathname base is "${p}" but pathname "${k.pathname}" was given in the \`location\` prop.`),b=k}else b=v;let w=b.pathname||"/",y=w;if(p!=="/"){let k=p.replace(/^\//,"").split("/");y="/"+w.replace(/^\//,"").split("/").slice(k.length).join("/")}let C=a&&a.state.matches.length?a.state.matches.map(k=>Object.assign(k,{route:a.manifest[k.route.id]||k.route})):l2(t,{pathname:y});fr(g||C!=null,`No routes matched location "${b.pathname}${b.search}${b.hash}" `),fr(C==null||C[C.length-1].route.element!==void 0||C[C.length-1].route.Component!==void 0||C[C.length-1].route.lazy!==void 0,`Matched leaf route at location "${b.pathname}${b.search}${b.hash}" does not have an element or Component. This means it will render an <Outlet /> with a null value by default resulting in an "empty" page.`);let S=SN(C&&C.map(k=>Object.assign({},k,{params:Object.assign({},d,k.params),pathname:Cr([p,o.encodeLocation?o.encodeLocation(k.pathname.replace(/%/g,"%25").replace(/\?/g,"%3F").replace(/#/g,"%23")).pathname:k.pathname]),pathnameBase:k.pathnameBase==="/"?p:Cr([p,o.encodeLocation?o.encodeLocation(k.pathnameBase.replace(/%/g,"%25").replace(/\?/g,"%3F").replace(/#/g,"%23")).pathname:k.pathnameBase])})),c,a);return n&&S?f.createElement(Jl.Provider,{value:{location:{pathname:"/",search:"",hash:"",state:null,key:"default",mask:void 0,...b},navigationType:"POP"}},S):S}function yN(){let t=MN(),n=rN(t)?`${t.status} ${t.statusText}`:t instanceof Error?t.message:JSON.stringify(t),a=t instanceof Error?t.stack:null,o="rgba(200,200,200, 0.5)",c={padding:"0.5rem",backgroundColor:o},l={padding:"2px 4px",backgroundColor:o},d=null;return console.error("Error handled by React Router default ErrorBoundary:",t),d=f.createElement(f.Fragment,null,f.createElement("p",null,"💿 Hey developer 👋"),f.createElement("p",null,"You can provide a way better UX than this when your app throws errors by providing your own ",f.createElement("code",{style:l},"ErrorBoundary")," or"," ",f.createElement("code",{style:l},"errorElement")," prop on your route.")),f.createElement(f.Fragment,null,f.createElement("h2",null,"Unexpected Application Error!"),f.createElement("h3",{style:{fontStyle:"italic"}},n),a?f.createElement("pre",{style:c},a):null,d)}var wN=f.createElement(yN,null),C2=class extends f.Component{constructor(t){super(t),this.state={location:t.location,revalidation:t.revalidation,error:t.error}}static getDerivedStateFromError(t){return{error:t}}static getDerivedStateFromProps(t,n){return n.location!==t.location||n.revalidation!=="idle"&&t.revalidation==="idle"?{error:t.error,location:t.location,revalidation:t.revalidation}:{error:t.error!==void 0?t.error:n.error,location:n.location,revalidation:t.revalidation||n.revalidation}}componentDidCatch(t,n){this.props.onError?this.props.onError(t,n):console.error("React Router caught the following error during render",t)}render(){let t=this.state.error;if(this.context&&typeof t=="object"&&t&&"digest"in t&&typeof t.digest=="string"){const a=pN(t.digest);a&&(t=a)}let n=t!==void 0?f.createElement(Er.Provider,{value:this.props.routeContext},f.createElement(Jh.Provider,{value:t,children:this.props.component})):this.props.children;return this.context?f.createElement(jN,{error:t},n):n}};C2.contextType=x2;var bp=new WeakMap;function jN({children:t,error:n}){let{basename:a}=f.useContext(qn);if(typeof n=="object"&&n&&"digest"in n&&typeof n.digest=="string"){let o=mN(n.digest);if(o){let c=bp.get(n);if(c)throw c;let l=h2(o.location,a),d=l.absoluteURL||l.to;if(iN(d))throw new Error("Invalid redirect location");if(p2&&!bp.get(n))if(l.isExternal||o.reloadDocument)window.location.href=d;else{const m=Promise.resolve().then(()=>window.__reactRouterDataRouter.navigate(l.to,{replace:o.replace}));throw bp.set(n,m),m}return f.createElement("meta",{httpEquiv:"refresh",content:`0;url=${d}`})}}return t}function CN({routeContext:t,match:n,children:a}){let o=f.useContext(bi);return o&&o.static&&o.staticContext&&(n.route.errorElement||n.route.ErrorBoundary)&&(o.staticContext._deepestRenderedBoundaryId=n.route.id),f.createElement(Er.Provider,{value:t},a)}function SN(t,n=[],a){let o=a==null?void 0:a.state;if(t==null){if(!o)return null;if(o.errors)t=o.matches;else if(n.length===0&&!o.initialized&&o.matches.length>0)t=o.matches;else return null}let c=t,l=o==null?void 0:o.errors;if(l!=null){let v=c.findIndex(b=>b.route.id&&(l==null?void 0:l[b.route.id])!==void 0);Nt(v>=0,`Could not find a matching route for errors on route IDs: ${Object.keys(l).join(",")}`),c=c.slice(0,Math.min(c.length,v+1))}let d=!1,m=-1;if(a&&o){d=o.renderFallback;for(let v=0;vLine truncated
|
||||
.`.concat(Bk,` {
|
||||
overflow: hidden `).concat(o,`;
|
||||
padding-right: `).concat(m,"px ").concat(o,`;
|
||||
}
|
||||
body[`).concat(di,`] {
|
||||
overflow: hidden `).concat(o,`;
|
||||
overscroll-behavior: contain;
|
||||
`).concat([n&&"position: relative ".concat(o,";"),a==="margin"&&`
|
||||
padding-left: `.concat(c,`px;
|
||||
padding-top: `).concat(l,`px;
|
||||
padding-right: `).concat(d,`px;
|
||||
margin-left:0;
|
||||
margin-top:0;
|
||||
margin-right: `).concat(m,"px ").concat(o,`;
|
||||
`),a==="padding"&&"padding-right: ".concat(m,"px ").concat(o,";")].filter(Boolean).join(""),`
|
||||
}
|
||||
|
||||
.`).concat(Zu,` {
|
||||
right: `).concat(m,"px ").concat(o,`;
|
||||
}
|
||||
|
||||
.`).concat(Qu,` {
|
||||
margin-right: `).concat(m,"px ").concat(o,`;
|
||||
}
|
||||
|
||||
.`).concat(Zu," .").concat(Zu,` {
|
||||
right: 0 `).concat(o,`;
|
||||
}
|
||||
|
||||
.`).concat(Qu," .").concat(Qu,` {
|
||||
margin-right: 0 `).concat(o,`;
|
||||
}
|
||||
|
||||
body[`).concat(di,`] {
|
||||
`).concat($k,": ").concat(m,`px;
|
||||
}
|
||||
`)},f0=function(){var t=parseInt(document.body.getAttribute(di)||"0",10);return isFinite(t)?t:0},o9=function(){f.useEffect(function(){return document.body.setAttribute(di,(f0()+1).toString()),function(){var t=f0()-1;t<=0?document.body.removeAttribute(di):document.body.setAttribute(di,t.toString())}},[])},i9=function(t){var n=t.noRelative,a=t.noImportant,o=t.gapMode,c=o===void 0?"margin":o;o9();var l=f.useMemo(function(){return r9(c)},[c]);return f.createElement(a9,{styles:s9(l,!n,c,a?"":"!important")})},bh=!1;if(typeof window<"u")try{var Ou=Object.defineProperty({},"passive",{get:function(){return bh=!0,!0}});window.addEventListener("test",Ou,Ou),window.removeEventListener("test",Ou,Ou)}catch{bh=!1}var ti=bh?{passive:!1}:!1,l9=function(t){return t.tagName==="TEXTAREA"},sj=function(t,n){if(!(t instanceof Element))return!1;var a=window.getComputedStyle(t);return a[n]!=="hidden"&&!(a.overflowY===a.overflowX&&!l9(t)&&a[n]==="visible")},c9=function(t){return sj(t,"overflowY")},u9=function(t){return sj(t,"overflowX")},m0=function(t,n){var a=n.ownerDocument,o=n;do{typeof ShadowRoot<"u"&&o instanceof ShadowRoot&&(o=o.host);var c=oj(t,o);if(c){var l=ij(t,o),d=l[1],m=l[2];if(d>m)return!0}o=o.parentNode}while(o&&o!==a.body);return!1},d9=function(t){var n=t.scrollTop,a=t.scrollHeight,o=t.clientHeight;return[n,a,o]},f9=function(t){var n=t.scrollLeft,a=t.scrollWidth,o=t.clientWidth;return[n,a,o]},oj=function(t,n){return t==="v"?c9(n):u9(n)},ij=function(t,n){return t==="v"?d9(n):f9(n)},m9=function(t,n){return t==="h"&&n==="rtl"?-1:1},p9=function(t,n,a,o,c){var l=m9(t,window.getComputedStyle(n).direction),d=l*o,m=a.target,p=n.contains(m),g=!1,v=d>0,b=0,w=0;do{if(!m)break;var y=ij(t,m),C=y[0],S=y[1],E=y[2],k=S-E-l*C;(C||k)&&oj(t,m)&&(b+=k,w+=C);var N=m.parentNode;m=N&&N.nodeType===Node.DOCUMENT_FRAGMENT_NODE?N.host:N}while(!p&&m!==document.body||p&&(n.contains(m)||n===m));return(v&&Math.abs(b)<1||!v&&Math.abs(w)<1)&&(g=!0),g},Lu=function(t){return"changedTouches"in t?[t.changedTouches[0].clientX,t.changedTouches[0].clientY]:[0,0]},p0=function(t){return[t.deltaX,t.deltaY]},h0=function(t){return t&&"current"in t?t.current:t},h9=function(t,n){return t[0]===n[0]&&t[1]===n[1]},g9=function(t){return`
|
||||
.block-interactivity-`.concat(t,` {pointer-events: none;}
|
||||
.allow-interactivity-`).concat(t,` {pointer-events: all;}
|
||||
`)},x9=0,ni=[];function v9(t){var n=f.useRef([]),a=f.useRef([0,0]),o=f.useRef(),c=f.useState(x9++)[0],l=f.useState(aj)[0],d=f.useRef(t);f.useEffect(function(){d.current=t},[t]),f.useEffect(function(){if(t.inert){document.body.classList.add("block-interactivity-".concat(c));var S=Uk([t.lockRef.current],(t.shards||[]).map(h0),!0).filter(Boolean);return S.forEach(function(E){return E.classList.add("allow-interactivity-".concat(c))}),function(){document.body.classList.remove("block-interactivity-".concat(c)),S.forEach(function(E){return E.classList.remove("allow-interactivity-".concat(c))})}}},[t.inert,t.lockRef.current,t.shards]);var m=f.useCallback(function(S,E){if("touches"in S&&S.touches.length===2||S.type==="wheel"&&S.ctrlKey)return!d.current.allowPinchZoom;var k=Lu(S),N=a.current,T="deltaX"in S?S.deltaX:N[0]-k[0],M="deltaY"in S?S.deltaY:N[1]-k[1],_,R=S.target,L=Math.abs(T)>Math.abs(M)?"h":"v";if("touches"in S&&L==="h"&&R.type==="range")return!1;var te=window.getSelection(),K=te&&te.anchorNode,I=K?K===R||K.contains(R):!1;if(I)return!1;var G=m0(L,R);if(!G)return!0;if(G?_=L:(_=L==="v"?"h":"v",G=m0(L,R)),!G)return!1;if(!o.current&&"changedTouches"in S&&(T||M)&&(o.current=_),!_)return!0;var Y=o.current||_;return p9(Y,E,S,Y==="h"?T:M)},[]),p=f.useCallback(function(S){var E=S;if(!(!ni.length||ni[ni.length-1]!==l)){var k="deltaY"in E?p0(E):Lu(E),N=n.current.filter(function(_){return _.name===E.type&&(_.target===E.target||E.target===_.shadowParent)&&h9(_.delta,k)})[0];if(N&&N.should){E.cancelable&&E.preventDefault();return}if(!N){var T=(d.current.shards||[]).map(h0).filter(Boolean).filter(function(_){return _.contains(E.target)}),M=T.length>0?m(E,T[0]):!d.current.noIsolation;M&&E.cancelable&&E.preventDefault()}}},[]),g=f.useCallback(function(S,E,k,N){var T={name:S,delta:E,target:k,should:N,shadowParent:b9(k)};n.current.push(T),setTimeout(function(){n.current=n.current.filter(function(M){return M!==T})},1)},[]),v=f.useCallback(function(S){a.current=Lu(S),o.current=void 0},[]),b=f.useCallback(function(S){g(S.type,p0(S),S.target,m(S,t.lockRef.current))},[]),w=f.useCallback(function(S){g(S.type,Lu(S),S.target,m(S,t.lockRef.current))},[]);f.useEffect(function(){return ni.push(l),t.setCallbacks({onScrollCapture:b,onWheelCapture:b,onTouchMoveCapture:w}),document.addEventListener("wheel",p,ti),document.addEventListener("touchmove",p,ti),document.addEventListener("touchstart",v,ti),function(){ni=ni.filter(function(S){return S!==l}),document.removeEventListener("wheel",p,ti),document.removeEventListener("touchmove",p,ti),document.removeEventListener("touchstart",v,ti)}},[]);var y=t.removeScrollBar,C=t.inert;return f.createElement(f.Fragment,null,C?f.createElement(l,{styles:g9(c)}):null,y?f.createElement(i9,{noRelative:t.noRelative,gapMode:t.gapMode}):null)}function b9(t){for(var n=null;t!==null;)t instanceof ShadowRoot&&(n=t.host,t=t.host),t=t.parentNode;return n}const y9=Kk(rj,v9);var ac=f.forwardRef(function(t,n){return f.createElement(Hd,Pr({},t,{ref:n,sideCar:y9}))});ac.classNames=Hd.classNames;var w9=Object.defineProperty,st=(t,n)=>w9(t,"name",{value:n,configurable:!0}),yh=["Enter"," "],j9=["ArrowDown","PageUp","Home"],lj=["ArrowUp","PageDown","End"],C9=[...j9,...lj],S9={ltr:[...yh,"ArrowRight"],rtl:[...yh,"ArrowLeft"]},N9={ltr:["ArrowLeft"],rtl:["ArrowRight"]},Gd="Menu",[Hl,E9,k9]=Td(Gd),[io,cj]=yn(Gd,[k9,Cs,Bd]),Rg=Cs(),uj=Bd(),[T9,lo]=io(Gd),[M9,sc]=io(Gd),R9=st(t=>{const{__scopeMenu:n,open:a=!1,children:o,dir:c,onOpenChange:l,modal:d=!0}=t,m=Rg(n),[p,g]=f.useState(null),v=f.useRef(!1),b=Vt(l),w=ki(c);return f.useEffect(()=>{const y=st(()=>{v.current=!0,document.addEventListener("pointerdown",C,{capture:!0,once:!0}),document.addEventListener("pointermove",C,{capture:!0,once:!0})},"handleKeyDown"),C=st(()=>v.current=!1,"handlePointer");return document.addEventListener("keydown",y,{capture:!0}),()=>{document.removeEventListener("keydown",y,{capture:!0}),document.removeEventListener("pointerdown",C,{capture:!0}),document.removeEventListener("pointermove",C,{capture:!0})}},[]),f.useEffect(()=>{if(!a)return;const y=st(()=>b(!1),"handleBlur");return window.addEventListener("blur",y),()=>window.removeEventListener("blur",y)},[a,b]),s.jsx(zd,{...m,children:s.jsx(T9,{scope:n,open:a,onOpenChange:b,content:p,onContentChange:g,children:s.jsx(M9,{scope:n,onClose:f.useCallback(()=>b(!1),[b]),isUsingKeyboardRef:v,dir:w,modal:d,children:o})})})},"Menu"),dj=f.forwardRef(st(function(n,a){const{__scopeMenu:o,...c}=n,l=Rg(o);return s.jsx(Pd,{...l,...c,ref:a})},"MenuAnchor")),fj="MenuPortal",[A9,mj]=io(fj,{forceMount:void 0}),_9=st(t=>{const{__scopeMenu:n,forceMount:a,children:o,container:c}=t,l=lo(fj,n);return s.jsx(A9,{scope:n,forceMount:a,children:s.jsx(dn,{present:a||l.open,children:s.jsx(rc,{asChild:!0,container:c,children:o})})})},"MenuPortal"),Sr="MenuContent",[D9,Ag]=io(Sr),O9=f.forwardRef(st(function(n,a){const o=mj(Sr,n.__scopeMenu),{forceMount:c=o.forceMount,...l}=n,d=lo(Sr,n.__scopeMenu),m=sc(Sr,n.__scopeMenu);return s.jsx(
Line truncated
|
||||
* @license lucide-react v0.475.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const MT=t=>t.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase(),Dj=(...t)=>t.filter((n,a,o)=>!!n&&n.trim()!==""&&o.indexOf(n)===a).join(" ").trim();/**
|
||||
* @license lucide-react v0.475.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/var RT={xmlns:"http://www.w3.org/2000/svg",width:24,height:24,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round"};/**
|
||||
* @license lucide-react v0.475.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const AT=f.forwardRef(({color:t="currentColor",size:n=24,strokeWidth:a=2,absoluteStrokeWidth:o,className:c="",children:l,iconNode:d,...m},p)=>f.createElement("svg",{ref:p,...RT,width:n,height:n,stroke:t,strokeWidth:o?Number(a)*24/Number(n):a,className:Dj("lucide",c),...m},[...d.map(([g,v])=>f.createElement(g,v)),...Array.isArray(l)?l:[l]]));/**
|
||||
* @license lucide-react v0.475.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const Ra=(t,n)=>{const a=f.forwardRef(({className:o,...c},l)=>f.createElement(AT,{ref:l,iconNode:n,className:Dj(`lucide-${MT(t)}`,o),...c}));return a.displayName=`${t}`,a};/**
|
||||
* @license lucide-react v0.475.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const _T=[["path",{d:"M20 6 9 17l-5-5",key:"1gmf2c"}]],Oj=Ra("Check",_T);/**
|
||||
* @license lucide-react v0.475.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const DT=[["path",{d:"m6 9 6 6 6-6",key:"qrunsl"}]],Lj=Ra("ChevronDown",DT);/**
|
||||
* @license lucide-react v0.475.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const OT=[["path",{d:"m9 18 6-6-6-6",key:"mthhwq"}]],LT=Ra("ChevronRight",OT);/**
|
||||
* @license lucide-react v0.475.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const zT=[["path",{d:"m18 15-6-6-6 6",key:"153udz"}]],PT=Ra("ChevronUp",zT);/**
|
||||
* @license lucide-react v0.475.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const IT=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"m9 12 2 2 4-4",key:"dzmm74"}]],x0=Ra("CircleCheck",IT);/**
|
||||
* @license lucide-react v0.475.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const UT=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}]],BT=Ra("Circle",UT);/**
|
||||
* @license lucide-react v0.475.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const $T=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",key:"afitv7"}],["path",{d:"M9 3v18",key:"fh3hqa"}]],HT=Ra("PanelLeft",$T);/**
|
||||
* @license lucide-react v0.475.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const GT=[["circle",{cx:"11",cy:"11",r:"8",key:"4ej97u"}],["path",{d:"m21 21-4.3-4.3",key:"1qie3q"}]],VT=Ra("Search",GT);/**
|
||||
* @license lucide-react v0.475.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const FT=[["path",{d:"M18 6 6 18",key:"1bl5f8"}],["path",{d:"m6 6 12 12",key:"d8bk6v"}]],zj=Ra("X",FT),zg=ET,Pg=kT,YT=f.forwardRef(({className:t,inset:n,children:a,...o},c)=>s.jsxs(Aj,{ref:c,className:pe("flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",n&&"pl-8",t),...o,children:[a,s.jsx(LT,{className:"ml-auto"})]}));YT.displayName=Aj.displayName;const qT=f.forwardRef(({className:t,...n},a)=>s.jsx(_j,{ref:a,className:pe("z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]",t),...n}));qT.displayName=_j.displayName;const Fd=f.forwardRef(({className:t,sideOffset:n=4,...a},o)=>s.jsx(TT,{children:s.jsx(Sj,{ref:o,sideOffset:n,className:pe("z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md","data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]",t),...a})}));Fd.displayName=Sj.displayName;const Vl=f.forwardRef(({className:t,inset:n,...a},o)=>s.jsx(Ej,{ref:o,className:pe("relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0",n&&"pl-8",t),...a}));Vl.displayName=Ej.displayName;const KT=f.forwardRef(({className:t,children:n,checked:a,...o},c)=>s.jsxs(kj,{ref:c,className:pe("relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",t),checked:a,...o,children:[s.jsx("span",{className:"absolute left-2 flex h-3.5 w-3.5 items-center justify-center",children:s.jsx(Mj,{children:s.jsx(Oj,{className:"h-4 w-4"})})}),n]}));KT.displayName=kj.displayName;const WT=f.forwardRef(({className:t,children:n,...a},o)=>s.jsxs(Tj,{ref:o,className:pe("relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",t),...a,children:[s.jsx("span",{className:"absolute left-2 flex h-3.5 w-3.5 items-center justify-center",children:s.jsx(Mj,{children:s.jsx(BT,{className:"h-2 w-2 fill-current"})})}),n]}));WT.displayName=Tj.displayName;const Ig=f.forwardRef(({className:t,inset:n,...a},o)=>s.jsx(Nj,{ref:o,className:pe("px-2 py-1.5 text-sm font-semibold",n&&"pl-8",t),...a}));Ig.displayName=Nj.displayName;const Fl=f.forwardRef(({className:t,...n},a)=>s.jsx(Rj,{ref:a,className:pe("-mx-1 my-1 h-px bg-muted",t),...n}));Fl.displayName=Rj.displayName;const zp=768;function XT(){const[t,n]=f.useState(void 0);return f.useEffect(()=>{const a=window.matchMedia(`(max-width: ${zp-1}px)`),o=()=>{n(window.innerWidth<zp)};return a.addEventListener("change",o),n(window.innerWidth<zp),()=>a.removeEventListener("change",o)},[]),!!t}const De=f.forwardRef(({className:t,type:n,...a},o)=>s.jsx("input",{type:n,className:pe("flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",t),ref:o,...a}));De.displayName="Input";var ZT=Object.defineProperty,Pj=(t,n)=>ZT(t,"name",{value:n,configurable:!0}),v0="horizontal",QT=["horizontal","vertical"],JT=f.forwardRef(Pj(function(n,a){const{decorative:o,orientation:c=v0,...l}=n,d=Ij(c)?c:v0,p=o?{role:"none"}:{"aria-orientation":d==="vertical"?d:void 0,role:"separator"};return s.jsx(Me.div,{"data-orientation":d,...p,...l,ref:a})},"Separator"));function Ij(t){return QT.includes(t)}Pj(Ij,"isValidOrientation");var Uj=JT;const Bj=f.forwardRef(({className:t,orientation:n="horizontal",decorative:a=!0,...o},c)=>s.jsx(Uj,{ref:c,decorative:a,orientation:n,className:pe(Line truncated
|
||||
--num-groups: ${l.groups.length};
|
||||
--indicator-opacity: ${l.searchMode?0:1};
|
||||
--num-skintones: ${z0};`}),d(()=>{l.customEmoji&&l.database&&L()}),d(()=>{l.customEmoji&&l.customEmoji.length?l.groups!==Fh&&(l.groups=Fh):l.groups!==Hp&&(l.currentGroupIndex&&l.currentGroupIndex--,l.groups=Hp)}),d(()=>{async function B(){l.databaseLoaded&&(l.currentSkinTone=await l.database.getPreferredSkinTone())}B()}),d(()=>{l.skinTones=Array(z0).fill().map((B,ie)=>ID(l.skinToneEmoji,ie))}),d(()=>{l.skinToneButtonText=l.skinTones[l.currentSkinTone]}),d(()=>{l.skinToneButtonLabel=l.i18n.skinToneLabel.replace("{skinTone}",l.i18n.skinTones[l.currentSkinTone])}),d(()=>{async function B(){const{database:ie}=l,we=(await Promise.all(TD.map(Ce=>ie.getEmojiByUnicodeOrName(Ce)))).filter(Boolean);l.defaultFavoriteEmojis=we}l.databaseLoaded&&B()});function L(){const{customEmoji:B,database:ie}=l,we=B||Kp;ie.customEmoji!==we&&(ie.customEmoji=we)}d(()=>{async function B(){L();const{database:ie,defaultFavoriteEmojis:we,numColumns:Ce}=l,ze=await ie.getTopFavoriteEmoji(Ce),Ke=await fe(XS([...ze,...we],ft=>ft.unicode||ft.name).slice(0,Ce));l.currentFavorites=Ke}l.databaseLoaded&&l.defaultFavoriteEmojis&&B()});function te(B){$D(B,c,()=>{{const ie=getComputedStyle(a.rootElement),we=parseInt(ie.getPropertyValue("--num-columns"),10),Ce=ie.getPropertyValue("direction")==="rtl";l.numColumns=we,l.isRtl=Ce}})}function K(B){iO(B,c,ie=>{for(const{target:we,isIntersecting:Ce}of ie)we.classList.toggle("onscreen",Ce)})}d(()=>{async function B(){const{searchText:ie,currentGroup:we,databaseLoaded:Ce,customEmoji:ze}=l;if(!Ce)l.currentEmojis=[],l.searchMode=!1;else if(ie.length>=CD){const Ke=await P(ie);l.searchText===ie&&(y(Ke),C(!0))}else{const{id:Ke}=we;if(Ke!==-1||ze&&ze.length){const ft=await se(Ke);l.currentGroup.id===Ke&&(y(ft),C(!1))}}}B()});const I=()=>{ed(()=>FD(a.tabpanelElement))};d(()=>{const{currentEmojis:B,emojiVersion:ie}=l,we=B.filter(Ce=>Ce.unicode).filter(Ce=>P0(Ce)&&!Yh.has(Ce.unicode));if(!ie&&we.length)y(B),ed(()=>G(we));else{const Ce=ie?B:B.filter(Y);y(Ce),I()}});function G(B){GD(B,a.baselineEmoji,g)?I():l.currentEmojis=[...l.currentEmojis]}function Y(B){return!B.unicode||!P0(B)||Yh.get(B.unicode)}async function ce(B){const ie=l.emojiVersion||await Vp();return B.filter(({version:we})=>!we||we<=ie)}async function fe(B){return UD(B,l.emojiVersion||await Vp())}async function se(B){const ie=B===-1?l.customEmoji:await l.database.getEmojiByGroup(B);return fe(await ce(ie))}async function P(B){return fe(await ce(await l.database.getEmojiBySearchQuery(B)))}d(()=>{}),d(()=>{function B(){const{searchMode:we,currentEmojis:Ce}=l;if(we)return[{category:"",emojis:Ce}];const ze=new Map;for(const Ke of Ce){const ft=Ke.category||"";let Pt=ze.get(ft);Pt||(Pt=[],ze.set(ft,Pt)),Pt.push(Ke)}return[...ze.entries()].map(([Ke,ft])=>({category:Ke,emojis:ft})).sort((Ke,ft)=>l.customCategorySorting(Ke.category,ft.category))}const ie=B();S(ie)}),d(()=>{l.activeSearchItemId=l.activeSearchItem!==-1&&l.currentEmojis[l.activeSearchItem].id}),d(()=>{const{rawSearchText:B}=l;KS(()=>{l.searchText=(B||"").trim(),l.activeSearchItem=-1})});function Z(B){if(!l.searchMode||!l.currentEmojis.length)return;const ie=we=>{zr(B),l.activeSearchItem=Fp(we,l.activeSearchItem,l.currentEmojis)};switch(B.key){case"ArrowDown":return ie(!1);case"ArrowUp":return ie(!0);case"Enter":if(l.activeSearchItem===-1)l.activeSearchItem=0;else return zr(B),O(l.currentEmojis[l.activeSearchItem].id)}}function ae(B){const{target:ie}=B,we=ie.closest(".nav-button");if(!we)return;const Ce=parseInt(we.dataset.groupId,10);a.searchElement.value="",l.rawSearchText="",l.searchText="",l.activeSearchItem=-1,l.currentGroupIndex=l.groups.findIndex(ze=>ze.id===Ce)}function re(B){const{target:ie,key:we}=B,Ce=ze=>{ze&&(zr(B),ze.focus())};switch(we){case"ArrowLeft":return Ce(ie.previousElementSibling);case"ArrowRight":return Ce(ie.nextElementSibling);case"Home":return Ce(ie.parentElement.firstElementChild);case"End":return Ce(ie.parentElement.lastElementChild)}}async function V(B){const ie=await l.database.getEmojiByUnicodeOrName(B),we=[...l.currentEmojis,...l.currentFavorites].find(ze=>ze.id===B),Ce=we.unicode&&E(we,l.currentSkinTone);return await l.database.incrementFavoriteEmojiCount(B),{emoji:ie,skinTone:l.currentSkinTone,...Ce&&{unicode:Ce},...we.name&&{name:we.name}}}async function O(B){const ie=V(B);v("emoji-click-sync",ie),v("emoji-click",await ie)}function D(B){const{target:ie}=B;if(!ie.classList.contains("emoji"))return;zr(B);const we=ie.id.substring(4);O(we)}function $(B){l.currentSkinTone=B,l.skinTonePickerExpanded=!1,p("skintone-button"),v("skin-tone-change",{skinTone:B}),l.database.setPreferredSkinTone(B)}function H(B){const{target:{id:ie}}=B,we=ie&&ie.match(/^skintone-(\d)/);if(!we)return;zr(B);const Ce=parseInt(we[1],10);$(Ce)}function X(B){l.skinTonePickerExpanded=!l.skinTonePickerExpanded,l.activeSkinTone=l.currentSkinTone,l.skinTonePickerExpanded&&(zr(B),ed(()=>p("skintone-list")))}d(()=>{l.skinTonePickerExpanded?a.skinToneDropdown.addEventListener("transitionend",()=>{l.skin
Line truncated
|
||||
`);function Hu(t){return t.label!==void 0}var IO=3,UO="32px",BO="16px",q0=4e3,$O=356,HO=14,GO=20,VO=200;function wr(...t){return t.filter(Boolean).join(" ")}function FO(t){let[n,a]=t.split("-"),o=[];return n&&o.push(n),a&&o.push(a),o}var YO=t=>{var n,a,o,c,l,d,m,p,g,v,b;let{invert:w,toast:y,unstyled:C,interacting:S,setHeights:E,visibleToasts:k,heights:N,index:T,toasts:M,expanded:_,removeToast:R,defaultRichColors:L,closeButton:te,style:K,cancelButtonStyle:I,actionButtonStyle:G,className:Y="",descriptionClassName:ce="",duration:fe,position:se,gap:P,loadingIcon:Z,expandByDefault:ae,classNames:re,icons:V,closeButtonAriaLabel:O="Close toast",pauseWhenPageIsHidden:D}=t,[$,H]=be.useState(null),[X,xe]=be.useState(null),[q,ge]=be.useState(!1),[me,B]=be.useState(!1),[ie,we]=be.useState(!1),[Ce,ze]=be.useState(!1),[Ke,ft]=be.useState(!1),[Pt,gn]=be.useState(0),[Yr,_n]=be.useState(0),wn=be.useRef(y.duration||fe||q0),Xn=be.useRef(null),Zn=be.useRef(null),It=T===0,po=T+1<=k,nn=y.type,Dn=y.dismissible!==!1,Da=y.className||"",ho=y.descriptionClassName||"",Qn=be.useMemo(()=>N.findIndex(Oe=>Oe.toastId===y.id)||0,[N,y.id]),Es=be.useMemo(()=>{var Oe;return(Oe=y.closeButton)!=null?Oe:te},[y.closeButton,te]),qr=be.useMemo(()=>y.duration||fe||q0,[y.duration,fe]),gr=be.useRef(0),Tt=be.useRef(0),On=be.useRef(0),Mt=be.useRef(null),[Oi,Li]=se.split("-"),go=be.useMemo(()=>N.reduce((Oe,tt,it)=>it>=Qn?Oe:Oe+tt.height,0),[N,Qn]),Kr=RO(),Oa=y.invert||w,Mr=nn==="loading";Tt.current=be.useMemo(()=>Qn*P+go,[Qn,go]),be.useEffect(()=>{wn.current=qr},[qr]),be.useEffect(()=>{ge(!0)},[]),be.useEffect(()=>{let Oe=Zn.current;if(Oe){let tt=Oe.getBoundingClientRect().height;return _n(tt),E(it=>[{toastId:y.id,height:tt,position:y.position},...it]),()=>E(it=>it.filter(rn=>rn.toastId!==y.id))}},[E,y.id]),be.useLayoutEffect(()=>{if(!q)return;let Oe=Zn.current,tt=Oe.style.height;Oe.style.height="auto";let it=Oe.getBoundingClientRect().height;Oe.style.height=tt,_n(it),E(rn=>rn.find(Ft=>Ft.toastId===y.id)?rn.map(Ft=>Ft.toastId===y.id?{...Ft,height:it}:Ft):[{toastId:y.id,height:it,position:y.position},...rn])},[q,y.title,y.description,E,y.id]);let Kt=be.useCallback(()=>{B(!0),gn(Tt.current),E(Oe=>Oe.filter(tt=>tt.toastId!==y.id)),setTimeout(()=>{R(y)},VO)},[y,R,E,Tt]);be.useEffect(()=>{if(y.promise&&nn==="loading"||y.duration===1/0||y.type==="loading")return;let Oe;return _||S||D&&Kr?(()=>{if(On.current<gr.current){let tt=new Date().getTime()-gr.current;wn.current=wn.current-tt}On.current=new Date().getTime()})():wn.current!==1/0&&(gr.current=new Date().getTime(),Oe=setTimeout(()=>{var tt;(tt=y.onAutoClose)==null||tt.call(y,y),Kt()},wn.current)),()=>clearTimeout(Oe)},[_,S,y,nn,D,Kr,Kt]),be.useEffect(()=>{y.delete&&Kt()},[Kt,y.delete]);function La(){var Oe,tt,it;return V!=null&&V.loading?be.createElement("div",{className:wr(re==null?void 0:re.loader,(Oe=y==null?void 0:y.classNames)==null?void 0:Oe.loader,"sonner-loader"),"data-visible":nn==="loading"},V.loading):Z?be.createElement("div",{className:wr(re==null?void 0:re.loader,(tt=y==null?void 0:y.classNames)==null?void 0:tt.loader,"sonner-loader"),"data-visible":nn==="loading"},Z):be.createElement(SO,{className:wr(re==null?void 0:re.loader,(it=y==null?void 0:y.classNames)==null?void 0:it.loader),visible:nn==="loading"})}return be.createElement("li",{tabIndex:0,ref:Zn,className:wr(Y,Da,re==null?void 0:re.toast,(n=y==null?void 0:y.classNames)==null?void 0:n.toast,re==null?void 0:re.default,re==null?void 0:re[nn],(a=y==null?void 0:y.classNames)==null?void 0:a[nn]),"data-sonner-toast":"","data-rich-colors":(o=y.richColors)!=null?o:L,"data-styled":!(y.jsx||y.unstyled||C),"data-mounted":q,"data-promise":!!y.promise,"data-swiped":Ke,"data-removed":me,"data-visible":po,"data-y-position":Oi,"data-x-position":Li,"data-index":T,"data-front":It,"data-swiping":ie,"data-dismissible":Dn,"data-type":nn,"data-invert":Oa,"data-swipe-out":Ce,"data-swipe-direction":X,"data-expanded":!!(_||ae&&q),style:{"--index":T,"--toasts-before":T,"--z-index":M.length-T,"--offset":`${me?Pt:Tt.current}px`,"--initial-height":ae?"auto":`${Yr}px`,...K,...y.style},onDragEnd:()=>{we(!1),H(null),Mt.current=null},onPointerDown:Oe=>{Mr||!Dn||(Xn.current=new Date,gn(Tt.current),Oe.target.setPointerCapture(Oe.pointerId),Oe.target.tagName!=="BUTTON"&&(we(!0),Mt.current={x:Oe.clientX,y:Oe.clientY}))},onPointerUp:()=>{var Oe,tt,it,rn;if(Ce||!Dn)return;Mt.current=null;let Ft=Number(((Oe=Zn.current)==null?void 0:Oe.style.getPropertyValue("--swipe-amount-x").replace("px",""))||0),Rt=Number(((tt=Zn.current)==null?void 0:tt.style.getPropertyValue("--swipe-amount-y").replace("px",""))||0),Jn=new Date().getTime()-((it=Xn.current)==null?void 0:it.getTime()),fn=$==="x"?Ft:Rt,Ln=Math.abs(fn)/Jn;if(Math.abs(fn)>=GO||Ln>.11){gn(Tt.current),(rn=y.onDismiss)==null||rn.call(y,y),xe($==="x"?Ft>0?"right":"left":Rt>0?"down":"up"),Kt(),ze(!0),ft(!1);return}we(!1),H(null)},onPointerMove:Oe=>{var tt,it,rn,Ft;if(!Mt.current||!Dn||((tt=window.getSelection())==null?void 0:tt.toS
Line truncated
|
||||
In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}function X0(t,n){var a=Object.keys(t);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(t);n&&(o=o.filter(function(c){return Object.getOwnPropertyDescriptor(t,c).enumerable})),a.push.apply(a,o)}return a}function Z0(t){for(var n=1;n<arguments.length;n++){var a=arguments[n]!=null?arguments[n]:{};n%2?X0(Object(a),!0).forEach(function(o){JO(t,o,a[o])}):Object.getOwnPropertyDescriptors?Object.defineProperties(t,Object.getOwnPropertyDescriptors(a)):X0(Object(a)).forEach(function(o){Object.defineProperty(t,o,Object.getOwnPropertyDescriptor(a,o))})}return t}function nL(t,n){if(t==null)return{};var a,o,c=rL(t,n);if(Object.getOwnPropertySymbols){var l=Object.getOwnPropertySymbols(t);for(o=0;o<l.length;o++)a=l[o],n.indexOf(a)===-1&&{}.propertyIsEnumerable.call(t,a)&&(c[a]=t[a])}return c}function rL(t,n){if(t==null)return{};var a={};for(var o in t)if({}.hasOwnProperty.call(t,o)){if(n.indexOf(o)!==-1)continue;a[o]=t[o]}return a}function aL(t,n){return QO(t)||eL(t,n)||iL(t,n)||tL()}function sL(t,n){if(typeof t!="object"||!t)return t;var a=t[Symbol.toPrimitive];if(a!==void 0){var o=a.call(t,n);if(typeof o!="object")return o;throw new TypeError("@@toPrimitive must return a primitive value.")}return(n==="string"?String:Number)(t)}function oL(t){var n=sL(t,"string");return typeof n=="symbol"?n:n+""}function iL(t,n){if(t){if(typeof t=="string")return W0(t,n);var a={}.toString.call(t).slice(8,-1);return a==="Object"&&t.constructor&&(a=t.constructor.name),a==="Map"||a==="Set"?Array.from(t):a==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(a)?W0(t,n):void 0}}function lL(t,n,a){return n in t?Object.defineProperty(t,n,{value:a,enumerable:!0,configurable:!0,writable:!0}):t[n]=a,t}function Q0(t,n){var a=Object.keys(t);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(t);n&&(o=o.filter(function(c){return Object.getOwnPropertyDescriptor(t,c).enumerable})),a.push.apply(a,o)}return a}function J0(t){for(var n=1;n<arguments.length;n++){var a=arguments[n]!=null?arguments[n]:{};n%2?Q0(Object(a),!0).forEach(function(o){lL(t,o,a[o])}):Object.getOwnPropertyDescriptors?Object.defineProperties(t,Object.getOwnPropertyDescriptors(a)):Q0(Object(a)).forEach(function(o){Object.defineProperty(t,o,Object.getOwnPropertyDescriptor(a,o))})}return t}function cL(){for(var t=arguments.length,n=new Array(t),a=0;a<t;a++)n[a]=arguments[a];return function(o){return n.reduceRight(function(c,l){return l(c)},o)}}function Al(t){return function n(){for(var a=this,o=arguments.length,c=new Array(o),l=0;l<o;l++)c[l]=arguments[l];return c.length>=t.length?t.apply(this,c):function(){for(var d=arguments.length,m=new Array(d),p=0;p<d;p++)m[p]=arguments[p];return n.apply(a,[].concat(c,m))}}}function hd(t){return{}.toString.call(t).includes("Object")}function uL(t){return!Object.keys(t).length}function Ql(t){return typeof t=="function"}function dL(t,n){return Object.prototype.hasOwnProperty.call(t,n)}function fL(t,n){return hd(n)||us("changeType"),Object.keys(n).some(function(a){return!dL(t,a)})&&us("changeField"),n}function mL(t){Ql(t)||us("selectorType")}function pL(t){Ql(t)||hd(t)||us("handlerType"),hd(t)&&Object.values(t).some(function(n){return!Ql(n)})&&us("handlersType")}function hL(t){t||us("initialIsRequired"),hd(t)||us("initialType"),uL(t)&&us("initialContent")}function gL(t,n){throw new Error(t[n]||t.default)}var xL={initialIsRequired:"initial state is required",initialType:"initial state should be an object",initialContent:"initial state shouldn't be an empty object",handlerType:"handler should be an object or a function",handlersType:"all handlers should be a functions",selectorType:"selector should be a function",changeType:"provided value of changes should be an object",changeField:'it seams you want to change a field in the state which is not specified in the "initial" state',default:"an unknown error accured in `state-local` package"},us=Al(gL)(xL),Gu={changes:fL,selector:mL,handler:pL,initial:hL};function vL(t){var n=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{};Gu.initial(t),Gu.handler(n);var a={current:t},o=Al(wL)(a,n),c=Al(yL)(a),l=Al(Gu.changes)(t),d=Al(bL)(a);function m(){var g=arguments.length>0&&arguments[0]!==void 0?arguments[0]:function(v){return v};return Gu.selector(g),g(a.current)}function p(g){cL(o,c,l,d)(g)}return[m,p]}function bL(t,n){return Ql(n)?n(t.current):n}function yL(t,n){return t.current=J0(J0({},t.current),n),n}function wL(t,n,a){return Ql(n)?n(t.current):Object.keys(a).forEach(function(o){var c;return(c=n[o])===null||c===void 0?void 0:c.call(n,t.current[o])}),a}var jL={create:vL},CL={paths:{vs:"https://cdn.jsdelivr.net/npm/monaco-editor@0.55.1/min/vs"}};function SL(t){return function n(){for(var a=this,o=arguments.length,c=new Array(o),l=0;l<o;l++)c[l]=arguments[l];return c.length>=t.length?t.apply(this,c):function(){for(var d=arguments.length,m=new Array(d),p=0;p<d;p++)m[p]=arguments[p];
Line truncated
|
||||
You are using deprecated way of configuration.
|
||||
|
||||
Instead of using
|
||||
monaco.config({ urls: { monacoBase: '...' } })
|
||||
use
|
||||
monaco.config({ paths: { vs: '...' } })
|
||||
|
||||
For more please check the link https://github.com/suren-atoyan/monaco-loader#config
|
||||
`},e2=SL(TL)(t5),ML={config:EL},RL=function(){for(var n=arguments.length,a=new Array(n),o=0;o<n;o++)a[o]=arguments[o];return function(c){return a.reduceRight(function(l,d){return d(l)},c)}};function n5(t,n){return Object.keys(n).forEach(function(a){n[a]instanceof Object&&t[a]&&Object.assign(n[a],n5(t[a],n[a]))}),Z0(Z0({},t),n)}var AL={type:"cancelation",msg:"operation is manually canceled"};function Wp(t){var n=!1,a=new Promise(function(o,c){t.then(function(l){return n?c(AL):o(l)}),t.catch(c)});return a.cancel=function(){return n=!0},a}var _L=["monaco"],DL=jL.create({config:CL,isInitialized:!1,resolve:null,reject:null,monaco:null}),r5=aL(DL,2),pc=r5[0],nf=r5[1];function OL(t){var n=ML.config(t),a=n.monaco,o=nL(n,_L);nf(function(c){return{config:n5(c.config,o),monaco:a}})}function LL(){var t=pc(function(n){var a=n.monaco,o=n.isInitialized,c=n.resolve;return{monaco:a,isInitialized:o,resolve:c}});if(!t.isInitialized){if(nf({isInitialized:!0}),t.monaco)return t.resolve(t.monaco),Wp(Xp);if(window.monaco&&window.monaco.editor)return a5(window.monaco),t.resolve(window.monaco),Wp(Xp);RL(zL,IL)(UL)}return Wp(Xp)}function zL(t){return document.body.appendChild(t)}function PL(t){var n=document.createElement("script");return t&&(n.src=t),n}function IL(t){var n=pc(function(o){var c=o.config,l=o.reject;return{config:c,reject:l}}),a=PL("".concat(n.config.paths.vs,"/loader.js"));return a.onload=function(){return t()},a.onerror=n.reject,a}function UL(){var t=pc(function(a){var o=a.config,c=a.resolve,l=a.reject;return{config:o,resolve:c,reject:l}}),n=window.require;n.config(t.config),n(["vs/editor/editor.main"],function(a){var o=a.m||a;a5(o),t.resolve(o)},function(a){t.reject(a)})}function a5(t){pc().monaco||nf({monaco:t})}function BL(){return pc(function(t){var n=t.monaco;return n})}var Xp=new Promise(function(t,n){return nf({resolve:t,reject:n})}),s5={config:OL,init:LL,__getMonacoInstance:BL},$L={wrapper:{display:"flex",position:"relative",textAlign:"initial"},fullWidth:{width:"100%"},hide:{display:"none"}},Zp=$L,HL={container:{display:"flex",height:"100%",width:"100%",justifyContent:"center",alignItems:"center"}},GL=HL;function VL({children:t}){return be.createElement("div",{style:GL.container},t)}var FL=VL,YL=FL;function qL({width:t,height:n,isEditorReady:a,loading:o,_ref:c,className:l,wrapperProps:d}){return be.createElement("section",{style:{...Zp.wrapper,width:t,height:n},...d},!a&&be.createElement(YL,null,o),be.createElement("div",{ref:c,style:{...Zp.fullWidth,...!a&&Zp.hide},className:l}))}var KL=qL,o5=f.memo(KL);function WL(t){f.useEffect(t,[])}var i5=WL;function XL(t,n,a=!0){let o=f.useRef(!0);f.useEffect(o.current||!a?()=>{o.current=!1}:t,n)}var Yn=XL;function zl(){}function li(t,n,a,o){return ZL(t,o)||QL(t,n,a,o)}function ZL(t,n){return t.editor.getModel(l5(t,n))}function QL(t,n,a,o){return t.editor.createModel(n,a,o?l5(t,o):void 0)}function l5(t,n){return t.Uri.parse(n)}function JL({original:t,modified:n,language:a,originalLanguage:o,modifiedLanguage:c,originalModelPath:l,modifiedModelPath:d,keepCurrentOriginalModel:m=!1,keepCurrentModifiedModel:p=!1,theme:g="light",loading:v="Loading...",options:b={},height:w="100%",width:y="100%",className:C,wrapperProps:S={},beforeMount:E=zl,onMount:k=zl}){let[N,T]=f.useState(!1),[M,_]=f.useState(!0),R=f.useRef(null),L=f.useRef(null),te=f.useRef(null),K=f.useRef(k),I=f.useRef(E),G=f.useRef(!1);i5(()=>{let se=s5.init();return se.then(P=>(L.current=P)&&_(!1)).catch(P=>(P==null?void 0:P.type)!=="cancelation"&&console.error("Monaco initialization: error:",P)),()=>R.current?fe():se.cancel()}),Yn(()=>{if(R.current&&L.current){let se=R.current.getOriginalEditor(),P=li(L.current,t||"",o||a||"text",l||"");P!==se.getModel()&&se.setModel(P)}},[l],N),Yn(()=>{if(R.current&&L.current){let se=R.current.getModifiedEditor(),P=li(L.current,n||"",c||a||"text",d||"");P!==se.getModel()&&se.setModel(P)}},[d],N),Yn(()=>{let se=R.current.getModifiedEditor();se.getOption(L.current.editor.EditorOption.readOnly)?se.setValue(n||""):n!==se.getValue()&&(se.executeEdits("",[{range:se.getModel().getFullModelRange(),text:n||"",forceMoveMarkers:!0}]),se.pushUndoStop())},[n],N),Yn(()=>{var se,P;(P=(se=R.current)==null?void 0:se.getModel())==null||P.original.setValue(t||"")},[t],N),Yn(()=>{let{original:se,modified:P}=R.current.getModel();L.current.editor.setModelLanguage(se,o||a||"text"),L.current.editor.setModelLanguage(P,c||a||"text")},[a,o,c],N),Yn(()=>{var se;(se=L.current)==null||se.editor.setTheme(g)},[g],N),Yn(()=>{var se;(se=R.current)==null||se.updateOptions(b)},[b],N);let Y=f.useCallback(()=>{var Z;if(!L.current)return;I.current(L.current);let se=li(L.current,t||"",o||a||"text",l||""),P=li(L.current,n||"",c||a||"text",d||"");(Z=R.current)==null||Z.setModel({original:se,modified:P})},[a,n,c,t,o,l,d]),ce=f.useCallback(()=>{var se;!G.current&&te.current&&(R.current=L.current.editor.createDiffEditor(te.current,{automaticLayout:!0,...b}),Y(),(se=L.current)==null||se.editor.setTheme(g),T(!0),G.current=!
Line truncated
|
||||
987654321098765432`,className:"min-h-[120px] font-mono text-sm"})]}),s.jsx(mo,{result:a.owners}),s.jsx("div",{className:"mt-auto",children:s.jsx(ue,{type:"button",variant:"secondary",disabled:o||!t.ownerUserIds.trim(),onClick:c,children:o?"Checking with Discord…":"Verify owner IDs"})})]})}function Mz({onContinue:t}){return s.jsx("div",{className:"mx-auto flex w-full max-w-lg flex-1 flex-col justify-center py-8",children:s.jsxs("div",{className:"rounded-2xl border border-border bg-card px-6 py-10 text-center md:px-10",children:[s.jsx("div",{className:"mx-auto mb-6 flex size-16 items-center justify-center rounded-2xl bg-primary/10 text-primary",children:s.jsx(ve,{icon:jd,size:40})}),s.jsx("h2",{className:"text-2xl font-semibold tracking-tight",children:"Setup complete"}),s.jsxs("p",{className:"mx-auto mt-3 max-w-sm text-sm leading-relaxed text-muted-foreground",children:["Your configuration has been saved. Restart the server for changes to take effect — stop the running process, then start it again (for example"," ",s.jsx("code",{className:"rounded-md bg-muted px-1.5 py-0.5 font-mono text-xs",children:"npm start"}),")."]}),s.jsx(Ye,{className:"mt-6 text-left",children:s.jsx(qe,{children:"After restarting, sign in with Discord to open the dashboard."})}),s.jsx(ue,{type:"button",size:"lg",className:"mt-8 min-w-[220px]",onClick:t,children:"Continue to sign in"})]})})}function Rz({currentIndex:t,validating:n,saving:a,onBack:o,onNext:c,onFinish:l}){const d=t===mi.length-1;return s.jsxs("div",{className:"flex flex-wrap items-center justify-between gap-3",children:[s.jsxs(ue,{type:"button",variant:"ghost",disabled:t===0||a,onClick:o,children:[s.jsx(ve,{icon:T2,size:18,className:"mr-1"}),"Back"]}),s.jsx("div",{className:"flex flex-1 justify-end gap-2",children:d?s.jsx(ue,{type:"button",size:"lg",disabled:a||n,onClick:l,className:"min-w-[200px]",children:a?"Saving…":"Finish setup"}):s.jsxs(ue,{type:"button",size:"lg",disabled:n||a,onClick:c,children:["Continue",s.jsx(ve,{icon:lg,size:18,className:"ml-1"})]})})]})}function Az({values:t,setValues:n,validation:a,validating:o,onValidate:c}){return s.jsxs("div",{className:"flex flex-1 flex-col gap-5",children:[s.jsxs("p",{className:"text-sm leading-relaxed text-muted-foreground",children:["An Emerald Services API key is optional, but required to use the one-click addon installer in the dashboard. You can get yours from the"," ",s.jsx("a",{href:"https://emeraldsrv.dev",target:"_blank",rel:"noreferrer",className:"font-medium text-primary hover:underline",children:"Emerald Services Marketplace"}),"."]}),s.jsxs("div",{className:"space-y-2",children:[s.jsx(_e,{htmlFor:"emeraldApiKey",children:"Emerald API key (Optional)"}),s.jsx(De,{id:"emeraldApiKey",type:"password",autoComplete:"off",value:t.emeraldApiKey,onChange:l=>n({...t,emeraldApiKey:l.target.value}),placeholder:"em_...",className:"h-11 font-mono text-sm"})]}),s.jsx(mo,{result:a.emeraldApi}),s.jsx("div",{className:"mt-auto",children:s.jsx(ue,{type:"button",variant:"secondary",disabled:o,onClick:c,children:o?"Checking…":t.emeraldApiKey?"Verify key":"Skip step"})})]})}function _z({values:t,setValues:n,validation:a,validating:o,onValidate:c}){const l=a.database,d=t.dbDriver==="mysql";return s.jsxs("div",{className:"flex flex-1 flex-col gap-6",children:[s.jsxs("p",{className:"text-sm leading-relaxed text-muted-foreground",children:["Choose your database storage engine. Select ",s.jsx("strong",{children:"SQLite"})," for instant zero-config local storage, or ",s.jsx("strong",{children:"MySQL / MariaDB"})," for external host databases."]}),s.jsxs("div",{className:"grid gap-4 sm:grid-cols-2",children:[s.jsxs("div",{onClick:()=>n(m=>({...m,dbDriver:"sqlite"})),className:pe("cursor-pointer rounded-xl border p-4 transition-all hover:border-primary",t.dbDriver==="sqlite"?"border-primary bg-primary/5 ring-2 ring-primary/20":"border-border bg-card"),children:[s.jsxs("div",{className:"flex items-center justify-between mb-2",children:[s.jsx("span",{className:"font-semibold text-foreground",children:"SQLite"}),s.jsx("span",{className:"rounded-full bg-emerald-500/10 px-2.5 py-0.5 text-xs font-medium text-emerald-500",children:"Zero Config"})]}),s.jsxs("p",{className:"text-xs text-muted-foreground",children:["Single-file SQL database stored locally at ",s.jsx("code",{className:"font-mono",children:"./Data/supportbot.db"}),". Zero extra setup required."]})]}),s.jsxs("div",{onClick:()=>n(m=>({...m,dbDriver:"mysql"})),className:pe("cursor-pointer rounded-xl border p-4 transition-all hover:border-primary",t.dbDriver==="mysql"?"border-primary bg-primary/5 ring-2 ring-primary/20":"border-border bg-card"),children:[s.jsxs("div",{className:"flex items-center justify-between mb-2",children:[s.jsx("span",{className:"font-semibold text-foreground",children:"MySQL / MariaDB"}),s.jsx("span",{className:"rounded-full bg-blue-500/10 px-2.5 py-0.5 text-xs font-medium text-blue-500",children:"External Database"})]}),s.jsx("p",{className:"text-xs te
Line truncated
|
||||
@@ -0,0 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>SupportBot Dashboard</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,400;0,500;0,600;0,700;1,400&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<script type="module" crossorigin src="/assets/index-xYh1zX07.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-Dx-VdUAO.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>␍
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 81 KiB |
@@ -1,18 +0,0 @@
|
||||
// SupportBot Addons
|
||||
// Default Addon
|
||||
|
||||
const Discord = require("discord.js");
|
||||
const bot = new Discord.Client()
|
||||
|
||||
bot.settings = require("../settings.json");
|
||||
|
||||
exports.run = async(bot, message, args) => {
|
||||
message.delete();
|
||||
|
||||
message.channel.send(":white_check_mark: **SupportBot Addon**\nThis addon is here by default.\nWe are currently working on some super amazing addons for SupportBot\nHowever, If you would like to start creating your own support bot addon\ncontact us at EmeraldServices.\n")
|
||||
|
||||
}
|
||||
|
||||
exports.help = {
|
||||
name: 'addon',
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
{
|
||||
|
||||
"SupportBot": "v3.4 / Download addons @ https://itzemerald.tk/",
|
||||
|
||||
"token": "TOKEN",
|
||||
"prefix": "!",
|
||||
"botname": "SupportBot",
|
||||
"colour": "#42f477",
|
||||
"footer": "Support Bot, By Emerald",
|
||||
|
||||
"BotStatus": "Support Bot",
|
||||
|
||||
"staff": "Staff",
|
||||
"support": "Support",
|
||||
"everyone": "@everyone",
|
||||
"Auto_Role": "Member",
|
||||
|
||||
"category": "Tickets",
|
||||
"Ticket_Message": "Your support ticket has been opened successfully\nPlease allow us some time to reach out to you.",
|
||||
"Ticket_Logs": "ticket-logs",
|
||||
|
||||
"Announcement_Title": "SupportBot Announcement",
|
||||
"Announcement_Channel": "announcements",
|
||||
|
||||
"Welcome_Message": "Welcome to the server!",
|
||||
"Welcome_Channel": "welcome",
|
||||
|
||||
"Leave_Message": "Thank you for visiting us!",
|
||||
"Leave_Channel": "welcome",
|
||||
|
||||
"Poll_Channel": "polls",
|
||||
"Poll_Title": "📊 Polls",
|
||||
"Reaction_Poll_1": "✅",
|
||||
"Reaction_Poll_2": "❎",
|
||||
|
||||
"Suggestion_Title": "SupportBot Suggestions",
|
||||
"Suggestion_Channel": "suggestions",
|
||||
"suggestyes": "✅",
|
||||
"suggestno": "❎",
|
||||
|
||||
"Report_Channel": "reports",
|
||||
"Report_Title": "SupportBot Report",
|
||||
|
||||
"LINK_1_NAME": "Discord",
|
||||
"LINK_2_NAME": "Twitter",
|
||||
"LINK_3_NAME": "Website",
|
||||
|
||||
"LINK_1": "https://dicord.gg/changemeinconfig",
|
||||
"LINK_2": "https://twitter.com/changemeinconfig",
|
||||
"LINK_3": "https://www.example.com/",
|
||||
|
||||
"LINKS_TITLE": "SupportBot Links",
|
||||
|
||||
"Ticket_Command": "ticket",
|
||||
"Suggest_Command": "suggest",
|
||||
"Say_Command": "say",
|
||||
"Remove_Command": "remove",
|
||||
"Add_Command": "add",
|
||||
"Ping_Command": "ping",
|
||||
"Close_Command": "close",
|
||||
"Forceclose_Command": "forceclose",
|
||||
"Rename_Command": "rename",
|
||||
"Help_Command": "help",
|
||||
"Link_Command": "links",
|
||||
"Lockchat_Command": "lock",
|
||||
"UnLockchat_Command": "unlock",
|
||||
"Poll_Command": "poll",
|
||||
"Report_Command": "report"
|
||||
|
||||
}
|
||||
Reference in new issue
Block a user