Compare commits
278
Commits
7.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 |
No files matched your search
@@ -0,0 +1,2 @@
|
|||||||
|
Configs/supportbot.yml filter=supportbot-token
|
||||||
|
Configs/api.yml filter=api-secrets
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
|
|
||||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
|
|
||||||
|
|
||||||
name: Node.js CI
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [master]
|
|
||||||
pull_request:
|
|
||||||
branches: [master]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
node-version: [10.x, 12.x, 14.x, 15.x]
|
|
||||||
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
|
||||||
uses: actions/setup-node@v1
|
|
||||||
with:
|
|
||||||
node-version: ${{ matrix.node-version }}
|
|
||||||
- run: npm ci
|
|
||||||
- run: npm install discord.js --save
|
|
||||||
- run: npm install js-yaml
|
|
||||||
- run: npm run build --if-present
|
|
||||||
- run: node index.js
|
|
||||||
+10
-1
@@ -1 +1,10 @@
|
|||||||
node_modules/
|
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],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
// SupportBot | Emerald Services
|
|
||||||
// Example (default) Addon
|
|
||||||
|
|
||||||
const fs = require("fs");
|
|
||||||
|
|
||||||
const Discord = require("discord.js");
|
|
||||||
const yaml = require("js-yaml");
|
|
||||||
const supportbot = yaml.load(fs.readFileSync("./Data/supportbot.yml", "utf8"));
|
|
||||||
const cmdconfig = yaml.load(fs.readFileSync("./Data/commands.yml", "utf8"));
|
|
||||||
|
|
||||||
// Your Addon Config file! Saved in /Addons/Settings/YourAddonConfig.yml
|
|
||||||
// const exampleaddon = yaml.load(fs.readFileSync('./Addons/Settings/YourAddonConfig.yml', 'utf8'));
|
|
||||||
|
|
||||||
const Command = require("../Structures/Command.js");
|
|
||||||
|
|
||||||
module.exports = new Command({
|
|
||||||
name: "hello",
|
|
||||||
description: "hello",
|
|
||||||
slashCommandOptions: [],
|
|
||||||
permission: "SEND_MESSAGES",
|
|
||||||
|
|
||||||
async run(interaction) {
|
|
||||||
const Embed = new Discord.MessageEmbed()
|
|
||||||
.setDescription("Hello World!")
|
|
||||||
.setColor(supportbot.InfoColour);
|
|
||||||
|
|
||||||
interaction.reply({
|
|
||||||
embeds: [Embed],
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
// SupportBot | Emerald Services
|
|
||||||
// Ticket Add Command
|
|
||||||
|
|
||||||
const fs = require("fs");
|
|
||||||
|
|
||||||
const Discord = require("discord.js");
|
|
||||||
const yaml = require("js-yaml");
|
|
||||||
const supportbot = yaml.load(fs.readFileSync("./Data/supportbot.yml", "utf8"));
|
|
||||||
const cmdconfig = yaml.load(fs.readFileSync("./Data/commands.yml", "utf8"));
|
|
||||||
|
|
||||||
const Command = require("../Structures/Command.js");
|
|
||||||
|
|
||||||
async function asyncForEach(array, callback) {
|
|
||||||
for (let index = 0; index < array.length; index++) {
|
|
||||||
await callback(array[index], index, array);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = new Command({
|
|
||||||
name: cmdconfig.TicketAdd,
|
|
||||||
description: cmdconfig.TicketAddDesc,
|
|
||||||
slashCommandOptions: [
|
|
||||||
{
|
|
||||||
name: "user",
|
|
||||||
description: "User to add",
|
|
||||||
type: "USER",
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
permission: "SEND_MESSAGES",
|
|
||||||
|
|
||||||
async run(interaction) {
|
|
||||||
let SupportStaff =
|
|
||||||
interaction.guild.roles.cache.find(
|
|
||||||
(SupportTeam) => SupportTeam.name === supportbot.Staff
|
|
||||||
) ||
|
|
||||||
interaction.guild.roles.cache.find(
|
|
||||||
(SupportTeam) => SupportTeam.id === supportbot.Staff
|
|
||||||
);
|
|
||||||
let Admins =
|
|
||||||
interaction.guild.roles.cache.find(
|
|
||||||
(AdminUser) => AdminUser.name === supportbot.Admin
|
|
||||||
) ||
|
|
||||||
interaction.guild.roles.cache.find(
|
|
||||||
(AdminUser) => AdminUser.id === 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 (
|
|
||||||
interaction.member.roles.cache.has(SupportStaff.id) ||
|
|
||||||
interaction.member.roles.cache.has(Admins.id)
|
|
||||||
) {
|
|
||||||
if (
|
|
||||||
!interaction.channel.name.startsWith(`${supportbot.TicketChannel}-`)
|
|
||||||
) {
|
|
||||||
const Exists = new Discord.MessageEmbed()
|
|
||||||
.setTitle("No Ticket Found!")
|
|
||||||
.setDescription(`${supportbot.NoValidTicket}`)
|
|
||||||
.setColor(supportbot.WarningColour);
|
|
||||||
return interaction.reply({ embeds: [Exists] });
|
|
||||||
}
|
|
||||||
|
|
||||||
let uMember = interaction.options.getUser("user");
|
|
||||||
const UserNotExist = new Discord.MessageEmbed()
|
|
||||||
.setTitle("User Not Found!")
|
|
||||||
.setDescription(
|
|
||||||
`${supportbot.UserNotFound}\n\nTry Again:\`/${cmdconfig.TicketAdd} <user#0000>\``
|
|
||||||
)
|
|
||||||
.setColor(supportbot.ErrorColour);
|
|
||||||
|
|
||||||
if (!uMember) return interaction.reply({ embeds: [UserNotExist] });
|
|
||||||
|
|
||||||
interaction.channel.permissionOverwrites.edit(uMember, {
|
|
||||||
VIEW_CHANNEL: true,
|
|
||||||
READ_MESSAGE_HISTORY: true,
|
|
||||||
SEND_MESSAGES: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const Complete = new Discord.MessageEmbed()
|
|
||||||
.setTitle("User Added!")
|
|
||||||
.setDescription(supportbot.AddedUser.replace(/%user%/g, uMember.id))
|
|
||||||
.setTimestamp()
|
|
||||||
.setColor(supportbot.EmbedColour);
|
|
||||||
interaction.reply({ embeds: [Complete] });
|
|
||||||
} else {
|
|
||||||
return interaction.reply({ embeds: [NoPerms] });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
// SupportBot | Emerald Services
|
|
||||||
// Ticket Remove Command
|
|
||||||
|
|
||||||
const fs = require("fs");
|
|
||||||
|
|
||||||
const Discord = require("discord.js");
|
|
||||||
const yaml = require("js-yaml");
|
|
||||||
const supportbot = yaml.load(fs.readFileSync("./Data/supportbot.yml", "utf8"));
|
|
||||||
const cmdconfig = yaml.load(fs.readFileSync("./Data/commands.yml", "utf8"));
|
|
||||||
|
|
||||||
const Command = require("../Structures/Command.js");
|
|
||||||
|
|
||||||
async function asyncForEach(array, callback) {
|
|
||||||
for (let index = 0; index < array.length; index++) {
|
|
||||||
await callback(array[index], index, array);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = new Command({
|
|
||||||
name: cmdconfig.TicketRemove,
|
|
||||||
description: cmdconfig.TicketRemoveDesc,
|
|
||||||
slashCommandOptions: [
|
|
||||||
{
|
|
||||||
name: "user",
|
|
||||||
description: "User to remove",
|
|
||||||
type: "USER",
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
permission: "SEND_MESSAGES",
|
|
||||||
|
|
||||||
async run(interaction) {
|
|
||||||
let SupportStaff =
|
|
||||||
interaction.guild.roles.cache.find(
|
|
||||||
(SupportTeam) => SupportTeam.name === supportbot.Staff
|
|
||||||
) ||
|
|
||||||
interaction.guild.roles.cache.find(
|
|
||||||
(SupportTeam) => SupportTeam.id === supportbot.Staff
|
|
||||||
);
|
|
||||||
let Admins =
|
|
||||||
interaction.guild.roles.cache.find(
|
|
||||||
(AdminUser) => AdminUser.name === supportbot.Admin
|
|
||||||
) ||
|
|
||||||
interaction.guild.roles.cache.find(
|
|
||||||
(AdminUser) => AdminUser.id === 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 (
|
|
||||||
interaction.member.roles.cache.has(SupportStaff.id) ||
|
|
||||||
interaction.member.roles.cache.has(Admins.id)
|
|
||||||
) {
|
|
||||||
if (
|
|
||||||
!interaction.channel.name.startsWith(`${supportbot.TicketChannel}-`)
|
|
||||||
) {
|
|
||||||
const Exists = new Discord.MessageEmbed()
|
|
||||||
.setTitle("No Ticket Found!")
|
|
||||||
.setDescription(`${supportbot.NoValidTicket}`)
|
|
||||||
.setColor(supportbot.WarningColour);
|
|
||||||
return interaction.reply({ embeds: [Exists] });
|
|
||||||
}
|
|
||||||
|
|
||||||
let uMember = interaction.options.getUser("user");
|
|
||||||
const UserNotExist = new Discord.MessageEmbed()
|
|
||||||
.setTitle("User Not Found!")
|
|
||||||
.setDescription(
|
|
||||||
`${supportbot.UserNotFound}\n\nTry Again:\`/${cmdconfig.TicketRemove} <user#0000>\``
|
|
||||||
)
|
|
||||||
.setColor(supportbot.ErrorColour);
|
|
||||||
|
|
||||||
if (!uMember) return interaction.reply({ embeds: [UserNotExist] });
|
|
||||||
|
|
||||||
interaction.channel.permissionOverwrites.edit(uMember, {
|
|
||||||
VIEW_CHANNEL: false,
|
|
||||||
READ_MESSAGE_HISTORY: false,
|
|
||||||
SEND_MESSAGES: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const Complete = new Discord.MessageEmbed()
|
|
||||||
.setTitle("User Removed!")
|
|
||||||
.setDescription(supportbot.RemovedUser.replace(/%user%/g, uMember.id))
|
|
||||||
.setTimestamp()
|
|
||||||
.setColor(supportbot.EmbedColour);
|
|
||||||
interaction.reply({ embeds: [Complete] });
|
|
||||||
} else {
|
|
||||||
return interaction.reply({ embeds: [NoPerms] });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -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 });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -1,227 +0,0 @@
|
|||||||
// SupportBot | Emerald Services
|
|
||||||
// Close Ticket Command
|
|
||||||
|
|
||||||
const fs = require("fs");
|
|
||||||
|
|
||||||
const Discord = require("discord.js");
|
|
||||||
const yaml = require("js-yaml");
|
|
||||||
const supportbot = yaml.load(fs.readFileSync("./Data/supportbot.yml", "utf8"));
|
|
||||||
const cmdconfig = yaml.load(fs.readFileSync("./Data/commands.yml", "utf8"));
|
|
||||||
|
|
||||||
const Command = require("../Structures/Command.js");
|
|
||||||
const TicketNumberID = require("../Structures/TicketID.js");
|
|
||||||
|
|
||||||
async function asyncForEach(array, callback) {
|
|
||||||
for (let index = 0; index < array.length; index++) {
|
|
||||||
await callback(array[index], index, array);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = new Command({
|
|
||||||
name: cmdconfig.CloseTicket,
|
|
||||||
description: cmdconfig.CloseTicketDesc,
|
|
||||||
slashCommandOptions: [
|
|
||||||
{
|
|
||||||
name: "reason",
|
|
||||||
description: "Ticket Close Reason",
|
|
||||||
type: "STRING",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
permission: "SEND_MESSAGES",
|
|
||||||
|
|
||||||
async run(interaction) {
|
|
||||||
if (!interaction.channel.name.startsWith(`${supportbot.TicketChannel}-`)) {
|
|
||||||
const Exists = new Discord.MessageEmbed()
|
|
||||||
.setTitle("No Ticket Found!")
|
|
||||||
.setDescription(`${supportbot.NoValidTicket}`)
|
|
||||||
.setColor(supportbot.WarningColour);
|
|
||||||
return interaction.reply({ embed: Exists });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (supportbot.CloseConfirmation) {
|
|
||||||
const CloseTicketRequest = new Discord.MessageEmbed()
|
|
||||||
.setTitle(`**${supportbot.ClosingTicket}**`)
|
|
||||||
.setDescription(
|
|
||||||
`Please confirm by repeating the following word.. \`${supportbot.ClosingConfirmation_Word}\` `
|
|
||||||
)
|
|
||||||
.setColor(supportbot.EmbedColour);
|
|
||||||
|
|
||||||
interaction
|
|
||||||
.reply(CloseTicketRequest)
|
|
||||||
.then((m) => {
|
|
||||||
interaction.channel
|
|
||||||
.awaitMessages(
|
|
||||||
(response) =>
|
|
||||||
response.content.toLowerCase() ===
|
|
||||||
supportbot.ClosingConfirmation_Word,
|
|
||||||
{
|
|
||||||
max: 1,
|
|
||||||
time: 20000,
|
|
||||||
errors: ["time"],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.then((collected) => {
|
|
||||||
let logChannel =
|
|
||||||
interaction.guild.channels.cache.find(
|
|
||||||
(channel) => channel.name === supportbot.TranscriptLog
|
|
||||||
) ||
|
|
||||||
interaction.guild.channels.cache.find(
|
|
||||||
(channel) => channel.id === supportbot.TranscriptLog
|
|
||||||
);
|
|
||||||
let user = interaction.user;
|
|
||||||
let reason =
|
|
||||||
interaction.options.getString("reason") ||
|
|
||||||
"No Reason Provided.";
|
|
||||||
|
|
||||||
let name = interaction.channel.name;
|
|
||||||
let ticketChannel = interaction.channel;
|
|
||||||
|
|
||||||
interaction
|
|
||||||
.reply(`**${supportbot.ClosingTicket}**`)
|
|
||||||
|
|
||||||
.then(() => {
|
|
||||||
const logEmbed = new Discord.MessageEmbed()
|
|
||||||
.setTitle(supportbot.TranscriptTitle)
|
|
||||||
.setColor(supportbot.EmbedColour)
|
|
||||||
.setFooter(supportbot.EmbedFooter)
|
|
||||||
.addField("Closed By", interaction.user.tag)
|
|
||||||
.addField("Reason", reason);
|
|
||||||
|
|
||||||
interaction.channel.interactions
|
|
||||||
.fetch({ limit: 100 })
|
|
||||||
.then((msgs) => {
|
|
||||||
let html = "";
|
|
||||||
|
|
||||||
msgs = msgs.sort(
|
|
||||||
(a, b) => a.createdTimestamp - b.createdTimestamp
|
|
||||||
);
|
|
||||||
html += `<strong>Server Name:</strong> ${interaction.guild.name}<br>`;
|
|
||||||
html += `<strong>Ticket:</strong> ${ticketChannel.name}<br>`;
|
|
||||||
html += `<strong>Message:</strong> ${msgs.size} Messages<br><br><br>`;
|
|
||||||
|
|
||||||
msgs.forEach((msg) => {
|
|
||||||
if (msg.content) {
|
|
||||||
html += `<strong>User:</strong> ${msg.author.tag}<br>`;
|
|
||||||
html += `<strong>Message:</strong> ${msg.content}<br>`;
|
|
||||||
html += `-----<br><br>`;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
logChannel.send({ embeds: [logEmbed] }).catch((err) => {
|
|
||||||
interaction.reply(err);
|
|
||||||
});
|
|
||||||
|
|
||||||
let file = new Discord.MessageAttachment(
|
|
||||||
Buffer.from(html),
|
|
||||||
`${name}.html`
|
|
||||||
);
|
|
||||||
logChannel
|
|
||||||
.send({ embeds: [logEmbed], files: [file] })
|
|
||||||
.catch((err) => {
|
|
||||||
interaction.reply(err);
|
|
||||||
});
|
|
||||||
|
|
||||||
interaction.channel.delete().catch((error) => {
|
|
||||||
if (
|
|
||||||
error.code !==
|
|
||||||
Discord.Constants.APIErrors.UNKNOWN_CHANNEL
|
|
||||||
) {
|
|
||||||
console.error(
|
|
||||||
"Failed to delete the interaction:",
|
|
||||||
error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
interaction
|
|
||||||
.followUp({
|
|
||||||
content: "The request to close the ticket has timed out.",
|
|
||||||
})
|
|
||||||
.then((m2) => setTimeout(() => interaction.channel.delete(3000)));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!supportbot.CloseConfirmation) {
|
|
||||||
let logChannel =
|
|
||||||
interaction.guild.channels.cache.find(
|
|
||||||
(channel) => channel.name === supportbot.TranscriptLog
|
|
||||||
) ||
|
|
||||||
interaction.guild.channels.cache.find(
|
|
||||||
(channel) => channel.id === supportbot.TranscriptLog
|
|
||||||
);
|
|
||||||
let user = interaction.user;
|
|
||||||
let reason =
|
|
||||||
(await interaction.options.getString("reason")) ||
|
|
||||||
"No Reason Provided.";
|
|
||||||
|
|
||||||
let name = interaction.channel.name;
|
|
||||||
let ticketChannel = interaction.channel;
|
|
||||||
|
|
||||||
interaction
|
|
||||||
.reply(`**${supportbot.ClosingTicket}**`)
|
|
||||||
|
|
||||||
.then(() => {
|
|
||||||
const logEmbed = new Discord.MessageEmbed()
|
|
||||||
.setTitle(supportbot.TranscriptTitle)
|
|
||||||
.setColor(supportbot.EmbedColour)
|
|
||||||
.setFooter(supportbot.EmbedFooter)
|
|
||||||
.addField("Closed By", interaction.user.tag)
|
|
||||||
.addField("Reason", reason);
|
|
||||||
|
|
||||||
interaction.channel.interactions
|
|
||||||
.fetch({ limit: 100 })
|
|
||||||
.then((msgs) => {
|
|
||||||
let html = "";
|
|
||||||
|
|
||||||
msgs = msgs.sort(
|
|
||||||
(a, b) => a.createdTimestamp - b.createdTimestamp
|
|
||||||
);
|
|
||||||
html += `<strong>Server Name:</strong> ${interaction.guild.name}<br>`;
|
|
||||||
html += `<strong>Ticket:</strong> ${ticketChannel.name}<br>`;
|
|
||||||
html += `<strong>Message:</strong> ${msgs.size} Messages<br><br><br>`;
|
|
||||||
|
|
||||||
msgs.forEach((msg) => {
|
|
||||||
if (msg.content) {
|
|
||||||
html += `<strong>User:</strong> ${msg.author.tag}<br>`;
|
|
||||||
html += `<strong>Message:</strong> ${msg.content}<br>`;
|
|
||||||
html += `-----<br><br>`;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
logChannel.send({ embeds: [logEmbed] }).catch((err) => {
|
|
||||||
interaction.reply(err);
|
|
||||||
});
|
|
||||||
|
|
||||||
let file = new Discord.MessageAttachment(
|
|
||||||
Buffer.from(html),
|
|
||||||
`${name}.html`
|
|
||||||
);
|
|
||||||
logChannel
|
|
||||||
.send({ embeds: [logEmbed], files: [file] })
|
|
||||||
.catch((err) => {
|
|
||||||
interaction.reply(err);
|
|
||||||
});
|
|
||||||
|
|
||||||
interaction.channel.delete().catch((error) => {
|
|
||||||
if (
|
|
||||||
error.code !== Discord.Constants.APIErrors.UNKNOWN_CHANNEL
|
|
||||||
) {
|
|
||||||
console.error("Failed to delete the interaction:", error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
interaction
|
|
||||||
.followUp({
|
|
||||||
content: "The request to close the ticket has timed out.",
|
|
||||||
})
|
|
||||||
.then((m2) => setTimeout(() => interaction.channel.delete(3000)));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
// SupportBot | Emerald Services
|
|
||||||
// Embed Command
|
|
||||||
|
|
||||||
const fs = require("fs");
|
|
||||||
|
|
||||||
const Discord = require("discord.js");
|
|
||||||
const yaml = require("js-yaml");
|
|
||||||
const supportbot = yaml.load(fs.readFileSync("./Data/supportbot.yml", "utf8"));
|
|
||||||
const cmdconfig = yaml.load(fs.readFileSync("./Data/commands.yml", "utf8"));
|
|
||||||
|
|
||||||
const Command = require("../Structures/Command.js");
|
|
||||||
|
|
||||||
module.exports = new Command({
|
|
||||||
name: cmdconfig.EmbedCommand,
|
|
||||||
description: cmdconfig.EmbedCommandDesc,
|
|
||||||
slashCommandOptions: [
|
|
||||||
{
|
|
||||||
name: "title",
|
|
||||||
description: "Embed Title",
|
|
||||||
type: "STRING",
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
name: "message",
|
|
||||||
description: "Embed Message",
|
|
||||||
type: "STRING",
|
|
||||||
},
|
|
||||||
|
|
||||||
],
|
|
||||||
permission: "SEND_MESSAGES",
|
|
||||||
|
|
||||||
async run(interaction) {
|
|
||||||
|
|
||||||
let SupportStaff =
|
|
||||||
interaction.guild.roles.cache.find(
|
|
||||||
(SupportTeam) => SupportTeam.name === supportbot.Staff
|
|
||||||
) ||
|
|
||||||
interaction.guild.roles.cache.find(
|
|
||||||
(SupportTeam) => SupportTeam.id === supportbot.Staff
|
|
||||||
);
|
|
||||||
let Admins =
|
|
||||||
interaction.guild.roles.cache.find(
|
|
||||||
(AdminUser) => AdminUser.name === supportbot.Admin
|
|
||||||
) ||
|
|
||||||
interaction.guild.roles.cache.find(
|
|
||||||
(AdminUser) => AdminUser.id === 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 (
|
|
||||||
interaction.member.roles.cache.has(SupportStaff.id) ||
|
|
||||||
interaction.member.roles.cache.has(Admins.id)
|
|
||||||
) {
|
|
||||||
|
|
||||||
const EmbedTitle = interaction.options.getString("title")
|
|
||||||
const EmbedSubject = interaction.options.getString("message")
|
|
||||||
|
|
||||||
const EmbedMsg = new Discord.MessageEmbed()
|
|
||||||
.setTitle(EmbedTitle)
|
|
||||||
.setDescription(EmbedSubject)
|
|
||||||
|
|
||||||
interaction.reply({
|
|
||||||
embeds: [EmbedMsg],
|
|
||||||
});
|
|
||||||
|
|
||||||
} else {
|
|
||||||
return interaction.reply({ embeds: [NoPerms] });
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
});
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
// SupportBot | Emerald Services
|
|
||||||
// Help Command
|
|
||||||
|
|
||||||
const fs = require("fs");
|
|
||||||
|
|
||||||
const Discord = require("discord.js");
|
|
||||||
const yaml = require("js-yaml");
|
|
||||||
const supportbot = yaml.load(fs.readFileSync("./Data/supportbot.yml", "utf8"));
|
|
||||||
const cmdconfig = yaml.load(fs.readFileSync("./Data/commands.yml", "utf8"));
|
|
||||||
|
|
||||||
const Command = require("../Structures/Command.js");
|
|
||||||
|
|
||||||
async function asyncForEach(array, callback) {
|
|
||||||
for (let index = 0; index < array.length; index++) {
|
|
||||||
await callback(array[index], index, array);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = new Command({
|
|
||||||
name: cmdconfig.HelpCommand,
|
|
||||||
description: cmdconfig.HelpCommandDesc,
|
|
||||||
slashCommandOptions: [],
|
|
||||||
permission: "SEND_MESSAGES",
|
|
||||||
|
|
||||||
async run(interaction) {
|
|
||||||
let SupportStaff =
|
|
||||||
interaction.guild.roles.cache.find(
|
|
||||||
(adminRole) => adminRole.name === supportbot.Staff
|
|
||||||
) ||
|
|
||||||
interaction.guild.roles.cache.find(
|
|
||||||
(staffRole) => staffRole.id === supportbot.Staff
|
|
||||||
);
|
|
||||||
let AdminStaff =
|
|
||||||
interaction.guild.roles.cache.find(
|
|
||||||
(adminRole) => adminRole.name === supportbot.Admin
|
|
||||||
) ||
|
|
||||||
interaction.guild.roles.cache.find(
|
|
||||||
(adminRole) => adminRole.id === supportbot.Admin
|
|
||||||
);
|
|
||||||
|
|
||||||
if (supportbot.HelpMenu) {
|
|
||||||
let botCommands = "";
|
|
||||||
botCommands += `**/${cmdconfig.HelpCommand}** ${cmdconfig.HelpCommandDesc}\n`;
|
|
||||||
botCommands += `**/${cmdconfig.InfoCommand}** ${cmdconfig.InfoCommandDesc}\n`;
|
|
||||||
botCommands += `**/${cmdconfig.PingCommand}** ${cmdconfig.PingCommandDesc}\n`;
|
|
||||||
|
|
||||||
let ticketCommands = "";
|
|
||||||
ticketCommands += `**/${cmdconfig.OpenTicket}** ${cmdconfig.OpenTicketDesc}\n`;
|
|
||||||
ticketCommands += `**/${cmdconfig.CloseTicket}** ${cmdconfig.CloseTicketDesc}\n`;
|
|
||||||
|
|
||||||
let staffCommands = "";
|
|
||||||
staffCommands += `**/${cmdconfig.TicketAdd}** ${cmdconfig.TicketAddDesc}\n`;
|
|
||||||
staffCommands += `**/${cmdconfig.TicketRemove}** ${cmdconfig.TicketRemoveDesc}\n`;
|
|
||||||
|
|
||||||
const HelpEmbed1 = new Discord.MessageEmbed()
|
|
||||||
.setTitle(supportbot.Name + " Commands")
|
|
||||||
.setThumbnail(interaction.user.displayAvatarURL())
|
|
||||||
|
|
||||||
.addFields(
|
|
||||||
{
|
|
||||||
name: "🖥️ General Commands\n",
|
|
||||||
value: `${botCommands}\n`,
|
|
||||||
inline: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "🎫 Support Commands\n",
|
|
||||||
value: `${ticketCommands}\n`,
|
|
||||||
inline: false,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
.setColor(supportbot.EmbedColour)
|
|
||||||
.setFooter(supportbot.EmbedFooter, interaction.user.displayAvatarURL());
|
|
||||||
|
|
||||||
if (interaction.member.roles.cache.has(SupportStaff.id)) {
|
|
||||||
HelpEmbed1.addFields({
|
|
||||||
name: "🔐 Staff Commands\n",
|
|
||||||
value: `${staffCommands}\n`,
|
|
||||||
inline: false,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
if (interaction.member.roles.cache.has(AdminStaff.id)) {
|
|
||||||
HelpEmbed1.addFields({
|
|
||||||
name: "🔐 Staff Commands\n",
|
|
||||||
value: `${staffCommands}\n`,
|
|
||||||
inline: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (supportbot.SendHelpPage === "dm") {
|
|
||||||
interaction.user.reply({
|
|
||||||
embeds: [HelpEmbed1],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (supportbot.SendHelpPage === "channel") {
|
|
||||||
interaction.reply({
|
|
||||||
embeds: [HelpEmbed1],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
// SupportBot | Emerald Services
|
|
||||||
// Info Command
|
|
||||||
|
|
||||||
const fs = require("fs");
|
|
||||||
|
|
||||||
const Discord = require("discord.js");
|
|
||||||
const yaml = require("js-yaml");
|
|
||||||
const supportbot = yaml.load(fs.readFileSync("./Data/supportbot.yml", "utf8"));
|
|
||||||
const cmdconfig = yaml.load(fs.readFileSync("./Data/commands.yml", "utf8"));
|
|
||||||
|
|
||||||
const Command = require("../Structures/Command.js");
|
|
||||||
|
|
||||||
module.exports = new Command({
|
|
||||||
name: cmdconfig.InfoCommand,
|
|
||||||
description: cmdconfig.InfoCommandDesc,
|
|
||||||
slashCommandOptions: [],
|
|
||||||
permission: "SEND_MESSAGES",
|
|
||||||
|
|
||||||
async run(interaction) {
|
|
||||||
const InfoButton = new Discord.MessageButton()
|
|
||||||
.setLabel(supportbot.InfoButtonText)
|
|
||||||
.setURL(supportbot.InfoURL)
|
|
||||||
.setStyle("LINK");
|
|
||||||
|
|
||||||
const inforow = new Discord.MessageActionRow().addComponents(InfoButton);
|
|
||||||
|
|
||||||
const InfoEmbed = new Discord.MessageEmbed()
|
|
||||||
.setURL(supportbot.InfoURL)
|
|
||||||
.setTitle(supportbot.InfoTitle)
|
|
||||||
.setDescription(supportbot.AboutMe)
|
|
||||||
.setColor(supportbot.InfoColour);
|
|
||||||
|
|
||||||
interaction.reply({
|
|
||||||
embeds: [InfoEmbed],
|
|
||||||
components: [inforow],
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
// SupportBot | Emerald Services
|
|
||||||
// Ping Command
|
|
||||||
|
|
||||||
const fs = require("fs");
|
|
||||||
|
|
||||||
const Discord = require("discord.js");
|
|
||||||
const yaml = require("js-yaml");
|
|
||||||
const supportbot = yaml.load(fs.readFileSync("./Data/supportbot.yml", "utf8"));
|
|
||||||
const cmdconfig = yaml.load(fs.readFileSync("./Data/commands.yml", "utf8"));
|
|
||||||
|
|
||||||
const Command = require("../Structures/Command.js");
|
|
||||||
|
|
||||||
module.exports = new Command({
|
|
||||||
name: cmdconfig.PingCommand,
|
|
||||||
description: cmdconfig.PingCommandDesc,
|
|
||||||
slashCommandOptions: [],
|
|
||||||
permission: "SEND_MESSAGES",
|
|
||||||
|
|
||||||
async run(interaction) {
|
|
||||||
const PingEmbed = new Discord.MessageEmbed()
|
|
||||||
.setDescription(
|
|
||||||
`:ping_pong: **Ping:** \`${interaction.client.ws.ping} ms\``
|
|
||||||
)
|
|
||||||
.setColor(supportbot.EmbedColour);
|
|
||||||
|
|
||||||
interaction.reply({
|
|
||||||
embeds: [PingEmbed],
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,361 +0,0 @@
|
|||||||
// SupportBot | Emerald Services
|
|
||||||
// Ticket Command
|
|
||||||
|
|
||||||
const fs = require("fs");
|
|
||||||
|
|
||||||
const Discord = require("discord.js");
|
|
||||||
const yaml = require("js-yaml");
|
|
||||||
const supportbot = yaml.load(fs.readFileSync("./Data/supportbot.yml", "utf8"));
|
|
||||||
const cmdconfig = yaml.load(fs.readFileSync("./Data/commands.yml", "utf8"));
|
|
||||||
|
|
||||||
const Command = require("../Structures/Command.js");
|
|
||||||
const TicketNumberID = require("../Structures/TicketID.js");
|
|
||||||
|
|
||||||
async function asyncForEach(array, callback) {
|
|
||||||
for (let index = 0; index < array.length; index++) {
|
|
||||||
await callback(array[index], index, array);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = new Command({
|
|
||||||
name: cmdconfig.OpenTicket,
|
|
||||||
description: cmdconfig.OpenTicketDesc,
|
|
||||||
slashCommandOptions: [
|
|
||||||
{
|
|
||||||
name: "reason",
|
|
||||||
description: "Ticket Reason",
|
|
||||||
type: "STRING",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
permission: "SEND_MESSAGES",
|
|
||||||
|
|
||||||
async run(interaction) {
|
|
||||||
let reactionUser = interaction.guild.members.cache.get(interaction.user.id);
|
|
||||||
|
|
||||||
if (
|
|
||||||
reactionUser.roles.cache.find(
|
|
||||||
(role) => role.name === supportbot.TicketBlackListRole
|
|
||||||
) ||
|
|
||||||
reactionUser.roles.cache.find(
|
|
||||||
(role) => role.id === supportbot.TicketBlackListRole
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return interaction
|
|
||||||
.reply({
|
|
||||||
content: `<@${interaction.user.id}> ${supportbot.TicketBlackListMessage}`,
|
|
||||||
})
|
|
||||||
.then((msg) => {
|
|
||||||
setTimeout(() => msg.delete(), 3500);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ticket ID
|
|
||||||
let ticketNumberID = TicketNumberID.pad(interaction.guild.id);
|
|
||||||
|
|
||||||
// Ticket Subject
|
|
||||||
const TicketSubject =
|
|
||||||
interaction.options?.getString("reason") || supportbot.NoTicketSubject;
|
|
||||||
|
|
||||||
const TicketExists = new Discord.MessageEmbed()
|
|
||||||
.setTitle("Ticket Exists!")
|
|
||||||
.setDescription(`${supportbot.TicketExists}`)
|
|
||||||
.setColor(supportbot.WarningColour);
|
|
||||||
|
|
||||||
if (
|
|
||||||
interaction.guild.channels.cache.find(
|
|
||||||
(ticketChannel) =>
|
|
||||||
ticketChannel.name === `${supportbot.TicketChannel}-${ticketNumberID}`
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return interaction.reply({ embeds: [TicketExists] });
|
|
||||||
}
|
|
||||||
const Staff =
|
|
||||||
interaction.guild.roles.cache.find(
|
|
||||||
(SupportTeam) => SupportTeam.name === supportbot.Staff
|
|
||||||
) ||
|
|
||||||
interaction.guild.roles.cache.find(
|
|
||||||
(SupportTeam) => SupportTeam.id === supportbot.Staff
|
|
||||||
);
|
|
||||||
const Admins =
|
|
||||||
interaction.guild.roles.cache.find(
|
|
||||||
(AdminUser) => AdminUser.name === supportbot.Admin
|
|
||||||
) ||
|
|
||||||
interaction.guild.roles.cache.find(
|
|
||||||
(AdminUser) => AdminUser.id === supportbot.Admin
|
|
||||||
);
|
|
||||||
const DeptRole1 =
|
|
||||||
interaction.guild.roles.cache.find(
|
|
||||||
(DepartmentRole) =>
|
|
||||||
DepartmentRole.name === `${supportbot.DepartmentRole_1}`
|
|
||||||
) ||
|
|
||||||
interaction.guild.roles.cache.find(
|
|
||||||
(DepartmentRole) =>
|
|
||||||
DepartmentRole.id === `${supportbot.DepartmentRole_1}`
|
|
||||||
);
|
|
||||||
const DeptRole2 =
|
|
||||||
interaction.guild.roles.cache.find(
|
|
||||||
(DepartmentRole) =>
|
|
||||||
DepartmentRole.name === `${supportbot.DepartmentRole_2}`
|
|
||||||
) ||
|
|
||||||
interaction.guild.roles.cache.find(
|
|
||||||
(DepartmentRole) =>
|
|
||||||
DepartmentRole.id === `${supportbot.DepartmentRole_2}`
|
|
||||||
);
|
|
||||||
const DeptRole3 =
|
|
||||||
interaction.guild.roles.cache.find(
|
|
||||||
(DepartmentRole) =>
|
|
||||||
DepartmentRole.name === `${supportbot.DepartmentRole_3}`
|
|
||||||
) ||
|
|
||||||
interaction.guild.roles.cache.find(
|
|
||||||
(DepartmentRole) =>
|
|
||||||
DepartmentRole.id === `${supportbot.DepartmentRole_3}`
|
|
||||||
);
|
|
||||||
const Author = interaction.user;
|
|
||||||
const Everyone = interaction.guild.id;
|
|
||||||
const ticketChannel = await interaction.guild.channels.create(
|
|
||||||
`${supportbot.TicketChannel}-${ticketNumberID}`,
|
|
||||||
{
|
|
||||||
type: "GUILD_TEXT",
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (
|
|
||||||
interaction.channel.name === supportbot.ReactionChannel ||
|
|
||||||
interaction.channel.id === supportbot.ReactionChannel
|
|
||||||
) {
|
|
||||||
const CreatedTicket = new Discord.MessageEmbed()
|
|
||||||
.setDescription(
|
|
||||||
supportbot.TicketCreatedAlert.replace(
|
|
||||||
/%ticketauthor%/g,
|
|
||||||
interaction.user.id
|
|
||||||
)
|
|
||||||
.replace(/%ticketid%/g, ticketChannel.id)
|
|
||||||
.replace(/%ticketusername%/g, interaction.user.username)
|
|
||||||
)
|
|
||||||
.setColor(supportbot.EmbedColour);
|
|
||||||
interaction.reply({ embeds: [CreatedTicket], ephemeral: true });
|
|
||||||
} else {
|
|
||||||
const CreatedTicket = new Discord.MessageEmbed()
|
|
||||||
.setDescription(
|
|
||||||
supportbot.TicketCreatedAlert.replace(
|
|
||||||
/%ticketauthor%/g,
|
|
||||||
interaction.user.id
|
|
||||||
)
|
|
||||||
.replace(/%ticketid%/g, ticketChannel.id)
|
|
||||||
.replace(/%ticketusername%/g, interaction.user.username)
|
|
||||||
)
|
|
||||||
.setColor(supportbot.EmbedColour);
|
|
||||||
interaction.reply({ embeds: [CreatedTicket], ephemeral: true });
|
|
||||||
}
|
|
||||||
let AllowArr = [Staff, Staff, Admins, Author];
|
|
||||||
await asyncForEach(AllowArr, (index) => {
|
|
||||||
ticketChannel.permissionOverwrites.create(index, { VIEW_CHANNEL: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
let DenyArr = [Everyone];
|
|
||||||
await asyncForEach(DenyArr, (index) => {
|
|
||||||
ticketChannel.permissionOverwrites.create(index, { VIEW_CHANNEL: false });
|
|
||||||
});
|
|
||||||
|
|
||||||
let TicketCategory =
|
|
||||||
interaction.guild.channels.cache.find(
|
|
||||||
(category) => category.name === supportbot.TicketCategory
|
|
||||||
) ||
|
|
||||||
interaction.guild.channels.cache.find(
|
|
||||||
(category) => category.id === supportbot.TicketCategory
|
|
||||||
);
|
|
||||||
if (TicketCategory) {
|
|
||||||
ticketChannel.setParent(TicketCategory.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (supportbot.AllowTicketMentions) {
|
|
||||||
ticketChannel.send({ content: `${interaction.user}` });
|
|
||||||
}
|
|
||||||
|
|
||||||
const TicketMessage = new Discord.MessageEmbed()
|
|
||||||
.setTitle(
|
|
||||||
supportbot.Ticket_Title.replace(/%ticketauthor%/g, interaction.user.id)
|
|
||||||
.replace(/%ticketid%/g, ticketChannel.id)
|
|
||||||
.replace(/%ticketusername%/g, interaction.user.username)
|
|
||||||
)
|
|
||||||
.setDescription(
|
|
||||||
supportbot.TicketMessage.replace(/%ticketauthor%/g, interaction.user.id)
|
|
||||||
.replace(/%ticketid%/g, ticketChannel.id)
|
|
||||||
.replace(/%ticketusername%/g, interaction.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.") {
|
|
||||||
ticketChannel.setTopic(
|
|
||||||
`Reason: ${TicketSubject} - User ID: ${interaction.user.id} - Ticket: ${ticketChannel.name}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (supportbot.TicketDepartments) {
|
|
||||||
TicketMessage.addFields({
|
|
||||||
name: "Departments",
|
|
||||||
value: ` **${supportbot.DepartmentTitle_1}**\n **${supportbot.DepartmentTitle_2}**\n **${supportbot.DepartmentTitle_3}**`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const CloseButton = new Discord.MessageButton()
|
|
||||||
.setCustomId("ticketclose")
|
|
||||||
.setLabel("Close")
|
|
||||||
.setStyle(supportbot.TicketCloseColour)
|
|
||||||
.setEmoji(supportbot.TicketCloseEmoji);
|
|
||||||
|
|
||||||
const LockButton = new Discord.MessageButton()
|
|
||||||
.setCustomId("ticketlock")
|
|
||||||
.setLabel("Lock")
|
|
||||||
.setStyle(supportbot.TicketLockColour)
|
|
||||||
.setEmoji(supportbot.TicketLockEmoji);
|
|
||||||
|
|
||||||
const row2 = new Discord.MessageActionRow().addComponents(
|
|
||||||
CloseButton,
|
|
||||||
LockButton
|
|
||||||
);
|
|
||||||
|
|
||||||
if (supportbot.TicketDepartments) {
|
|
||||||
let DenyArr = [DeptRole1, DeptRole2, DeptRole3];
|
|
||||||
await asyncForEach(DenyArr, (index) => {
|
|
||||||
ticketChannel.permissionOverwrites.create(index, {
|
|
||||||
VIEW_CHANNEL: false,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
const Department1Button = new Discord.MessageButton()
|
|
||||||
.setCustomId("Department1")
|
|
||||||
.setLabel(`${supportbot.DepartmentTitle_1}`)
|
|
||||||
.setStyle(supportbot.TicketDept1Colour)
|
|
||||||
.setEmoji(supportbot.TicketDept1Emoji);
|
|
||||||
|
|
||||||
const Department2Button = new Discord.MessageButton()
|
|
||||||
.setCustomId("Department2")
|
|
||||||
.setLabel(`${supportbot.DepartmentTitle_2}`)
|
|
||||||
.setStyle(supportbot.TicketDept2Colour)
|
|
||||||
.setEmoji(supportbot.TicketDept2Emoji);
|
|
||||||
|
|
||||||
const Department3Button = new Discord.MessageButton()
|
|
||||||
.setCustomId("Department3")
|
|
||||||
.setLabel(`${supportbot.DepartmentTitle_3}`)
|
|
||||||
.setStyle(supportbot.TicketDept3Colour)
|
|
||||||
.setEmoji(supportbot.TicketDept3Emoji);
|
|
||||||
|
|
||||||
const row = new Discord.MessageActionRow().addComponents(
|
|
||||||
Department1Button,
|
|
||||||
Department2Button,
|
|
||||||
Department3Button
|
|
||||||
);
|
|
||||||
|
|
||||||
const m = await ticketChannel.send({
|
|
||||||
embeds: [TicketMessage],
|
|
||||||
components: [row],
|
|
||||||
});
|
|
||||||
|
|
||||||
const filter = (i) => {
|
|
||||||
i.deferUpdate();
|
|
||||||
return i.user.id === interaction.user.id;
|
|
||||||
};
|
|
||||||
const collector = m.createMessageComponentCollector({
|
|
||||||
filter,
|
|
||||||
max: 1,
|
|
||||||
componentType: "BUTTON",
|
|
||||||
time: 60000,
|
|
||||||
});
|
|
||||||
|
|
||||||
collector.on("collect", async (i) => {
|
|
||||||
if (i.customId === "Department1") {
|
|
||||||
ticketChannel.permissionOverwrites.edit(DeptRole1, {
|
|
||||||
VIEW_CHANNEL: true,
|
|
||||||
READ_MESSAGE_HISTORY: true,
|
|
||||||
SEND_MESSAGES: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!supportbot.AllowStaff) {
|
|
||||||
ticketChannel.permissionOverwrites.edit(Staff, {
|
|
||||||
VIEW_CHANNEL: false,
|
|
||||||
READ_MESSAGE_HISTORY: false,
|
|
||||||
SEND_MESSAGES: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const Department1 = new Discord.MessageEmbed()
|
|
||||||
.setDescription(
|
|
||||||
`> Thank for reaching out to the **${supportbot.DepartmentTitle_1} Department**. Please provide us information regarding your query.`
|
|
||||||
)
|
|
||||||
.setColor(supportbot.EmbedColour);
|
|
||||||
ticketChannel.send({ embeds: [Department1] });
|
|
||||||
|
|
||||||
if (supportbot.AllowTicketMentions) {
|
|
||||||
ticketChannel.send({ content: `@here` });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
i.message.edit({ embeds: [TicketMessage], components: [row2] });
|
|
||||||
|
|
||||||
if (i.customId === "Department2") {
|
|
||||||
ticketChannel.permissionOverwrites.edit(DeptRole2, {
|
|
||||||
VIEW_CHANNEL: true,
|
|
||||||
READ_MESSAGE_HISTORY: true,
|
|
||||||
SEND_MESSAGES: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!supportbot.AllowStaff) {
|
|
||||||
ticketChannel.permissionOverwrites.edit(Staff, {
|
|
||||||
VIEW_CHANNEL: false,
|
|
||||||
READ_MESSAGE_HISTORY: false,
|
|
||||||
SEND_MESSAGES: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const Department2 = new Discord.MessageEmbed()
|
|
||||||
.setDescription(
|
|
||||||
`> Thank for reaching out to the **${supportbot.DepartmentTitle_2} Department**. Please provide us information regarding your query.`
|
|
||||||
)
|
|
||||||
.setColor(supportbot.EmbedColour);
|
|
||||||
ticketChannel.send({ embeds: [Department2] });
|
|
||||||
|
|
||||||
if (supportbot.AllowTicketMentions) {
|
|
||||||
ticketChannel.send({ content: `@here` });
|
|
||||||
}
|
|
||||||
|
|
||||||
i.message.edit({ embeds: [TicketMessage], components: [row2] });
|
|
||||||
}
|
|
||||||
if (i.customId === "Department3") {
|
|
||||||
ticketChannel.permissionOverwrites.edit(DeptRole3, {
|
|
||||||
VIEW_CHANNEL: true,
|
|
||||||
READ_MESSAGE_HISTORY: true,
|
|
||||||
SEND_MESSAGES: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!supportbot.AllowStaff) {
|
|
||||||
ticketChannel.permissionOverwrites.edit(Staff, {
|
|
||||||
VIEW_CHANNEL: false,
|
|
||||||
READ_MESSAGE_HISTORY: false,
|
|
||||||
SEND_MESSAGES: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const Department3 = new Discord.MessageEmbed()
|
|
||||||
.setDescription(
|
|
||||||
`> Thank for reaching out to the **${supportbot.DepartmentTitle_3} Department**. Please provide us information regarding your query.`
|
|
||||||
)
|
|
||||||
.setColor(supportbot.EmbedColour);
|
|
||||||
ticketChannel.send({ embeds: [Department3] });
|
|
||||||
|
|
||||||
if (supportbot.AllowTicketMentions) {
|
|
||||||
ticketChannel.send({ content: `@here` });
|
|
||||||
}
|
|
||||||
i.message.edit({ embeds: [TicketMessage], components: [row2] });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
ticketChannel.send({ embeds: [TicketMessage], components: [row2] });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -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: ''
|
||||||
@@ -1 +0,0 @@
|
|||||||
{}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
# ___ _ ___ _
|
|
||||||
# / __> _ _ ___ ___ ___ _ _ _| |_ | . > ___ _| |_
|
|
||||||
# \__ \| | || . \| . \/ . \| '_> | | | . \/ . \ | |
|
|
||||||
# <___/`___|| _/| _/\___/|_| |_| |___/\___/ |_|
|
|
||||||
# |_| |_|
|
|
||||||
#
|
|
||||||
# SupportBot created by Emerald Services
|
|
||||||
# Installed with MIT License
|
|
||||||
#
|
|
||||||
# Discord Support: https:#emeraldsrv.dev/discord
|
|
||||||
# Community Resources: https:#emeraldsrv.dev/resources
|
|
||||||
|
|
||||||
# -------------------------------
|
|
||||||
# Command Name Configuration
|
|
||||||
# -------------------------------
|
|
||||||
|
|
||||||
TicketAdd: "add"
|
|
||||||
TicketRemove: "remove"
|
|
||||||
OpenTicket: "ticket"
|
|
||||||
CloseTicket: "close"
|
|
||||||
InfoCommand: "info"
|
|
||||||
PingCommand: "ping"
|
|
||||||
HelpCommand: "help"
|
|
||||||
EmbedCommand: "embed"
|
|
||||||
|
|
||||||
# ---------------------------------------
|
|
||||||
# Command Description Configuration
|
|
||||||
# ---------------------------------------
|
|
||||||
|
|
||||||
TicketAddDesc: "Add a user to a support ticket."
|
|
||||||
TicketRemoveDesc: "Remove a user from a support ticket."
|
|
||||||
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."
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,188 +0,0 @@
|
|||||||
# ___ _ ___ _
|
|
||||||
# / __> _ _ ___ ___ ___ _ _ _| |_ | . > ___ _| |_
|
|
||||||
# \__ \| | || . \| . \/ . \| '_> | | | . \/ . \ | |
|
|
||||||
# <___/`___|| _/| _/\___/|_| |_| |___/\___/ |_|
|
|
||||||
# |_| |_|
|
|
||||||
#
|
|
||||||
# SupportBot created by Emerald Services
|
|
||||||
# Installed with MIT License
|
|
||||||
#
|
|
||||||
# Discord Support: https:#emeraldsrv.dev/discord
|
|
||||||
# Community Resources: https:#emeraldsrv.dev/resources
|
|
||||||
|
|
||||||
SupportBot_Version: "7.2 pre-3"
|
|
||||||
|
|
||||||
# -------------------------
|
|
||||||
# General Configuration
|
|
||||||
# -------------------------
|
|
||||||
|
|
||||||
Name: "SupportBot"
|
|
||||||
Prefix: "!" # Everything uses the new Slash (/) Commands feature!
|
|
||||||
Token: "BOT_TOKEN"
|
|
||||||
|
|
||||||
# -------------------------
|
|
||||||
# Activity Configuration
|
|
||||||
# -------------------------
|
|
||||||
|
|
||||||
BotActivity: "Need Help? /ticket"
|
|
||||||
|
|
||||||
StreamingURL: "https://www.twitch.tv/YOUR_TWITCH_NAME"
|
|
||||||
# Set The ActivityType to "STREAMING" for a streaming status!
|
|
||||||
|
|
||||||
ActivityType: "PLAYING"
|
|
||||||
# Activity status can either be PLAYING, WATCHING, LISTENING or STREAMING.
|
|
||||||
|
|
||||||
# -------------------------
|
|
||||||
# Embed Configuration
|
|
||||||
# -------------------------
|
|
||||||
|
|
||||||
EmbedColour: "#f74343"
|
|
||||||
EmbedFooter: "SupportBot | An Advanced Ticket System"
|
|
||||||
|
|
||||||
ErrorColour: "#d12519"
|
|
||||||
SuccessColour: "#04b545"
|
|
||||||
WarningColour: "#fca117"
|
|
||||||
|
|
||||||
# -------------------------
|
|
||||||
# Information Configuration
|
|
||||||
# -------------------------
|
|
||||||
|
|
||||||
InfoTitle: "Information"
|
|
||||||
AboutMe: "Hi, I'm SupportBot an advanced discord ticket bot with 100% customization! [Download](https://emeraldsrv.dev/download)"
|
|
||||||
|
|
||||||
InfoButtonText: "Visit Site"
|
|
||||||
InfoURL: "https://emeraldsrv.dev"
|
|
||||||
InfoColour: "#f74343"
|
|
||||||
|
|
||||||
# -------------------------
|
|
||||||
# Help Configuration
|
|
||||||
# -------------------------
|
|
||||||
|
|
||||||
HelpMenu: true # Do not change!
|
|
||||||
SendHelpPage: "channel" # Set to channel or dm
|
|
||||||
|
|
||||||
# -------------------------
|
|
||||||
# Role Configuration
|
|
||||||
# -------------------------
|
|
||||||
|
|
||||||
AutoRole: true
|
|
||||||
AutoRole_Role: "Member"
|
|
||||||
LockdownRole: "@everyone"
|
|
||||||
|
|
||||||
Staff: "Support Team"
|
|
||||||
Admin: "Project Developer"
|
|
||||||
TicketBlackListRole: "Blacklisted"
|
|
||||||
|
|
||||||
# -------------------------
|
|
||||||
# Tickets Configuration
|
|
||||||
# -------------------------
|
|
||||||
|
|
||||||
TicketCategory: "▬▬ TICKETS ▬▬"
|
|
||||||
TicketChannel: "ticket"
|
|
||||||
|
|
||||||
TicketLog: "ticket-log"
|
|
||||||
TranscriptLog: "ticket-data"
|
|
||||||
|
|
||||||
TranscriptTitle: "Ticket Transcript"
|
|
||||||
TicketLog_Title: "Ticket Log"
|
|
||||||
TicketBlackListMessage: "You are not allowed to open a ticket"
|
|
||||||
|
|
||||||
TicketSubject: "description"
|
|
||||||
# "description" will displays the ticket reason within the channel description
|
|
||||||
# "embed" will displays the ticket reason within the ticket embed
|
|
||||||
|
|
||||||
AllowTicketMentions: true
|
|
||||||
# Set to true to allow the ticket to mention the author and the support team.
|
|
||||||
# Set to false to dis-allow mentions within tickets.
|
|
||||||
|
|
||||||
# --- Creation Config ---
|
|
||||||
# --- Start ---
|
|
||||||
|
|
||||||
# Available Placeholders:
|
|
||||||
|
|
||||||
# %ticketusername% - User who created the ticket.
|
|
||||||
# <@%ticketauthor%> - Tags the user who created the ticket.
|
|
||||||
# <#%ticketid%> - Mentions the ticket channel.
|
|
||||||
|
|
||||||
Ticket_Title: "Server Support | %ticketusername%"
|
|
||||||
TicketMessage: "Hello <@%ticketauthor%>, Thank you for creating a support ticket\nPlease wait patiently whilst a member of our team reaches out to you."
|
|
||||||
|
|
||||||
Ticket_Thumbnail: "https://images.emojiterra.com/google/android-pie/512px/1f39f.png"
|
|
||||||
NoTicketSubject: "No Reason Provided!"
|
|
||||||
|
|
||||||
TicketCreatedAlert: ":ticket: Your support ticket has been created: <#%ticketid%>"
|
|
||||||
|
|
||||||
AddedUser: "👍 <@%user%> has been added to this ticket"
|
|
||||||
RemovedUser: "👍 <@%user%> has been removed from this ticket"
|
|
||||||
|
|
||||||
# --- Buttons Config ---
|
|
||||||
# --- Start ---
|
|
||||||
|
|
||||||
# Discord Button Colour Selection
|
|
||||||
# PRIMARY - Blurple
|
|
||||||
# DANGER - Red
|
|
||||||
# SUCCESS - Green
|
|
||||||
# Secondary - Grey
|
|
||||||
|
|
||||||
TicketLockReaction: "🔒"
|
|
||||||
TicketDeleteReaction: "⛔"
|
|
||||||
LockTicketCategory: "Archive"
|
|
||||||
|
|
||||||
TicketCloseEmoji: "❌"
|
|
||||||
TicketDeleteEmoji: "⛔"
|
|
||||||
TicketLockEmoji: "🔒"
|
|
||||||
|
|
||||||
TicketCloseColour: "DANGER"
|
|
||||||
TicketDeleteColour: "DANGER"
|
|
||||||
TicketLockColour: "PRIMARY"
|
|
||||||
|
|
||||||
TicketDept1Colour: "PRIMARY"
|
|
||||||
TicketDept2Colour: "PRIMARY"
|
|
||||||
TicketDept3Colour: "PRIMARY"
|
|
||||||
|
|
||||||
TicketDept1Emoji: "1️⃣"
|
|
||||||
TicketDept2Emoji: "2️⃣"
|
|
||||||
TicketDept3Emoji: "3️⃣"
|
|
||||||
|
|
||||||
TranscriptSavedMessage: "📝 **Transcript Saved!**"
|
|
||||||
TicketLockMessage: "❌ **Ticket Locked!**"
|
|
||||||
TicketDeleteMessage: "⚠ **Ticket Deleting!**" # Tickets deleting after 5 seconds by default.
|
|
||||||
TicketDeleteTime: 5000 # Time must be set in milliseconds.
|
|
||||||
|
|
||||||
# --- Departments Config ---
|
|
||||||
# --- Start ---
|
|
||||||
|
|
||||||
TicketDepartments: true
|
|
||||||
|
|
||||||
AllowStaff: true
|
|
||||||
# If set to false users with the "Staff" role will not be able to view tickets
|
|
||||||
# without a valid department role set below.
|
|
||||||
|
|
||||||
# Set to false to allow "Staff" to see tickets whether its a department for them or not.
|
|
||||||
|
|
||||||
DepartmentTitle_1: "General Support"
|
|
||||||
DepartmentTitle_2: "Billing Support"
|
|
||||||
DepartmentTitle_3: "Admin Team"
|
|
||||||
# Ticket department title, This is the title for a ticket department to help users know what is available.
|
|
||||||
|
|
||||||
DepartmentRole_1: "Support Team"
|
|
||||||
DepartmentRole_2: "Support Team"
|
|
||||||
DepartmentRole_3: "Project Developer"
|
|
||||||
# Ticket roles are the roles that will be able to gain access to tickets depending on what department has been chosen.
|
|
||||||
|
|
||||||
# --- Closing Config ---
|
|
||||||
# --- Start ---
|
|
||||||
|
|
||||||
ClosingTicket: ":warning: You have requested to close your support ticket."
|
|
||||||
ClosingConfirmation_Word: "confirm"
|
|
||||||
CloseConfirmation: false
|
|
||||||
|
|
||||||
# ---------------------------
|
|
||||||
# Error / Warning Messages
|
|
||||||
# ---------------------------
|
|
||||||
|
|
||||||
IncorrectPerms: ":x: **Err!** You do not have the correct permissions to use this command."
|
|
||||||
InvalidChannel: ":x: **Err!** Channel not found. Please create it!"
|
|
||||||
UserNotFound: ":x: **Err!** This user doesn't exist, Are they in this server?"
|
|
||||||
NoValidTicket: ":warning: You cannot execute this command here."
|
|
||||||
TicketExists: ":warning: This ticket cannot be opened, It already exists!"
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
TicketPanelID: ''
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
# ___ _ ___ _
|
|
||||||
# / __> _ _ ___ ___ ___ _ _ _| |_ | . > ___ _| |_
|
|
||||||
# \__ \| | || . \| . \/ . \| '_> | | | . \/ . \ | |
|
|
||||||
# <___/`___|| _/| _/\___/|_| |_| |___/\___/ |_|
|
|
||||||
# |_| |_|
|
|
||||||
#
|
|
||||||
# SupportBot created by Emerald Services
|
|
||||||
# Installed with MIT License
|
|
||||||
#
|
|
||||||
# Discord Support: https:#emeraldsrv.dev/discord
|
|
||||||
# Community Resources: https:#emeraldsrv.dev/resources
|
|
||||||
|
|
||||||
# -------------------------------
|
|
||||||
# Ticket Panel Configuration
|
|
||||||
# -------------------------------
|
|
||||||
|
|
||||||
TicketPanel: true
|
|
||||||
|
|
||||||
# ----------------------------------------
|
|
||||||
# Ticket Panel Message Configuration
|
|
||||||
# ----------------------------------------
|
|
||||||
|
|
||||||
Channel: "create-ticket"
|
|
||||||
ButtonEmoji: "📩"
|
|
||||||
ButtonLabel: "Open Ticket"
|
|
||||||
ButtonColour: "SUCCESS"
|
|
||||||
# Colour Options:
|
|
||||||
# -> PRIMARY - Blurple
|
|
||||||
# -> SECONDARY - Grey
|
|
||||||
# -> SUCCESS - Green
|
|
||||||
# -> DANGER - Red
|
|
||||||
|
|
||||||
PanelTitle: "Need Help? Create a Support Ticket."
|
|
||||||
|
|
||||||
TicketPanel_Description: true
|
|
||||||
TicketPanel_Thumbnail: false
|
|
||||||
TicketPanel_Image: false
|
|
||||||
|
|
||||||
PanelMessage: "Click the button to open a new support ticket."
|
|
||||||
PanelThumbnail: "https://i.imgur.com/ieeZejl.png"
|
|
||||||
PanelImage: "https://i.imgur.com/5NkcmDq.png"
|
|
||||||
@@ -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`);
|
||||||
|
});
|
||||||
+986
-131
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] });
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -1,43 +1,68 @@
|
|||||||
// SupportBot | Emerald Services
|
const supportbot = require("../Structures/ConfigStore").supportbot;
|
||||||
// Ready Event
|
const supportbotai = require("../Structures/ConfigStore").supportbotAi;
|
||||||
|
const cmdconfig = require("../Structures/ConfigStore").commands;
|
||||||
const fs = require("fs");
|
|
||||||
|
|
||||||
const Discord = require("discord.js");
|
|
||||||
const yaml = require("js-yaml");
|
|
||||||
const supportbot = yaml.load(fs.readFileSync("./Data/supportbot.yml", "utf8"));
|
|
||||||
const cmdconfig = yaml.load(fs.readFileSync("./Data/commands.yml", "utf8"));
|
|
||||||
const panelconfig = yaml.load(
|
|
||||||
fs.readFileSync("./Data/ticket-panel.yml", "utf8")
|
|
||||||
);
|
|
||||||
|
|
||||||
const Event = require("../Structures/Event.js");
|
const Event = require("../Structures/Event.js");
|
||||||
|
|
||||||
module.exports = new Event("messageReactionAdd", async (client) => {
|
module.exports = new Event(
|
||||||
let message = {
|
"messageReactionAdd",
|
||||||
guild: reaction.message.channel.guild,
|
async (client, reaction, user) => {
|
||||||
author: user,
|
try {
|
||||||
content: "N/A",
|
if (user.bot) return;
|
||||||
channel: reaction.message.channel,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (panelconfig.TicketPanel) {
|
if (reaction.partial) {
|
||||||
if (reaction.message.id === panelconfig.TicketPanelID) {
|
await reaction.fetch().catch(() => null);
|
||||||
if (
|
|
||||||
reaction.emoji.name == panelconfig.ButtonEmoji ||
|
|
||||||
reaction.emoji.id == panelconfig.ButtonEmoji
|
|
||||||
) {
|
|
||||||
await reaction.users.remove(user.id);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const cmd = client.commands.get(cmdconfig.OpenTicket);
|
|
||||||
if (!cmd) return;
|
|
||||||
|
|
||||||
cmd.run(message, message.content);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
+5
-123
@@ -1,126 +1,8 @@
|
|||||||
// SupportBot | Emerald Services
|
|
||||||
// Ready Event
|
|
||||||
|
|
||||||
const fs = require("fs");
|
|
||||||
|
|
||||||
const Discord = require("discord.js");
|
|
||||||
const yaml = require("js-yaml");
|
|
||||||
const supportbot = yaml.load(fs.readFileSync("./Data/supportbot.yml", "utf8"));
|
|
||||||
const panelconfig = yaml.load(
|
|
||||||
fs.readFileSync("./Data/ticket-panel.yml", "utf8")
|
|
||||||
);
|
|
||||||
|
|
||||||
const Event = require("../Structures/Event.js");
|
const Event = require("../Structures/Event.js");
|
||||||
|
const { runBotReady } = require("../Structures/BotStartup.js");
|
||||||
|
|
||||||
module.exports = new Event("ready", (client) => {
|
module.exports = new Event("clientReady", async (client) => {
|
||||||
client.user.setActivity(supportbot.BotActivity, {
|
// restartDiscordBot runs startup manually — clientReady often does not re-fire anyway
|
||||||
type: supportbot.ActivityType,
|
if (client.__explicitStartup) return;
|
||||||
url: supportbot.StreamingURL,
|
await runBotReady(client, { clearConsole: true, logCommands: false });
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`\u001b[32m`, `┏━━━┓╋╋╋╋╋╋╋╋╋╋╋╋╋┏┓┏━━┓╋╋╋┏┓`);
|
|
||||||
console.log(`\u001b[32m`, `┃┏━┓┃╋╋╋╋╋╋╋╋╋╋╋╋┏┛┗┫┏┓┃╋╋┏┛┗┓`);
|
|
||||||
console.log(`\u001b[32m`, `┃┗━━┳┓┏┳━━┳━━┳━━┳┻┓┏┫┗┛┗┳━┻┓┏┛`);
|
|
||||||
console.log(`\u001b[32m`, `┗━━┓┃┃┃┃┏┓┃┏┓┃┏┓┃┏┫┃┃┏━┓┃┏┓┃┃`);
|
|
||||||
console.log(`\u001b[32m`, `┃┗━┛┃┗┛┃┗┛┃┗┛┃┗┛┃┃┃┗┫┗━┛┃┗┛┃┗┓`);
|
|
||||||
console.log(`\u001b[32m`, `┗━━━┻━━┫┏━┫┏━┻━━┻┛┗━┻━━━┻━━┻━┛`);
|
|
||||||
console.log(`\u001b[32m`, `┗╋╋╋╋╋╋╋┃┃╋┃┃`);
|
|
||||||
console.log(`\u001b[32m`, `╋╋╋╋╋╋╋┗┛╋┗┛`);
|
|
||||||
console.log(` `);
|
|
||||||
console.log(`\u001b[31m`, `――――――――――――――――――――――――――――――――――――――――――――`);
|
|
||||||
console.log(` `);
|
|
||||||
console.log(
|
|
||||||
`\u001b[33m`,
|
|
||||||
`${supportbot.Name} v${supportbot.SupportBot_Version}`,
|
|
||||||
`\u001b[36m`,
|
|
||||||
`Connected to Discord`
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
`\u001b[33m`,
|
|
||||||
`Resources:`,
|
|
||||||
`\u001b[36m`,
|
|
||||||
`https://emeraldsrv.dev/resources`
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
`\u001b[33m`,
|
|
||||||
`Documentation:`,
|
|
||||||
`\u001b[36m`,
|
|
||||||
`https://supportbot.emeraldsrv.dev`
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
`\u001b[33m`,
|
|
||||||
`Discord:`,
|
|
||||||
`\u001b[36m`,
|
|
||||||
`https://emeraldsrv.dev/discord`
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
`\u001b[33m`,
|
|
||||||
`Hosting:`,
|
|
||||||
`\u001b[36m`,
|
|
||||||
`https://emeraldsrv.dev/hosting`
|
|
||||||
);
|
|
||||||
console.log(` `);
|
|
||||||
console.log(`\u001b[37m`, `SupportBot proudly created by Emerald Services`);
|
|
||||||
console.log(` `);
|
|
||||||
console.log(`\u001b[31m`, `――――――――――――――――――――――――――――――――――――――――――――`);
|
|
||||||
|
|
||||||
if (panelconfig.TicketPanel) {
|
|
||||||
let chan1 = client.channels.cache.find(
|
|
||||||
(channel) => channel.name === panelconfig.Channel
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!chan1) {
|
|
||||||
console.log(
|
|
||||||
"\u001b[33m",
|
|
||||||
"[WARN] Ticket reaction panel is not setup, You can do so via the configuration file!"
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const panelid = yaml.load(
|
|
||||||
fs.readFileSync("./Data/ticket-panel-id.yml", "utf8")
|
|
||||||
);
|
|
||||||
|
|
||||||
chan1.messages.fetch(panelid.TicketPanelID).catch(async (r) => {
|
|
||||||
let embed = new Discord.MessageEmbed()
|
|
||||||
.setTitle(`${panelconfig.PanelTitle}`)
|
|
||||||
.setColor(supportbot.SuccessColour)
|
|
||||||
.setFooter(supportbot.EmbedFooter);
|
|
||||||
|
|
||||||
if (panelconfig.TicketPanel_Description) {
|
|
||||||
embed.setDescription(panelconfig.PanelMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (panelconfig.TicketPanel_Thumbnail) {
|
|
||||||
embed.setThumbnail(panelconfig.PanelThumbnail);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (panelconfig.TicketPanel_Image) {
|
|
||||||
embed.setImage(panelconfig.PanelImage);
|
|
||||||
}
|
|
||||||
|
|
||||||
let button = new Discord.MessageButton()
|
|
||||||
.setLabel(panelconfig.ButtonLabel)
|
|
||||||
.setCustomId("openticket")
|
|
||||||
.setStyle(panelconfig.ButtonColour);
|
|
||||||
|
|
||||||
let row = new Discord.MessageActionRow().addComponents(button);
|
|
||||||
|
|
||||||
await chan1
|
|
||||||
.send({
|
|
||||||
embeds: [embed],
|
|
||||||
components: [row],
|
|
||||||
})
|
|
||||||
.then((r) => {
|
|
||||||
let data = {
|
|
||||||
TicketPanelID: `${r.id}`,
|
|
||||||
};
|
|
||||||
let yamlStr = yaml.dump(data);
|
|
||||||
fs.writeFileSync("./Data/ticket-panel-id.yml", yamlStr, "utf8");
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
console.log("Raw" + e);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Emerald Services
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +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>
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
<a href="https://opensource.org/licenses/MIT">
|
||||||
|
<img src="https://img.shields.io/badge/license-MIT-green?style=for-the-badge" alt="shield.png">
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="https://github.com/discordjs">
|
||||||
|
<img src="https://img.shields.io/badge/DISCORD.JS%20-%20v14-blue?style=for-the-badge" alt="shield.png">
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="https://github.com/nodejs/node">
|
||||||
|
<img src="https://img.shields.io/badge/NODE.JS%20-%20v20.x-green?style=for-the-badge" alt="shield.png">
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="https://github.com/Emerald-Services/SupportBot/releases">
|
||||||
|
<img src="https://img.shields.io/github/downloads/Emerald-Services/SupportBot/total?style=for-the-badge" alt="shield.png">
|
||||||
|
</a>
|
||||||
|
|
||||||
|
## Supported Versions
|
||||||
|
|
||||||
|
| SupportBot | Node.JS | Discord.JS | Supported |
|
||||||
|
|------------|---------|------------|------------|
|
||||||
|
| 26.x | 22.x | 14.x. | ✅ Yes
|
||||||
|
| 8.x | 22.x | 14.x. | ❌ Yes
|
||||||
|
| 7.7.3 | 16.x | 13.x. | ❌ No
|
||||||
|
| 7.x | 16.x | 13.x. | ❌ No
|
||||||
|
| 6.x | 14.x | 13.x. | ❌ No
|
||||||
|
| 5.x | 12.x | 13.x. | ❌ No
|
||||||
|
| 4.x | 11.x | 13.x. | ❌ No
|
||||||
|
| Older | 10.x | 13.x. | ❌ No
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://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,
|
||||||
|
};
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
// SupportBot | Emerald Services
|
|
||||||
// Addon Structure
|
|
||||||
|
|
||||||
const fs = require("fs");
|
|
||||||
|
|
||||||
const Discord = require("discord.js");
|
|
||||||
|
|
||||||
const yaml = require("js-yaml");
|
|
||||||
const supportbot = yaml.load(fs.readFileSync("./Data/supportbot.yml", "utf8"));
|
|
||||||
|
|
||||||
function RunFunction(interaction) {}
|
|
||||||
|
|
||||||
class Addon {
|
|
||||||
constructor(options) {
|
|
||||||
this.name = options.name;
|
|
||||||
this.description = options.description;
|
|
||||||
this.permission = options.permission;
|
|
||||||
this.slashCommandOptions = options.slashCommandOptions || [];
|
|
||||||
this.run = options.run;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = Addon;
|
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
+162
-68
@@ -1,98 +1,192 @@
|
|||||||
// SupportBot | Emerald Services
|
|
||||||
// Client Structure
|
|
||||||
|
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
const Discord = require("discord.js");
|
const Discord = require("discord.js");
|
||||||
const Command = require("./Command.js");
|
const { GatewayIntentBits, Partials } = require("discord.js");
|
||||||
const Event = require("./Event.js");
|
const { Command, Event } = require("./Addon.js");
|
||||||
const intents = new Discord.Intents(32767);
|
const configStore = require("./ConfigStore.js");
|
||||||
|
const { getPrimaryGuild } = require("./GuildManager.js");
|
||||||
|
|
||||||
const yaml = require("js-yaml");
|
const supportbot = configStore.supportbot;
|
||||||
const supportbot = yaml.load(fs.readFileSync("./Data/supportbot.yml", "utf8"));
|
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 {
|
class Client extends Discord.Client {
|
||||||
constructor() {
|
constructor() {
|
||||||
super({ intents });
|
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();
|
this.commands = new Discord.Collection();
|
||||||
}
|
}
|
||||||
|
|
||||||
start(token) {
|
async getChannel(channel, guild) {
|
||||||
const commandFiles = fs
|
if (!channel) return null;
|
||||||
.readdirSync("./Commands")
|
const channelStr = String(channel).toLowerCase();
|
||||||
.filter((file) => file.endsWith(".js"));
|
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)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const commands = commandFiles.map((file) => require(`../Commands/${file}`));
|
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),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
console.log(`\u001b[34;1m`, "▬▬▬▬▬▬▬ Commands ▬▬▬▬▬▬▬");
|
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)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
commands.forEach((cmd) => {
|
async start(token) {
|
||||||
console.log(`\u001b[31;1m`, `${cmd.name}`, `\u001b[32;1m`, "Loaded");
|
const commandFiles = walk("./Commands");
|
||||||
this.commands.set(cmd.name, cmd);
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`\u001b[34;1m`, "▬▬▬▬▬▬▬ Commands ▬▬▬▬▬▬▬");
|
const allCommands = commandFiles.map((file) => ({
|
||||||
|
file,
|
||||||
// Addons
|
command: require(`../${file}`),
|
||||||
|
|
||||||
const addonFiles = fs
|
|
||||||
.readdirSync("./Addons")
|
|
||||||
.filter((file) => file.endsWith(".js"));
|
|
||||||
|
|
||||||
const addons = addonFiles.map((file) => require(`../Addons/${file}`));
|
|
||||||
|
|
||||||
console.log(" ");
|
|
||||||
console.log(`\u001b[34;1m`, "▬▬▬▬▬▬▬ Addons ▬▬▬▬▬▬▬");
|
|
||||||
|
|
||||||
addons.forEach((addon) => {
|
|
||||||
console.log(`\u001b[31;1m`, `${addon.name}`, `\u001b[32;1m`, "Loaded");
|
|
||||||
this.commands.set(addon.name, addon);
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`\u001b[34;1m`, "▬▬▬▬▬▬▬ Addons ▬▬▬▬▬▬▬");
|
|
||||||
console.log(" ");
|
|
||||||
|
|
||||||
// Slash Commands
|
|
||||||
|
|
||||||
const slashCommands = commands.map((cmd) => ({
|
|
||||||
name: cmd.name.toLowerCase(),
|
|
||||||
description: cmd.description,
|
|
||||||
permissions: [],
|
|
||||||
options: cmd.slashCommandOptions,
|
|
||||||
defaultPermission: true,
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
this.removeAllListeners();
|
const enabledCommands = allCommands.filter(({ command }) => {
|
||||||
|
const matchingConfig = Object.values(cmdconfig).find(
|
||||||
this.on("ready", async () => {
|
(entry) =>
|
||||||
const cmds = await this.application?.commands.set(slashCommands);
|
entry &&
|
||||||
|
typeof entry === "object" &&
|
||||||
console.log(" ");
|
entry.Command &&
|
||||||
console.log(`\u001b[34;1m`, "▬▬▬▬▬▬▬ Slash Command ▬▬▬▬▬▬▬"),
|
String(entry.Command).toLowerCase() === String(command.name).toLowerCase(),
|
||||||
console.log(" ");
|
|
||||||
|
|
||||||
cmds.forEach((cmd) =>
|
|
||||||
console.log(`Slash Command ${cmd.name} registered`)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!matchingConfig) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return matchingConfig.Enabled !== false;
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(" ");
|
section("Commands");
|
||||||
console.log(`\u001b[34;1m`, "▬▬▬▬▬▬▬ Events ▬▬▬▬▬▬▬");
|
|
||||||
|
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")
|
fs.readdirSync("./Events")
|
||||||
.filter((file) => file.endsWith(".js"))
|
.filter((file) => file.endsWith(".js"))
|
||||||
.forEach((file) => {
|
.forEach((file) => {
|
||||||
const event = require(`../Events/${file}`);
|
const event = require(`../Events/${file}`);
|
||||||
console.log(`\u001b[31;1m`, `${event.event}`, `\u001b[32;1m`, "Loaded");
|
console.log(
|
||||||
|
`${c.green}✓${c.reset} ${c.white}${event.event}${c.reset} ${c.gray}(${file})${c.reset}`,
|
||||||
this.on(event.event, event.run.bind(null, this));
|
);
|
||||||
|
this.on(event.event, (...args) => event.run(this, ...args));
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`\u001b[34;1m`, "▬▬▬▬▬▬▬ Events ▬▬▬▬▬▬▬");
|
console.log("");
|
||||||
console.log(" ");
|
|
||||||
|
|
||||||
this.login(token);
|
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}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3
-12
@@ -1,23 +1,14 @@
|
|||||||
// SupportBot | Emerald Services
|
// SupportBot | Emerald Services
|
||||||
// Command Structure
|
// Command Structure
|
||||||
|
|
||||||
const fs = require("fs");
|
|
||||||
|
|
||||||
const Discord = require("discord.js");
|
|
||||||
|
|
||||||
const yaml = require("js-yaml");
|
|
||||||
const supportbot = yaml.load(fs.readFileSync("./Data/supportbot.yml", "utf8"));
|
|
||||||
|
|
||||||
function RunFunction(interaction) {}
|
|
||||||
|
|
||||||
class Command {
|
class Command {
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
this.name = options.name;
|
this.name = options.name;
|
||||||
this.description = options.description;
|
this.description = options.description;
|
||||||
this.permission = options.permission;
|
this.permissions = options.permissions || [];
|
||||||
this.slashCommandOptions = options.slashCommandOptions || [];
|
this.options = options.options || [];
|
||||||
this.run = options.run;
|
this.run = options.run;
|
||||||
|
this.defaultPermission = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = Command;
|
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,
|
||||||
|
};
|
||||||
+2
-11
@@ -1,19 +1,10 @@
|
|||||||
// SupportBot | Emerald Services
|
// SupportBot | Emerald Services
|
||||||
// Event Structure
|
// Event Structure
|
||||||
|
|
||||||
const fs = require("fs");
|
|
||||||
|
|
||||||
const Discord = require("discord.js");
|
|
||||||
|
|
||||||
const yaml = require("js-yaml");
|
|
||||||
const supportbot = yaml.load(fs.readFileSync("./Data/supportbot.yml", "utf8"));
|
|
||||||
|
|
||||||
function RunFunction(client, ...eventArgs) {}
|
|
||||||
|
|
||||||
class Event {
|
class Event {
|
||||||
constructor(event, runFunction) {
|
constructor(event, run) {
|
||||||
this.event = event;
|
this.event = event;
|
||||||
this.run = runFunction;
|
this.run = run;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
};
|
||||||
+16
-76
@@ -1,90 +1,30 @@
|
|||||||
// SupportBot | Emerald Services
|
// SupportBot | Emerald Services
|
||||||
// Ticket ID Structure
|
// Ticket ID Structure
|
||||||
|
|
||||||
const fs = require("fs");
|
const db = require("./Database")
|
||||||
|
|
||||||
const Discord = require("discord.js");
|
|
||||||
const Command = require("./Command.js");
|
|
||||||
const Client = require("./Client.js");
|
|
||||||
const Event = require("./Event.js");
|
|
||||||
|
|
||||||
const yaml = require("js-yaml");
|
|
||||||
const supportbot = yaml.load(fs.readFileSync("./Data/supportbot.yml", "utf8"));
|
|
||||||
|
|
||||||
class TicketNumberID {
|
class TicketNumberID {
|
||||||
static pad(guildId) {
|
static async pad() {
|
||||||
let n = this.get(guildId) + "";
|
let num = await this.get()
|
||||||
let z = "0";
|
let n = num + ""
|
||||||
let width = 4;
|
await this.set(num + 1)
|
||||||
setTimeout(() => {
|
return n.length >= 4 ? n : new Array(4 - n.length + 1).join("0") + n
|
||||||
this.set(guildId, this.get(guildId) + 1);
|
|
||||||
}, 50);
|
|
||||||
return n.length >= width ? n : new Array(width - n.length + 1).join(z) + n;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static get(guildId) {
|
static async get() {
|
||||||
let ticketnumberID = JSON.parse(
|
const row = db.getTicketPanel()
|
||||||
fs.readFileSync("./Data/TicketData.json", "utf8")
|
if (!row) return await this.set(0)
|
||||||
);
|
return row.id
|
||||||
|
|
||||||
if (!ticketnumberID[guildId]) {
|
|
||||||
ticketnumberID[guildId] = {
|
|
||||||
id: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
fs.writeFileSync(
|
|
||||||
"./Data/TicketData.json",
|
|
||||||
JSON.stringify(ticketnumberID, null, 4),
|
|
||||||
(err) => {
|
|
||||||
if (err) console.log(err);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
} else {
|
|
||||||
return ticketnumberID[guildId].id;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static set(guildId, value) {
|
static async set(value) {
|
||||||
let ticketnumberID = JSON.parse(
|
db.saveTicketPanel(`panel-${value}`, "none")
|
||||||
fs.readFileSync("./Data/TicketData.json", "utf8")
|
return value
|
||||||
);
|
|
||||||
|
|
||||||
ticketnumberID[guildId] = {
|
|
||||||
id: value,
|
|
||||||
};
|
|
||||||
|
|
||||||
fs.writeFileSync(
|
|
||||||
"./Data/TicketData.json",
|
|
||||||
JSON.stringify(ticketnumberID, null, 4),
|
|
||||||
(err) => {
|
|
||||||
if (err) console.log(err);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static reset(guildId) {
|
static async reset() {
|
||||||
let ticketnumberID = JSON.parse(
|
return await this.set(0)
|
||||||
fs.readFileSync("./Data/TicketData.json", "utf8")
|
|
||||||
);
|
|
||||||
|
|
||||||
ticketnumberID[guildId] = {
|
|
||||||
id: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
fs.writeFileSync(
|
|
||||||
"./Data/TicketData.json",
|
|
||||||
JSON.stringify(ticketnumberID, null, 4),
|
|
||||||
(err) => {
|
|
||||||
if (err) console.log(err);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = TicketNumberID;
|
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,
|
||||||
|
};
|
||||||
@@ -8,15 +8,91 @@
|
|||||||
// Installed with MIT License
|
// Installed with MIT License
|
||||||
//
|
//
|
||||||
// Discord Support: https://emeraldsrv.dev/discord
|
// Discord Support: https://emeraldsrv.dev/discord
|
||||||
// Community Resources: https://emeraldsrv.dev/resources
|
// Community Resources: https://community.emeraldsrv.dev
|
||||||
|
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
const Discord = require("discord.js");
|
const configStore = require("./Structures/ConfigStore.js");
|
||||||
const yaml = require("js-yaml");
|
|
||||||
const supportbot = yaml.load(fs.readFileSync("./Data/supportbot.yml", "utf8"));
|
|
||||||
|
|
||||||
const Client = require("./Structures/Client.js");
|
const Client = require("./Structures/Client.js");
|
||||||
const client = new Client({});
|
const client = new Client({
|
||||||
|
intents: ["Guilds", "GuildMembers", "GuildMessages", "MessageContent"],
|
||||||
|
});
|
||||||
|
|
||||||
client.start(supportbot.Token);
|
const APIServer = require("./API/server.js");
|
||||||
|
|
||||||
|
// Start dashboard API immediately — do not wait for Discord clientReady
|
||||||
|
const api = new APIServer(client);
|
||||||
|
client.apiServer = api;
|
||||||
|
|
||||||
|
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");
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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
-526
@@ -1,526 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "supportbot",
|
|
||||||
"version": "7.2.0",
|
|
||||||
"lockfileVersion": 2,
|
|
||||||
"requires": true,
|
|
||||||
"packages": {
|
|
||||||
"": {
|
|
||||||
"name": "supportbot",
|
|
||||||
"version": "7.2.0",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"discord.js": "^13.2.0",
|
|
||||||
"js-yaml": "^4.1.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@discordjs/builders": {
|
|
||||||
"version": "0.6.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-0.6.0.tgz",
|
|
||||||
"integrity": "sha512-mH3Gx61LKk2CD05laCI9K5wp+a3NyASHDUGx83DGJFkqJlRlSV5WMJNY6RS37A5SjqDtGMF4wVR9jzFaqShe6Q==",
|
|
||||||
"dependencies": {
|
|
||||||
"@sindresorhus/is": "^4.0.1",
|
|
||||||
"discord-api-types": "^0.22.0",
|
|
||||||
"ow": "^0.27.0",
|
|
||||||
"ts-mixer": "^6.0.0",
|
|
||||||
"tslib": "^2.3.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14.0.0",
|
|
||||||
"npm": ">=7.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@discordjs/builders/node_modules/discord-api-types": {
|
|
||||||
"version": "0.22.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.22.0.tgz",
|
|
||||||
"integrity": "sha512-l8yD/2zRbZItUQpy7ZxBJwaLX/Bs2TGaCthRppk8Sw24LOIWg12t9JEreezPoYD0SQcC2htNNo27kYEpYW/Srg==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@discordjs/collection": {
|
|
||||||
"version": "0.2.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-0.2.1.tgz",
|
|
||||||
"integrity": "sha512-vhxqzzM8gkomw0TYRF3tgx7SwElzUlXT/Aa41O7mOcyN6wIJfj5JmDWaO5XGKsGSsNx7F3i5oIlrucCCWV1Nog==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@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==",
|
|
||||||
"dependencies": {
|
|
||||||
"asynckit": "^0.4.0",
|
|
||||||
"combined-stream": "^1.0.8",
|
|
||||||
"mime-types": "^2.1.12"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@sapphire/async-queue": {
|
|
||||||
"version": "1.1.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.1.7.tgz",
|
|
||||||
"integrity": "sha512-EBRERa9NqK/EV6DIPBVtjjdHBsu/DSdMuYAydmoIyIPONzp0UAxf2G6JGJ52WkiONtPRx6KNuqB5Q8dm14fwyw==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=v14.0.0",
|
|
||||||
"npm": ">=7.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@sindresorhus/is": {
|
|
||||||
"version": "4.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.2.0.tgz",
|
|
||||||
"integrity": "sha512-VkE3KLBmJwcCaVARtQpfuKcKv8gcBmUubrfHGF84dXuuW6jgsRYxPtzcIhPyK9WAPpRt2/xY6zkD9MnRaJzSyw==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sindresorhus/is?sponsor=1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@types/node": {
|
|
||||||
"version": "16.11.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.4.tgz",
|
|
||||||
"integrity": "sha512-TMgXmy0v2xWyuCSCJM6NCna2snndD8yvQF67J29ipdzMcsPa9u+o0tjF5+EQNdhcuZplYuouYqpc4zcd5I6amQ=="
|
|
||||||
},
|
|
||||||
"node_modules/@types/ws": {
|
|
||||||
"version": "8.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.2.0.tgz",
|
|
||||||
"integrity": "sha512-cyeefcUCgJlEk+hk2h3N+MqKKsPViQgF5boi9TTHSK+PoR9KWBb/C5ccPcDyAqgsbAYHTwulch725DV84+pSpg==",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/node": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/argparse": {
|
|
||||||
"version": "2.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
|
||||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
|
|
||||||
},
|
|
||||||
"node_modules/asynckit": {
|
|
||||||
"version": "0.4.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
|
||||||
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
|
|
||||||
},
|
|
||||||
"node_modules/callsites": {
|
|
||||||
"version": "3.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
|
||||||
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/combined-stream": {
|
|
||||||
"version": "1.0.8",
|
|
||||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
|
||||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
|
||||||
"dependencies": {
|
|
||||||
"delayed-stream": "~1.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/delayed-stream": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
|
||||||
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.4.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/discord-api-types": {
|
|
||||||
"version": "0.23.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.23.1.tgz",
|
|
||||||
"integrity": "sha512-igWmn+45mzXRWNEPU25I/pr8MwxHb767wAr51oy3VRLRcTlp5ADBbrBR0lq3SA1Rfw3MtM4TQu1xo3kxscfVdQ==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/discord.js": {
|
|
||||||
"version": "13.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/discord.js/-/discord.js-13.2.0.tgz",
|
|
||||||
"integrity": "sha512-nyxUvL8wuQG38zx13wUMkpcA8koFszyiXdkSLwwM9opKW2LC2H5gD0cTZxImeJ6GtEnKPWT8xBiE8lLBmbNIhw==",
|
|
||||||
"dependencies": {
|
|
||||||
"@discordjs/builders": "^0.6.0",
|
|
||||||
"@discordjs/collection": "^0.2.1",
|
|
||||||
"@discordjs/form-data": "^3.0.1",
|
|
||||||
"@sapphire/async-queue": "^1.1.5",
|
|
||||||
"@types/ws": "^8.2.0",
|
|
||||||
"discord-api-types": "^0.23.1",
|
|
||||||
"node-fetch": "^2.6.1",
|
|
||||||
"ws": "^8.2.3"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=16.6.0",
|
|
||||||
"npm": ">=7.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/dot-prop": {
|
|
||||||
"version": "6.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz",
|
|
||||||
"integrity": "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==",
|
|
||||||
"dependencies": {
|
|
||||||
"is-obj": "^2.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/is-obj": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/js-yaml": {
|
|
||||||
"version": "4.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
|
||||||
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
|
||||||
"dependencies": {
|
|
||||||
"argparse": "^2.0.1"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"js-yaml": "bin/js-yaml.js"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/lodash.isequal": {
|
|
||||||
"version": "4.5.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
|
|
||||||
"integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA="
|
|
||||||
},
|
|
||||||
"node_modules/mime-db": {
|
|
||||||
"version": "1.50.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.50.0.tgz",
|
|
||||||
"integrity": "sha512-9tMZCDlYHqeERXEHO9f/hKfNXhre5dK2eE/krIvUjZbS2KPcqGDfNShIWS1uW9XOTKQKqK6qbeOci18rbfW77A==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/mime-types": {
|
|
||||||
"version": "2.1.33",
|
|
||||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.33.tgz",
|
|
||||||
"integrity": "sha512-plLElXp7pRDd0bNZHw+nMd52vRYjLwQjygaNg7ddJ2uJtTlmnTCjWuPKxVu6//AdaRuME84SvLW91sIkBqGT0g==",
|
|
||||||
"dependencies": {
|
|
||||||
"mime-db": "1.50.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/node-fetch": {
|
|
||||||
"version": "2.6.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.5.tgz",
|
|
||||||
"integrity": "sha512-mmlIVHJEu5rnIxgEgez6b9GgWXbkZj5YZ7fx+2r94a2E+Uirsp6HsPTPlomfdHtpt/B0cdKviwkoaM6pyvUOpQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"whatwg-url": "^5.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "4.x || >=6.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/ow": {
|
|
||||||
"version": "0.27.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/ow/-/ow-0.27.0.tgz",
|
|
||||||
"integrity": "sha512-SGnrGUbhn4VaUGdU0EJLMwZWSupPmF46hnTRII7aCLCrqixTAC5eKo8kI4/XXf1eaaI8YEVT+3FeGNJI9himAQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"@sindresorhus/is": "^4.0.1",
|
|
||||||
"callsites": "^3.1.0",
|
|
||||||
"dot-prop": "^6.0.1",
|
|
||||||
"lodash.isequal": "^4.5.0",
|
|
||||||
"type-fest": "^1.2.1",
|
|
||||||
"vali-date": "^1.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/tr46": {
|
|
||||||
"version": "0.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
|
||||||
"integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o="
|
|
||||||
},
|
|
||||||
"node_modules/ts-mixer": {
|
|
||||||
"version": "6.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.0.tgz",
|
|
||||||
"integrity": "sha512-nXIb1fvdY5CBSrDIblLn73NW0qRDk5yJ0Sk1qPBF560OdJfQp9jhl+0tzcY09OZ9U+6GpeoI9RjwoIKFIoB9MQ=="
|
|
||||||
},
|
|
||||||
"node_modules/tslib": {
|
|
||||||
"version": "2.3.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
|
|
||||||
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
|
|
||||||
},
|
|
||||||
"node_modules/type-fest": {
|
|
||||||
"version": "1.4.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz",
|
|
||||||
"integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/vali-date": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/vali-date/-/vali-date-1.0.0.tgz",
|
|
||||||
"integrity": "sha1-G5BKWWCfsyjvB4E4Qgk09rhnCaY=",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/webidl-conversions": {
|
|
||||||
"version": "3.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
|
||||||
"integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE="
|
|
||||||
},
|
|
||||||
"node_modules/whatwg-url": {
|
|
||||||
"version": "5.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
|
||||||
"integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=",
|
|
||||||
"dependencies": {
|
|
||||||
"tr46": "~0.0.3",
|
|
||||||
"webidl-conversions": "^3.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/ws": {
|
|
||||||
"version": "8.2.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz",
|
|
||||||
"integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10.0.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"bufferutil": "^4.0.1",
|
|
||||||
"utf-8-validate": "^5.0.2"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"bufferutil": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"utf-8-validate": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@discordjs/builders": {
|
|
||||||
"version": "0.6.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-0.6.0.tgz",
|
|
||||||
"integrity": "sha512-mH3Gx61LKk2CD05laCI9K5wp+a3NyASHDUGx83DGJFkqJlRlSV5WMJNY6RS37A5SjqDtGMF4wVR9jzFaqShe6Q==",
|
|
||||||
"requires": {
|
|
||||||
"@sindresorhus/is": "^4.0.1",
|
|
||||||
"discord-api-types": "^0.22.0",
|
|
||||||
"ow": "^0.27.0",
|
|
||||||
"ts-mixer": "^6.0.0",
|
|
||||||
"tslib": "^2.3.1"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"discord-api-types": {
|
|
||||||
"version": "0.22.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.22.0.tgz",
|
|
||||||
"integrity": "sha512-l8yD/2zRbZItUQpy7ZxBJwaLX/Bs2TGaCthRppk8Sw24LOIWg12t9JEreezPoYD0SQcC2htNNo27kYEpYW/Srg=="
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"@discordjs/collection": {
|
|
||||||
"version": "0.2.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-0.2.1.tgz",
|
|
||||||
"integrity": "sha512-vhxqzzM8gkomw0TYRF3tgx7SwElzUlXT/Aa41O7mOcyN6wIJfj5JmDWaO5XGKsGSsNx7F3i5oIlrucCCWV1Nog=="
|
|
||||||
},
|
|
||||||
"@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"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"@sapphire/async-queue": {
|
|
||||||
"version": "1.1.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.1.7.tgz",
|
|
||||||
"integrity": "sha512-EBRERa9NqK/EV6DIPBVtjjdHBsu/DSdMuYAydmoIyIPONzp0UAxf2G6JGJ52WkiONtPRx6KNuqB5Q8dm14fwyw=="
|
|
||||||
},
|
|
||||||
"@sindresorhus/is": {
|
|
||||||
"version": "4.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.2.0.tgz",
|
|
||||||
"integrity": "sha512-VkE3KLBmJwcCaVARtQpfuKcKv8gcBmUubrfHGF84dXuuW6jgsRYxPtzcIhPyK9WAPpRt2/xY6zkD9MnRaJzSyw=="
|
|
||||||
},
|
|
||||||
"@types/node": {
|
|
||||||
"version": "16.11.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.4.tgz",
|
|
||||||
"integrity": "sha512-TMgXmy0v2xWyuCSCJM6NCna2snndD8yvQF67J29ipdzMcsPa9u+o0tjF5+EQNdhcuZplYuouYqpc4zcd5I6amQ=="
|
|
||||||
},
|
|
||||||
"@types/ws": {
|
|
||||||
"version": "8.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.2.0.tgz",
|
|
||||||
"integrity": "sha512-cyeefcUCgJlEk+hk2h3N+MqKKsPViQgF5boi9TTHSK+PoR9KWBb/C5ccPcDyAqgsbAYHTwulch725DV84+pSpg==",
|
|
||||||
"requires": {
|
|
||||||
"@types/node": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"argparse": {
|
|
||||||
"version": "2.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
|
||||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
|
|
||||||
},
|
|
||||||
"asynckit": {
|
|
||||||
"version": "0.4.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
|
||||||
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
|
|
||||||
},
|
|
||||||
"callsites": {
|
|
||||||
"version": "3.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
|
||||||
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="
|
|
||||||
},
|
|
||||||
"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-api-types": {
|
|
||||||
"version": "0.23.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.23.1.tgz",
|
|
||||||
"integrity": "sha512-igWmn+45mzXRWNEPU25I/pr8MwxHb767wAr51oy3VRLRcTlp5ADBbrBR0lq3SA1Rfw3MtM4TQu1xo3kxscfVdQ=="
|
|
||||||
},
|
|
||||||
"discord.js": {
|
|
||||||
"version": "13.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/discord.js/-/discord.js-13.2.0.tgz",
|
|
||||||
"integrity": "sha512-nyxUvL8wuQG38zx13wUMkpcA8koFszyiXdkSLwwM9opKW2LC2H5gD0cTZxImeJ6GtEnKPWT8xBiE8lLBmbNIhw==",
|
|
||||||
"requires": {
|
|
||||||
"@discordjs/builders": "^0.6.0",
|
|
||||||
"@discordjs/collection": "^0.2.1",
|
|
||||||
"@discordjs/form-data": "^3.0.1",
|
|
||||||
"@sapphire/async-queue": "^1.1.5",
|
|
||||||
"@types/ws": "^8.2.0",
|
|
||||||
"discord-api-types": "^0.23.1",
|
|
||||||
"node-fetch": "^2.6.1",
|
|
||||||
"ws": "^8.2.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"dot-prop": {
|
|
||||||
"version": "6.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz",
|
|
||||||
"integrity": "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==",
|
|
||||||
"requires": {
|
|
||||||
"is-obj": "^2.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"is-obj": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w=="
|
|
||||||
},
|
|
||||||
"js-yaml": {
|
|
||||||
"version": "4.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
|
||||||
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
|
||||||
"requires": {
|
|
||||||
"argparse": "^2.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"lodash.isequal": {
|
|
||||||
"version": "4.5.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
|
|
||||||
"integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA="
|
|
||||||
},
|
|
||||||
"mime-db": {
|
|
||||||
"version": "1.50.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.50.0.tgz",
|
|
||||||
"integrity": "sha512-9tMZCDlYHqeERXEHO9f/hKfNXhre5dK2eE/krIvUjZbS2KPcqGDfNShIWS1uW9XOTKQKqK6qbeOci18rbfW77A=="
|
|
||||||
},
|
|
||||||
"mime-types": {
|
|
||||||
"version": "2.1.33",
|
|
||||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.33.tgz",
|
|
||||||
"integrity": "sha512-plLElXp7pRDd0bNZHw+nMd52vRYjLwQjygaNg7ddJ2uJtTlmnTCjWuPKxVu6//AdaRuME84SvLW91sIkBqGT0g==",
|
|
||||||
"requires": {
|
|
||||||
"mime-db": "1.50.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node-fetch": {
|
|
||||||
"version": "2.6.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.5.tgz",
|
|
||||||
"integrity": "sha512-mmlIVHJEu5rnIxgEgez6b9GgWXbkZj5YZ7fx+2r94a2E+Uirsp6HsPTPlomfdHtpt/B0cdKviwkoaM6pyvUOpQ==",
|
|
||||||
"requires": {
|
|
||||||
"whatwg-url": "^5.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ow": {
|
|
||||||
"version": "0.27.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/ow/-/ow-0.27.0.tgz",
|
|
||||||
"integrity": "sha512-SGnrGUbhn4VaUGdU0EJLMwZWSupPmF46hnTRII7aCLCrqixTAC5eKo8kI4/XXf1eaaI8YEVT+3FeGNJI9himAQ==",
|
|
||||||
"requires": {
|
|
||||||
"@sindresorhus/is": "^4.0.1",
|
|
||||||
"callsites": "^3.1.0",
|
|
||||||
"dot-prop": "^6.0.1",
|
|
||||||
"lodash.isequal": "^4.5.0",
|
|
||||||
"type-fest": "^1.2.1",
|
|
||||||
"vali-date": "^1.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tr46": {
|
|
||||||
"version": "0.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
|
||||||
"integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o="
|
|
||||||
},
|
|
||||||
"ts-mixer": {
|
|
||||||
"version": "6.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.0.tgz",
|
|
||||||
"integrity": "sha512-nXIb1fvdY5CBSrDIblLn73NW0qRDk5yJ0Sk1qPBF560OdJfQp9jhl+0tzcY09OZ9U+6GpeoI9RjwoIKFIoB9MQ=="
|
|
||||||
},
|
|
||||||
"tslib": {
|
|
||||||
"version": "2.3.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
|
|
||||||
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
|
|
||||||
},
|
|
||||||
"type-fest": {
|
|
||||||
"version": "1.4.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz",
|
|
||||||
"integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA=="
|
|
||||||
},
|
|
||||||
"vali-date": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/vali-date/-/vali-date-1.0.0.tgz",
|
|
||||||
"integrity": "sha1-G5BKWWCfsyjvB4E4Qgk09rhnCaY="
|
|
||||||
},
|
|
||||||
"webidl-conversions": {
|
|
||||||
"version": "3.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
|
||||||
"integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE="
|
|
||||||
},
|
|
||||||
"whatwg-url": {
|
|
||||||
"version": "5.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
|
||||||
"integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=",
|
|
||||||
"requires": {
|
|
||||||
"tr46": "~0.0.3",
|
|
||||||
"webidl-conversions": "^3.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ws": {
|
|
||||||
"version": "8.2.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz",
|
|
||||||
"integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==",
|
|
||||||
"requires": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+26
-8
@@ -1,23 +1,41 @@
|
|||||||
{
|
{
|
||||||
"name": "supportbot",
|
"name": "supportbot",
|
||||||
"version": "7.2.0",
|
"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.",
|
"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",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "npm i && node index.js"
|
"setup": "npm i && node index.js",
|
||||||
|
"start": "node index.js",
|
||||||
|
"git:setup": "node scripts/setup-git-security.js"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/1tzemerald/SupportBot.git"
|
"url": "git+https://github.com/Emerald-Services/SupportBot.git"
|
||||||
},
|
},
|
||||||
"author": "Emerald Services",
|
"author": "Emerald Services",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/1tzemerald/SupportBot/issues"
|
"url": "https://github.com/Emerald-Services/SupportBot/issues"
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/1tzemerald/SupportBot#readme",
|
"homepage": "https://emeraldsrv.dev",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"discord.js": "^13.2.0",
|
"@vitalets/google-translate-api": "^9.2.1",
|
||||||
"js-yaml": "^4.1.0"
|
"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 |
Reference in new issue
Block a user