Compare commits
507
Commits
6.0.2
...
v26.2-pre-2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
No files matched your search
@@ -0,0 +1,2 @@
|
||||
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.
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
node_modules
|
||||
|
||||
# Dashboard UI source at repo root only (do not ignore public/dashboard/)
|
||||
/dashboard/
|
||||
|
||||
# Local git hooks + secret-stripping scripts (run npm run git:setup)
|
||||
scripts/
|
||||
|
||||
# Local secrets — use *.example as templates
|
||||
Configs/api.yml
|
||||
+2058
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,493 @@
|
||||
|
||||
|
||||
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));
|
||||
|
||||
if (lines.length > 0) {
|
||||
container.addTextDisplayComponents(
|
||||
new Discord.TextDisplayBuilder().setContent(lines.join("\n")),
|
||||
);
|
||||
}
|
||||
|
||||
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,342 @@
|
||||
// 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)
|
||||
.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,261 @@
|
||||
const fs = require("fs");
|
||||
const { ApplicationCommandOptionType, ApplicationCommandType, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, ComponentType, MessageFlags } = require("discord.js");
|
||||
const yaml = require("js-yaml");
|
||||
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.Command,
|
||||
type: ApplicationCommandType.ChatInput,
|
||||
options: [
|
||||
{
|
||||
name: cmdconfig.Mod.TicketBlacklist.Command,
|
||||
description: cmdconfig.Mod.TicketBlacklist.Description,
|
||||
type: ApplicationCommandOptionType.SubcommandGroup,
|
||||
options: [
|
||||
{
|
||||
name: cmdconfig.Mod.TicketBlacklist.Add.Command,
|
||||
description: msgconfig.Mod.TicketBlacklist.Add.Description || "Add a user to the ticket blacklist",
|
||||
type: ApplicationCommandOptionType.Subcommand,
|
||||
options: [
|
||||
{
|
||||
name: 'user',
|
||||
description: msgconfig.Mod.TicketBlacklist.Add.UserDescription || "User to blacklist",
|
||||
type: ApplicationCommandOptionType.User,
|
||||
required: true
|
||||
},
|
||||
{
|
||||
name: 'reason',
|
||||
description: msgconfig.Mod.TicketBlacklist.Add.ReasonDescription || "Reason for blacklisting",
|
||||
type: ApplicationCommandOptionType.String,
|
||||
required: false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: cmdconfig.Mod.TicketBlacklist.Remove.Command,
|
||||
description: msgconfig.Mod.TicketBlacklist.Remove.Description || "Remove a user from the ticket blacklist",
|
||||
type: ApplicationCommandOptionType.Subcommand,
|
||||
options: [
|
||||
{
|
||||
name: 'user',
|
||||
description: msgconfig.Mod.TicketBlacklist.Remove.UserDescription || "User to remove from blacklist",
|
||||
type: ApplicationCommandOptionType.User,
|
||||
required: true
|
||||
},
|
||||
{
|
||||
name: 'reason',
|
||||
description: msgconfig.Mod.TicketBlacklist.Remove.ReasonDescription || "Reason for removing from blacklist",
|
||||
type: ApplicationCommandOptionType.String,
|
||||
required: false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "view",
|
||||
description: msgconfig.Mod.TicketBlacklist.View.Description || "View all blacklisted users",
|
||||
type: ApplicationCommandOptionType.Subcommand,
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
permissions: cmdconfig.Mod.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);
|
||||
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] });
|
||||
}
|
||||
|
||||
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.Mod.AllowSupportStaff || !interaction.member.roles.cache.has(SupportStaff.id))) {
|
||||
return interaction.reply({ embeds: [NoPerms] });
|
||||
}
|
||||
|
||||
const subcommand = interaction.options.getSubcommand(false);
|
||||
const user = interaction.options.getUser('user');
|
||||
const reason = interaction.options.getString('reason') || "No reason provided";
|
||||
|
||||
try {
|
||||
let blacklistedUsers;
|
||||
try {
|
||||
blacklistedUsers = JSON.parse(fs.readFileSync("./Data/BlacklistedUsers.json", "utf8")).blacklistedUsers;
|
||||
if (!Array.isArray(blacklistedUsers)) {
|
||||
blacklistedUsers = [];
|
||||
}
|
||||
} catch (error) {
|
||||
blacklistedUsers = [];
|
||||
}
|
||||
|
||||
if (subcommand === cmdconfig.Mod.TicketBlacklist.Add.Command) {
|
||||
if (blacklistedUsers.includes(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 });
|
||||
}
|
||||
|
||||
blacklistedUsers.push(user.id);
|
||||
fs.writeFileSync("./Data/BlacklistedUsers.json", JSON.stringify({ "blacklistedUsers": blacklistedUsers }, null, 4));
|
||||
|
||||
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: "\`\`\`Removed from 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 (!blacklistedUsers.includes(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 });
|
||||
}
|
||||
|
||||
blacklistedUsers = blacklistedUsers.filter(id => id !== user.id);
|
||||
fs.writeFileSync("./Data/BlacklistedUsers.json", JSON.stringify({ "blacklistedUsers": blacklistedUsers }, null, 4));
|
||||
|
||||
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,324 @@
|
||||
# ___ _ ___ _
|
||||
# / __> _ _ ___ ___ ___ _ _ _| |_ | . > ___ _| |_
|
||||
# \__ \| | || . \| . \/ . \| '_> | | | . \/ . \ | |
|
||||
# <___/`___|| _/| _/\___/|_| |_| |___/\___/ |_|
|
||||
# |_| |_|
|
||||
#
|
||||
# SupportBot created by Emerald Services
|
||||
# Installed with MIT License
|
||||
#
|
||||
# Discord Support: https://discord.gg/emeraldsrv
|
||||
# Official Website & Marketplace: https://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,101 @@
|
||||
# ___ _ ___ _
|
||||
# / __> _ _ ___ ___ ___ _ _ _| |_ | . > ___ _| |_
|
||||
# \__ \| | || . \| . \/ . \| '_> | | | . \/ . \ | |
|
||||
# <___/`___|| _/| _/\___/|_| |_| |___/\___/ |_|
|
||||
# |_| |_|
|
||||
#
|
||||
# SupportBot created by Emerald Services
|
||||
# Installed with MIT License
|
||||
#
|
||||
# Discord Support: https://discord.gg/emeraldsrv
|
||||
# Official Website & Marketplace: https://emeraldsrv.com
|
||||
|
||||
# -------------------------
|
||||
# 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: "groq"
|
||||
Model: "MODEL_NAME"
|
||||
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: "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,368 @@
|
||||
# ___ _ ___ _
|
||||
# / __> _ _ ___ ___ ___ _ _ _| |_ | . > ___ _| |_
|
||||
# \__ \| | || . \| . \/ . \| '_> | | | . \/ . \ | |
|
||||
# <___/`___|| _/| _/\___/|_| |_| |___/\___/ |_|
|
||||
# |_| |_|
|
||||
#
|
||||
# SupportBot created by Emerald Services
|
||||
# Installed with MIT License
|
||||
#
|
||||
# Discord Support: https://discord.gg/emeraldsrv
|
||||
# Official Website & Marketplace: https://emeraldsrv.com
|
||||
#
|
||||
# SupportBot Configuration (supportbot.yml)
|
||||
|
||||
SupportBot_Version: "v26.2-pre1"
|
||||
|
||||
# -------------------------
|
||||
# General
|
||||
# -------------------------
|
||||
|
||||
General:
|
||||
Name: "SupportBot"
|
||||
Token: "DISCORD_BOT_TOKEN"
|
||||
Addons:
|
||||
Enabled: true
|
||||
GuildId: "GUILD_ID"
|
||||
|
||||
# -------------------------
|
||||
# Activity
|
||||
# -------------------------
|
||||
|
||||
Activity:
|
||||
Status: "SupportBot"
|
||||
Type: "Watching"
|
||||
StreamingURL: "https://www.twitch.tv/YOUR_TWITCH_NAME"
|
||||
# Available Activity 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"
|
||||
|
||||
# -------------------------
|
||||
# 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,65 @@
|
||||
# ___ _ ___ _
|
||||
# / __> _ _ ___ ___ ___ _ _ _| |_ | . > ___ _| |_
|
||||
# \__ \| | || . \| . \/ . \| '_> | | | . \/ . \ | |
|
||||
# <___/`___|| _/| _/\___/|_| |_| |___/\___/ |_|
|
||||
# |_| |_|
|
||||
#
|
||||
# SupportBot created by Emerald Services
|
||||
# Installed with MIT License
|
||||
#
|
||||
# Discord Support: https://discord.gg/emeraldsrv
|
||||
# Official Website & Marketplace: https://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: ''
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
FROM node:alpine
|
||||
|
||||
MAINTAINER Thien Tran, <contact@thientran.io>
|
||||
|
||||
RUN apk update \
|
||||
&& apk upgrade
|
||||
|
||||
RUN apk add --no-cache git && \
|
||||
adduser -D -h /home/container container
|
||||
|
||||
# Ensure UTF-8
|
||||
ENV LANG en_US.UTF-8
|
||||
ENV LC_ALL en_US.UTF-8
|
||||
|
||||
WORKDIR /home/container
|
||||
|
||||
COPY ./entrypoint.sh /entrypoint.sh
|
||||
CMD ["/bin/ash", "/entrypoint.sh"]
|
||||
@@ -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 });
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 Emerald SRV
|
||||
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
|
||||
@@ -1,15 +1,93 @@
|
||||

|
||||
<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>
|
||||
|
||||
SupportBot the #1 self-hosted discord ticket system with any customizations and features to help make your support system look clean, tidy and professional. It's self-hosted! No need to use someone's public discord bot thats used in a handful of servers. Create, Configure & Use your own discord bot installed with SupportBot.
|
||||
<div align="center">
|
||||
|
||||
### SupportBot Compatibility
|
||||
<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>
|
||||
|
||||
> Anything older than v6 we will no longer be supporting, However our terms & conditions still remain valid for all copies of our product "SupportBot".
|
||||
<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>
|
||||
|
||||
|---------|---------|
|
||||
| Version | |
|
||||
|---------|---------|
|
||||
| 6.0. | ✅ |
|
||||
|---------|---------|
|
||||
<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://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>
|
||||
|
||||
## About
|
||||
|
||||
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.
|
||||
|
||||
## Support
|
||||
|
||||
<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>
|
||||
|
||||
|
||||
## License
|
||||
|
||||
Released under the [MIT](https://opensource.org/licenses/MIT) license.
|
||||
|
||||
|
||||
## Third-Party
|
||||
|
||||
- [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)
|
||||
|
||||
|
||||
## Contributors
|
||||
|
||||
<a href="https://github.com/Emerald-Services/SupportBot/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=Emerald-Services/SupportBot">
|
||||
</a>
|
||||
|
||||
## Hosting
|
||||
|
||||
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,438 @@
|
||||
const Database = require("better-sqlite3");
|
||||
const path = require("path");
|
||||
|
||||
const db = new Database(path.join(__dirname, "../Data/ai-memory.db"));
|
||||
|
||||
db.pragma("journal_mode = WAL");
|
||||
|
||||
db.prepare(
|
||||
`
|
||||
CREATE TABLE IF NOT EXISTS ai_conversations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
guild_id TEXT,
|
||||
channel_id TEXT,
|
||||
user_id TEXT,
|
||||
role TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL
|
||||
)
|
||||
`,
|
||||
).run();
|
||||
|
||||
db.prepare(
|
||||
`
|
||||
CREATE TABLE IF NOT EXISTS ai_summaries (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
guild_id TEXT,
|
||||
channel_id TEXT,
|
||||
summary TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
)
|
||||
`,
|
||||
).run();
|
||||
|
||||
db.prepare(
|
||||
`
|
||||
CREATE TABLE IF NOT EXISTS ai_facts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
guild_id TEXT,
|
||||
channel_id TEXT,
|
||||
user_id TEXT,
|
||||
fact_type TEXT NOT NULL,
|
||||
fact_key TEXT NOT NULL,
|
||||
fact_value TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
)
|
||||
`,
|
||||
).run();
|
||||
|
||||
db.prepare(
|
||||
`
|
||||
CREATE TABLE IF NOT EXISTS ai_knowledge (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
source_type TEXT NOT NULL,
|
||||
source_name TEXT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
)
|
||||
`,
|
||||
).run();
|
||||
|
||||
db.prepare(
|
||||
`
|
||||
CREATE TABLE IF NOT EXISTS ai_sessions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
guild_id TEXT,
|
||||
channel_id TEXT,
|
||||
user_id TEXT,
|
||||
model TEXT,
|
||||
mode TEXT,
|
||||
last_message_at INTEGER,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
)
|
||||
`,
|
||||
).run();
|
||||
|
||||
try {
|
||||
db.prepare(`ALTER TABLE ai_facts ADD COLUMN user_id TEXT`).run();
|
||||
} catch (e) {
|
||||
if (!String(e).includes("duplicate column name")) throw e;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
saveUserFact({ guildId, userId, factType, factKey, factValue }) {
|
||||
const existing = db
|
||||
.prepare(
|
||||
`
|
||||
SELECT id FROM ai_facts
|
||||
WHERE guild_id IS ? AND user_id IS ? AND fact_type = ? AND fact_key = ?
|
||||
LIMIT 1
|
||||
`,
|
||||
)
|
||||
.get(guildId || null, userId || null, factType, factKey);
|
||||
|
||||
if (existing) {
|
||||
db.prepare(
|
||||
`
|
||||
UPDATE ai_facts
|
||||
SET fact_value = ?, updated_at = ?
|
||||
WHERE id = ?
|
||||
`,
|
||||
).run(factValue, Date.now(), existing.id);
|
||||
return;
|
||||
}
|
||||
|
||||
db.prepare(
|
||||
`
|
||||
INSERT INTO ai_facts (
|
||||
guild_id, channel_id, user_id, fact_type, fact_key, fact_value, created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`,
|
||||
).run(
|
||||
guildId || null,
|
||||
null,
|
||||
userId || null,
|
||||
factType,
|
||||
factKey,
|
||||
factValue,
|
||||
Date.now(),
|
||||
Date.now(),
|
||||
);
|
||||
},
|
||||
|
||||
getUserFacts(guildId, userId) {
|
||||
return db
|
||||
.prepare(
|
||||
`
|
||||
SELECT * FROM ai_facts
|
||||
WHERE guild_id IS ? AND user_id IS ?
|
||||
ORDER BY updated_at DESC
|
||||
`,
|
||||
)
|
||||
.all(guildId || null, userId || null);
|
||||
},
|
||||
|
||||
getUserFactsByScope({ guildId = null, userId = null, scope = "guild" }) {
|
||||
if (!userId) return [];
|
||||
|
||||
if (scope === "global") {
|
||||
return db
|
||||
.prepare(
|
||||
`
|
||||
SELECT * FROM ai_facts
|
||||
WHERE user_id IS ?
|
||||
ORDER BY updated_at DESC
|
||||
`,
|
||||
)
|
||||
.all(userId || null);
|
||||
}
|
||||
|
||||
return db
|
||||
.prepare(
|
||||
`
|
||||
SELECT * FROM ai_facts
|
||||
WHERE guild_id IS ? AND user_id IS ?
|
||||
ORDER BY updated_at DESC
|
||||
`,
|
||||
)
|
||||
.all(guildId || null, userId || null);
|
||||
},
|
||||
|
||||
addMessage({ guildId, channelId, userId, role, content }) {
|
||||
db.prepare(
|
||||
`
|
||||
INSERT INTO ai_conversations (
|
||||
guild_id, channel_id, user_id, role, content, created_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?)
|
||||
`,
|
||||
).run(
|
||||
guildId || null,
|
||||
channelId || null,
|
||||
userId || null,
|
||||
role,
|
||||
content,
|
||||
Date.now(),
|
||||
);
|
||||
},
|
||||
|
||||
getRecentMessages(channelId, limit = 15) {
|
||||
return db
|
||||
.prepare(
|
||||
`
|
||||
SELECT * FROM ai_conversations
|
||||
WHERE channel_id = ?
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ?
|
||||
`,
|
||||
)
|
||||
.all(channelId, limit)
|
||||
.reverse();
|
||||
},
|
||||
|
||||
getRecentMessagesByScope({ guildId = null, channelId = null, limit = 15, scope = "guild" }) {
|
||||
if (scope === "channel") {
|
||||
return db
|
||||
.prepare(
|
||||
`
|
||||
SELECT * FROM ai_conversations
|
||||
WHERE channel_id = ?
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ?
|
||||
`,
|
||||
)
|
||||
.all(channelId || null, limit)
|
||||
.reverse();
|
||||
}
|
||||
|
||||
if (scope === "global") {
|
||||
return db
|
||||
.prepare(
|
||||
`
|
||||
SELECT * FROM ai_conversations
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ?
|
||||
`,
|
||||
)
|
||||
.all(limit)
|
||||
.reverse();
|
||||
}
|
||||
|
||||
return db
|
||||
.prepare(
|
||||
`
|
||||
SELECT * FROM ai_conversations
|
||||
WHERE guild_id IS ?
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ?
|
||||
`,
|
||||
)
|
||||
.all(guildId || null, limit)
|
||||
.reverse();
|
||||
},
|
||||
|
||||
saveSummary({ guildId, channelId, summary }) {
|
||||
const existing = db
|
||||
.prepare(
|
||||
`
|
||||
SELECT id FROM ai_summaries
|
||||
WHERE guild_id IS ? AND channel_id IS ?
|
||||
ORDER BY updated_at DESC
|
||||
LIMIT 1
|
||||
`,
|
||||
)
|
||||
.get(guildId || null, channelId || null);
|
||||
|
||||
if (existing) {
|
||||
db.prepare(
|
||||
`
|
||||
UPDATE ai_summaries
|
||||
SET summary = ?, updated_at = ?
|
||||
WHERE id = ?
|
||||
`,
|
||||
).run(summary, Date.now(), existing.id);
|
||||
return;
|
||||
}
|
||||
|
||||
db.prepare(
|
||||
`
|
||||
INSERT INTO ai_summaries (
|
||||
guild_id, channel_id, summary, created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?)
|
||||
`,
|
||||
).run(guildId || null, channelId || null, summary, Date.now(), Date.now());
|
||||
},
|
||||
|
||||
getLatestSummary(channelId) {
|
||||
return db
|
||||
.prepare(
|
||||
`
|
||||
SELECT * FROM ai_summaries
|
||||
WHERE channel_id = ?
|
||||
ORDER BY updated_at DESC
|
||||
LIMIT 1
|
||||
`,
|
||||
)
|
||||
.get(channelId);
|
||||
},
|
||||
|
||||
getLatestSummaryByScope({ guildId = null, channelId = null, scope = "guild" }) {
|
||||
if (scope === "channel") {
|
||||
return db
|
||||
.prepare(
|
||||
`
|
||||
SELECT * FROM ai_summaries
|
||||
WHERE channel_id = ?
|
||||
ORDER BY updated_at DESC
|
||||
LIMIT 1
|
||||
`,
|
||||
)
|
||||
.get(channelId || null);
|
||||
}
|
||||
|
||||
if (scope === "global") {
|
||||
return db
|
||||
.prepare(
|
||||
`
|
||||
SELECT * FROM ai_summaries
|
||||
ORDER BY updated_at DESC
|
||||
LIMIT 1
|
||||
`,
|
||||
)
|
||||
.get();
|
||||
}
|
||||
|
||||
return db
|
||||
.prepare(
|
||||
`
|
||||
SELECT * FROM ai_summaries
|
||||
WHERE guild_id IS ?
|
||||
ORDER BY updated_at DESC
|
||||
LIMIT 1
|
||||
`,
|
||||
)
|
||||
.get(guildId || null);
|
||||
},
|
||||
|
||||
saveFact({ guildId, channelId, factType, factKey, factValue }) {
|
||||
const existing = db
|
||||
.prepare(
|
||||
`
|
||||
SELECT id FROM ai_facts
|
||||
WHERE guild_id IS ? AND channel_id IS ? AND fact_type = ? AND fact_key = ?
|
||||
LIMIT 1
|
||||
`,
|
||||
)
|
||||
.get(guildId || null, channelId || null, factType, factKey);
|
||||
|
||||
if (existing) {
|
||||
db.prepare(
|
||||
`
|
||||
UPDATE ai_facts
|
||||
SET fact_value = ?, updated_at = ?
|
||||
WHERE id = ?
|
||||
`,
|
||||
).run(factValue, Date.now(), existing.id);
|
||||
return;
|
||||
}
|
||||
|
||||
db.prepare(
|
||||
`
|
||||
INSERT INTO ai_facts (
|
||||
guild_id, channel_id, fact_type, fact_key, fact_value, created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`,
|
||||
).run(
|
||||
guildId || null,
|
||||
channelId || null,
|
||||
factType,
|
||||
factKey,
|
||||
factValue,
|
||||
Date.now(),
|
||||
Date.now(),
|
||||
);
|
||||
},
|
||||
|
||||
getFacts(channelId) {
|
||||
return db
|
||||
.prepare(
|
||||
`
|
||||
SELECT * FROM ai_facts
|
||||
WHERE channel_id = ?
|
||||
ORDER BY updated_at DESC
|
||||
`,
|
||||
)
|
||||
.all(channelId);
|
||||
},
|
||||
|
||||
getFactsByScope({ guildId = null, channelId = null, scope = "guild" }) {
|
||||
if (scope === "channel") {
|
||||
return db
|
||||
.prepare(
|
||||
`
|
||||
SELECT * FROM ai_facts
|
||||
WHERE channel_id = ? AND user_id IS NULL
|
||||
ORDER BY updated_at DESC
|
||||
`,
|
||||
)
|
||||
.all(channelId || null);
|
||||
}
|
||||
|
||||
if (scope === "global") {
|
||||
return db
|
||||
.prepare(
|
||||
`
|
||||
SELECT * FROM ai_facts
|
||||
WHERE user_id IS NULL
|
||||
ORDER BY updated_at DESC
|
||||
`,
|
||||
)
|
||||
.all();
|
||||
}
|
||||
|
||||
return db
|
||||
.prepare(
|
||||
`
|
||||
SELECT * FROM ai_facts
|
||||
WHERE guild_id IS ? AND user_id IS NULL
|
||||
ORDER BY updated_at DESC
|
||||
`,
|
||||
)
|
||||
.all(guildId || null);
|
||||
},
|
||||
|
||||
addKnowledge({ sourceType, sourceName, title, content }) {
|
||||
db.prepare(
|
||||
`
|
||||
INSERT INTO ai_knowledge (
|
||||
source_type, source_name, title, content, created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?)
|
||||
`,
|
||||
).run(sourceType, sourceName, title, content, Date.now(), Date.now());
|
||||
},
|
||||
|
||||
getKnowledge(limit = 25) {
|
||||
return db
|
||||
.prepare(
|
||||
`
|
||||
SELECT * FROM ai_knowledge
|
||||
ORDER BY updated_at DESC
|
||||
LIMIT ?
|
||||
`,
|
||||
)
|
||||
.all(limit);
|
||||
},
|
||||
|
||||
clearKnowledge() {
|
||||
db.prepare(`DELETE FROM ai_knowledge`).run();
|
||||
},
|
||||
|
||||
clearConversations(channelId) {
|
||||
if (!channelId) return;
|
||||
db.prepare(`DELETE FROM ai_conversations WHERE channel_id = ?`).run(channelId);
|
||||
},
|
||||
};
|
||||
@@ -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,180 @@
|
||||
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}`);
|
||||
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("");
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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,199 @@
|
||||
// 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(),
|
||||
},
|
||||
{
|
||||
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 = {}) {
|
||||
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,141 @@
|
||||
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() {
|
||||
return {
|
||||
overview: true,
|
||||
tickets: true,
|
||||
logs: true,
|
||||
transcripts: true,
|
||||
settings: { view: true, update: true },
|
||||
configs: configMap(true, true),
|
||||
users: { view: true, manage: true },
|
||||
};
|
||||
}
|
||||
|
||||
function permissionsFromRole(role, customPermissions) {
|
||||
switch (role) {
|
||||
case "owner":
|
||||
case "admin":
|
||||
return fullPermissions();
|
||||
case "moderator":
|
||||
return {
|
||||
overview: true,
|
||||
tickets: true,
|
||||
logs: true,
|
||||
transcripts: true,
|
||||
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,
|
||||
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,
|
||||
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,
|
||||
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 === "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,440 @@
|
||||
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;
|
||||
|
||||
return [
|
||||
{
|
||||
id: "botToken",
|
||||
title: "Bot token",
|
||||
description: "From the Discord Developer Portal → your application → Bot → Reset Token.",
|
||||
complete: isValidBotToken(token),
|
||||
},
|
||||
{
|
||||
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 validateSetupPayload(payload) {
|
||||
const results = {};
|
||||
|
||||
if (payload.botToken != null) {
|
||||
results.botToken = await validateBotToken(payload.botToken);
|
||||
}
|
||||
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"));
|
||||
sbDoc.setIn(["General", "Token"], String(payload.botToken).trim());
|
||||
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,293 @@
|
||||
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) {
|
||||
bootstrapFromOAuth(oauth);
|
||||
const store = readStore();
|
||||
const yamlOwners = getOwnerIds(oauth);
|
||||
|
||||
return Object.values(store.users).map((entry) => {
|
||||
const access = resolvePermissions(entry, oauth, entry.id) || {
|
||||
permissions: permissionsFromRole("viewer"),
|
||||
role: entry.role,
|
||||
isOwner: false,
|
||||
};
|
||||
|
||||
return {
|
||||
id: entry.id,
|
||||
username: entry.username,
|
||||
globalName: entry.globalName,
|
||||
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,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
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,490 @@
|
||||
// SupportBot | Emerald Services
|
||||
const Database = require("better-sqlite3");
|
||||
const path = require("path");
|
||||
|
||||
const db = new Database(path.join(__dirname, "../Data/supportbot.db"));
|
||||
|
||||
db.prepare(
|
||||
`
|
||||
CREATE TABLE IF NOT EXISTS suggestions (
|
||||
thread_id TEXT PRIMARY KEY,
|
||||
author_id TEXT,
|
||||
text TEXT,
|
||||
status TEXT DEFAULT 'Created'
|
||||
)
|
||||
`,
|
||||
).run();
|
||||
|
||||
db.prepare(
|
||||
`
|
||||
CREATE TABLE IF NOT EXISTS profiles (
|
||||
user_id TEXT PRIMARY KEY,
|
||||
bio TEXT,
|
||||
timezone TEXT,
|
||||
clockedIn INTEGER DEFAULT 0
|
||||
)
|
||||
`,
|
||||
).run();
|
||||
|
||||
db.prepare(
|
||||
`
|
||||
CREATE TABLE IF NOT EXISTS tickets (
|
||||
ticket_id TEXT PRIMARY KEY,
|
||||
user_id TEXT,
|
||||
status TEXT DEFAULT 'open',
|
||||
created_at INTEGER,
|
||||
updated_at INTEGER,
|
||||
subject TEXT,
|
||||
description TEXT
|
||||
)
|
||||
`,
|
||||
).run();
|
||||
|
||||
// Safe column upgrades
|
||||
const safeAlter = (sql) => {
|
||||
try {
|
||||
db.prepare(sql).run();
|
||||
} catch (e) {
|
||||
if (!String(e).includes("duplicate column name")) throw e;
|
||||
}
|
||||
};
|
||||
|
||||
safeAlter(`ALTER TABLE tickets ADD COLUMN voiceChannelId TEXT`);
|
||||
safeAlter(`ALTER TABLE tickets ADD COLUMN department TEXT DEFAULT 'general'`);
|
||||
safeAlter(`ALTER TABLE tickets ADD COLUMN priority TEXT DEFAULT 'medium'`);
|
||||
safeAlter(`ALTER TABLE tickets ADD COLUMN aiModeEnabled INTEGER DEFAULT 0`);
|
||||
safeAlter(`ALTER TABLE tickets ADD COLUMN aiChannelId TEXT`);
|
||||
safeAlter(`ALTER TABLE tickets ADD COLUMN aiType TEXT`);
|
||||
safeAlter(`ALTER TABLE tickets ADD COLUMN aiEnabledBy TEXT`);
|
||||
safeAlter(`ALTER TABLE tickets ADD COLUMN aiEnabledAt INTEGER`);
|
||||
safeAlter(`ALTER TABLE tickets ADD COLUMN questionAnswers TEXT`);
|
||||
safeAlter(`ALTER TABLE tickets ADD COLUMN ticketName TEXT`);
|
||||
safeAlter(`ALTER TABLE tickets ADD COLUMN ticketNumber TEXT`);
|
||||
|
||||
db.prepare(
|
||||
`
|
||||
CREATE TABLE IF NOT EXISTS ticket_panel (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
message_id TEXT,
|
||||
channel_id TEXT,
|
||||
created_at INTEGER
|
||||
)
|
||||
`,
|
||||
).run();
|
||||
|
||||
db.prepare(
|
||||
`
|
||||
CREATE TABLE IF NOT EXISTS ticket_users (
|
||||
ticket_id TEXT,
|
||||
user_id TEXT,
|
||||
PRIMARY KEY(ticket_id, user_id)
|
||||
)
|
||||
`,
|
||||
).run();
|
||||
|
||||
db.prepare(
|
||||
`
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
id INTEGER PRIMARY KEY,
|
||||
data TEXT
|
||||
)
|
||||
`,
|
||||
).run();
|
||||
|
||||
function ensureProfile(userId) {
|
||||
const existing = db
|
||||
.prepare(
|
||||
`
|
||||
SELECT user_id FROM profiles WHERE user_id = ?
|
||||
`,
|
||||
)
|
||||
.get(userId);
|
||||
|
||||
if (!existing) {
|
||||
db.prepare(
|
||||
`
|
||||
INSERT INTO profiles (user_id, bio, timezone, clockedIn)
|
||||
VALUES (?, '', '', 0)
|
||||
`,
|
||||
).run(userId);
|
||||
}
|
||||
}
|
||||
|
||||
function normaliseTicketRow(row) {
|
||||
if (!row) return null;
|
||||
|
||||
let questionAnswers = [];
|
||||
|
||||
if (row.questionAnswers) {
|
||||
try {
|
||||
questionAnswers = JSON.parse(row.questionAnswers);
|
||||
} catch {
|
||||
questionAnswers = [];
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...row,
|
||||
aiModeEnabled: Boolean(row.aiModeEnabled),
|
||||
clockedIn:
|
||||
row.clockedIn !== undefined ? Boolean(row.clockedIn) : row.clockedIn,
|
||||
questionAnswers,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
addTicket(ticket) {
|
||||
const now = Date.now();
|
||||
|
||||
db.prepare(
|
||||
`
|
||||
INSERT INTO tickets (
|
||||
ticket_id,
|
||||
user_id,
|
||||
subject,
|
||||
description,
|
||||
created_at,
|
||||
updated_at,
|
||||
department,
|
||||
priority,
|
||||
aiModeEnabled,
|
||||
aiChannelId,
|
||||
aiType,
|
||||
aiEnabledBy,
|
||||
aiEnabledAt,
|
||||
questionAnswers,
|
||||
ticketName,
|
||||
ticketNumber
|
||||
)
|
||||
VALUES (
|
||||
@ticket_id,
|
||||
@user_id,
|
||||
@subject,
|
||||
@description,
|
||||
@created_at,
|
||||
@updated_at,
|
||||
@department,
|
||||
@priority,
|
||||
@aiModeEnabled,
|
||||
@aiChannelId,
|
||||
@aiType,
|
||||
@aiEnabledBy,
|
||||
@aiEnabledAt,
|
||||
@questionAnswers,
|
||||
@ticketName,
|
||||
@ticketNumber
|
||||
)
|
||||
ON CONFLICT(ticket_id) DO NOTHING
|
||||
`,
|
||||
).run({
|
||||
ticket_id: ticket.id,
|
||||
user_id: ticket.user,
|
||||
subject: ticket.reason || "",
|
||||
description: ticket.description || "",
|
||||
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,
|
||||
});
|
||||
|
||||
if (global.apiServer) {
|
||||
global.apiServer.broadcast("ticket_created", {
|
||||
ticket_id: ticket.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 = []) {
|
||||
db.prepare(
|
||||
`
|
||||
UPDATE tickets
|
||||
SET questionAnswers=@questionAnswers, updated_at=@updatedAt
|
||||
WHERE ticket_id=@ticketId
|
||||
`,
|
||||
).run({
|
||||
ticketId,
|
||||
questionAnswers: JSON.stringify(questionAnswers),
|
||||
updatedAt: Date.now(),
|
||||
});
|
||||
},
|
||||
|
||||
updateTicketVoice(ticketId, voiceChannelId) {
|
||||
db.prepare(
|
||||
`
|
||||
UPDATE tickets
|
||||
SET voiceChannelId=@voiceChannelId, updated_at=@updatedAt
|
||||
WHERE ticket_id=@ticketId
|
||||
`,
|
||||
).run({
|
||||
ticketId,
|
||||
voiceChannelId,
|
||||
updatedAt: Date.now(),
|
||||
});
|
||||
},
|
||||
|
||||
updateTicketStatus(ticketId, status) {
|
||||
db.prepare(
|
||||
`
|
||||
UPDATE tickets
|
||||
SET status=@status, updated_at=@updatedAt
|
||||
WHERE ticket_id=@ticketId
|
||||
`,
|
||||
).run({
|
||||
status,
|
||||
updatedAt: Date.now(),
|
||||
ticketId,
|
||||
});
|
||||
|
||||
if (global.apiServer && status === "closed") {
|
||||
global.apiServer.broadcast("ticket_closed", { ticket_id: ticketId });
|
||||
}
|
||||
},
|
||||
|
||||
updateTicketDepartment(ticketId, department) {
|
||||
db.prepare(
|
||||
`
|
||||
UPDATE tickets
|
||||
SET department=@department, updated_at=@updatedAt
|
||||
WHERE ticket_id=@ticketId
|
||||
`,
|
||||
).run({
|
||||
ticketId,
|
||||
department,
|
||||
updatedAt: Date.now(),
|
||||
});
|
||||
},
|
||||
|
||||
updateTicketPriority(ticketId, priority) {
|
||||
db.prepare(
|
||||
`
|
||||
UPDATE tickets
|
||||
SET priority=@priority, updated_at=@updatedAt
|
||||
WHERE ticket_id=@ticketId
|
||||
`,
|
||||
).run({
|
||||
ticketId,
|
||||
priority,
|
||||
updatedAt: Date.now(),
|
||||
});
|
||||
},
|
||||
|
||||
setTicketAIState(ticketId, aiState = {}) {
|
||||
db.prepare(
|
||||
`
|
||||
UPDATE tickets
|
||||
SET
|
||||
aiModeEnabled=@aiModeEnabled,
|
||||
aiChannelId=@aiChannelId,
|
||||
aiType=@aiType,
|
||||
aiEnabledBy=@aiEnabledBy,
|
||||
aiEnabledAt=@aiEnabledAt,
|
||||
updated_at=@updatedAt
|
||||
WHERE ticket_id=@ticketId
|
||||
`,
|
||||
).run({
|
||||
ticketId,
|
||||
aiModeEnabled: aiState.enabled ? 1 : 0,
|
||||
aiChannelId: aiState.aiChannelId || null,
|
||||
aiType: aiState.aiType || null,
|
||||
aiEnabledBy: aiState.enabledBy || null,
|
||||
aiEnabledAt: aiState.enabledAt
|
||||
? new Date(aiState.enabledAt).getTime()
|
||||
: aiState.enabled
|
||||
? Date.now()
|
||||
: null,
|
||||
updatedAt: Date.now(),
|
||||
});
|
||||
},
|
||||
|
||||
enableTicketAI(
|
||||
ticketId,
|
||||
aiChannelId = null,
|
||||
aiType = null,
|
||||
enabledBy = null,
|
||||
) {
|
||||
db.prepare(
|
||||
`
|
||||
UPDATE tickets
|
||||
SET
|
||||
aiModeEnabled=1,
|
||||
aiChannelId=@aiChannelId,
|
||||
aiType=@aiType,
|
||||
aiEnabledBy=@aiEnabledBy,
|
||||
aiEnabledAt=@aiEnabledAt,
|
||||
updated_at=@updatedAt
|
||||
WHERE ticket_id=@ticketId
|
||||
`,
|
||||
).run({
|
||||
ticketId,
|
||||
aiChannelId,
|
||||
aiType,
|
||||
aiEnabledBy,
|
||||
aiEnabledAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
});
|
||||
},
|
||||
|
||||
disableTicketAI(ticketId) {
|
||||
db.prepare(
|
||||
`
|
||||
UPDATE tickets
|
||||
SET
|
||||
aiModeEnabled=0,
|
||||
updated_at=@updatedAt
|
||||
WHERE ticket_id=@ticketId
|
||||
`,
|
||||
).run({
|
||||
ticketId,
|
||||
updatedAt: Date.now(),
|
||||
});
|
||||
},
|
||||
|
||||
getTicket(ticketId) {
|
||||
const row = db
|
||||
.prepare(
|
||||
`
|
||||
SELECT * FROM tickets WHERE ticket_id=@ticketId
|
||||
`,
|
||||
)
|
||||
.get({ ticketId });
|
||||
|
||||
return normaliseTicketRow(row);
|
||||
},
|
||||
|
||||
getTicketByAIChannel(aiChannelId) {
|
||||
const row = db
|
||||
.prepare(
|
||||
`
|
||||
SELECT * FROM tickets WHERE aiChannelId=@aiChannelId LIMIT 1
|
||||
`,
|
||||
)
|
||||
.get({ aiChannelId });
|
||||
|
||||
return normaliseTicketRow(row);
|
||||
},
|
||||
|
||||
getAllTickets() {
|
||||
return db.prepare(`SELECT * FROM tickets`).all().map(normaliseTicketRow);
|
||||
},
|
||||
|
||||
saveTicketPanel(messageId, channelId) {
|
||||
db.prepare(
|
||||
`
|
||||
INSERT INTO ticket_panel (message_id, channel_id, created_at)
|
||||
VALUES (@messageId, @channelId, @createdAt)
|
||||
`,
|
||||
).run({ messageId, channelId, createdAt: Date.now() });
|
||||
},
|
||||
|
||||
getTicketPanel() {
|
||||
return db
|
||||
.prepare(
|
||||
`
|
||||
SELECT * FROM ticket_panel ORDER BY id DESC LIMIT 1
|
||||
`,
|
||||
)
|
||||
.get();
|
||||
},
|
||||
|
||||
addUserToTicket(ticketId, userId) {
|
||||
db.prepare(
|
||||
`
|
||||
INSERT INTO ticket_users (ticket_id, user_id)
|
||||
VALUES (?, ?)
|
||||
ON CONFLICT(ticket_id, user_id) DO NOTHING
|
||||
`,
|
||||
).run(ticketId, userId);
|
||||
},
|
||||
|
||||
removeUserFromTicket(ticketId, userId) {
|
||||
db.prepare(
|
||||
`
|
||||
DELETE FROM ticket_users WHERE ticket_id=? AND user_id=?
|
||||
`,
|
||||
).run(ticketId, userId);
|
||||
},
|
||||
|
||||
getTicketUsers(ticketId) {
|
||||
return db
|
||||
.prepare(
|
||||
`
|
||||
SELECT user_id FROM ticket_users WHERE ticket_id=?
|
||||
`,
|
||||
)
|
||||
.all(ticketId)
|
||||
.map((r) => r.user_id);
|
||||
},
|
||||
|
||||
getSettings() {
|
||||
const row = db.prepare(`SELECT data FROM settings WHERE id=1`).get();
|
||||
if (!row) return {};
|
||||
try {
|
||||
return JSON.parse(row.data);
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
|
||||
getProfile(userId) {
|
||||
ensureProfile(userId);
|
||||
|
||||
const row = db
|
||||
.prepare(
|
||||
`
|
||||
SELECT * FROM profiles WHERE user_id = ?
|
||||
`,
|
||||
)
|
||||
.get(userId);
|
||||
|
||||
if (!row) {
|
||||
return {
|
||||
user_id: userId,
|
||||
bio: "",
|
||||
timezone: "",
|
||||
clockedIn: false,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...row,
|
||||
clockedIn: Boolean(row.clockedIn),
|
||||
};
|
||||
},
|
||||
|
||||
setClockedIn(userId, clockedIn) {
|
||||
ensureProfile(userId);
|
||||
|
||||
db.prepare(
|
||||
`
|
||||
UPDATE profiles
|
||||
SET clockedIn = ?
|
||||
WHERE user_id = ?
|
||||
`,
|
||||
).run(clockedIn ? 1 : 0, userId);
|
||||
},
|
||||
|
||||
saveSettings(obj) {
|
||||
const data = JSON.stringify(obj);
|
||||
db.prepare(
|
||||
`
|
||||
INSERT INTO settings (id, data)
|
||||
VALUES (1, @data)
|
||||
ON CONFLICT(id) DO UPDATE SET data = excluded.data
|
||||
`,
|
||||
).run({ data });
|
||||
},
|
||||
};
|
||||
@@ -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,276 @@
|
||||
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,
|
||||
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,30 @@
|
||||
// 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 row = db.getTicketPanel()
|
||||
if (!row) return await this.set(0)
|
||||
return row.id
|
||||
}
|
||||
|
||||
static async set(value) {
|
||||
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,20 +0,0 @@
|
||||
// SupportBot 6.0, Created by Emerald Services
|
||||
// Help Command
|
||||
|
||||
const Discord = require("discord.js");
|
||||
const fs = require("fs");
|
||||
|
||||
const yaml = require('js-yaml');
|
||||
|
||||
// location for the addons configuration, to keep it simple we generate a settings folder within the addons directory.
|
||||
const example = yaml.load(fs.readFileSync('./addons/settings/exampleaddon.yml', 'utf8'));
|
||||
|
||||
module.exports = {
|
||||
name: example.ExampleAddonCommand,
|
||||
description: example.ExampleAddonDesc,
|
||||
|
||||
execute(message, args) {
|
||||
if (supportbot.DeleteMessages == "true") message.delete();
|
||||
message.channel.send(example.ExampleAddonMessage)
|
||||
}
|
||||
};
|
||||
@@ -1,9 +0,0 @@
|
||||
#
|
||||
# SupportBot 6.0
|
||||
# Example Addon
|
||||
#
|
||||
|
||||
ExampleAddonCommand: "exampleaddon"
|
||||
ExampleAddonDesc: "This is the default/example addon for SupportBot."
|
||||
|
||||
ExampleAddonMessage: "> :white_check_mark: This is the **default** addon for SupportBot. You can download addons from our website, https://emeraldservices.xyz/"
|
||||
@@ -1,65 +0,0 @@
|
||||
// SupportBot 6.0, Created by Emerald Services
|
||||
// Add User
|
||||
|
||||
const Discord = require("discord.js");
|
||||
const fs = require("fs");
|
||||
|
||||
const yaml = require('js-yaml');
|
||||
const supportbot = yaml.load(fs.readFileSync('./supportbot-config.yml', 'utf8'));
|
||||
|
||||
module.exports = {
|
||||
name: supportbot.AddUser,
|
||||
description: supportbot.AddUserDesc,
|
||||
|
||||
execute(message, args) {
|
||||
if (supportbot.DeleteMessages == "true") message.delete();
|
||||
console.log(`\u001b[32m`, `[${supportbot.Bot_Name}]:`, `\u001b[32m`, `${message.author.tag} has executed ${supportbot.Prefix}${supportbot.AddUser}!`);
|
||||
|
||||
let SupportStaff = message.guild.roles.cache.find(SupportTeam => SupportTeam.name === supportbot.Staff)
|
||||
let Admins = message.guild.roles.cache.find(AdminUser => AdminUser.name === supportbot.Admin)
|
||||
|
||||
const NoPerms = new Discord.MessageEmbed()
|
||||
.setTitle("Invalid Permissions!")
|
||||
.setDescription(`${supportbot.IncorrectPerms}\n\nRole Required: \`${supportbot.Staff}\` or \`${supportbot.Admin}\``)
|
||||
.setColor(supportbot.WarningColour)
|
||||
|
||||
if (message.member.roles.cache.has(SupportStaff.id) || message.member.roles.cache.has(Admins.id)) {
|
||||
if (!message.channel.name.startsWith( `${supportbot.TicketChannel}-` )) {
|
||||
const Exists = new Discord.MessageEmbed()
|
||||
.setTitle("No Ticket Found!")
|
||||
.setDescription(`${supportbot.NoValidTicket}`)
|
||||
.setColor(supportbot.WarningColour);
|
||||
message.channel.send({ embed: Exists });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let rUser = message.guild.member(message.mentions.users.first() || message.guild.members.cache.get(args[0]));
|
||||
const UserNotExist = new Discord.MessageEmbed()
|
||||
.setTitle("User Not Found!")
|
||||
.setDescription(`${supportbot.UserNotFound}\n\nTry Again:\`${supportbot.Prefix}${supportbot.AddUser} <user#0000>\``)
|
||||
.setColor(supportbot.ErrorColour)
|
||||
|
||||
if (!rUser) return message.channel.send({ embed: UserNotExist });
|
||||
|
||||
message.channel.updateOverwrite(rUser, {
|
||||
VIEW_CHANNEL: true,
|
||||
CREATE_INVITE: false,
|
||||
SEND_MESSAGES: true,
|
||||
READ_MESSAGES: true
|
||||
});
|
||||
|
||||
const Complete = new Discord.MessageEmbed()
|
||||
.setTitle("User Added!")
|
||||
.setDescription(supportbot.AddedUser.replace(/%user%/g, rUser.id))
|
||||
.setTimestamp()
|
||||
.setColor(supportbot.EmbedColour)
|
||||
message.channel.send({ embed: Complete });
|
||||
} else {
|
||||
|
||||
return message.channel.send({ embed: NoPerms });
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
@@ -1,70 +0,0 @@
|
||||
// SupportBot 6.0, Created by Emerald Services
|
||||
// Links Command
|
||||
|
||||
const Discord = require("discord.js");
|
||||
const fs = require("fs");
|
||||
|
||||
const yaml = require('js-yaml');
|
||||
const { execute } = require("./help");
|
||||
const supportbot = yaml.load(fs.readFileSync('./supportbot-config.yml', 'utf8'));
|
||||
|
||||
module.exports = {
|
||||
name: supportbot.AnnounceCommand,
|
||||
description: supportbot.AnnounceDesc,
|
||||
|
||||
execute(message, args) {
|
||||
|
||||
if (supportbot.DeleteMessages == "true") message.delete();
|
||||
|
||||
let locateChannel = message.guild.channels.cache.find(AnnouncementChannel => AnnouncementChannel.name === supportbot.AnnouncementChannel);
|
||||
|
||||
const errornochannel = new Discord.MessageEmbed()
|
||||
.setTitle("Invalid Channel")
|
||||
.setDescription(`${supportbot.InvalidChannel}\n\nChannel Required: \`${supportbot.AnnouncementChannel}`)
|
||||
.setColor(supportbot.ErrorColour);
|
||||
|
||||
if(!locateChannel) return message.channel.send({ embed: errornochannel });
|
||||
|
||||
let Admins = message.guild.roles.cache.find(adminRole => adminRole.name === supportbot.Admin);
|
||||
|
||||
const NoPerms = new Discord.MessageEmbed()
|
||||
.setTitle("Invalid Permissions!")
|
||||
.setDescription(`${supportbot.IncorrectPerms}\n\nRole Required: \`${Admins.name}\``)
|
||||
.setColor(supportbot.WarningColour)
|
||||
|
||||
if (!message.member.roles.cache.has(Admins.id))
|
||||
return message.channel.send({ embed: NoPerms });
|
||||
|
||||
|
||||
const embed = new Discord.MessageEmbed()
|
||||
.setDescription(`> **${supportbot.AnnouncementStarter}**`)
|
||||
.setColor(supportbot.EmbedColour)
|
||||
message.channel.send({ embed: embed });
|
||||
|
||||
let announcement = []
|
||||
message.channel.awaitMessages(response => response.content.length > 2, {
|
||||
max: 1,
|
||||
time: 500000,
|
||||
errors: ['time'],
|
||||
}).then((collected) => {
|
||||
announcement.push(collected.map(r => r.content));
|
||||
|
||||
const AnnouncementEmbed = new Discord.MessageEmbed()
|
||||
.setTitle(supportbot.AnnouncementTitle)
|
||||
.setThumbnail(supportbot.AnnouncementIcon)
|
||||
.setDescription(announcement)
|
||||
.setColor(supportbot.EmbedColour)
|
||||
.setFooter(supportbot.EmbedFooter, message.author.displayAvatarURL());
|
||||
|
||||
locateChannel.send({ embed: AnnouncementEmbed }).then(async function(msg) {
|
||||
const AnnouncementComplete = new Discord.MessageEmbed()
|
||||
.setColor(supportbot.SuccessColour)
|
||||
.setDescription(`:white_check_mark: You have successfully created an announcement. <#${locateChannel.id}>`)
|
||||
message.channel.send({ embed: AnnouncementComplete })
|
||||
})
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
@@ -1,51 +0,0 @@
|
||||
// SupportBot 6.0, Created by Emerald Services
|
||||
// Say Command
|
||||
|
||||
const Discord = require("discord.js");
|
||||
const fs = require("fs");
|
||||
|
||||
const yaml = require('js-yaml');
|
||||
const { execute } = require("./help");
|
||||
const supportbot = yaml.load(fs.readFileSync('./supportbot-config.yml', 'utf8'));
|
||||
|
||||
module.exports = {
|
||||
name: supportbot.BotSay,
|
||||
description: supportbot.BotSayDesc,
|
||||
|
||||
execute(message, args) {
|
||||
if (supportbot.DeleteMessages == "true") message.delete();
|
||||
|
||||
let SupportStaff = message.guild.roles.cache.find(adminRole => adminRole.name === supportbot.Staff);
|
||||
|
||||
const NoPerms = new Discord.MessageEmbed()
|
||||
.setTitle("Invalid Permissions!")
|
||||
.setDescription(`${supportbot.IncorrectPerms}\n\nRole Required: \`${SupportStaff.name}\``)
|
||||
.setColor(supportbot.WarningColour)
|
||||
|
||||
if (!message.member.roles.cache.has(SupportStaff.id))
|
||||
return message.channel.send({ embed: NoPerms });
|
||||
|
||||
|
||||
const embed = new Discord.MessageEmbed()
|
||||
.setDescription(`> **What would you like me to say!**`)
|
||||
.setColor(supportbot.EmbedColour)
|
||||
message.channel.send({ embed: embed });
|
||||
|
||||
let announcement = []
|
||||
message.channel.awaitMessages(response => response.content.length > 2, {
|
||||
max: 1,
|
||||
time: 500000,
|
||||
errors: ['time'],
|
||||
}).then((collected) => {
|
||||
announcement.push(collected.map(r => r.content));
|
||||
|
||||
const AnnouncementEmbed = new Discord.MessageEmbed()
|
||||
.setDescription(announcement)
|
||||
.setColor(supportbot.EmbedColour)
|
||||
|
||||
message.channel.send({ embed: AnnouncementEmbed })
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
@@ -1,149 +0,0 @@
|
||||
// SupportBot 6.0, Created by Emerald Services
|
||||
// Links Command
|
||||
|
||||
const Discord = require("discord.js");
|
||||
const fs = require("fs");
|
||||
|
||||
const yaml = require('js-yaml');
|
||||
const { execute } = require("./help");
|
||||
const supportbot = yaml.load(fs.readFileSync('./supportbot-config.yml', 'utf8'));
|
||||
|
||||
module.exports = {
|
||||
name: supportbot.CloseTicket,
|
||||
description: supportbot.CloseTicketDesc,
|
||||
|
||||
execute(message, args) {
|
||||
if (supportbot.DeleteMessages == "true") message.delete();
|
||||
|
||||
console.log(`\u001b[32m`, `[${supportbot.Bot_Name}]:`, `\u001b[32m`, `${message.author.tag} has executed ${supportbot.Prefix}${supportbot.CloseTicket}!`);
|
||||
|
||||
if (!message.channel.name.startsWith( `${supportbot.TicketChannel}-` )) {
|
||||
const Exists = new Discord.MessageEmbed()
|
||||
.setTitle("No Ticket Found!")
|
||||
.setDescription(`${supportbot.NoValidTicket}`)
|
||||
.setColor(supportbot.WarningColour);
|
||||
message.channel.send({ embed: Exists });
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
if (supportbot.CloseConfirmation === "true") {
|
||||
const CloseTicketRequest = new Discord.MessageEmbed()
|
||||
.setTitle(`**${supportbot.ClosingTicket}**`)
|
||||
.setDescription(`Please confirm by repeating the following word.. \`${supportbot.ClosingConfirmation_Word}\` `)
|
||||
.setColor(supportbot.EmbedColour);
|
||||
|
||||
message.channel.send(CloseTicketRequest).then(m => {
|
||||
message.channel.awaitMessages( response => response.content.toLowerCase() === supportbot.ClosingConfirmation_Word, {
|
||||
max: 1,
|
||||
time: 20000,
|
||||
errors: [ 'time' ],
|
||||
|
||||
|
||||
}).then( collected => {
|
||||
let logChannel = message.guild.channels.cache.find(channel => channel.name === supportbot.TranscriptLog);
|
||||
let user = message.author;
|
||||
let reason = args.join(" ") || "No Reason Provided.";
|
||||
|
||||
let name = message.channel.name;
|
||||
let ticketChannel = message.channel;
|
||||
|
||||
message.channel.send(`**${supportbot.ClosingTicket}**`)
|
||||
|
||||
.then(() => {
|
||||
const logEmbed = new Discord.MessageEmbed()
|
||||
.setTitle(supportbot.TranscriptTitle)
|
||||
.setColor(supportbot.EmbedColour)
|
||||
.setFooter(supportbot.EmbedFooter)
|
||||
.addField("Closed By", message.author.tag)
|
||||
.addField("Reason", reason);
|
||||
|
||||
message.channel.messages.fetch({ limit: 100 }).then( msgs => {
|
||||
let txt = '';
|
||||
|
||||
msgs = msgs.sort( ( a, b ) => a.createdTimestamp - b.createdTimestamp );
|
||||
txt += `${message.guild.name}\n`;
|
||||
txt += `#${ticketChannel.name}\n`;
|
||||
txt += `${msgs.size} messages\n\n\n`;
|
||||
|
||||
msgs.forEach( msg => {
|
||||
if ( msg.content ) {
|
||||
txt += `----- ${msg.author.tag} at ${msg.createdAt}\n`;
|
||||
txt += `${msg.content}\n`;
|
||||
txt += `-----\n\n`;
|
||||
}
|
||||
});
|
||||
|
||||
logChannel.send(logEmbed).catch(err => {
|
||||
message.reply(err)
|
||||
})
|
||||
|
||||
logChannel.send( new Discord.MessageAttachment( Buffer.from( txt ), `${name}.txt` ) );
|
||||
|
||||
message.channel.delete();
|
||||
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
}).catch( () => {
|
||||
m.edit('The request to close the ticket has timed out.').then( m2 => m2.delete( 3000 ) );
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
if (supportbot.CloseConfirmation === "false") {
|
||||
|
||||
let logChannel = message.guild.channels.cache.find(channel => channel.name === supportbot.TranscriptLog);
|
||||
let user = message.author;
|
||||
let reason = args.join(" ") || "No Reason Provided.";
|
||||
|
||||
let name = message.channel.name;
|
||||
let ticketChannel = message.channel;
|
||||
|
||||
message.channel.send(`**${supportbot.ClosingTicket}**`)
|
||||
|
||||
.then(() => {
|
||||
const logEmbed = new Discord.MessageEmbed()
|
||||
.setTitle(supportbot.TranscriptTitle)
|
||||
.setColor(supportbot.EmbedColour)
|
||||
.setFooter(supportbot.EmbedFooter)
|
||||
.addField("Closed By", message.author.tag)
|
||||
.addField("Reason", reason);
|
||||
|
||||
message.channel.messages.fetch({ limit: 100 }).then( msgs => {
|
||||
let txt = '';
|
||||
|
||||
msgs = msgs.sort( ( a, b ) => a.createdTimestamp - b.createdTimestamp );
|
||||
txt += `${message.guild.name}\n`;
|
||||
txt += `#${ticketChannel.name}\n`;
|
||||
txt += `${msgs.size} messages\n\n\n`;
|
||||
|
||||
msgs.forEach( msg => {
|
||||
if ( msg.content ) {
|
||||
txt += `----- ${msg.author.tag} at ${msg.createdAt}\n`;
|
||||
txt += `${msg.content}\n`;
|
||||
txt += `-----\n\n`;
|
||||
}
|
||||
});
|
||||
|
||||
logChannel.send(logEmbed).catch(err => {
|
||||
message.reply(err)
|
||||
})
|
||||
|
||||
logChannel.send( new Discord.MessageAttachment( Buffer.from( txt ), `${name}.txt` ) );
|
||||
|
||||
message.channel.delete();
|
||||
|
||||
})
|
||||
|
||||
}).catch( () => {
|
||||
m.edit('The request to close the ticket has timed out.').then( m2 => m2.delete( 3000 ) );
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
@@ -1,53 +0,0 @@
|
||||
// SupportBot 6.0, Created by Emerald Services
|
||||
// Help Command
|
||||
|
||||
const Discord = require("discord.js");
|
||||
const fs = require("fs");
|
||||
|
||||
const yaml = require('js-yaml');
|
||||
const supportbot = yaml.load(fs.readFileSync('./supportbot-config.yml', 'utf8'));
|
||||
|
||||
module.exports = {
|
||||
name: supportbot.HelpCommand,
|
||||
description: supportbot.HelpDesc,
|
||||
|
||||
execute(message, args) {
|
||||
if (supportbot.DeleteMessages == "true") message.delete();
|
||||
|
||||
let generalCommands = "";
|
||||
generalCommands += `**${supportbot.Prefix}${supportbot.HelpCommand}** ${supportbot.HelpDesc}\n`;
|
||||
generalCommands += `**${supportbot.Prefix}${supportbot.LinksCommand}** ${supportbot.LinkDesc}\n`;
|
||||
generalCommands += `**${supportbot.Prefix}${supportbot.SuggestCommand}** ${supportbot.SuggestionDesc}\n`;
|
||||
generalCommands += `**${supportbot.Prefix}${supportbot.PingCommand}** ${supportbot.PingDesc}\n`;
|
||||
|
||||
let supportCommands = "";
|
||||
supportCommands += `**${supportbot.Prefix}${supportbot.NewTicket} [reason]** ${supportbot.NewTicketDesc}\n`
|
||||
supportCommands += `**${supportbot.Prefix}${supportbot.CloseTicket} [reason]** ${supportbot.CloseTicketDesc}\n`
|
||||
|
||||
let staffCommands = "";
|
||||
staffCommands += `**${supportbot.Prefix}${supportbot.AddUser} <user#0000>** ${supportbot.AddUserDesc}\n`
|
||||
staffCommands += `**${supportbot.Prefix}${supportbot.RemoveUser} <user#0000>** ${supportbot.RemoveUserDesc}\n`
|
||||
staffCommands += `**${supportbot.Prefix}${supportbot.BotSay}** ${supportbot.BotSayDesc}\n`
|
||||
staffCommands += `**${supportbot.Prefix}${supportbot.AnnounceCommand}** ${supportbot.AnnounceDesc}\n`
|
||||
|
||||
const HelpCommandEmbed = new Discord.MessageEmbed()
|
||||
.setTitle(supportbot.Bot_Name)
|
||||
.setThumbnail(message.author.displayAvatarURL())
|
||||
|
||||
.addFields(
|
||||
{ name: '🖥️ General Commands', value: `${generalCommands}\n`, inline: false },
|
||||
{ name: '🎫 Support Commands', value: `${supportCommands}\n`, inline: false },
|
||||
)
|
||||
|
||||
.setColor(supportbot.EmbedColour)
|
||||
.setFooter(supportbot.EmbedFooter, message.author.displayAvatarURL());
|
||||
|
||||
if (message.member.roles.cache.some(role => role.name === supportbot.Staff || supportbot.Admin)) {
|
||||
HelpCommandEmbed.addFields(
|
||||
{ name: '🔑 Staff Commands', value: `${staffCommands}\n`, inline: false },
|
||||
)
|
||||
}
|
||||
|
||||
message.channel.send({ embed: HelpCommandEmbed });
|
||||
}
|
||||
};
|
||||
@@ -1,34 +0,0 @@
|
||||
// SupportBot 6.0, Created by Emerald Services
|
||||
// Links Command
|
||||
|
||||
const Discord = require("discord.js");
|
||||
const fs = require("fs");
|
||||
|
||||
const yaml = require('js-yaml');
|
||||
const { execute } = require("./help");
|
||||
const supportbot = yaml.load(fs.readFileSync('./supportbot-config.yml', 'utf8'));
|
||||
|
||||
module.exports = {
|
||||
name: supportbot.LinksCommand,
|
||||
description: supportbot.LinksDesc,
|
||||
|
||||
execute(message, args) {
|
||||
if (supportbot.DeleteMessages == "true") message.delete();
|
||||
|
||||
const LinksEmbed = new Discord.MessageEmbed()
|
||||
.setColor(supportbot.EmbedColour)
|
||||
|
||||
let botLinks = supportbot.LINKS;
|
||||
|
||||
let links = '';
|
||||
|
||||
for ( let name in botLinks ) {
|
||||
links += `[${name}](${botLinks[ name ]})\n`;
|
||||
};
|
||||
|
||||
LinksEmbed.addField(supportbot.LinksTitle, links)
|
||||
|
||||
message.channel.send({ embed: LinksEmbed });
|
||||
|
||||
}
|
||||
};
|
||||
@@ -1,239 +0,0 @@
|
||||
const Discord = require("discord.js");
|
||||
const bot = new Discord.Client()
|
||||
|
||||
const fs = require("fs")
|
||||
const yaml = require('js-yaml');
|
||||
|
||||
const supportbot = yaml.load(fs.readFileSync('./supportbot-config.yml', 'utf8'));
|
||||
const TicketNumberID = require('../utils/TicketNumber.js');
|
||||
|
||||
const reacted = [];
|
||||
|
||||
module.exports = {
|
||||
name: supportbot.NewTicket,
|
||||
description: supportbot.NewTicketDesc,
|
||||
|
||||
execute(message, args) {
|
||||
if (supportbot.DeleteMessages == "true") message.delete();
|
||||
|
||||
console.log(`\u001b[32m`, `[${supportbot.Bot_Name}]:`, `\u001b[32m`, `${message.author.tag} has executed ${supportbot.Prefix}${supportbot.NewTicket}!`);
|
||||
|
||||
// Ticket ID
|
||||
let ticketNumberID = TicketNumberID.pad(message.guild.id);
|
||||
|
||||
// Ticket Subject
|
||||
const TicketSubject = args.join(" ") || supportbot.NoTicketSubject;
|
||||
|
||||
// Ticket Exists
|
||||
const TicketExists = new Discord.MessageEmbed()
|
||||
.setTitle("Ticket Exists!")
|
||||
.setDescription(`${supportbot.TicketExists}`)
|
||||
.setColor(supportbot.WarningColour)
|
||||
|
||||
if (message.guild.channels.cache.find(SupportTicket => SupportTicket.name === `${supportbot.TicketChannel}-${ticketNumberID}`))
|
||||
return message.channel.send({ embed: TicketExists });
|
||||
|
||||
message.guild.channels.create(`${supportbot.TicketChannel}-${ticketNumberID}`, {
|
||||
type: "text",
|
||||
}).then(SupportTicket => {
|
||||
|
||||
let AllUsers = message.guild.roles.cache.find(everyone => everyone.name === '@everyone')
|
||||
let SupportStaff = message.guild.roles.cache.find(SupportTeam => SupportTeam.name === supportbot.Staff)
|
||||
let Admins = message.guild.roles.cache.find(AdminUser => AdminUser.name === supportbot.Admin)
|
||||
|
||||
let DeptRole1 = message.guild.roles.cache.find(DepartmentRole => DepartmentRole.name === `${supportbot.DepartmentRole_1}`)
|
||||
let DeptRole2 = message.guild.roles.cache.find(DepartmentRole => DepartmentRole.name === `${supportbot.DepartmentRole_2}`)
|
||||
let DeptRole3 = message.guild.roles.cache.find(DepartmentRole => DepartmentRole.name === `${supportbot.DepartmentRole_3}`)
|
||||
|
||||
SupportTicket.updateOverwrite(AllUsers, {
|
||||
VIEW_CHANNEL: false,
|
||||
READ_MESSAGES: false,
|
||||
SEND_MESSAGES: false,
|
||||
})
|
||||
|
||||
SupportTicket.updateOverwrite(message.author, {
|
||||
VIEW_CHANNEL: true,
|
||||
READ_MESSAGES: true,
|
||||
SEND_MESSAGES: true,
|
||||
})
|
||||
|
||||
SupportTicket.updateOverwrite(SupportStaff, {
|
||||
VIEW_CHANNEL: true,
|
||||
READ_MESSAGES: true,
|
||||
SEND_MESSAGES: true,
|
||||
})
|
||||
|
||||
SupportTicket.updateOverwrite(Admins, {
|
||||
VIEW_CHANNEL: true,
|
||||
READ_MESSAGES: true,
|
||||
SEND_MESSAGES: true,
|
||||
})
|
||||
|
||||
// Ticket Category
|
||||
let TicketCategory = message.guild.channels.cache.find(category => category.name === supportbot.TicketCategory)
|
||||
|
||||
if (TicketCategory) {
|
||||
SupportTicket.setParent(TicketCategory.id)
|
||||
} else {
|
||||
if (message.guild.channels.cache.get(supportbot.TicketCategory)) {
|
||||
SupportTicket.setParent(message.guild.channels.cache.get(supportbot.TicketCategory).id);
|
||||
}
|
||||
}
|
||||
|
||||
// Ticket Created, Message Sent
|
||||
const TicketMessage = new Discord.MessageEmbed()
|
||||
.setTitle(supportbot.Ticket_Title.replace(/%ticketauthor%/g, message.author.id).replace(/%ticketid%/g, SupportTicket.id).replace(/%ticketusername%/g, message.member.user.username))
|
||||
.setDescription(supportbot.TicketMessage.replace(/%ticketauthor%/g, message.author.id).replace(/%ticketid%/g, SupportTicket.id).replace(/%ticketusername%/g, message.member.user.username))
|
||||
.setColor(supportbot.EmbedColour)
|
||||
|
||||
if (supportbot.TicketSubject === "embed") {
|
||||
|
||||
if (TicketSubject != 'No Reason Provided.') {
|
||||
TicketMessage.addFields({ name: 'Reason', value: TicketSubject })
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (supportbot.TicketSubject === "description") {
|
||||
|
||||
if (TicketSubject != 'No Reason Provided.') {
|
||||
SupportTicket.setTopic(`Reason: ${TicketSubject} - User ID: ${message.author.id} - Ticket: ${SupportTicket.name}`)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (supportbot.TicketDepartments === "true") {
|
||||
TicketMessage.addFields({ name: 'Departments', value: `${supportbot.DepartmentEmoji_1} **${supportbot.DepartmentTitle_1}**\n${supportbot.DepartmentEmoji_2} **${supportbot.DepartmentTitle_2}**\n${supportbot.DepartmentEmoji_3} **${supportbot.DepartmentTitle_3}**` })
|
||||
}
|
||||
|
||||
SupportTicket.send({ embed: TicketMessage }).then( async function(msg) {
|
||||
|
||||
const Emoji_1 = supportbot.DepartmentEmoji_1;
|
||||
const Emoji_2 = supportbot.DepartmentEmoji_2;
|
||||
const Emoji_3 = supportbot.DepartmentEmoji_3;
|
||||
|
||||
reacted[ticketNumberID] = false;
|
||||
|
||||
if (supportbot.TicketDepartments === "true") {
|
||||
await msg.react(Emoji_1);
|
||||
await msg.react(Emoji_2);
|
||||
await msg.react(Emoji_3);
|
||||
let filter = (reaction, user) => {
|
||||
if (user.id !== message.author.id) return;
|
||||
if (typeof reacted[ticketNumberID] !== "boolean") return;
|
||||
delete reacted[ticketNumberID];
|
||||
if (reaction.emoji.name === Emoji_1) {
|
||||
SupportTicket.updateOverwrite(message.author.id, { VIEW_CHANNEL: true, READ_MESSAGES: true, SEND_MESSAGES: true, })
|
||||
SupportTicket.updateOverwrite(Admins, { VIEW_CHANNEL: true, READ_MESSAGES: true, SEND_MESSAGES: true, })
|
||||
SupportTicket.updateOverwrite(DeptRole1, { VIEW_CHANNEL: true, READ_MESSAGES: true, SEND_MESSAGES: true, })
|
||||
|
||||
if (supportbot.AllowStaff === "true") {
|
||||
SupportTicket.updateOverwrite(SupportStaff, { VIEW_CHANNEL: true, READ_MESSAGES: true, SEND_MESSAGES: true, })
|
||||
}
|
||||
|
||||
if (supportbot.AllowStaff === "false") {
|
||||
SupportTicket.updateOverwrite(SupportStaff, { VIEW_CHANNEL: false, READ_MESSAGES: true, SEND_MESSAGES: true, })
|
||||
}
|
||||
|
||||
const Department1 = new Discord.MessageEmbed()
|
||||
.setTitle(`> Thank for reaching out to the **${supportbot.DepartmentTitle_1} Department**. Please provide us information regarding your query.`)
|
||||
.setColor(supportbot.EmbedColour)
|
||||
SupportTicket.send({ embed: Department1 });
|
||||
|
||||
}
|
||||
|
||||
if (reaction.emoji.name === Emoji_2) {
|
||||
SupportTicket.updateOverwrite(message.author.id, { VIEW_CHANNEL: true, READ_MESSAGES: true, SEND_MESSAGES: true, })
|
||||
SupportTicket.updateOverwrite(Admins, { VIEW_CHANNEL: true, READ_MESSAGES: true, SEND_MESSAGES: true, })
|
||||
SupportTicket.updateOverwrite(DeptRole2, { VIEW_CHANNEL: true, READ_MESSAGES: true, SEND_MESSAGES: true, })
|
||||
|
||||
if (supportbot.AllowStaff === "true") {
|
||||
SupportTicket.updateOverwrite(SupportStaff, { VIEW_CHANNEL: true, READ_MESSAGES: true, SEND_MESSAGES: true, })
|
||||
}
|
||||
|
||||
if (supportbot.AllowStaff === "false") {
|
||||
SupportTicket.updateOverwrite(SupportStaff, { VIEW_CHANNEL: false, READ_MESSAGES: true, SEND_MESSAGES: true, })
|
||||
}
|
||||
|
||||
const Department2 = new Discord.MessageEmbed()
|
||||
.setTitle(`> Thank for reaching out to the **${supportbot.DepartmentTitle_2} Department**. Please provide us information regarding your query.`)
|
||||
.setColor(supportbot.EmbedColour)
|
||||
SupportTicket.send({ embed: Department2 });
|
||||
|
||||
}
|
||||
|
||||
if (reaction.emoji.name === `${Emoji_3}`) {
|
||||
SupportTicket.updateOverwrite(message.author.id, { VIEW_CHANNEL: true, READ_MESSAGES: true, SEND_MESSAGES: true, })
|
||||
SupportTicket.updateOverwrite(Admins, { VIEW_CHANNEL: true, READ_MESSAGES: true, SEND_MESSAGES: true, })
|
||||
SupportTicket.updateOverwrite(DeptRole3, { VIEW_CHANNEL: true, READ_MESSAGES: true, SEND_MESSAGES: true, })
|
||||
SupportTicket.updateOverwrite(SupportStaff, { VIEW_CHANNEL: false, READ_MESSAGES: true, SEND_MESSAGES: true, })
|
||||
|
||||
if (supportbot.AllowStaff === "true") {
|
||||
SupportTicket.updateOverwrite(SupportStaff, { VIEW_CHANNEL: true, READ_MESSAGES: true, SEND_MESSAGES: true, })
|
||||
}
|
||||
|
||||
if (supportbot.AllowStaff === "false") {
|
||||
SupportTicket.updateOverwrite(SupportStaff, { VIEW_CHANNEL: false, READ_MESSAGES: true, SEND_MESSAGES: true, })
|
||||
}
|
||||
|
||||
const Department3 = new Discord.MessageEmbed()
|
||||
.setTitle(`> Thank for reaching out to the **${supportbot.DepartmentTitle_3} Department**. Please provide us information regarding your query.`)
|
||||
.setColor(supportbot.EmbedColour)
|
||||
SupportTicket.send({ embed: Department3 });
|
||||
}
|
||||
return [Emoji_1, Emoji_2, Emoji_3].includes(reaction.emoji.name) && user.id !== message.author.id;
|
||||
}
|
||||
msg.awaitReactions(filter, {
|
||||
max: 4,
|
||||
time: 60000,
|
||||
errors: ['time']
|
||||
}).then(async collected => {
|
||||
});
|
||||
|
||||
setTimeout(async function() {
|
||||
if (typeof reacted[ticketNumberID] == "boolean") {
|
||||
delete reacted[ticketNumberID];
|
||||
//delete channel because didn't react in a minute
|
||||
}
|
||||
}, 60000);
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
const CreatedTicket = new Discord.MessageEmbed()
|
||||
.setDescription(supportbot.TicketCreatedAlert.replace(/%ticketauthor%/g, message.author.id).replace(/%ticketid%/g, SupportTicket.id).replace(/%ticketusername%/g, message.member.user.username))
|
||||
.setColor(supportbot.EmbedColour)
|
||||
message.channel.send({ embed: CreatedTicket });
|
||||
|
||||
fs.writeFileSync("./storage/SupportTickets.json", '{\n "ticket": "' + SupportTicket.name + '", \n "id":' + message.author.id + "\n}", (err) => {
|
||||
if (!err) return;
|
||||
console.error(err)
|
||||
})
|
||||
|
||||
// Ticket Logs
|
||||
|
||||
const errornochannel = new Discord.MessageEmbed()
|
||||
.setTitle("SupportBot Error!")
|
||||
.setDescription(`:x: **Error!** Channel not Found, This command cannot be executed proberbly as their is no channel within this server.\nThis is configurable via **supportbot-config.yml**\n\nChannel Required: \`${supportbot.TicketLog}\`\n\nError Code: \`SB-03\``)
|
||||
.setColor(supportbot.ErrorColour);
|
||||
|
||||
const TicketLogs = new Discord.MessageEmbed()
|
||||
.setTitle("Ticket Log")
|
||||
.setThumbnail(supportbot.Ticket_Thumbnail)
|
||||
.addFields(
|
||||
{ name: 'Ticket', value: `<#${SupportTicket.id}>` },
|
||||
{ name: 'User', value: `<@${message.author.id}>` },
|
||||
)
|
||||
.setColor(supportbot.EmbedColour)
|
||||
.setFooter(supportbot.EmbedFooter)
|
||||
|
||||
let locateChannel = message.guild.channels.cache.find(LocateChannel => LocateChannel.name === supportbot.TicketLog)
|
||||
|
||||
if(!locateChannel) return message.channel.send({ embed: errornochannel });
|
||||
|
||||
locateChannel.send({ embed: TicketLogs });
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
@@ -1,25 +0,0 @@
|
||||
// SupportBot 6.0, Created by Emerald Services
|
||||
// Help Command
|
||||
|
||||
const Discord = require("discord.js");
|
||||
const fs = require("fs");
|
||||
|
||||
const yaml = require('js-yaml');
|
||||
const supportbot = yaml.load(fs.readFileSync('./supportbot-config.yml', 'utf8'));
|
||||
|
||||
module.exports = {
|
||||
name: supportbot.PingCommand,
|
||||
description: supportbot.PingDesc,
|
||||
|
||||
execute(message, args) {
|
||||
if (supportbot.DeleteMessages == "true") message.delete();
|
||||
|
||||
let ping = Date.now() - message.createdTimestamp + " ms";
|
||||
|
||||
const PingCommandEmbed = new Discord.MessageEmbed()
|
||||
.setDescription(`🏓 **Pong!** \`${Date.now() - message.createdTimestamp}ms\``)
|
||||
.setColor(supportbot.EmbedColour)
|
||||
|
||||
message.channel.send({ embed: PingCommandEmbed });
|
||||
}
|
||||
};
|
||||
@@ -1,64 +0,0 @@
|
||||
// SupportBot 6.0, Created by Emerald Services
|
||||
// Add User
|
||||
|
||||
const Discord = require("discord.js");
|
||||
const fs = require("fs");
|
||||
|
||||
const yaml = require('js-yaml');
|
||||
const supportbot = yaml.load(fs.readFileSync('./supportbot-config.yml', 'utf8'));
|
||||
|
||||
module.exports = {
|
||||
name: supportbot.RemoveUser,
|
||||
description: supportbot.RemoveUserDesc,
|
||||
|
||||
execute(message, args) {
|
||||
if (supportbot.DeleteMessages == "true") message.delete();
|
||||
|
||||
console.log(`\u001b[32m`, `[${supportbot.Bot_Name}]:`, `\u001b[32m`, `${message.author.tag} has executed ${supportbot.Prefix}${supportbot.RemoveUser}!`);
|
||||
|
||||
let SupportStaff = message.guild.roles.cache.find(SupportTeam => SupportTeam.name === supportbot.Staff)
|
||||
let Admins = message.guild.roles.cache.find(AdminUser => AdminUser.name === supportbot.Admin)
|
||||
|
||||
const NoPerms = new Discord.MessageEmbed()
|
||||
.setTitle("Invalid Permissions!")
|
||||
.setDescription(`${supportbot.IncorrectPerms}\n\nRole Required: \`${supportbot.Staff}\` or \`${supportbot.Admin}\``)
|
||||
.setColor(supportbot.WarningColour)
|
||||
|
||||
if (message.member.roles.cache.has(SupportStaff.id) || message.member.roles.cache.has(Admins.id)) {
|
||||
if (!message.channel.name.startsWith( `${supportbot.TicketChannel}-` )) {
|
||||
const Exists = new Discord.MessageEmbed()
|
||||
.setTitle("No Ticket Found!")
|
||||
.setDescription(`${supportbot.NoValidTicket}`)
|
||||
.setColor(supportbot.WarningColour);
|
||||
message.channel.send({ embed: Exists });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let rUser = message.guild.member(message.mentions.users.first() || message.guild.members.cache.get(args[0]));
|
||||
const UserNotExist = new Discord.MessageEmbed()
|
||||
.setTitle("User Not Found!")
|
||||
.setDescription(`${supportbot.UserNotFound}\n\nTry Again:\`${supportbot.Prefix}${supportbot.RemoveUser} <user#0000>\``)
|
||||
.setColor(supportbot.ErrorColour)
|
||||
|
||||
if (!rUser) return message.channel.send({ embed: UserNotExist });
|
||||
|
||||
message.channel.updateOverwrite(rUser, {
|
||||
VIEW_CHANNEL: false,
|
||||
CREATE_INVITE: false,
|
||||
SEND_MESSAGES: false,
|
||||
READ_MESSAGES: false
|
||||
});
|
||||
|
||||
const Complete = new Discord.MessageEmbed()
|
||||
.setTitle("User Removed!")
|
||||
.setDescription(supportbot.RemovedUser.replace(/%user%/g, rUser.id))
|
||||
.setTimestamp()
|
||||
.setColor(supportbot.EmbedColour)
|
||||
message.channel.send({ embed: Complete });
|
||||
} else {
|
||||
return message.channel.send({ embed: NoPerms });
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
@@ -1,60 +0,0 @@
|
||||
// SupportBot 6.0, Created by Emerald Services
|
||||
// Suggest Command
|
||||
|
||||
const Discord = require("discord.js");
|
||||
const fs = require("fs");
|
||||
|
||||
const yaml = require('js-yaml');
|
||||
const supportbot = yaml.load(fs.readFileSync('./supportbot-config.yml', 'utf8'));
|
||||
|
||||
module.exports = {
|
||||
name: supportbot.SuggestCommand,
|
||||
description: supportbot.SuggestionDesc,
|
||||
|
||||
execute(message, args) {
|
||||
if (supportbot.DeleteMessages == "true") message.delete();
|
||||
|
||||
let locateChannel = message.guild.channels.cache.find(SuggestionChannel => SuggestionChannel.name === supportbot.SuggestionChannel);
|
||||
|
||||
const errornochannel = new Discord.MessageEmbed()
|
||||
.setTitle("Invalid Channel")
|
||||
.setDescription(`${supportbot.InvalidChannel}\n\nChannel Required: \`${supportbot.SuggestionChannel}\``)
|
||||
.setColor(supportbot.ErrorColour);
|
||||
|
||||
if(!locateChannel) return message.channel.send({ embed: errornochannel });
|
||||
|
||||
const embed = new Discord.MessageEmbed()
|
||||
.setDescription(`> **${supportbot.SuggestionStarter}**`)
|
||||
.setColor(supportbot.EmbedColour)
|
||||
message.channel.send({ embed: embed });
|
||||
|
||||
let suggestion = []
|
||||
message.channel.awaitMessages(response => response.content.length > 2, {
|
||||
max: 1,
|
||||
time: 500000,
|
||||
errors: ['time'],
|
||||
}).then((collected) => {
|
||||
suggestion.push(collected.map(r => r.content));
|
||||
|
||||
const SuggestionMessage = new Discord.MessageEmbed()
|
||||
.setColor(supportbot.EmbedColour)
|
||||
.setFooter(supportbot.EmbedFooter)
|
||||
|
||||
.setTitle(supportbot.SuggestionTitle)
|
||||
.setDescription(`\`\`\`${suggestion}\`\`\``)
|
||||
|
||||
.addFields( { name: "From", value: `<@${message.author.id}>`, inline: false }, )
|
||||
|
||||
locateChannel.send({ embed: SuggestionMessage }).then(async function(msg) {
|
||||
msg.react(supportbot.SuggestReact_1).then(() => msg.react(supportbot.SuggestReact_2));
|
||||
});
|
||||
|
||||
const SuggestionComplete = new Discord.MessageEmbed()
|
||||
.setColor(supportbot.SuccessColour)
|
||||
.setDescription(`:white_check_mark: Your suggestion has been successfully created. <#${locateChannel.id}>`)
|
||||
message.channel.send({ embed: SuggestionComplete })
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
};
|
||||
@@ -1,49 +0,0 @@
|
||||
// SupportBot 6.0, Created by Emerald Services
|
||||
// Ready Event
|
||||
|
||||
const Discord = require("discord.js");
|
||||
const fs = require("fs");
|
||||
|
||||
const yaml = require('js-yaml');
|
||||
const supportbot = yaml.load(fs.readFileSync('./supportbot-config.yml', 'utf8'));
|
||||
|
||||
module.exports = async (bot, member) => {
|
||||
|
||||
if (supportbot.SystemMessages === "true") {
|
||||
const SystemChannel = member.guild.channels.cache.find(channel => channel.name === supportbot.SystemMessage_Channel)
|
||||
|
||||
if (!SystemChannel) return;
|
||||
|
||||
console.log(`[${supportbot.Bot_Name}]: ${member.user.username} has joined ${member.guild.name}!`)
|
||||
console.log(supportbot.AutoRole_Role)
|
||||
|
||||
if (supportbot.SystemMessage_Type === "embed") {
|
||||
const GuildAddMember = new Discord.MessageEmbed()
|
||||
.setTitle(supportbot.Welcome_Title)
|
||||
.setDescription(supportbot.WelcomeMessage.replace(/%member%/g, member.user.username).replace(/%guildname%/g, member.guild.name))
|
||||
.setColor(supportbot.EmbedColour)
|
||||
|
||||
if (supportbot.SystemMessage_Icon === "BOT") {
|
||||
GuildAddMember.setThumbnail(bot.user.displayAvatarURL())
|
||||
}
|
||||
|
||||
if (supportbot.SystemMessage_Icon === "USER") {
|
||||
GuildAddMember.setThumbnail(member.user.displayAvatarURL())
|
||||
}
|
||||
|
||||
|
||||
if (supportbot.SystemMessage_EmbedFooter === "true") {
|
||||
GuildAddMember.setFooter(supportbot.EmbedFooter)
|
||||
}
|
||||
|
||||
SystemChannel.send({ embed: GuildAddMember });
|
||||
|
||||
}
|
||||
|
||||
if (supportbot.SystemMessage_Type === "normal") {
|
||||
SystemChannel.send(supportbot.WelcomeMessage.replace(/%member%/g, member.user.username).replace(/%guildname%/g, member.guild.name))
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
@@ -1,48 +0,0 @@
|
||||
// SupportBot 6.0, Created by Emerald Services
|
||||
// Ready Event
|
||||
|
||||
const Discord = require("discord.js");
|
||||
const fs = require("fs");
|
||||
|
||||
const yaml = require('js-yaml');
|
||||
const supportbot = yaml.load(fs.readFileSync('./supportbot-config.yml', 'utf8'));
|
||||
|
||||
module.exports = async (bot, member) => {
|
||||
|
||||
if (supportbot.SystemMessages === "true") {
|
||||
const SystemChannel = member.guild.channels.cache.find(channel => channel.name === supportbot.SystemMessage_Channel)
|
||||
|
||||
if (!SystemChannel) return;
|
||||
|
||||
console.log(`[${supportbot.Bot_Name}]: ${member.user.username} has left ${member.guild.name}!`)
|
||||
|
||||
if (supportbot.SystemMessage_Type === "embed") {
|
||||
const GuildRemoveMember = new Discord.MessageEmbed()
|
||||
.setTitle(supportbot.Leave_Title)
|
||||
.setDescription(supportbot.LeaveMessage.replace(/%member%/g, member.user.username).replace(/%guildname%/g, member.guild.name))
|
||||
.setColor(supportbot.EmbedColour)
|
||||
|
||||
if (supportbot.SystemMessage_Icon === "BOT") {
|
||||
GuildRemoveMember.setThumbnail(bot.user.displayAvatarURL())
|
||||
}
|
||||
|
||||
if (supportbot.SystemMessage_Icon === "USER") {
|
||||
GuildRemoveMember.setThumbnail(member.user.displayAvatarURL())
|
||||
}
|
||||
|
||||
|
||||
if (supportbot.SystemMessage_EmbedFooter === "true") {
|
||||
GuildRemoveMember.setFooter(supportbot.EmbedFooter)
|
||||
}
|
||||
|
||||
SystemChannel.send({ embed: GuildRemoveMember });
|
||||
|
||||
}
|
||||
|
||||
if (supportbot.SystemMessage_Type === "normal") {
|
||||
SystemChannel.send(supportbot.LeaveMessage.replace(/%member%/g, member.user.username).replace(/%guildname%/g, member.guild.name))
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
@@ -1,22 +0,0 @@
|
||||
// SupportBot 6.0, Created by Emerald Services
|
||||
// Message Event
|
||||
|
||||
const Discord = require("discord.js");
|
||||
const fs = require("fs");
|
||||
|
||||
const yaml = require('js-yaml');
|
||||
const supportbot = yaml.load(fs.readFileSync('./supportbot-config.yml', 'utf8'));
|
||||
|
||||
module.exports = async (bot, message) => {
|
||||
if(message.author.bot) return;
|
||||
if(message.channel.type === "dm") return;
|
||||
if (message.content.indexOf(supportbot.Prefix) !== 0) return;
|
||||
|
||||
let messageArray = message.content.split(" ");
|
||||
const args = message.content.slice(supportbot.Prefix.length).trim().split(/ +/g);
|
||||
const command = args.shift().toLowerCase();
|
||||
|
||||
const cmd = bot.commands.get(command);
|
||||
if(!cmd) return;
|
||||
cmd.execute(message, args);
|
||||
};
|
||||
@@ -1,19 +0,0 @@
|
||||
// SupportBot 6.0, Created by Emerald Services
|
||||
// Ready Event
|
||||
|
||||
const Discord = require("discord.js");
|
||||
const fs = require("fs");
|
||||
|
||||
const yaml = require('js-yaml');
|
||||
const supportbot = yaml.load(fs.readFileSync('./supportbot-config.yml', 'utf8'));
|
||||
|
||||
|
||||
module.exports = async (bot) => {
|
||||
bot.user.setActivity(supportbot.BotActivity, {
|
||||
type: supportbot.ActvityType
|
||||
});
|
||||
|
||||
console.log(`\u001b[32m`, `―――――――――――――――――― SupportBot ――――――――――――――――――`)
|
||||
console.log(`\u001b[37m`, `${supportbot.Bot_Name} has successfully connected to discord`)
|
||||
console.log(`\u001b[32m`, `―――――――――――――――――― SupportBot ――――――――――――――――――`)
|
||||
};
|
||||
@@ -1,90 +1,98 @@
|
||||
// _________ __ __________ __
|
||||
// / _____/__ ________ ______ ____________/ |\______ \ _____/ |_
|
||||
// \_____ \| | \____ \\____ \ / _ \_ __ \ __\ | _// _ \ __\
|
||||
// / \ | / |_> > |_> > <_> ) | \/| | | | ( <_> ) |
|
||||
// /_______ /____/| __/| __/ \____/|__| |__| |______ /\____/|__|
|
||||
// \/ |__| |__| \/
|
||||
// ___ _ ___ _
|
||||
// / __> _ _ ___ ___ ___ _ _ _| |_ | . > ___ _| |_
|
||||
// \__ \| | || . \| . \/ . \| '_> | | | . \/ . \ | |
|
||||
// <___/`___|| _/| _/\___/|_| |_| |___/\___/ |_|
|
||||
// |_| |_|
|
||||
//
|
||||
// SupportBot version 6.0.2
|
||||
// Created by Emerald Services
|
||||
// Installed with MIT License
|
||||
// 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();
|
||||
const configStore = require("./Structures/ConfigStore.js");
|
||||
|
||||
const yaml = require('js-yaml');
|
||||
const supportbot = yaml.load(fs.readFileSync('./supportbot-config.yml', 'utf8'));
|
||||
|
||||
const { promisify } = require("util");
|
||||
const readdir = promisify(require("fs").readdir);
|
||||
|
||||
String.prototype.toProperCase = function () {
|
||||
return this.replace(
|
||||
/([^\W_]+[^\s-]*) */g,
|
||||
(txt) => txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase()
|
||||
);
|
||||
};
|
||||
|
||||
const init = async () => {
|
||||
const evtFiles = await readdir("./events/");
|
||||
|
||||
evtFiles.forEach((file, i) => {
|
||||
const eventName = file.split(".")[0];
|
||||
const event = require(`./events/${file}`);
|
||||
|
||||
console.info(`\u001b[37m`, `[${supportbot.Bot_Name}]:`, `\u001b[31m`, `#${i + 1} Event Loaded: ${eventName.toProperCase()} `);
|
||||
|
||||
bot.on(eventName, event.bind(null, bot));
|
||||
delete require.cache[require.resolve(`./events/${file}`)];
|
||||
});
|
||||
|
||||
console.info(`\u001b[37m`, `[${supportbot.Bot_Name}]:`, `\u001b[32m`, `Successfully loaded ${evtFiles.length} events.`);
|
||||
console.info(`\u001b[37m`, "-----------------------------------------------------------------------------");
|
||||
};
|
||||
|
||||
init();
|
||||
|
||||
fs.readdir("./commands/", (err, files) => {
|
||||
if (err) console.info(err, "error");
|
||||
|
||||
let jsfiles = files.filter((f) => f.split(".").pop() === "js");
|
||||
|
||||
if (jsfiles.length <= 0) {
|
||||
console.log(`[${supportbot.Bot_Name}]: No commands found. Try to Re-download the resource`);
|
||||
return;
|
||||
}
|
||||
|
||||
jsfiles.forEach((f, i) => {
|
||||
let props = require(`./commands/${f}`);
|
||||
console.info(`\u001b[37m`, `[${supportbot.Bot_Name}]:`, `\u001b[31m`, `#${i + 1} Command Loaded: ${props.name.toProperCase()}`);
|
||||
bot.commands.set(props.name, props);
|
||||
});
|
||||
|
||||
console.info(`\u001b[37m`, `[${supportbot.Bot_Name}]:`, `\u001b[32m`, `Loaded ${jsfiles.length} commands!`);
|
||||
console.info(`\u001b[37m`, "-----------------------------------------------------------------------------");
|
||||
const Client = require("./Structures/Client.js");
|
||||
const client = new Client({
|
||||
intents: ["Guilds", "GuildMembers", "GuildMessages", "MessageContent"],
|
||||
});
|
||||
|
||||
fs.readdir("./addons/", (err, files) => {
|
||||
if (err) console.info(err, "error");
|
||||
|
||||
let jsfiles = files.filter((f) => f.split(".").pop() === "js");
|
||||
const APIServer = require("./API/server.js");
|
||||
|
||||
if (jsfiles.length <= 0) {
|
||||
console.log(`[${supportbot.Bot_Name}]: No addons found. You can download addons from https://emeraldservices.xyz`);
|
||||
return;
|
||||
}
|
||||
|
||||
jsfiles.forEach((f, i) => {
|
||||
let props = require(`./addons/${f}`);
|
||||
console.info(`\u001b[37m`, `[${supportbot.Bot_Name}]:`, `\u001b[31m`, `#${i + 1} Command Loaded: ${props.name.toProperCase()}`);
|
||||
bot.commands.set(props.name, props);
|
||||
});
|
||||
// Start dashboard API immediately — do not wait for Discord clientReady
|
||||
const api = new APIServer(client);
|
||||
client.apiServer = api;
|
||||
|
||||
console.info(`\u001b[37m`, `[${supportbot.Bot_Name}]:`, `\u001b[32m`, `Loaded ${jsfiles.length} commands!`);
|
||||
console.info(`\u001b[37m`, "-----------------------------------------------------------------------------");
|
||||
if (api.config?.Enabled) {
|
||||
const port = api.config.Port || 3000;
|
||||
api.start(port);
|
||||
console.log(
|
||||
`[Dashboard] API listening on http://localhost:${port} (bot may still be connecting)`,
|
||||
);
|
||||
} else {
|
||||
console.warn("[Dashboard] API disabled in Configs/api.yml");
|
||||
}
|
||||
|
||||
client.start(configStore.supportbot.General.Token);
|
||||
|
||||
client.on("clientReady", () => {
|
||||
console.log("[Dashboard] Discord bot is ready — full stats and guild tools are available");
|
||||
});
|
||||
|
||||
bot.login(supportbot.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)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const origLog = console.log;
|
||||
console.log = (...args) => {
|
||||
origLog(...args);
|
||||
logToFile("Output", args.join(" "));
|
||||
};
|
||||
|
||||
const origWarn = console.warn;
|
||||
console.warn = (...args) => {
|
||||
origWarn(...args);
|
||||
logToFile("Warn", args.join(" "));
|
||||
};
|
||||
|
||||
const origError = console.error;
|
||||
console.error = (...args) => {
|
||||
origError(...args);
|
||||
logToFile("Error", args.join(" "));
|
||||
};
|
||||
|
||||
process.on("unhandledRejection", (reason) => {
|
||||
console.error("Unhandled Rejection:", reason);
|
||||
});
|
||||
|
||||
process.on("uncaughtException", (err) => {
|
||||
console.error("Uncaught Exception:", err);
|
||||
});
|
||||
Generated
-134
@@ -1,134 +0,0 @@
|
||||
{
|
||||
"name": "supportbot",
|
||||
"version": "6.0.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
"@discordjs/collection": {
|
||||
"version": "0.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-0.1.5.tgz",
|
||||
"integrity": "sha512-CU1q0UXQUpFNzNB7gufgoisDHP7n+T3tkqTsp3MNUkVJ5+hS3BCvME8uCXAUFlz+6T2FbTCu75A+yQ7HMKqRKw=="
|
||||
},
|
||||
"@discordjs/form-data": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@discordjs/form-data/-/form-data-3.0.1.tgz",
|
||||
"integrity": "sha512-ZfFsbgEXW71Rw/6EtBdrP5VxBJy4dthyC0tpQKGKmYFImlmmrykO14Za+BiIVduwjte0jXEBlhSKf0MWbFp9Eg==",
|
||||
"requires": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"mime-types": "^2.1.12"
|
||||
}
|
||||
},
|
||||
"abort-controller": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
|
||||
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
|
||||
"requires": {
|
||||
"event-target-shim": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"argparse": {
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
|
||||
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
|
||||
"requires": {
|
||||
"sprintf-js": "~1.0.2"
|
||||
}
|
||||
},
|
||||
"asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
|
||||
},
|
||||
"combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"requires": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
}
|
||||
},
|
||||
"delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk="
|
||||
},
|
||||
"discord.js": {
|
||||
"version": "12.2.0",
|
||||
"resolved": "https://registry.npmjs.org/discord.js/-/discord.js-12.2.0.tgz",
|
||||
"integrity": "sha512-Ueb/0SOsxXyqwvwFYFe0msMrGqH1OMqpp2Dpbplnlr4MzcRrFWwsBM9gKNZXPVBHWUKiQkwU8AihXBXIvTTSvg==",
|
||||
"requires": {
|
||||
"@discordjs/collection": "^0.1.5",
|
||||
"@discordjs/form-data": "^3.0.1",
|
||||
"abort-controller": "^3.0.0",
|
||||
"node-fetch": "^2.6.0",
|
||||
"prism-media": "^1.2.0",
|
||||
"setimmediate": "^1.0.5",
|
||||
"tweetnacl": "^1.0.3",
|
||||
"ws": "^7.2.1"
|
||||
}
|
||||
},
|
||||
"esprima": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
|
||||
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="
|
||||
},
|
||||
"event-target-shim": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
|
||||
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="
|
||||
},
|
||||
"js-yaml": {
|
||||
"version": "3.14.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz",
|
||||
"integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==",
|
||||
"requires": {
|
||||
"argparse": "^1.0.7",
|
||||
"esprima": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"mime-db": {
|
||||
"version": "1.44.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz",
|
||||
"integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg=="
|
||||
},
|
||||
"mime-types": {
|
||||
"version": "2.1.27",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz",
|
||||
"integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==",
|
||||
"requires": {
|
||||
"mime-db": "1.44.0"
|
||||
}
|
||||
},
|
||||
"node-fetch": {
|
||||
"version": "2.6.1",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz",
|
||||
"integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw=="
|
||||
},
|
||||
"prism-media": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/prism-media/-/prism-media-1.2.2.tgz",
|
||||
"integrity": "sha512-I+nkWY212lJ500jLe4tN9tWO7nRiBAVdMv76P9kffZjYhw20raMlW1HSSvS+MLXC9MmbNZCazMrAr+5jEEgTuw=="
|
||||
},
|
||||
"setimmediate": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
|
||||
"integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU="
|
||||
},
|
||||
"sprintf-js": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
||||
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
|
||||
},
|
||||
"tweetnacl": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz",
|
||||
"integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw=="
|
||||
},
|
||||
"ws": {
|
||||
"version": "7.3.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.3.1.tgz",
|
||||
"integrity": "sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA=="
|
||||
}
|
||||
}
|
||||
}
|
||||
+29
-14
@@ -1,26 +1,41 @@
|
||||
{
|
||||
"name": "supportbot",
|
||||
"version": "6.0.0",
|
||||
"description": "SupportBot | #1 Self-Hosted Ticket Bot",
|
||||
"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": "^12.2.0",
|
||||
"js-yaml": "^3.14.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"node-fetch": ">=2.6.1"
|
||||
},
|
||||
"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/1tzemerald/SupportBot.git"
|
||||
"url": "git+https://github.com/Emerald-Services/SupportBot.git"
|
||||
},
|
||||
"author": "Emerald Services",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/1tzemerald/SupportBot/issues"
|
||||
"url": "https://github.com/Emerald-Services/SupportBot/issues"
|
||||
},
|
||||
"homepage": "https://emeraldservices.xyz"
|
||||
}
|
||||
"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",
|
||||
"openai": "^4.50.0",
|
||||
"systeminformation": "^5.31.5",
|
||||
"yaml": "^2.8.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
var h3=Object.defineProperty;var g3=(e,n,a)=>n in e?h3(e,n,{enumerable:!0,configurable:!0,writable:!0,value:a}):e[n]=a;var $b=(e,n,a)=>g3(e,typeof n!="symbol"?n+"":n,a);function x3(e,n){for(var a=0;a<n.length;a++){const o=n[a];if(typeof o!="string"&&!Array.isArray(o)){for(const l in o)if(l!=="default"&&!(l in e)){const c=Object.getOwnPropertyDescriptor(o,l);c&&Object.defineProperty(e,l,c.get?c:{enumerable:!0,get:()=>o[l]})}}}return Object.freeze(Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}))}(function(){const n=document.createElement("link").relList;if(n&&n.supports&&n.supports("modulepreload"))return;for(const l of document.querySelectorAll('link[rel="modulepreload"]'))o(l);new MutationObserver(l=>{for(const c of l)if(c.type==="childList")for(const d of c.addedNodes)d.tagName==="LINK"&&d.rel==="modulepreload"&&o(d)}).observe(document,{childList:!0,subtree:!0});function a(l){const c={};return l.integrity&&(c.integrity=l.integrity),l.referrerPolicy&&(c.referrerPolicy=l.referrerPolicy),l.crossOrigin==="use-credentials"?c.credentials="include":l.crossOrigin==="anonymous"?c.credentials="omit":c.credentials="same-origin",c}function o(l){if(l.ep)return;l.ep=!0;const c=a(l);fetch(l.href,c)}})();function A0(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var Jm={exports:{}},Fi={};/**
|
||||
* @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 Bb;function v3(){if(Bb)return Fi;Bb=1;var e=Symbol.for("react.transitional.element"),n=Symbol.for("react.fragment");function a(o,l,c){var d=null;if(c!==void 0&&(d=""+c),l.key!==void 0&&(d=""+l.key),"key"in l){c={};for(var m in l)m!=="key"&&(c[m]=l[m])}else c=l;return l=c.ref,{$$typeof:e,type:o,key:d,ref:l!==void 0?l:null,props:c}}return Fi.Fragment=n,Fi.jsx=a,Fi.jsxs=a,Fi}var Ub;function b3(){return Ub||(Ub=1,Jm.exports=v3()),Jm.exports}var s=b3(),ep={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 Hb;function y3(){if(Hb)return Pe;Hb=1;var e=Symbol.for("react.transitional.element"),n=Symbol.for("react.portal"),a=Symbol.for("react.fragment"),o=Symbol.for("react.strict_mode"),l=Symbol.for("react.profiler"),c=Symbol.for("react.consumer"),d=Symbol.for("react.context"),m=Symbol.for("react.forward_ref"),p=Symbol.for("react.suspense"),h=Symbol.for("react.memo"),b=Symbol.for("react.lazy"),g=Symbol.for("react.activity"),y=Symbol.iterator;function w(_){return _===null||typeof _!="object"?null:(_=y&&_[y]||_["@@iterator"],typeof _=="function"?_:null)}var j={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},S=Object.assign,E={};function N(_,D,G){this.props=_,this.context=D,this.refs=E,this.updater=G||j}N.prototype.isReactComponent={},N.prototype.setState=function(_,D){if(typeof _!="object"&&typeof _!="function"&&_!=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,_,D,"setState")},N.prototype.forceUpdate=function(_){this.updater.enqueueForceUpdate(this,_,"forceUpdate")};function k(){}k.prototype=N.prototype;function T(_,D,G){this.props=_,this.context=D,this.refs=E,this.updater=G||j}var A=T.prototype=new k;A.constructor=T,S(A,N.prototype),A.isPureReactComponent=!0;var O=Array.isArray;function M(){}var z={H:null,A:null,T:null,S:null},B=Object.prototype.hasOwnProperty;function K(_,D,G){var V=G.ref;return{$$typeof:e,type:_,key:D,ref:V!==void 0?V:null,props:G}}function U(_,D){return K(_.type,D,_.props)}function q(_){return typeof _=="object"&&_!==null&&_.$$typeof===e}function ue(_){var D={"=":"=0",":":"=2"};return"$"+_.replace(/[=:]/g,function(G){return D[G]})}var pe=/\/+/g;function xe(_,D){return typeof _=="object"&&_!==null&&_.key!=null?ue(""+_.key):D.toString(36)}function se(_){switch(_.status){case"fulfilled":return _.value;case"rejected":throw _.reason;default:switch(typeof _.status=="string"?_.then(M,M):(_.status="pending",_.then(function(D){_.status==="pending"&&(_.status="fulfilled",_.value=D)},function(D){_.status==="pending"&&(_.status="rejected",_.reason=D)})),_.status){case"fulfilled":return _.value;case"rejected":throw _.reason}}throw _}function L(_,D,G,V,F){var oe=typeof _;(oe==="undefined"||oe==="boolean")&&(_=null);var ae=!1;if(_===null)ae=!0;else switch(oe){case"bigint":case"string":case"number":ae=!0;break;case"object":switch(_.$$typeof){case e:case n:ae=!0;break;case b:return ae=_._init,L(ae(_._payload),D,G,V,F)}}if(ae)return F=F(_),ae=V===""?"."+xe(_,0):V,O(F)?(G="",ae!=null&&(G=ae.replace(pe,"$&/")+"/"),L(F,D,G,"",function(Y){return Y})):F!=null&&(q(F)&&(F=U(F,G+(F.key==null||_&&_.key===F.key?"":(""+F.key).replace(pe,"$&/")+"/")+ae)),D.push(F)),1;ae=0;var be=V===""?".":V+":";if(O(_))for(var ve=0;ve<_.length;ve++)V=_[ve],oe=be+xe(V,ve),ae+=L(V,D,G,oe,F);else if(ve=w(_),typeof ve=="function")for(_=ve.call(_),ve=0;!(V=_.next()).done;)V=V.value,oe=be+xe(V,ve++),ae+=L(V,D,G,oe,F);else if(oe==="object"){if(typeof _.then=="function")return L(se(_),D,G,V,F);throw D=String(_),Error("Objects are not valid as a React child (found: "+(D==="[object Object]"?"object with keys {"+Object.keys(_).join(", ")+"}":D)+"). If you meant to render a collection of children, use an array instead.")}return ae}function I(_,D,G){if(_==null)return _;var V=[],F=0;return L(_,V,"","",function(oe){return D.call(G,oe,F++)}),V}function H(_){if(_._status===-1){var D=_._result;D=D(),D.then(function(G){(_._status===0||_._status===-1)&&(_._status=1,_._result=G)},function(G){(_._status===0||_._status===-1)&&(_._status=2,_._result=G)}),_._status===-1&&(_._status=0,_._result=D)}if(_._status===1)return _._result.default;throw _._result}var Z=typeof reportError=="function"?reportError:function(_){if(typeof window=="object"&&typeof window.ErrorEvent=="function"){var D=new window.ErrorEvent("error",{bubbles:!0,cancelable:!0,message:typeof _=="object"&&_!==null&&typeof _.message=="string"?String(_.message):String(_),error:_});if(!window.dispatchEvent(D))return}else if(typeof process=="object"&&typeof process.emit=="function"){process.emit("uncaughtException",_);return}console.error(_)},te={map:I,forEach:function(_,D,G){I(_,function(){D.apply(this,arguments)},G)},count:function(_){var D=0;return I(_,function(){D++}),D},toArray:function(_){return I(_,function(D){return D})||[]},only:function(_){if(!q(_))throw Error("React.Children.only expected to receive a single React element child.");return _}};return Pe.Activity=g,Pe.Children=te,Pe.Component=N,Pe.Fragment=a,Pe.Profiler=l,Pe.PureComponent=T,Pe.StrictMode=o,Pe.Suspense=p,Pe.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE=z,Pe.__COMPILER_RUNTIME={__proto__:null,c:function(_){return z.H.useMemoCache(_)}},Pe.cache=function(_){return function(){return _.apply(null,arguments)}},Pe.cacheSignal=function(){return null},Pe.cloneElement=function(_,D,G){if(_==null)throw Error("The argument must be a ReaLine 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 Gb;function w3(){return Gb||(Gb=1,(function(e){function n(L,I){var H=L.length;L.push(I);e:for(;0<H;){var Z=H-1>>>1,te=L[Z];if(0<l(te,I))L[Z]=I,L[H]=te,H=Z;else break e}}function a(L){return L.length===0?null:L[0]}function o(L){if(L.length===0)return null;var I=L[0],H=L.pop();if(H!==I){L[0]=H;e:for(var Z=0,te=L.length,_=te>>>1;Z<_;){var D=2*(Z+1)-1,G=L[D],V=D+1,F=L[V];if(0>l(G,H))V<te&&0>l(F,G)?(L[Z]=F,L[V]=H,Z=V):(L[Z]=G,L[D]=H,Z=D);else if(V<te&&0>l(F,H))L[Z]=F,L[V]=H,Z=V;else break e}}return I}function l(L,I){var H=L.sortIndex-I.sortIndex;return H!==0?H:L.id-I.id}if(e.unstable_now=void 0,typeof performance=="object"&&typeof performance.now=="function"){var c=performance;e.unstable_now=function(){return c.now()}}else{var d=Date,m=d.now();e.unstable_now=function(){return d.now()-m}}var p=[],h=[],b=1,g=null,y=3,w=!1,j=!1,S=!1,E=!1,N=typeof setTimeout=="function"?setTimeout:null,k=typeof clearTimeout=="function"?clearTimeout:null,T=typeof setImmediate<"u"?setImmediate:null;function A(L){for(var I=a(h);I!==null;){if(I.callback===null)o(h);else if(I.startTime<=L)o(h),I.sortIndex=I.expirationTime,n(p,I);else break;I=a(h)}}function O(L){if(S=!1,A(L),!j)if(a(p)!==null)j=!0,M||(M=!0,ue());else{var I=a(h);I!==null&&se(O,I.startTime-L)}}var M=!1,z=-1,B=5,K=-1;function U(){return E?!0:!(e.unstable_now()-K<B)}function q(){if(E=!1,M){var L=e.unstable_now();K=L;var I=!0;try{e:{j=!1,S&&(S=!1,k(z),z=-1),w=!0;var H=y;try{t:{for(A(L),g=a(p);g!==null&&!(g.expirationTime>L&&U());){var Z=g.callback;if(typeof Z=="function"){g.callback=null,y=g.priorityLevel;var te=Z(g.expirationTime<=L);if(L=e.unstable_now(),typeof te=="function"){g.callback=te,A(L),I=!0;break t}g===a(p)&&o(p),A(L)}else o(p);g=a(p)}if(g!==null)I=!0;else{var _=a(h);_!==null&&se(O,_.startTime-L),I=!1}}break e}finally{g=null,y=H,w=!1}I=void 0}}finally{I?ue():M=!1}}}var ue;if(typeof T=="function")ue=function(){T(q)};else if(typeof MessageChannel<"u"){var pe=new MessageChannel,xe=pe.port2;pe.port1.onmessage=q,ue=function(){xe.postMessage(null)}}else ue=function(){N(q,0)};function se(L,I){z=N(function(){L(e.unstable_now())},I)}e.unstable_IdlePriority=5,e.unstable_ImmediatePriority=1,e.unstable_LowPriority=4,e.unstable_NormalPriority=3,e.unstable_Profiling=null,e.unstable_UserBlockingPriority=2,e.unstable_cancelCallback=function(L){L.callback=null},e.unstable_forceFrameRate=function(L){0>L||125<L?console.error("forceFrameRate takes a positive int between 0 and 125, forcing frame rates higher than 125 fps is not supported"):B=0<L?Math.floor(1e3/L):5},e.unstable_getCurrentPriorityLevel=function(){return y},e.unstable_next=function(L){switch(y){case 1:case 2:case 3:var I=3;break;default:I=y}var H=y;y=I;try{return L()}finally{y=H}},e.unstable_requestPaint=function(){E=!0},e.unstable_runWithPriority=function(L,I){switch(L){case 1:case 2:case 3:case 4:case 5:break;default:L=3}var H=y;y=L;try{return I()}finally{y=H}},e.unstable_scheduleCallback=function(L,I,H){var Z=e.unstable_now();switch(typeof H=="object"&&H!==null?(H=H.delay,H=typeof H=="number"&&0<H?Z+H:Z):H=Z,L){case 1:var te=-1;break;case 2:te=250;break;case 5:te=1073741823;break;case 4:te=1e4;break;default:te=5e3}return te=H+te,L={id:b++,callback:I,priorityLevel:L,startTime:H,expirationTime:te,sortIndex:-1},H>Z?(L.sortIndex=H,n(h,L),a(p)===null&&L===a(h)&&(S?(k(z),z=-1):S=!0,se(O,H-Z))):(L.sortIndex=te,n(p,L),j||w||(j=!0,M||(M=!0,ue()))),L},e.unstable_shouldYield=U,e.unstable_wrapCallback=function(L){var I=y;return function(){var H=y;y=I;try{return L.apply(this,arguments)}finally{y=H}}}})(rp)),rp}var Fb;function C3(){return Fb||(Fb=1,np.exports=w3()),np.exports}var ap={exports:{}},ln={};/**
|
||||
* @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 Wb;function S3(){if(Wb)return ln;Wb=1;var e=Ku();function n(p){var h="https://react.dev/errors/"+p;if(1<arguments.length){h+="?args[]="+encodeURIComponent(arguments[1]);for(var b=2;b<arguments.length;b++)h+="&args[]="+encodeURIComponent(arguments[b])}return"Minified React error #"+p+"; visit "+h+" 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},l=Symbol.for("react.portal");function c(p,h,b){var g=3<arguments.length&&arguments[3]!==void 0?arguments[3]:null;return{$$typeof:l,key:g==null?null:""+g,children:p,containerInfo:h,implementation:b}}var d=e.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;function m(p,h){if(p==="font")return"";if(typeof h=="string")return h==="use-credentials"?h:""}return ln.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE=o,ln.createPortal=function(p,h){var b=2<arguments.length&&arguments[2]!==void 0?arguments[2]:null;if(!h||h.nodeType!==1&&h.nodeType!==9&&h.nodeType!==11)throw Error(n(299));return c(p,h,null,b)},ln.flushSync=function(p){var h=d.T,b=o.p;try{if(d.T=null,o.p=2,p)return p()}finally{d.T=h,o.p=b,o.d.f()}},ln.preconnect=function(p,h){typeof p=="string"&&(h?(h=h.crossOrigin,h=typeof h=="string"?h==="use-credentials"?h:"":void 0):h=null,o.d.C(p,h))},ln.prefetchDNS=function(p){typeof p=="string"&&o.d.D(p)},ln.preinit=function(p,h){if(typeof p=="string"&&h&&typeof h.as=="string"){var b=h.as,g=m(b,h.crossOrigin),y=typeof h.integrity=="string"?h.integrity:void 0,w=typeof h.fetchPriority=="string"?h.fetchPriority:void 0;b==="style"?o.d.S(p,typeof h.precedence=="string"?h.precedence:void 0,{crossOrigin:g,integrity:y,fetchPriority:w}):b==="script"&&o.d.X(p,{crossOrigin:g,integrity:y,fetchPriority:w,nonce:typeof h.nonce=="string"?h.nonce:void 0})}},ln.preinitModule=function(p,h){if(typeof p=="string")if(typeof h=="object"&&h!==null){if(h.as==null||h.as==="script"){var b=m(h.as,h.crossOrigin);o.d.M(p,{crossOrigin:b,integrity:typeof h.integrity=="string"?h.integrity:void 0,nonce:typeof h.nonce=="string"?h.nonce:void 0})}}else h==null&&o.d.M(p)},ln.preload=function(p,h){if(typeof p=="string"&&typeof h=="object"&&h!==null&&typeof h.as=="string"){var b=h.as,g=m(b,h.crossOrigin);o.d.L(p,b,{crossOrigin:g,integrity:typeof h.integrity=="string"?h.integrity:void 0,nonce:typeof h.nonce=="string"?h.nonce:void 0,type:typeof h.type=="string"?h.type:void 0,fetchPriority:typeof h.fetchPriority=="string"?h.fetchPriority:void 0,referrerPolicy:typeof h.referrerPolicy=="string"?h.referrerPolicy:void 0,imageSrcSet:typeof h.imageSrcSet=="string"?h.imageSrcSet:void 0,imageSizes:typeof h.imageSizes=="string"?h.imageSizes:void 0,media:typeof h.media=="string"?h.media:void 0})}},ln.preloadModule=function(p,h){if(typeof p=="string")if(h){var b=m(h.as,h.crossOrigin);o.d.m(p,{as:typeof h.as=="string"&&h.as!=="script"?h.as:void 0,crossOrigin:b,integrity:typeof h.integrity=="string"?h.integrity:void 0})}else o.d.m(p)},ln.requestFormReset=function(p){o.d.r(p)},ln.unstable_batchedUpdates=function(p,h){return p(h)},ln.useFormState=function(p,h,b){return d.H.useFormState(p,h,b)},ln.useFormStatus=function(){return d.H.useHostTransitionStatus()},ln.version="19.2.6",ln}var Yb;function M0(){if(Yb)return ap.exports;Yb=1;function e(){if(!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__>"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(e)}catch(n){console.error(n)}}return e(),ap.exports=S3(),ap.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 qb;function j3(){if(qb)return Wi;qb=1;var e=C3(),n=Ku(),a=M0();function o(t){var r="https://react.dev/errors/"+t;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 #"+t+"; visit "+r+" for the full message or use the non-minified dev environment for full errors and additional helpful warnings."}function l(t){return!(!t||t.nodeType!==1&&t.nodeType!==9&&t.nodeType!==11)}function c(t){var r=t,i=t;if(t.alternate)for(;r.return;)r=r.return;else{t=r;do r=t,(r.flags&4098)!==0&&(i=r.return),t=r.return;while(t)}return r.tag===3?i:null}function d(t){if(t.tag===13){var r=t.memoizedState;if(r===null&&(t=t.alternate,t!==null&&(r=t.memoizedState)),r!==null)return r.dehydrated}return null}function m(t){if(t.tag===31){var r=t.memoizedState;if(r===null&&(t=t.alternate,t!==null&&(r=t.memoizedState)),r!==null)return r.dehydrated}return null}function p(t){if(c(t)!==t)throw Error(o(188))}function h(t){var r=t.alternate;if(!r){if(r=c(t),r===null)throw Error(o(188));return r!==t?null:t}for(var i=t,u=r;;){var x=i.return;if(x===null)break;var v=x.alternate;if(v===null){if(u=x.return,u!==null){i=u;continue}break}if(x.child===v.child){for(v=x.child;v;){if(v===i)return p(x),t;if(v===u)return p(x),r;v=v.sibling}throw Error(o(188))}if(i.return!==u.return)i=x,u=v;else{for(var C=!1,R=x.child;R;){if(R===i){C=!0,i=x,u=v;break}if(R===u){C=!0,u=x,i=v;break}R=R.sibling}if(!C){for(R=v.child;R;){if(R===i){C=!0,i=v,u=x;break}if(R===u){C=!0,u=v,i=x;break}R=R.sibling}if(!C)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?t:r}function b(t){var r=t.tag;if(r===5||r===26||r===27||r===6)return t;for(t=t.child;t!==null;){if(r=b(t),r!==null)return r;t=t.sibling}return null}var g=Object.assign,y=Symbol.for("react.element"),w=Symbol.for("react.transitional.element"),j=Symbol.for("react.portal"),S=Symbol.for("react.fragment"),E=Symbol.for("react.strict_mode"),N=Symbol.for("react.profiler"),k=Symbol.for("react.consumer"),T=Symbol.for("react.context"),A=Symbol.for("react.forward_ref"),O=Symbol.for("react.suspense"),M=Symbol.for("react.suspense_list"),z=Symbol.for("react.memo"),B=Symbol.for("react.lazy"),K=Symbol.for("react.activity"),U=Symbol.for("react.memo_cache_sentinel"),q=Symbol.iterator;function ue(t){return t===null||typeof t!="object"?null:(t=q&&t[q]||t["@@iterator"],typeof t=="function"?t:null)}var pe=Symbol.for("react.client.reference");function xe(t){if(t==null)return null;if(typeof t=="function")return t.$$typeof===pe?null:t.displayName||t.name||null;if(typeof t=="string")return t;switch(t){case S:return"Fragment";case N:return"Profiler";case E:return"StrictMode";case O:return"Suspense";case M:return"SuspenseList";case K:return"Activity"}if(typeof t=="object")switch(t.$$typeof){case j:return"Portal";case T:return t.displayName||"Context";case k:return(t._context.displayName||"Context")+".Consumer";case A:var r=t.render;return t=t.displayName,t||(t=r.displayName||r.name||"",t=t!==""?"ForwardRef("+t+")":"ForwardRef"),t;case z:return r=t.displayName||null,r!==null?r:xe(t.type)||"Memo";case B:r=t._payload,t=t._init;try{return xe(t(r))}catch{}}return null}var se=Array.isArray,L=n.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE,I=a.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE,H={pending:!1,data:null,method:null,action:null},Z=[],te=-1;function _(t){return{current:t}}function D(t){0>te||(t.current=Z[te],Z[te]=null,te--)}function G(t,r){te++,Z[te]=t.current,t.current=r}var V=_(null),F=_(null),oe=_(null),ae=_(null);function be(t,r){switch(G(oe,r),G(F,t),G(V,null),r.nodeType){case 9:case 11:t=(t=r.documentElement)&&(t=t.namespaceURI)?lb(t):0;break;default:if(t=r.tagName,r=r.namespaceURI)r=lb(r),t=cb(r,t);else switch(t){case"svg":t=1;break;case"math":t=2;break;default:t=0}}D(V),G(V,t)}function ve(){D(V),D(F),D(oe)}function Y(t){t.memoizedState!==null&&G(ae,t);var r=V.current,i=cb(r,t.type);r!==i&&(G(F,t),G(V,i))}function le(t){F.current===t&&(D(V),D(F)),ae.current===t&&(D(ae),Ui._currentValue=H)}var ye,Te;function Le(t){if(ye===void 0)try{throw Error()}catch(i){var r=i.stack.trim().match(/\n( *(at )?)/);ye=r&&r[1]||"",Te=-1<i.stack.indexOf(`
|
||||
at`)?" (<anonymous>)":-1<i.stack.indexOf("@")?"@unknown:0:0":""}return`
|
||||
`+ye+t+Te}var rt=!1;function ht(t,r){if(!t||rt)return"";rt=!0;var i=Error.prepareStackTrace;Error.prepareStackTrace=void 0;try{var u={DetermineComponentFrameRoot:function(){try{if(r){var de=function(){throw Error()};if(Object.defineProperty(de.prototype,"props",{set:function(){throw Error()}}),typeof Reflect=="object"&&Reflect.construct){try{Reflect.construct(de,[])}catch(ne){var J=ne}Reflect.construct(t,[],de)}else{try{de.call()}catch(ne){J=ne}t.call(de.prototype)}}else{try{throw Error()}catch(ne){J=ne}(de=t())&&typeof de.catch=="function"&&de.catch(function(){})}}catch(ne){if(ne&&J&&typeof ne.stack=="string")return[ne.stack,J.stack]}return[null,null]}};u.DetermineComponentFrameRoot.displayName="DetermineComponentFrameRoot";var x=Object.getOwnPropertyDescriptor(u.DetermineComponentFrameRoot,"name");x&&x.configurable&&Object.defineProperty(u.DetermineComponentFrameRoot,"name",{value:"DetermineComponentFrameRoot"});var v=u.DetermineComponentFrameRoot(),C=v[0],R=v[1];if(C&&R){var P=C.split(`
|
||||
`),Q=R.split(`
|
||||
`);for(x=u=0;u<P.length&&!P[u].includes("DetermineComponentFrameRoot");)u++;for(;x<Q.length&&!Q[x].includes("DetermineComponentFrameRoot");)x++;if(u===P.length||x===Q.length)for(u=P.length-1,x=Q.length-1;1<=u&&0<=x&&P[u]!==Q[x];)x--;for(;1<=u&&0<=x;u--,x--)if(P[u]!==Q[x]){if(u!==1||x!==1)do if(u--,x--,0>x||P[u]!==Q[x]){var ie=`
|
||||
`+P[u].replace(" at new "," at ");return t.displayName&&ie.includes("<anonymous>")&&(ie=ie.replace("<anonymous>",t.displayName)),ie}while(1<=u&&0<=x);break}}}finally{rt=!1,Error.prepareStackTrace=i}return(i=t?t.displayName||t.name:"")?Le(i):""}function Mt(t,r){switch(t.tag){case 26:case 27:case 5:return Le(t.type);case 16:return Le("Lazy");case 13:return t.child!==r&&r!==null?Le("Suspense Fallback"):Le("Suspense");case 19:return Le("SuspenseList");case 0:case 15:return ht(t.type,!1);case 11:return ht(t.type.render,!1);case 1:return ht(t.type,!0);case 31:return Le("Activity");default:return""}}function Tr(t){try{var r="",i=null;do r+=Mt(t,i),i=t,t=t.return;while(t);return r}catch(u){return`
|
||||
Error generating stack: `+u.message+`
|
||||
`+u.stack}}var _t=Object.prototype.hasOwnProperty,Ya=e.unstable_scheduleCallback,qa=e.unstable_cancelCallback,qt=e.unstable_shouldYield,dr=e.unstable_requestPaint,Kt=e.unstable_now,ti=e.unstable_getCurrentPriorityLevel,fr=e.unstable_ImmediatePriority,Lo=e.unstable_UserBlockingPriority,Rr=e.unstable_NormalPriority,Xt=e.unstable_LowPriority,Qn=e.unstable_IdlePriority,zo=e.log,mr=e.unstable_setDisableYieldValue,pr=null,Ht=null;function ee(t){if(typeof zo=="function"&&mr(t),Ht&&typeof Ht.setStrictMode=="function")try{Ht.setStrictMode(pr,t)}catch{}}var re=Math.clz32?Math.clz32:De,ke=Math.log,we=Math.LN2;function De(t){return t>>>=0,t===0?32:31-(ke(t)/we|0)|0}var at=256,ct=262144,mt=4194304;function Zt(t){var r=t&42;if(r!==0)return r;switch(t&-t){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 t&261888;case 262144:case 524288:case 1048576:case 2097152:return t&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return t&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return t}}function Me(t,r,i){var u=t.pendingLanes;if(u===0)return 0;var x=0,v=t.suspendedLanes,C=t.pingedLanes;t=t.warmLanes;var R=u&134217727;return R!==0?(u=R&~v,u!==0?x=Zt(u):(C&=R,C!==0?x=Zt(C):i||(i=R&~t,i!==0&&(x=Zt(i))))):(R=u&~v,R!==0?x=Zt(R):C!==0?x=Zt(C):i||(i=u&~t,i!==0&&(x=Zt(i)))),x===0?0:r!==0&&r!==x&&(r&v)===0&&(v=x&-x,i=r&-r,v>=i||v===32&&(i&4194048)!==0)?r:x}function Je(t,r){return(t.pendingLanes&~(t.suspendedLanes&~t.pingedLanes)&r)===0}function St(t,r){switch(t){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 kt(){var t=mt;return mt<<=1,(mt&62914560)===0&&(mt=4194304),t}function Jn(t){for(var r=[],i=0;31>i;i++)r.push(t);return r}function wt(t,r){t.pendingLanes|=r,r!==268435456&&(t.suspendedLanes=0,t.pingedLanes=0,t.warmLanes=0)}function dn(t,r,i,u,x,v){var C=t.pendingLanes;t.pendingLanes=i,t.suspendedLanes=0,t.pingedLanes=0,t.warmLanes=0,t.expiredLanes&=i,t.entangledLanes&=i,t.errorRecoveryDisabledLanes&=i,t.shellSuspendCounter=0;var R=t.entanglements,P=t.expirationTimes,Q=t.hiddenUpdates;for(i=C&~i;0<i;){var ie=31-re(i),de=1<<ie;R[ie]=0,P[ie]=-1;var J=Q[ie];if(J!==null)for(Q[ie]=null,ie=0;ie<J.length;ie++){var ne=J[ie];ne!==null&&(ne.lane&=-536870913)}i&=~de}u!==0&&Ka(t,u,0),v!==0&&x===0&&t.tag!==0&&(t.suspendedLanes|=v&~(C&~r))}function Ka(t,r,i){t.pendingLanes|=r,t.suspendedLanes&=~r;var u=31-re(r);t.entangledLanes|=r,t.entanglements[u]=t.entanglements[u]|1073741824|i&261930}function fn(t,r){var i=t.entangledLanes|=r;for(t=t.entanglements;i;){var u=31-re(i),x=1<<u;x&r|t[u]&r&&(t[u]|=r),i&=~x}}function mn(t,r){var i=r&-r;return i=(i&42)!==0?1:Po(i),(i&(t.suspendedLanes|r))!==0?0:i}function Po(t){switch(t){case 2:t=1;break;case 8:t=4;break;case 32:t=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:t=128;break;case 268435456:t=134217728;break;default:t=0}return t}function er(t){return t&=-t,2<t?8<t?(t&134217727)!==0?32:268435456:8:2}function Hd(){var t=I.p;return t!==0?t:(t=window.event,t===void 0?32:_b(t.type))}function qg(t,r){var i=I.p;try{return I.p=t,r()}finally{I.p=i}}var sa=Math.random().toString(36).slice(2),Qt="__reactFiber$"+sa,pn="__reactProps$"+sa,Io="__reactContainer$"+sa,Vd="__reactEvents$"+sa,aN="__reactListeners$"+sa,oN="__reactHandles$"+sa,Kg="__reactResources$"+sa,ni="__reactMarker$"+sa;function Gd(t){delete t[Qt],delete t[pn],delete t[Vd],delete t[aN],delete t[oN]}function $o(t){var r=t[Qt];if(r)return r;for(var i=t.parentNode;i;){if(r=i[Io]||i[Qt]){if(i=r.alternate,r.child!==null||i!==null&&i.child!==null)for(t=gb(t);t!==null;){if(i=t[Qt])return i;t=gb(t)}return r}t=i,i=t.parentNode}return null}function Bo(t){if(t=t[Qt]||t[Io]){var r=t.tag;if(r===5||r===6||r===13||r===31||r===26||r===27||r===3)return t}return null}function ri(t){var r=t.tag;if(r===5||r===26||r===27||r===6)return t.stateNode;throw Error(o(33))}function Uo(t){var r=t[Kg];return r||(r=t[Kg]={hoistableStyles:new Map,hoistableScripts:new Map}),r}function Vt(t){t[ni]=!0}var Xg=new Set,Zg={};function Xa(t,r){Ho(t,r),Ho(t+"Capture",r)}function Ho(t,r){for(Zg[t]=r,t=0;t<r.length;t++)Xg.add(r[t])}var sN=RegExp("^[:A-Z_a-z\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u20
Line truncated
|
||||
`).replace(P4,"")}function sb(t,r){return r=ob(r),ob(t)===r}function dt(t,r,i,u,x,v){switch(i){case"children":typeof u=="string"?r==="body"||r==="textarea"&&u===""||Go(t,u):(typeof u=="number"||typeof u=="bigint")&&r!=="body"&&Go(t,""+u);break;case"className":Vl(t,"class",u);break;case"tabIndex":Vl(t,"tabindex",u);break;case"dir":case"role":case"viewBox":case"width":case"height":Vl(t,i,u);break;case"style":sx(t,u,v);break;case"data":if(r!=="object"){Vl(t,"data",u);break}case"src":case"href":if(u===""&&(r!=="a"||i!=="href")){t.removeAttribute(i);break}if(u==null||typeof u=="function"||typeof u=="symbol"||typeof u=="boolean"){t.removeAttribute(i);break}u=Fl(""+u),t.setAttribute(i,u);break;case"action":case"formAction":if(typeof u=="function"){t.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 v=="function"&&(i==="formAction"?(r!=="input"&&dt(t,r,"name",x.name,x,null),dt(t,r,"formEncType",x.formEncType,x,null),dt(t,r,"formMethod",x.formMethod,x,null),dt(t,r,"formTarget",x.formTarget,x,null)):(dt(t,r,"encType",x.encType,x,null),dt(t,r,"method",x.method,x,null),dt(t,r,"target",x.target,x,null)));if(u==null||typeof u=="symbol"||typeof u=="boolean"){t.removeAttribute(i);break}u=Fl(""+u),t.setAttribute(i,u);break;case"onClick":u!=null&&(t.onclick=Mr);break;case"onScroll":u!=null&&Fe("scroll",t);break;case"onScrollEnd":u!=null&&Fe("scrollend",t);break;case"dangerouslySetInnerHTML":if(u!=null){if(typeof u!="object"||!("__html"in u))throw Error(o(61));if(i=u.__html,i!=null){if(x.children!=null)throw Error(o(60));t.innerHTML=i}}break;case"multiple":t.multiple=u&&typeof u!="function"&&typeof u!="symbol";break;case"muted":t.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"){t.removeAttribute("xlink:href");break}i=Fl(""+u),t.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"?t.setAttribute(i,""+u):t.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"?t.setAttribute(i,""):t.removeAttribute(i);break;case"capture":case"download":u===!0?t.setAttribute(i,""):u!==!1&&u!=null&&typeof u!="function"&&typeof u!="symbol"?t.setAttribute(i,u):t.removeAttribute(i);break;case"cols":case"rows":case"size":case"span":u!=null&&typeof u!="function"&&typeof u!="symbol"&&!isNaN(u)&&1<=u?t.setAttribute(i,u):t.removeAttribute(i);break;case"rowSpan":case"start":u==null||typeof u=="function"||typeof u=="symbol"||isNaN(u)?t.removeAttribute(i):t.setAttribute(i,u);break;case"popover":Fe("beforetoggle",t),Fe("toggle",t),Hl(t,"popover",u);break;case"xlinkActuate":Ar(t,"http://www.w3.org/1999/xlink","xlink:actuate",u);break;case"xlinkArcrole":Ar(t,"http://www.w3.org/1999/xlink","xlink:arcrole",u);break;case"xlinkRole":Ar(t,"http://www.w3.org/1999/xlink","xlink:role",u);break;case"xlinkShow":Ar(t,"http://www.w3.org/1999/xlink","xlink:show",u);break;case"xlinkTitle":Ar(t,"http://www.w3.org/1999/xlink","xlink:title",u);break;case"xlinkType":Ar(t,"http://www.w3.org/1999/xlink","xlink:type",u);break;case"xmlBase":Ar(t,"http://www.w3.org/XML/1998/namespace","xml:base",u);break;case"xmlLang":Ar(t,"http://www.w3.org/XML/1998/namespace","xml:lang",u);break;case"xmlSpace":Ar(t,"http://www.w3.org/XML/1998/namespace","xml:space",u);break;case"is":Hl(t,"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=dN.get(i)||i,Hl(t,i,u))}}function Lm(t,r,i,u,x,v){switch(i){case"style":sx(t,u,v);break;case"dangerouslySetInnerHTML":if(u!=null){if(typeof u!="object"||!("__html"in u))throw Error(o(61));if(i=u.__html,i!=null){if(x.children!=null)throw Error(o(60));t.innerHTML=i}}break;case"children":typeof u=="string"?Go(t,u):(typeof u=="number"||typeof u=="bigint")&&Go(t,""+u);break;case"onScroll":u!=null&&Fe("scroll",t);break;case"onScrollEnd":u!=null&&Fe("scrollend",t);break;case"onClick":u!=null&&(t.onclick=Mr);break;case"suppressContentEditableWarning":case"suppressHydrationWarning":case"inne
Line truncated
|
||||
* react-router v7.15.1
|
||||
*
|
||||
* 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 Xb="popstate";function Zb(e){return typeof e=="object"&&e!=null&&"pathname"in e&&"search"in e&&"hash"in e&&"state"in e&&"key"in e}function k3(e={}){function n(o,l){var h;let c=(h=l.state)==null?void 0:h.masked,{pathname:d,search:m,hash:p}=c||o.location;return Up("",{pathname:d,search:m,hash:p},l.state&&l.state.usr||null,l.state&&l.state.key||"default",c?{pathname:o.location.pathname,search:o.location.search,hash:o.location.hash}:void 0)}function a(o,l){return typeof l=="string"?l:sl(l)}return R3(n,a,null,e)}function yt(e,n){if(e===!1||e===null||typeof e>"u")throw new Error(n)}function Xn(e,n){if(!e){typeof console<"u"&&console.warn(n);try{throw new Error(n)}catch{}}}function T3(){return Math.random().toString(36).substring(2,10)}function Qb(e,n){return{usr:e.state,key:e.key,idx:n,masked:e.mask?{pathname:e.pathname,search:e.search,hash:e.hash}:void 0}}function Up(e,n,a=null,o,l){return{pathname:typeof e=="string"?e:e.pathname,search:"",hash:"",...typeof n=="string"?$s(n):n,state:a,key:n&&n.key||o||T3(),mask:l}}function sl({pathname:e="/",search:n="",hash:a=""}){return n&&n!=="?"&&(e+=n.charAt(0)==="?"?n:"?"+n),a&&a!=="#"&&(e+=a.charAt(0)==="#"?a:"#"+a),e}function $s(e){let n={};if(e){let a=e.indexOf("#");a>=0&&(n.hash=e.substring(a),e=e.substring(0,a));let o=e.indexOf("?");o>=0&&(n.search=e.substring(o),e=e.substring(0,o)),e&&(n.pathname=e)}return n}function R3(e,n,a,o={}){let{window:l=document.defaultView,v5Compat:c=!1}=o,d=l.history,m="POP",p=null,h=b();h==null&&(h=0,d.replaceState({...d.state,idx:h},""));function b(){return(d.state||{idx:null}).idx}function g(){m="POP";let E=b(),N=E==null?null:E-h;h=E,p&&p({action:m,location:S.location,delta:N})}function y(E,N){m="PUSH";let k=Zb(E)?E:Up(S.location,E,N);h=b()+1;let T=Qb(k,h),A=S.createHref(k.mask||k);try{d.pushState(T,"",A)}catch(O){if(O instanceof DOMException&&O.name==="DataCloneError")throw O;l.location.assign(A)}c&&p&&p({action:m,location:S.location,delta:1})}function w(E,N){m="REPLACE";let k=Zb(E)?E:Up(S.location,E,N);h=b();let T=Qb(k,h),A=S.createHref(k.mask||k);d.replaceState(T,"",A),c&&p&&p({action:m,location:S.location,delta:0})}function j(E){return A3(E)}let S={get action(){return m},get location(){return e(l,d)},listen(E){if(p)throw new Error("A history only accepts one active listener");return l.addEventListener(Xb,g),p=E,()=>{l.removeEventListener(Xb,g),p=null}},createHref(E){return n(l,E)},createURL:j,encodeLocation(E){let N=j(E);return{pathname:N.pathname,search:N.search,hash:N.hash}},push:y,replace:w,go(E){return d.go(E)}};return S}function A3(e,n=!1){let a="http://localhost";typeof window<"u"&&(a=window.location.origin!=="null"?window.location.origin:window.location.href),yt(a,"No window.location.(origin|href) available to create URL");let o=typeof e=="string"?e:sl(e);return o=o.replace(/ $/,"%20"),!n&&o.startsWith("//")&&(o=a+o),new URL(o,a)}function _0(e,n,a="/"){return M3(e,n,a,!1)}function M3(e,n,a,o,l){let c=typeof n=="string"?$s(n):n,d=Jr(c.pathname||"/",a);if(d==null)return null;let m=_3(e),p=null,h=G3(d);for(let b=0;p==null&&b<m.length;++b)p=H3(m[b],h,o);return p}function _3(e){let n=O0(e);return O3(n),n}function O0(e,n=[],a=[],o="",l=!1){let c=(d,m,p=l,h)=>{let b={relativePath:h===void 0?d.path||"":h,caseSensitive:d.caseSensitive===!0,childrenIndex:m,route:d};if(b.relativePath.startsWith("/")){if(!b.relativePath.startsWith(o)&&p)return;yt(b.relativePath.startsWith(o),`Absolute route path "${b.relativePath}" nested under path "${o}" is not valid. An absolute child route path must start with the combined path of all its parent routes.`),b.relativePath=b.relativePath.slice(o.length)}let g=sr([o,b.relativePath]),y=a.concat(b);d.children&&d.children.length>0&&(yt(d.index!==!0,`Index routes must not have child routes. Please remove all child routes from route path "${g}".`),O0(d.children,n,y,g,p)),!(d.path==null&&!d.index)&&n.push({path:g,score:B3(g,d.index),routesMeta:y})};return e.forEach((d,m)=>{var p;if(d.path===""||!((p=d.path)!=null&&p.includes("?")))c(d,m);else for(let h of D0(d.path))c(d,m,!0,h)}),n}function D0(e){let n=e.split("/");if(n.length===0)return[];let[a,...o]=n,l=a.endsWith("?"),c=a.replace(/\?$/,"");if(o.length===0)return l?[c,""]:[c];let d=D0(o.join("/")),m=[];return m.push(...d.map(p=>p===""?c:[c,p].join("/"))),l&&m.push(...d),m.map(p=>e.startsWith("/")&&p===""?"/":p)}function O3(e){e.sort((n,a)=>n.score!==a.score?a.score-n.score:U3(n.routesMeta.map(o=>o.childrenIndex),a.routesMeta.map(o=>o.childrenIndex)))}var D3=/^:[\w-]+$/,L3=3,z3=2,P3=1,I3=10,$3=-2,Jb=e=>e==="*";function B3(e,n){let a=e.split("/"),o=a.length;return a.some(Jb)&&(o+=$3),n&&(o+=z3),a.filter(l=>!Jb(l)).reduce((l,c)=>l+(D3.test(c)?L3:c===""?P3:I3),o)}function U3(e,n){return e.length===n.length&&e.slice(0,-1).every((o,l)=>o===n[l])?e[e.length-1]-n[n.length-1]:0}function H3(e,n,a=!1){let{routesMeta:o}=e,l={},c="/",d=[];for(let m=0;m<o.length;++m){let p=o[m],h=m===o.length-1,b=c==="/"?n:n.slice(c.length)||"/",g=Tu({path:p.relativePath,caseSensit
Line truncated
|
||||
|
||||
Please change the parent <Route path="${N}"> to <Route path="${N==="/"?"*":`${N}/*`}">.`)}let b=Ln(),g;if(n){let N=typeof n=="string"?$s(n):n;yt(p==="/"||((E=N.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 "${N.pathname}" was given in the \`location\` prop.`),g=N}else g=b;let y=g.pathname||"/",w=y;if(p!=="/"){let N=p.replace(/^\//,"").split("/");w="/"+y.replace(/^\//,"").split("/").slice(N.length).join("/")}let j=a&&a.state.matches.length?a.state.matches.map(N=>Object.assign(N,{route:a.manifest[N.route.id]||N.route})):_0(e,{pathname:w});Xn(h||j!=null,`No routes matched location "${g.pathname}${g.search}${g.hash}" `),Xn(j==null||j[j.length-1].route.element!==void 0||j[j.length-1].route.Component!==void 0||j[j.length-1].route.lazy!==void 0,`Matched leaf route at location "${g.pathname}${g.search}${g.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=xE(j&&j.map(N=>Object.assign({},N,{params:Object.assign({},d,N.params),pathname:sr([p,o.encodeLocation?o.encodeLocation(N.pathname.replace(/%/g,"%25").replace(/\?/g,"%3F").replace(/#/g,"%23")).pathname:N.pathname]),pathnameBase:N.pathnameBase==="/"?p:sr([p,o.encodeLocation?o.encodeLocation(N.pathnameBase.replace(/%/g,"%25").replace(/\?/g,"%3F").replace(/#/g,"%23")).pathname:N.pathnameBase])})),l,a);return n&&S?f.createElement(bl.Provider,{value:{location:{pathname:"/",search:"",hash:"",state:null,key:"default",mask:void 0,...g},navigationType:"POP"}},S):S}function mE(){let e=CE(),n=Q3(e)?`${e.status} ${e.statusText}`:e instanceof Error?e.message:JSON.stringify(e),a=e instanceof Error?e.stack:null,o="rgba(200,200,200, 0.5)",l={padding:"0.5rem",backgroundColor:o},c={padding:"2px 4px",backgroundColor:o},d=null;return console.error("Error handled by React Router default ErrorBoundary:",e),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:c},"ErrorBoundary")," or"," ",f.createElement("code",{style:c},"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:l},a):null,d)}var pE=f.createElement(mE,null),F0=class extends f.Component{constructor(e){super(e),this.state={location:e.location,revalidation:e.revalidation,error:e.error}}static getDerivedStateFromError(e){return{error:e}}static getDerivedStateFromProps(e,n){return n.location!==e.location||n.revalidation!=="idle"&&e.revalidation==="idle"?{error:e.error,location:e.location,revalidation:e.revalidation}:{error:e.error!==void 0?e.error:n.error,location:n.location,revalidation:e.revalidation||n.revalidation}}componentDidCatch(e,n){this.props.onError?this.props.onError(e,n):console.error("React Router caught the following error during render",e)}render(){let e=this.state.error;if(this.context&&typeof e=="object"&&e&&"digest"in e&&typeof e.digest=="string"){const a=iE(e.digest);a&&(e=a)}let n=e!==void 0?f.createElement(cr.Provider,{value:this.props.routeContext},f.createElement(_h.Provider,{value:e,children:this.props.component})):this.props.children;return this.context?f.createElement(hE,{error:e},n):n}};F0.contextType=$0;var sp=new WeakMap;function hE({children:e,error:n}){let{basename:a}=f.useContext(Dn);if(typeof n=="object"&&n&&"digest"in n&&typeof n.digest=="string"){let o=sE(n.digest);if(o){let l=sp.get(n);if(l)throw l;let c=P0(o.location,a);if(z0&&!sp.get(n))if(c.isExternal||o.reloadDocument)window.location.href=c.absoluteURL||c.to;else{const d=Promise.resolve().then(()=>window.__reactRouterDataRouter.navigate(c.to,{replace:o.replace}));throw sp.set(n,d),d}return f.createElement("meta",{httpEquiv:"refresh",content:`0;url=${c.absoluteURL||c.to}`})}}return e}function gE({routeContext:e,match:n,children:a}){let o=f.useContext(Bs);return o&&o.static&&o.staticContext&&(n.route.errorElement||n.route.ErrorBoundary)&&(o.staticContext._deepestRenderedBoundaryId=n.route.id),f.createElement(cr.Provider,{value:e},a)}function xE(e,n=[],a){let o=a==null?void 0:a.state;if(e==null){if(!o)return null;if(o.errors)e=o.matches;else if(n.length===0&&!o.initialized&&o.matches.length>0)e=o.matches;else return null}let l=e,c=o==null?void 0:o.errors;if(c!=null){let b=l.findIndex(g=>g.route.id&&(c==null?void 0:c[g.route.id])!==void 0);yt(b>=0,`Could not find a matching route for errors on route IDs: ${Object.keys(c).join(",")}`),l=l.slice(0,Math.min(l.length,b+1))}let d=!1,m=-1;if(a&&o){d=o.renderFallback;for(let b=0;b<l.length;b++){let g=l[b];if((g.route.HyLine truncated
|
||||
.`.concat(JT,` {
|
||||
overflow: hidden `).concat(o,`;
|
||||
padding-right: `).concat(m,"px ").concat(o,`;
|
||||
}
|
||||
body[`).concat(Ms,`] {
|
||||
overflow: hidden `).concat(o,`;
|
||||
overscroll-behavior: contain;
|
||||
`).concat([n&&"position: relative ".concat(o,";"),a==="margin"&&`
|
||||
padding-left: `.concat(l,`px;
|
||||
padding-top: `).concat(c,`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(Cu,` {
|
||||
right: `).concat(m,"px ").concat(o,`;
|
||||
}
|
||||
|
||||
.`).concat(Su,` {
|
||||
margin-right: `).concat(m,"px ").concat(o,`;
|
||||
}
|
||||
|
||||
.`).concat(Cu," .").concat(Cu,` {
|
||||
right: 0 `).concat(o,`;
|
||||
}
|
||||
|
||||
.`).concat(Su," .").concat(Su,` {
|
||||
margin-right: 0 `).concat(o,`;
|
||||
}
|
||||
|
||||
body[`).concat(Ms,`] {
|
||||
`).concat(e7,": ").concat(m,`px;
|
||||
}
|
||||
`)},Py=function(){var e=parseInt(document.body.getAttribute(Ms)||"0",10);return isFinite(e)?e:0},b7=function(){f.useEffect(function(){return document.body.setAttribute(Ms,(Py()+1).toString()),function(){var e=Py()-1;e<=0?document.body.removeAttribute(Ms):document.body.setAttribute(Ms,e.toString())}},[])},y7=function(e){var n=e.noRelative,a=e.noImportant,o=e.gapMode,l=o===void 0?"margin":o;b7();var c=f.useMemo(function(){return g7(l)},[l]);return f.createElement(x7,{styles:v7(c,!n,l,a?"":"!important")})},Jp=!1;if(typeof window<"u")try{var lu=Object.defineProperty({},"passive",{get:function(){return Jp=!0,!0}});window.addEventListener("test",lu,lu),window.removeEventListener("test",lu,lu)}catch{Jp=!1}var Ss=Jp?{passive:!1}:!1,w7=function(e){return e.tagName==="TEXTAREA"},yw=function(e,n){if(!(e instanceof Element))return!1;var a=window.getComputedStyle(e);return a[n]!=="hidden"&&!(a.overflowY===a.overflowX&&!w7(e)&&a[n]==="visible")},C7=function(e){return yw(e,"overflowY")},S7=function(e){return yw(e,"overflowX")},Iy=function(e,n){var a=n.ownerDocument,o=n;do{typeof ShadowRoot<"u"&&o instanceof ShadowRoot&&(o=o.host);var l=ww(e,o);if(l){var c=Cw(e,o),d=c[1],m=c[2];if(d>m)return!0}o=o.parentNode}while(o&&o!==a.body);return!1},j7=function(e){var n=e.scrollTop,a=e.scrollHeight,o=e.clientHeight;return[n,a,o]},N7=function(e){var n=e.scrollLeft,a=e.scrollWidth,o=e.clientWidth;return[n,a,o]},ww=function(e,n){return e==="v"?C7(n):S7(n)},Cw=function(e,n){return e==="v"?j7(n):N7(n)},E7=function(e,n){return e==="h"&&n==="rtl"?-1:1},k7=function(e,n,a,o,l){var c=E7(e,window.getComputedStyle(n).direction),d=c*o,m=a.target,p=n.contains(m),h=!1,b=d>0,g=0,y=0;do{if(!m)break;var w=Cw(e,m),j=w[0],S=w[1],E=w[2],N=S-E-c*j;(j||N)&&ww(e,m)&&(g+=N,y+=j);var k=m.parentNode;m=k&&k.nodeType===Node.DOCUMENT_FRAGMENT_NODE?k.host:k}while(!p&&m!==document.body||p&&(n.contains(m)||n===m));return(b&&Math.abs(g)<1||!b&&Math.abs(y)<1)&&(h=!0),h},cu=function(e){return"changedTouches"in e?[e.changedTouches[0].clientX,e.changedTouches[0].clientY]:[0,0]},$y=function(e){return[e.deltaX,e.deltaY]},By=function(e){return e&&"current"in e?e.current:e},T7=function(e,n){return e[0]===n[0]&&e[1]===n[1]},R7=function(e){return`
|
||||
.block-interactivity-`.concat(e,` {pointer-events: none;}
|
||||
.allow-interactivity-`).concat(e,` {pointer-events: all;}
|
||||
`)},A7=0,js=[];function M7(e){var n=f.useRef([]),a=f.useRef([0,0]),o=f.useRef(),l=f.useState(A7++)[0],c=f.useState(bw)[0],d=f.useRef(e);f.useEffect(function(){d.current=e},[e]),f.useEffect(function(){if(e.inert){document.body.classList.add("block-interactivity-".concat(l));var S=QT([e.lockRef.current],(e.shards||[]).map(By),!0).filter(Boolean);return S.forEach(function(E){return E.classList.add("allow-interactivity-".concat(l))}),function(){document.body.classList.remove("block-interactivity-".concat(l)),S.forEach(function(E){return E.classList.remove("allow-interactivity-".concat(l))})}}},[e.inert,e.lockRef.current,e.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 N=cu(S),k=a.current,T="deltaX"in S?S.deltaX:k[0]-N[0],A="deltaY"in S?S.deltaY:k[1]-N[1],O,M=S.target,z=Math.abs(T)>Math.abs(A)?"h":"v";if("touches"in S&&z==="h"&&M.type==="range")return!1;var B=window.getSelection(),K=B&&B.anchorNode,U=K?K===M||K.contains(M):!1;if(U)return!1;var q=Iy(z,M);if(!q)return!0;if(q?O=z:(O=z==="v"?"h":"v",q=Iy(z,M)),!q)return!1;if(!o.current&&"changedTouches"in S&&(T||A)&&(o.current=O),!O)return!0;var ue=o.current||O;return k7(ue,E,S,ue==="h"?T:A)},[]),p=f.useCallback(function(S){var E=S;if(!(!js.length||js[js.length-1]!==c)){var N="deltaY"in E?$y(E):cu(E),k=n.current.filter(function(O){return O.name===E.type&&(O.target===E.target||E.target===O.shadowParent)&&T7(O.delta,N)})[0];if(k&&k.should){E.cancelable&&E.preventDefault();return}if(!k){var T=(d.current.shards||[]).map(By).filter(Boolean).filter(function(O){return O.contains(E.target)}),A=T.length>0?m(E,T[0]):!d.current.noIsolation;A&&E.cancelable&&E.preventDefault()}}},[]),h=f.useCallback(function(S,E,N,k){var T={name:S,delta:E,target:N,should:k,shadowParent:_7(N)};n.current.push(T),setTimeout(function(){n.current=n.current.filter(function(A){return A!==T})},1)},[]),b=f.useCallback(function(S){a.current=cu(S),o.current=void 0},[]),g=f.useCallback(function(S){h(S.type,$y(S),S.target,m(S,e.lockRef.current))},[]),y=f.useCallback(function(S){h(S.type,cu(S),S.target,m(S,e.lockRef.current))},[]);f.useEffect(function(){return js.push(c),e.setCallbacks({onScrollCapture:g,onWheelCapture:g,onTouchMoveCapture:y}),document.addEventListener("wheel",p,Ss),document.addEventListener("touchmove",p,Ss),document.addEventListener("touchstart",b,Ss),function(){js=js.filter(function(S){return S!==c}),document.removeEventListener("wheel",p,Ss),document.removeEventListener("touchmove",p,Ss),document.removeEventListener("touchstart",b,Ss)}},[]);var w=e.removeScrollBar,j=e.inert;return f.createElement(f.Fragment,null,j?f.createElement(c,{styles:R7(l)}):null,w?f.createElement(y7,{noRelative:e.noRelative,gapMode:e.gapMode}):null)}function _7(e){for(var n=null;e!==null;)e instanceof ShadowRoot&&(n=e.host,e=e.host),e=e.parentNode;return n}const O7=i7(vw,M7);var Tl=f.forwardRef(function(e,n){return f.createElement(xd,Cr({},e,{ref:n,sideCar:O7}))});Tl.classNames=xd.classNames;var eh=["Enter"," "],D7=["ArrowDown","PageUp","Home"],Sw=["ArrowUp","PageDown","End"],L7=[...D7,...Sw],z7={ltr:[...eh,"ArrowRight"],rtl:[...eh,"ArrowLeft"]},P7={ltr:["ArrowLeft"],rtl:["ArrowRight"]},Rl="Menu",[ul,I7,$7]=Kh(Rl),[_o,jw]=e6(Rl,[$7,Va,hd]),vd=Va(),Nw=hd(),[B7,Oo]=_o(Rl),[U7,Al]=_o(Rl),Ew=e=>{const{__scopeMenu:n,open:a=!1,children:o,dir:l,onOpenChange:c,modal:d=!0}=e,m=vd(n),[p,h]=f.useState(null),b=f.useRef(!1),g=Wt(c),y=Cl(l);return f.useEffect(()=>{const w=()=>{b.current=!0,document.addEventListener("pointerdown",j,{capture:!0,once:!0}),document.addEventListener("pointermove",j,{capture:!0,once:!0})},j=()=>b.current=!1;return document.addEventListener("keydown",w,{capture:!0}),()=>{document.removeEventListener("keydown",w,{capture:!0}),document.removeEventListener("pointerdown",j,{capture:!0}),document.removeEventListener("pointermove",j,{capture:!0})}},[]),s.jsx(fd,{...m,children:s.jsx(B7,{scope:n,open:a,onOpenChange:g,content:p,onContentChange:h,children:s.jsx(U7,{scope:n,onClose:f.useCallback(()=>g(!1),[g]),isUsingKeyboardRef:b,dir:y,modal:d,children:o})})})};Ew.displayName=Rl;var H7="MenuAnchor",ag=f.forwardRef((e,n)=>{const{__scopeMenu:a,...o}=e,l=vd(a);return s.jsx(Nl,{...l,...o,ref:n})});ag.displayName=H7;var og="MenuPortal",[V7,kw]=_o(og,{forceMount:void 0}),Tw=e=>{const{__scopeMenu:n,forceMount:a,children:o,container:l}=e,c=Oo(og,n);return s.jsx(V7,{scope:n,forceMount:a,children:s.jsx(sn,{present:a||c.open,children:s.jsx(Ys,{asChild:!0,container:l,children:o})})})};Tw.displayName=og;var Kn="MenuContent",[G7,sg]=_o(Kn),Rw=f.forwardRef((e,n)=>{const a=kw(Kn,e.__scopeMenu),{forceMount:o=a.forceMount,...l}=e,c=Oo(Kn,e.__scopeMenu),d=Al(Kn,e.__scopeMenu);return s.jsx(ul.Provider,{scope:e.__scopeMenu,children:s.jsx(sn,{present:o||c.open,children:s.jsx(ul.Slot,{scope:e.__scopeMenu,children:d.modal?s.jsx(F7,{...l,ref:n}):s.jsx(W7,{...l,ref:n})})})})}),F7=f.forwardRef((e,n)=>{const a=Oo(Kn,e.__scopeMenu),o=f.useRe
Line truncated
|
||||
* @license lucide-react v1.16.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const gC=(...e)=>e.filter((n,a,o)=>!!n&&n.trim()!==""&&o.indexOf(n)===a).join(" ").trim();/**
|
||||
* @license lucide-react v1.16.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const VR=e=>e.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase();/**
|
||||
* @license lucide-react v1.16.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const GR=e=>e.replace(/^([A-Z])|[\s-_]+(\w)/g,(n,a,o)=>o?o.toUpperCase():a.toLowerCase());/**
|
||||
* @license lucide-react v1.16.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const Hy=e=>{const n=GR(e);return n.charAt(0).toUpperCase()+n.slice(1)};/**
|
||||
* @license lucide-react v1.16.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/var Cp={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 v1.16.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const FR=e=>{for(const n in e)if(n.startsWith("aria-")||n==="role"||n==="title")return!0;return!1},WR=f.createContext({}),YR=()=>f.useContext(WR),qR=f.forwardRef(({color:e,size:n,strokeWidth:a,absoluteStrokeWidth:o,className:l="",children:c,iconNode:d,...m},p)=>{const{size:h=24,strokeWidth:b=2,absoluteStrokeWidth:g=!1,color:y="currentColor",className:w=""}=YR()??{},j=o??g?Number(a??b)*24/Number(n??h):a??b;return f.createElement("svg",{ref:p,...Cp,width:n??h??Cp.width,height:n??h??Cp.height,stroke:e??y,strokeWidth:j,className:gC("lucide",w,l),...!c&&!FR(m)&&{"aria-hidden":"true"},...m},[...d.map(([S,E])=>f.createElement(S,E)),...Array.isArray(c)?c:[c]])});/**
|
||||
* @license lucide-react v1.16.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const aa=(e,n)=>{const a=f.forwardRef(({className:o,...l},c)=>f.createElement(qR,{ref:c,iconNode:n,className:gC(`lucide-${VR(Hy(e))}`,`lucide-${e}`,o),...l}));return a.displayName=Hy(e),a};/**
|
||||
* @license lucide-react v1.16.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const KR=[["path",{d:"M20 6 9 17l-5-5",key:"1gmf2c"}]],xC=aa("check",KR);/**
|
||||
* @license lucide-react v1.16.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const XR=[["path",{d:"m6 9 6 6 6-6",key:"qrunsl"}]],vC=aa("chevron-down",XR);/**
|
||||
* @license lucide-react v1.16.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const ZR=[["path",{d:"m9 18 6-6-6-6",key:"mthhwq"}]],QR=aa("chevron-right",ZR);/**
|
||||
* @license lucide-react v1.16.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const JR=[["path",{d:"m18 15-6-6-6 6",key:"153udz"}]],eA=aa("chevron-up",JR);/**
|
||||
* @license lucide-react v1.16.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const tA=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"m9 12 2 2 4-4",key:"dzmm74"}]],Vy=aa("circle-check",tA);/**
|
||||
* @license lucide-react v1.16.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const nA=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}]],rA=aa("circle",nA);/**
|
||||
* @license lucide-react v1.16.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const aA=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",key:"afitv7"}],["path",{d:"M9 3v18",key:"fh3hqa"}]],oA=aa("panel-left",aA);/**
|
||||
* @license lucide-react v1.16.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const sA=[["path",{d:"m21 21-4.34-4.34",key:"14j7rj"}],["circle",{cx:"11",cy:"11",r:"8",key:"4ej97u"}]],iA=aa("search",sA);/**
|
||||
* @license lucide-react v1.16.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const lA=[["path",{d:"M18 6 6 18",key:"1bl5f8"}],["path",{d:"m6 6 12 12",key:"d8bk6v"}]],bC=aa("x",lA),dg=BR,fg=UR,cA=f.forwardRef(({className:e,inset:n,children:a,...o},l)=>s.jsxs(pC,{ref:l,className:fe("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",e),...o,children:[a,s.jsx(QR,{className:"ml-auto"})]}));cA.displayName=pC.displayName;const uA=f.forwardRef(({className:e,...n},a)=>s.jsx(hC,{ref:a,className:fe("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]",e),...n}));uA.displayName=hC.displayName;const wd=f.forwardRef(({className:e,sideOffset:n=4,...a},o)=>s.jsx(HR,{children:s.jsx(iC,{ref:o,sideOffset:n,className:fe("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]",e),...a})}));wd.displayName=iC.displayName;const fl=f.forwardRef(({className:e,inset:n,...a},o)=>s.jsx(cC,{ref:o,className:fe("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",e),...a}));fl.displayName=cC.displayName;const dA=f.forwardRef(({className:e,children:n,checked:a,...o},l)=>s.jsxs(uC,{ref:l,className:fe("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",e),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(fC,{children:s.jsx(xC,{className:"h-4 w-4"})})}),n]}));dA.displayName=uC.displayName;const fA=f.forwardRef(({className:e,children:n,...a},o)=>s.jsxs(dC,{ref:o,className:fe("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",e),...a,children:[s.jsx("span",{className:"absolute left-2 flex h-3.5 w-3.5 items-center justify-center",children:s.jsx(fC,{children:s.jsx(rA,{className:"h-2 w-2 fill-current"})})}),n]}));fA.displayName=dC.displayName;const mg=f.forwardRef(({className:e,inset:n,...a},o)=>s.jsx(lC,{ref:o,className:fe("px-2 py-1.5 text-sm font-semibold",n&&"pl-8",e),...a}));mg.displayName=lC.displayName;const ml=f.forwardRef(({className:e,...n},a)=>s.jsx(mC,{ref:a,className:fe("-mx-1 my-1 h-px bg-muted",e),...n}));ml.displayName=mC.displayName;const Sp=768;function mA(){const[e,n]=f.useState(void 0);return f.useEffect(()=>{const a=window.matchMedia(`(max-width: ${Sp-1}px)`),o=()=>{n(window.innerWidth<Sp)};return a.addEventListener("change",o),n(window.innerWidth<Sp),()=>a.removeEventListener("change",o)},[]),!!e}const Ie=f.forwardRef(({className:e,type:n,...a},o)=>s.jsx("input",{type:n,className:fe("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",e),ref:o,...a}));Ie.displayName="Input";var pA=["a","button","div","form","h2","h3","img","input","label","li","nav","ol","p","select","span","svg","ul"],zn=pA.reduce((e,n)=>{const a=_2(`Primitive.${n}`),o=f.forwardRef((l,c)=>{const{asChild:d,...m}=l,p=d?a:n;return typeof window<"u"&&(window[Symbol.for("radix-ui")]=!0),s.jsx(p,{...m,ref:c})});return o.displayName=`Primitive.${n}`,{...e,[n]:o}},{}),hA="Separator",Gy="horizontal",gA=["horizontal","vertical"],yC=f.forwardRef((e,n)=>{const{decorative:a,orientation:o=Gy,...l}=e,c=xA(o)?o:Gy,m=a?{role:"none"}:{"aria-orientation":c==="vertical"?c:void 0,role:"separator"};returnLine truncated
|
||||
|
||||
If you want to hide the \`${n.titleName}\`, you can wrap it with our VisuallyHidden component.
|
||||
|
||||
For more information, see https://radix-ui.com/primitives/docs/components/${n.docsSlug}`;return f.useEffect(()=>{e&&(document.getElementById(e)||console.error(a))},[a,e]),null},DA="DialogDescriptionWarning",LA=({contentRef:e,descriptionId:n})=>{const o=`Warning: Missing \`Description\` or \`aria-describedby={undefined}\` for {${$C(DA).contentName}}.`;return f.useEffect(()=>{var c;const l=(c=e.current)==null?void 0:c.getAttribute("aria-describedby");n&&l&&(document.getElementById(n)||console.warn(o))},[o,e,n]),null},xg=NC,zA=kC,vg=RC,_l=AC,Ol=MC,Sd=OC,jd=LC,BC=PC;const UC=xg,PA=vg,HC=f.forwardRef(({className:e,...n},a)=>s.jsx(_l,{className:fe("fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",e),...n,ref:a}));HC.displayName=_l.displayName;const IA=Vs("fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out",{variants:{side:{top:"inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",bottom:"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",left:"inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",right:"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm"}},defaultVariants:{side:"right"}}),bg=f.forwardRef(({side:e="right",className:n,children:a,...o},l)=>s.jsxs(PA,{children:[s.jsx(HC,{}),s.jsxs(Ol,{ref:l,className:fe(IA({side:e}),n),...o,children:[s.jsxs(BC,{className:"absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary",children:[s.jsx(bC,{className:"h-4 w-4"}),s.jsx("span",{className:"sr-only",children:"Close"})]}),a]})]}));bg.displayName=Ol.displayName;const yg=({className:e,...n})=>s.jsx("div",{className:fe("flex flex-col space-y-2 text-center sm:text-left",e),...n});yg.displayName="SheetHeader";const wg=f.forwardRef(({className:e,...n},a)=>s.jsx(Sd,{ref:a,className:fe("text-lg font-semibold text-foreground",e),...n}));wg.displayName=Sd.displayName;const Cg=f.forwardRef(({className:e,...n},a)=>s.jsx(jd,{ref:a,className:fe("text-sm text-muted-foreground",e),...n}));Cg.displayName=jd.displayName;function Be({className:e,...n}){return s.jsx("div",{className:fe("animate-pulse rounded-md bg-primary/10",e),...n})}function $A(e,n=[]){let a=[];function o(c,d){const m=f.createContext(d),p=a.length;a=[...a,d];const h=g=>{var N;const{scope:y,children:w,...j}=g,S=((N=y==null?void 0:y[e])==null?void 0:N[p])||m,E=f.useMemo(()=>j,Object.values(j));return s.jsx(S.Provider,{value:E,children:w})};h.displayName=c+"Provider";function b(g,y){var S;const w=((S=y==null?void 0:y[e])==null?void 0:S[p])||m,j=f.useContext(w);if(j)return j;if(d!==void 0)return d;throw new Error(`\`${g}\` must be used within \`${c}\``)}return[h,b]}const l=()=>{const c=a.map(d=>f.createContext(d));return function(m){const p=(m==null?void 0:m[e])||c;return f.useMemo(()=>({[`__scope${e}`]:{...m,[e]:p}}),[m,p])}};return l.scopeName=e,[o,BA(l,...n)]}function BA(...e){const n=e[0];if(e.length===1)return n;const a=()=>{const o=e.map(l=>({useScope:l(),scopeName:l.scopeName}));return function(c){const d=o.reduce((m,{useScope:p,scopeName:h})=>{const g=p(c)[`__scope${h}`];return{...m,...g}},{});return f.useMemo(()=>({[`__scope${n.scopeName}`]:d}),[d])}};return a.scopeName=n.scopeName,a}function UA(e){const n=HA(e),a=f.forwardRef((o,l)=>{const{children:c,...d}=o,m=f.Children.toArray(c),p=m.find(GA);if(p){const h=p.props.children,b=m.map(g=>g===p?f.Children.count(h)>1?f.Children.only(null):f.isValidElement(h)?h.props.children:null:g);return s.jsx(n,{...d,ref:l,children:f.isValidElement(h)?f.cloneElement(h,void 0,b):null})}return s.jsx(n,{...d,ref:l,children:c})});return a.displayName=`${e}.Slot`,a}function HA(e){const n=f.forwardRef((a,o)=>{const{children:l,...c}=a;if(f.isValidElement(l)){const d=WA(l),m=FA(c,l.props);return l.type!==f.Fragment&&(m.ref=o?gt(o,d):d),f.cloneElement(l,m)}return f.Children.count(l)>1?f.Children.only(null):null});return n.displayName=`${e}.SlotClone`,n}var VC=Symbol("radix.slottable");function VA(e){const n=({children:a})=>s.jsx(s.Fragment,{children:a});return n.displayName=`${e}.Slottable`,n.__radixId=VC,n}function GA(e){return f.isValidElement(e)&&typeof e.type=="function"&&"__radixId"in e.type&&e.type.__radixId===VC}function FA(e,n){const a={...n};for(const o in n){const l=e[o],c=n[o];/^on[A-Z]/.test(o)?l&&c?a[o]=(...m)=>{const p=c(...m);return l(...m),p}:l&&(a[o]=l):o==="style"?a[o]={...l,...c}:o==="className"&&(a[o]=[l,c].filter(Boolean).join(" "))}return{...e,...a}
Line truncated
|
||||
* @license React
|
||||
* use-sync-external-store-shim.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 q_(){if(Ky)return Tp;Ky=1;var e=Ku();function n(g,y){return g===y&&(g!==0||1/g===1/y)||g!==g&&y!==y}var a=typeof Object.is=="function"?Object.is:n,o=e.useState,l=e.useEffect,c=e.useLayoutEffect,d=e.useDebugValue;function m(g,y){var w=y(),j=o({inst:{value:w,getSnapshot:y}}),S=j[0].inst,E=j[1];return c(function(){S.value=w,S.getSnapshot=y,p(S)&&E({inst:S})},[g,w,y]),l(function(){return p(S)&&E({inst:S}),g(function(){p(S)&&E({inst:S})})},[g]),d(w),w}function p(g){var y=g.getSnapshot;g=g.value;try{var w=y();return!a(g,w)}catch{return!0}}function h(g,y){return y()}var b=typeof window>"u"||typeof window.document>"u"||typeof window.document.createElement>"u"?h:m;return Tp.useSyncExternalStore=e.useSyncExternalStore!==void 0?e.useSyncExternalStore:b,Tp}var Xy;function K_(){return Xy||(Xy=1,kp.exports=q_()),kp.exports}var X_=K_();function Z_(){return X_.useSyncExternalStore(Q_,()=>!0,()=>!1)}function Q_(){return()=>{}}var Eg="Avatar",[J_]=W_(Eg),[eO,ES]=J_(Eg),kS=f.forwardRef((e,n)=>{const{__scopeAvatar:a,...o}=e,[l,c]=f.useState("idle");return s.jsx(eO,{scope:a,imageLoadingStatus:l,onImageLoadingStatusChange:c,children:s.jsx(zn.span,{...o,ref:n})})});kS.displayName=Eg;var TS="AvatarImage",RS=f.forwardRef((e,n)=>{const{__scopeAvatar:a,src:o,onLoadingStatusChange:l=()=>{},...c}=e,d=ES(TS,a),m=tO(o,c),p=Wt(h=>{l(h),d.onImageLoadingStatusChange(h)});return Pt(()=>{m!=="idle"&&p(m)},[m,p]),m==="loaded"?s.jsx(zn.img,{...c,ref:n,src:o}):null});RS.displayName=TS;var AS="AvatarFallback",MS=f.forwardRef((e,n)=>{const{__scopeAvatar:a,delayMs:o,...l}=e,c=ES(AS,a),[d,m]=f.useState(o===void 0);return f.useEffect(()=>{if(o!==void 0){const p=window.setTimeout(()=>m(!0),o);return()=>window.clearTimeout(p)}},[o]),d&&c.imageLoadingStatus!=="loaded"?s.jsx(zn.span,{...l,ref:n}):null});MS.displayName=AS;function Zy(e,n){return e?n?(e.src!==n&&(e.src=n),e.complete&&e.naturalWidth>0?"loaded":"loading"):"error":"idle"}function tO(e,{referrerPolicy:n,crossOrigin:a}){const o=Z_(),l=f.useRef(null),c=o?(l.current||(l.current=new window.Image),l.current):null,[d,m]=f.useState(()=>Zy(c,e));return Pt(()=>{m(Zy(c,e))},[c,e]),Pt(()=>{const p=g=>()=>{m(g)};if(!c)return;const h=p("loaded"),b=p("error");return c.addEventListener("load",h),c.addEventListener("error",b),n&&(c.referrerPolicy=n),typeof a=="string"&&(c.crossOrigin=a),()=>{c.removeEventListener("load",h),c.removeEventListener("error",b)}},[c,a,n]),d}var _S=kS,OS=RS,DS=MS;const Ia=f.forwardRef(({className:e,...n},a)=>s.jsx(_S,{ref:a,className:fe("relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",e),...n}));Ia.displayName=_S.displayName;const Co=f.forwardRef(({className:e,...n},a)=>s.jsx(OS,{ref:a,className:fe("aspect-square h-full w-full",e),...n}));Co.displayName=OS.displayName;const $a=f.forwardRef(({className:e,...n},a)=>s.jsx(DS,{ref:a,className:fe("flex h-full w-full items-center justify-center rounded-full bg-muted",e),...n}));$a.displayName=DS.displayName;const LS=f.createContext(null);function nO({children:e}){const[n,a]=f.useState([]),[o,l]=f.useState(0),[c,d]=f.useState(!0),m=f.useCallback(async()=>{try{const g=await Ee.getNotifications();g.data&&(a(g.data.items),l(g.data.unreadCount))}catch{}finally{d(!1)}},[]),p=f.useCallback(async()=>{var g;try{const y=await Ee.markNotificationsRead("all");a(w=>w.map(j=>({...j,read:!0}))),l(((g=y.data)==null?void 0:g.unreadCount)??0)}catch{}},[]),h=f.useCallback(async g=>{var y;try{const w=await Ee.dismissNotification(g);a(j=>j.filter(S=>S.id!==g)),l(((y=w.data)==null?void 0:y.unreadCount)??0)}catch{}},[]);f.useEffect(()=>{m();const g=setInterval(m,3e4);return()=>clearInterval(g)},[m]);const b=f.useMemo(()=>({items:n,unreadCount:o,loading:c,refresh:m,markAllRead:p,dismiss:h}),[n,o,c,m,p,h]);return s.jsx(LS.Provider,{value:b,children:e})}function rO(){const e=f.useContext(LS);if(!e)throw new Error("useNotifications must be used within NotificationsProvider");return e}const aO=Vs("inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",{variants:{variant:{default:"border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",secondary:"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",destructive:"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",outline:"text-foreground"}},defaultVariants:{variant:"default"}});function An({className:e,variant:n,...a}){return s.jsx("div",{className:fe(aO({variant:n}),e),...a})}function oO(e){const n=sO(e),a=f.forwardRef((o,l)=>{const{children:c,...d}=o,m=f.Children.toArray(c),p=m.find(lO);if(p){const h=p.props.children,b=m.map(g=>g===p?f.Children.count(h)>1?f.Children.only(null):f.isValidElement(h)?h.props.children:null:g);return s.jsx(n,{...d,ref:l,children:f.isValidElement(h)?f.cloneElement(h,void 0,b):null})}return s.jsx(n,{...d,ref:l,children:c})});return a.displayName=`${e}.S
Line truncated
|
||||
--num-groups: ${c.groups.length};
|
||||
--indicator-opacity: ${c.searchMode?0:1};
|
||||
--num-skintones: ${i0};`}),d(()=>{c.customEmoji&&c.database&&z()}),d(()=>{c.customEmoji&&c.customEmoji.length?c.groups!==Nh&&(c.groups=Nh):c.groups!==Mp&&(c.currentGroupIndex&&c.currentGroupIndex--,c.groups=Mp)}),d(()=>{async function Y(){c.databaseLoaded&&(c.currentSkinTone=await c.database.getPreferredSkinTone())}Y()}),d(()=>{c.skinTones=Array(i0).fill().map((Y,le)=>qz(c.skinToneEmoji,le))}),d(()=>{c.skinToneButtonText=c.skinTones[c.currentSkinTone]}),d(()=>{c.skinToneButtonLabel=c.i18n.skinToneLabel.replace("{skinTone}",c.i18n.skinTones[c.currentSkinTone])}),d(()=>{async function Y(){const{database:le}=c,ye=(await Promise.all(Iz.map(Te=>le.getEmojiByUnicodeOrName(Te)))).filter(Boolean);c.defaultFavoriteEmojis=ye}c.databaseLoaded&&Y()});function z(){const{customEmoji:Y,database:le}=c,ye=Y||Pp;le.customEmoji!==ye&&(le.customEmoji=ye)}d(()=>{async function Y(){z();const{database:le,defaultFavoriteEmojis:ye,numColumns:Te}=c,Le=await le.getTopFavoriteEmoji(Te),rt=await xe(H5([...Le,...ye],ht=>ht.unicode||ht.name).slice(0,Te));c.currentFavorites=rt}c.databaseLoaded&&c.defaultFavoriteEmojis&&Y()});function B(Y){Zz(Y,l,()=>{{const le=getComputedStyle(a.rootElement),ye=parseInt(le.getPropertyValue("--num-columns"),10),Te=le.getPropertyValue("direction")==="rtl";c.numColumns=ye,c.isRtl=Te}})}function K(Y){xP(Y,l,le=>{for(const{target:ye,isIntersecting:Te}of le)ye.classList.toggle("onscreen",Te)})}d(()=>{async function Y(){const{searchText:le,currentGroup:ye,databaseLoaded:Te,customEmoji:Le}=c;if(!Te)c.currentEmojis=[],c.searchMode=!1;else if(le.length>=Oz){const rt=await L(le);c.searchText===le&&(w(rt),j(!0))}else{const{id:rt}=ye;if(rt!==-1||Le&&Le.length){const ht=await se(rt);c.currentGroup.id===rt&&(w(ht),j(!1))}}}Y()});const U=()=>{Nu(()=>tP(a.tabpanelElement))};d(()=>{const{currentEmojis:Y,emojiVersion:le}=c,ye=Y.filter(Te=>Te.unicode).filter(Te=>l0(Te)&&!Eh.has(Te.unicode));if(!le&&ye.length)w(Y),Nu(()=>q(ye));else{const Te=le?Y:Y.filter(ue);w(Te),U()}});function q(Y){Jz(Y,a.baselineEmoji,h)?U():c.currentEmojis=[...c.currentEmojis]}function ue(Y){return!Y.unicode||!l0(Y)||Eh.get(Y.unicode)}async function pe(Y){const le=c.emojiVersion||await Op();return Y.filter(({version:ye})=>!ye||ye<=le)}async function xe(Y){return Kz(Y,c.emojiVersion||await Op())}async function se(Y){const le=Y===-1?c.customEmoji:await c.database.getEmojiByGroup(Y);return xe(await pe(le))}async function L(Y){return xe(await pe(await c.database.getEmojiBySearchQuery(Y)))}d(()=>{}),d(()=>{function Y(){const{searchMode:ye,currentEmojis:Te}=c;if(ye)return[{category:"",emojis:Te}];const Le=new Map;for(const rt of Te){const ht=rt.category||"";let Mt=Le.get(ht);Mt||(Mt=[],Le.set(ht,Mt)),Mt.push(rt)}return[...Le.entries()].map(([rt,ht])=>({category:rt,emojis:ht})).sort((rt,ht)=>c.customCategorySorting(rt.category,ht.category))}const le=Y();S(le)}),d(()=>{c.activeSearchItemId=c.activeSearchItem!==-1&&c.currentEmojis[c.activeSearchItem].id}),d(()=>{const{rawSearchText:Y}=c;B5(()=>{c.searchText=(Y||"").trim(),c.activeSearchItem=-1})});function I(Y){if(!c.searchMode||!c.currentEmojis.length)return;const le=ye=>{yr(Y),c.activeSearchItem=Dp(ye,c.activeSearchItem,c.currentEmojis)};switch(Y.key){case"ArrowDown":return le(!1);case"ArrowUp":return le(!0);case"Enter":if(c.activeSearchItem===-1)c.activeSearchItem=0;else return yr(Y),_(c.currentEmojis[c.activeSearchItem].id)}}function H(Y){const{target:le}=Y,ye=le.closest(".nav-button");if(!ye)return;const Te=parseInt(ye.dataset.groupId,10);a.searchElement.value="",c.rawSearchText="",c.searchText="",c.activeSearchItem=-1,c.currentGroupIndex=c.groups.findIndex(Le=>Le.id===Te)}function Z(Y){const{target:le,key:ye}=Y,Te=Le=>{Le&&(yr(Y),Le.focus())};switch(ye){case"ArrowLeft":return Te(le.previousElementSibling);case"ArrowRight":return Te(le.nextElementSibling);case"Home":return Te(le.parentElement.firstElementChild);case"End":return Te(le.parentElement.lastElementChild)}}async function te(Y){const le=await c.database.getEmojiByUnicodeOrName(Y),ye=[...c.currentEmojis,...c.currentFavorites].find(Le=>Le.id===Y),Te=ye.unicode&&E(ye,c.currentSkinTone);return await c.database.incrementFavoriteEmojiCount(Y),{emoji:le,skinTone:c.currentSkinTone,...Te&&{unicode:Te},...ye.name&&{name:ye.name}}}async function _(Y){const le=te(Y);b("emoji-click-sync",le),b("emoji-click",await le)}function D(Y){const{target:le}=Y;if(!le.classList.contains("emoji"))return;yr(Y);const ye=le.id.substring(4);_(ye)}function G(Y){c.currentSkinTone=Y,c.skinTonePickerExpanded=!1,p("skintone-button"),b("skin-tone-change",{skinTone:Y}),c.database.setPreferredSkinTone(Y)}function V(Y){const{target:{id:le}}=Y,ye=le&&le.match(/^skintone-(\d)/);if(!ye)return;yr(Y);const Te=parseInt(ye[1],10);G(Te)}function F(Y){c.skinTonePickerExpanded=!c.skinTonePickerExpanded,c.activeSkinTone=c.currentSkinTone,c.skinTonePickerExpanded&&(yr(Y),Nu(()=>p("skintone-list")))}d(()=>{c.skinTonePickerExpanded?a.skinToneDropdown.addEventListener("transitionend",()=>{c.ski
Line truncated
|
||||
In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}function w0(e,n){var a=Object.keys(e);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);n&&(o=o.filter(function(l){return Object.getOwnPropertyDescriptor(e,l).enumerable})),a.push.apply(a,o)}return a}function C0(e){for(var n=1;n<arguments.length;n++){var a=arguments[n]!=null?arguments[n]:{};n%2?w0(Object(a),!0).forEach(function(o){cI(e,o,a[o])}):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(a)):w0(Object(a)).forEach(function(o){Object.defineProperty(e,o,Object.getOwnPropertyDescriptor(a,o))})}return e}function fI(e,n){if(e==null)return{};var a,o,l=mI(e,n);if(Object.getOwnPropertySymbols){var c=Object.getOwnPropertySymbols(e);for(o=0;o<c.length;o++)a=c[o],n.indexOf(a)===-1&&{}.propertyIsEnumerable.call(e,a)&&(l[a]=e[a])}return l}function mI(e,n){if(e==null)return{};var a={};for(var o in e)if({}.hasOwnProperty.call(e,o)){if(n.indexOf(o)!==-1)continue;a[o]=e[o]}return a}function pI(e,n){return lI(e)||uI(e,n)||xI(e,n)||dI()}function hI(e,n){if(typeof e!="object"||!e)return e;var a=e[Symbol.toPrimitive];if(a!==void 0){var o=a.call(e,n);if(typeof o!="object")return o;throw new TypeError("@@toPrimitive must return a primitive value.")}return(n==="string"?String:Number)(e)}function gI(e){var n=hI(e,"string");return typeof n=="symbol"?n:n+""}function xI(e,n){if(e){if(typeof e=="string")return y0(e,n);var a={}.toString.call(e).slice(8,-1);return a==="Object"&&e.constructor&&(a=e.constructor.name),a==="Map"||a==="Set"?Array.from(e):a==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(a)?y0(e,n):void 0}}function vI(e,n,a){return n in e?Object.defineProperty(e,n,{value:a,enumerable:!0,configurable:!0,writable:!0}):e[n]=a,e}function S0(e,n){var a=Object.keys(e);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);n&&(o=o.filter(function(l){return Object.getOwnPropertyDescriptor(e,l).enumerable})),a.push.apply(a,o)}return a}function j0(e){for(var n=1;n<arguments.length;n++){var a=arguments[n]!=null?arguments[n]:{};n%2?S0(Object(a),!0).forEach(function(o){vI(e,o,a[o])}):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(a)):S0(Object(a)).forEach(function(o){Object.defineProperty(e,o,Object.getOwnPropertyDescriptor(a,o))})}return e}function bI(){for(var e=arguments.length,n=new Array(e),a=0;a<e;a++)n[a]=arguments[a];return function(o){return n.reduceRight(function(l,c){return c(l)},o)}}function Ji(e){return function n(){for(var a=this,o=arguments.length,l=new Array(o),c=0;c<o;c++)l[c]=arguments[c];return l.length>=e.length?e.apply(this,l):function(){for(var d=arguments.length,m=new Array(d),p=0;p<d;p++)m[p]=arguments[p];return n.apply(a,[].concat(l,m))}}}function Yu(e){return{}.toString.call(e).includes("Object")}function yI(e){return!Object.keys(e).length}function vl(e){return typeof e=="function"}function wI(e,n){return Object.prototype.hasOwnProperty.call(e,n)}function CI(e,n){return Yu(n)||Oa("changeType"),Object.keys(n).some(function(a){return!wI(e,a)})&&Oa("changeField"),n}function SI(e){vl(e)||Oa("selectorType")}function jI(e){vl(e)||Yu(e)||Oa("handlerType"),Yu(e)&&Object.values(e).some(function(n){return!vl(n)})&&Oa("handlersType")}function NI(e){e||Oa("initialIsRequired"),Yu(e)||Oa("initialType"),yI(e)&&Oa("initialContent")}function EI(e,n){throw new Error(e[n]||e.default)}var kI={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"},Oa=Ji(EI)(kI),hu={changes:CI,selector:SI,handler:jI,initial:NI};function TI(e){var n=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{};hu.initial(e),hu.handler(n);var a={current:e},o=Ji(MI)(a,n),l=Ji(AI)(a),c=Ji(hu.changes)(e),d=Ji(RI)(a);function m(){var h=arguments.length>0&&arguments[0]!==void 0?arguments[0]:function(b){return b};return hu.selector(h),h(a.current)}function p(h){bI(o,l,c,d)(h)}return[m,p]}function RI(e,n){return vl(n)?n(e.current):n}function AI(e,n){return e.current=j0(j0({},e.current),n),n}function MI(e,n,a){return vl(n)?n(e.current):Object.keys(a).forEach(function(o){var l;return(l=n[o])===null||l===void 0?void 0:l.call(n,e.current[o])}),a}var _I={create:TI},OI={paths:{vs:"https://cdn.jsdelivr.net/npm/monaco-editor@0.55.1/min/vs"}};function DI(e){return function n(){for(var a=this,o=arguments.length,l=new Array(o),c=0;c<o;c++)l[c]=arguments[c];return l.length>=e.length?e.apply(this,l):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
|
||||
`},N0=DI(II)(Y5),$I={config:zI},BI=function(){for(var n=arguments.length,a=new Array(n),o=0;o<n;o++)a[o]=arguments[o];return function(l){return a.reduceRight(function(c,d){return d(c)},l)}};function q5(e,n){return Object.keys(n).forEach(function(a){n[a]instanceof Object&&e[a]&&Object.assign(n[a],q5(e[a],n[a]))}),C0(C0({},e),n)}var UI={type:"cancelation",msg:"operation is manually canceled"};function Ip(e){var n=!1,a=new Promise(function(o,l){e.then(function(c){return n?l(UI):o(c)}),e.catch(l)});return a.cancel=function(){return n=!0},a}var HI=["monaco"],VI=_I.create({config:OI,isInitialized:!1,resolve:null,reject:null,monaco:null}),K5=pI(VI,2),Ul=K5[0],Ud=K5[1];function GI(e){var n=$I.config(e),a=n.monaco,o=fI(n,HI);Ud(function(l){return{config:q5(l.config,o),monaco:a}})}function FI(){var e=Ul(function(n){var a=n.monaco,o=n.isInitialized,l=n.resolve;return{monaco:a,isInitialized:o,resolve:l}});if(!e.isInitialized){if(Ud({isInitialized:!0}),e.monaco)return e.resolve(e.monaco),Ip($p);if(window.monaco&&window.monaco.editor)return X5(window.monaco),e.resolve(window.monaco),Ip($p);BI(WI,qI)(KI)}return Ip($p)}function WI(e){return document.body.appendChild(e)}function YI(e){var n=document.createElement("script");return e&&(n.src=e),n}function qI(e){var n=Ul(function(o){var l=o.config,c=o.reject;return{config:l,reject:c}}),a=YI("".concat(n.config.paths.vs,"/loader.js"));return a.onload=function(){return e()},a.onerror=n.reject,a}function KI(){var e=Ul(function(a){var o=a.config,l=a.resolve,c=a.reject;return{config:o,resolve:l,reject:c}}),n=window.require;n.config(e.config),n(["vs/editor/editor.main"],function(a){var o=a.m||a;X5(o),e.resolve(o)},function(a){e.reject(a)})}function X5(e){Ul().monaco||Ud({monaco:e})}function XI(){return Ul(function(e){var n=e.monaco;return n})}var $p=new Promise(function(e,n){return Ud({resolve:e,reject:n})}),Z5={config:GI,init:FI,__getMonacoInstance:XI},ZI={wrapper:{display:"flex",position:"relative",textAlign:"initial"},fullWidth:{width:"100%"},hide:{display:"none"}},Bp=ZI,QI={container:{display:"flex",height:"100%",width:"100%",justifyContent:"center",alignItems:"center"}},JI=QI;function e$({children:e}){return me.createElement("div",{style:JI.container},e)}var t$=e$,n$=t$;function r$({width:e,height:n,isEditorReady:a,loading:o,_ref:l,className:c,wrapperProps:d}){return me.createElement("section",{style:{...Bp.wrapper,width:e,height:n},...d},!a&&me.createElement(n$,null,o),me.createElement("div",{ref:l,style:{...Bp.fullWidth,...!a&&Bp.hide},className:c}))}var a$=r$,Q5=f.memo(a$);function o$(e){f.useEffect(e,[])}var J5=o$;function s$(e,n,a=!0){let o=f.useRef(!0);f.useEffect(o.current||!a?()=>{o.current=!1}:e,n)}var Mn=s$;function al(){}function Rs(e,n,a,o){return i$(e,o)||l$(e,n,a,o)}function i$(e,n){return e.editor.getModel(eN(e,n))}function l$(e,n,a,o){return e.editor.createModel(n,a,o?eN(e,o):void 0)}function eN(e,n){return e.Uri.parse(n)}function c$({original:e,modified:n,language:a,originalLanguage:o,modifiedLanguage:l,originalModelPath:c,modifiedModelPath:d,keepCurrentOriginalModel:m=!1,keepCurrentModifiedModel:p=!1,theme:h="light",loading:b="Loading...",options:g={},height:y="100%",width:w="100%",className:j,wrapperProps:S={},beforeMount:E=al,onMount:N=al}){let[k,T]=f.useState(!1),[A,O]=f.useState(!0),M=f.useRef(null),z=f.useRef(null),B=f.useRef(null),K=f.useRef(N),U=f.useRef(E),q=f.useRef(!1);J5(()=>{let se=Z5.init();return se.then(L=>(z.current=L)&&O(!1)).catch(L=>(L==null?void 0:L.type)!=="cancelation"&&console.error("Monaco initialization: error:",L)),()=>M.current?xe():se.cancel()}),Mn(()=>{if(M.current&&z.current){let se=M.current.getOriginalEditor(),L=Rs(z.current,e||"",o||a||"text",c||"");L!==se.getModel()&&se.setModel(L)}},[c],k),Mn(()=>{if(M.current&&z.current){let se=M.current.getModifiedEditor(),L=Rs(z.current,n||"",l||a||"text",d||"");L!==se.getModel()&&se.setModel(L)}},[d],k),Mn(()=>{let se=M.current.getModifiedEditor();se.getOption(z.current.editor.EditorOption.readOnly)?se.setValue(n||""):n!==se.getValue()&&(se.executeEdits("",[{range:se.getModel().getFullModelRange(),text:n||"",forceMoveMarkers:!0}]),se.pushUndoStop())},[n],k),Mn(()=>{var se,L;(L=(se=M.current)==null?void 0:se.getModel())==null||L.original.setValue(e||"")},[e],k),Mn(()=>{let{original:se,modified:L}=M.current.getModel();z.current.editor.setModelLanguage(se,o||a||"text"),z.current.editor.setModelLanguage(L,l||a||"text")},[a,o,l],k),Mn(()=>{var se;(se=z.current)==null||se.editor.setTheme(h)},[h],k),Mn(()=>{var se;(se=M.current)==null||se.updateOptions(g)},[g],k);let ue=f.useCallback(()=>{var I;if(!z.current)return;U.current(z.current);let se=Rs(z.current,e||"",o||a||"text",c||""),L=Rs(z.current,n||"",l||a||"text",d||"");(I=M.current)==null||I.setModel({original:se,modified:L})},[a,n,l,e,o,c,d]),pe=f.useCallback(()=>{var se;!q.current&&B.current&&(M.current=z.current.editor.createDiffEditor(B.current,{automaticLayout:!0,...g}),ue(),(se=z.current)==null||se.editor.setTheme(h),T(!0),q.current=!0
Line truncated
|
||||
987654321098765432`,className:"min-h-[120px] font-mono text-sm"})]}),s.jsx(ei,{result:a.owners}),s.jsx("div",{className:"mt-auto",children:s.jsx(he,{type:"button",variant:"secondary",disabled:o||!e.ownerUserIds.trim(),onClick:l,children:o?"Checking with Discord…":"Verify owner IDs"})})]})}function I$({onContinue:e}){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(ge,{icon:ed,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(We,{className:"mt-6 text-left",children:s.jsx(Ye,{children:"After restarting, sign in with Discord to open the dashboard."})}),s.jsx(he,{type:"button",size:"lg",className:"mt-8 min-w-[220px]",onClick:e,children:"Continue to sign in"})]})})}function $$({currentIndex:e,validating:n,saving:a,onBack:o,onNext:l,onFinish:c}){const d=e===Os.length-1;return s.jsxs("div",{className:"flex flex-wrap items-center justify-between gap-3",children:[s.jsxs(he,{type:"button",variant:"ghost",disabled:e===0||a,onClick:o,children:[s.jsx(ge,{icon:J0,size:18,className:"mr-1"}),"Back"]}),s.jsx("div",{className:"flex flex-1 justify-end gap-2",children:d?s.jsx(he,{type:"button",size:"lg",disabled:a||n,onClick:c,className:"min-w-[200px]",children:a?"Saving…":"Finish setup"}):s.jsxs(he,{type:"button",size:"lg",disabled:n||a,onClick:l,children:["Continue",s.jsx(ge,{icon:Uh,size:18,className:"ml-1"})]})})]})}function B$({values:e,setValues:n,validation:a,validating:o,onValidate:l}){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(ze,{htmlFor:"emeraldApiKey",children:"Emerald API key (Optional)"}),s.jsx(Ie,{id:"emeraldApiKey",type:"password",autoComplete:"off",value:e.emeraldApiKey,onChange:c=>n({...e,emeraldApiKey:c.target.value}),placeholder:"em_...",className:"h-11 font-mono text-sm"})]}),s.jsx(ei,{result:a.emeraldApi}),s.jsx("div",{className:"mt-auto",children:s.jsx(he,{type:"button",variant:"secondary",disabled:o,onClick:l,children:o?"Checking…":e.emeraldApiKey?"Verify key":"Skip step"})})]})}function U$(){const e=crypto.randomUUID().replace(/-/g,""),n=crypto.randomUUID().replace(/-/g,"");return e+n}function H$(e){return e.split(/[\s,]+/).map(n=>n.trim()).filter(Boolean)}function E0(e){return e==="oauth"?"oauth":e==="owners"?"owners":e==="emeraldApi"?"emeraldApi":e}function V$(){const e=Ro(),{branding:n}=od(),{loading:a,error:o,refresh:l,status:c}=Pj(),[d,m]=f.useState(0),[p,h]=f.useState({botToken:"",secretKey:"",clientId:"",clientSecret:"",redirectUri:"",ownerUserIds:"",emeraldApiKey:""}),[b,g]=f.useState({}),[y,w]=f.useState(!1),[j,S]=f.useState(!1),[E,N]=f.useState(""),[k,T]=f.useState(!1),A=Os[d];f.useEffect(()=>{c!=null&&c.suggestedRedirectUri&&!p.redirectUri&&h(q=>({...q,redirectUri:c.suggestedRedirectUri}))},[c==null?void 0:c.suggestedRedirectUri,p.redirectUri]);const O=f.useMemo(()=>({botToken:p.botToken,secretKey:p.secretKey,clientId:p.clientId,clientSecret:p.clientSecret,redirectUri:p.redirectUri,ownerUserIds:H$(p.ownerUserIds),emeraldApiKey:p.emeraldApiKey}),[p]);async function M(){w(!0),N("");try{const q=await Ee.validateSetup(O);return g(ue=>{var pe;return{...ue,...(pe=q.data)==null?void 0:pe.results}}),q.data}catch(q){return N(q instanceof Error?q.message:"Validation failed"),null}finally{w(!1)}}async function z(){var pe,xe;const q=E0(A.id),ue=await M();return!!((xe=(pe=ue==null?void 0:ue.results)==null?void 0:pe[q])!=null&&xe.ok)}async function B(){const q=E0(A.id),ue=b[q];!(ue!=null&&ue.ok)&&!await z()||m(pe=>Math.min(pe+1,Os.length-1))}async function K(){S(!0),N("");try{const q=await M();if(!(q!=null&&q.ok)){N("Fix the steps marked with errors before finishing.");return}await Ee.completeSetup(O),T(!0)}catch(q){N(q instanceof Error?q.message:"Could not save setup")}finally{S(!1)}}function U(){const q={values:p,setValues:h,validation:b,validating:y};switch(A.id){case"botToken":return s.jsx(O$,{...q,onValidate:()=>void z()}
Line truncated
|
||||
@@ -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,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-BpDEEtaJ.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DW3eyGbF.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 81 KiB |
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"ticket": "ticket-0064",
|
||||
"id":207937608779694080
|
||||
}
|
||||
Loaded 100 of 103 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user