Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b941909983 | ||
|
|
01f9ebe387 | ||
|
|
a746c468e5 | ||
|
|
2017c33756 | ||
|
|
4eef1867e3 | ||
|
|
a7598d26aa | ||
|
|
762fcf45c3 | ||
|
|
ea2a8c97ae | ||
|
|
004b638dbf | ||
|
|
69371ae619 | ||
|
|
b145d5fa01 | ||
|
|
8c9fcb32e0 | ||
|
|
17d45de378 | ||
|
|
b96c60912c | ||
|
|
e97e281bda | ||
|
|
00cebb7bb5 | ||
|
|
014f263bf1 | ||
|
|
942d71040b | ||
|
|
92d21e7fc0 | ||
|
|
07e4705a6a | ||
|
|
6320296f0f | ||
|
|
f4b14e6c01 | ||
|
|
7e22831d08 | ||
|
|
9332fc9254 | ||
|
|
018c949104 | ||
|
|
b55976508e | ||
|
|
264dcb3053 | ||
|
|
22c33c0f7a | ||
|
|
57d420a9f4 | ||
|
|
f8cc08ba3e | ||
|
|
60d5c4cb1f | ||
|
|
0288e2f4de | ||
|
|
33d5a79759 | ||
|
|
30b89a3cf9 | ||
|
|
427dc2d040 | ||
|
|
033f32d596 | ||
|
|
d5fa42b55d | ||
|
|
4bb35b1e50 | ||
|
|
d451857d7b | ||
|
|
78b804176a | ||
|
|
15b780ab62 | ||
|
|
9448c4e3d6 | ||
|
|
4af9759700 | ||
|
|
835cbbe937 | ||
|
|
491bb9ba65 | ||
|
|
ced29535f9 | ||
|
|
d99e9a105b | ||
|
|
c72c7c5c34 | ||
|
|
23960bc30d | ||
|
|
abb48c281b | ||
|
|
7acb825713 | ||
|
|
dd2f329485 | ||
|
|
75577ec0bd | ||
|
|
7366bcda71 | ||
|
|
23e43cfad3 | ||
|
|
ad9a8085aa | ||
|
|
2f23e8f23d | ||
|
|
4c07b8a236 | ||
|
|
571bd586fa | ||
|
|
eac2d88873 | ||
|
|
93c793f393 | ||
|
|
1aa97b63f9 | ||
|
|
c1b76a2b84 | ||
|
|
a1429aa723 | ||
|
|
2de7c7b19c | ||
|
|
7383bd2c48 | ||
|
|
a05d65099b | ||
|
|
c6db929d12 | ||
|
|
dc88f6c51a | ||
|
|
d69fb0e26d | ||
|
|
4b2d53d3f4 | ||
|
|
e3fa8b6d72 | ||
|
|
5c6a9f7306 | ||
|
|
71e33dbf66 | ||
|
|
d468ab1575 | ||
|
|
74482af471 | ||
|
|
0b8b092cb9 | ||
|
|
3cdfaef942 | ||
|
|
fcf441a190 | ||
|
|
5a1c4ad99c | ||
|
|
d9cb72f22e | ||
|
|
58eb37124e | ||
|
|
afeeda3a8d | ||
|
|
fece90d2bd | ||
|
|
357a85fa3d | ||
|
|
f34158caa6 | ||
|
|
1bb343e49a | ||
|
|
dffa5d4161 | ||
|
|
13590b08a8 | ||
|
|
6e14ce209d | ||
|
|
72b8c2247e | ||
|
|
925504a928 | ||
|
|
236d67707f | ||
|
|
753f432c95 | ||
|
|
c824bc51b2 | ||
|
|
39ee540b55 | ||
|
|
903b214051 | ||
|
|
ac0daf5fe7 | ||
|
|
80e38a07e9 | ||
|
|
d2598e09f7 | ||
|
|
28aab47150 | ||
|
|
81bda67ae4 | ||
|
|
25bbd1ff85 | ||
|
|
164d695a5f | ||
|
|
57e4a3e77c | ||
|
|
a6ac532b2b | ||
|
|
4f14722e14 | ||
|
|
9a6eece7a1 | ||
|
|
fd65d0a9a7 | ||
|
|
48500fe8a4 | ||
|
|
8d8d9e4bee | ||
|
|
dcbddf9f23 | ||
|
|
7449c0a61a | ||
|
|
c95b8ccb32 | ||
|
|
2c5bfe8e33 | ||
|
|
a3bbd97333 | ||
|
|
4ea488951d | ||
|
|
481424d5a6 | ||
|
|
fe82e96cc1 | ||
|
|
976258c959 | ||
|
|
83cf0689e2 | ||
|
|
c911cb4423 | ||
|
|
4eadef8b65 | ||
|
|
4e081358ff | ||
|
|
209ec0b435 | ||
|
|
6d363abb54 | ||
|
|
6f8b84aeda | ||
|
|
5b8c5da33a | ||
|
|
b6ef0f95cb | ||
|
|
cce1480a42 | ||
|
|
44fbe9f2d6 | ||
|
|
29db6c7d67 | ||
|
|
26e9b56961 | ||
|
|
979271cfb5 | ||
|
|
69f0b00ed3 | ||
|
|
c1a1839457 | ||
|
|
dd4b53800e | ||
|
|
1fc94528fc | ||
|
|
bb1a817980 | ||
|
|
53305acbc7 | ||
|
|
d0f1bae0bd | ||
|
|
c0cf3b1bb7 | ||
|
|
ce67b668d5 | ||
|
|
3273cfd0e1 | ||
|
|
fd992a6514 | ||
|
|
550d681fe1 | ||
|
|
7d29226953 | ||
|
|
38da72ca91 | ||
|
|
eab11361cb | ||
|
|
b281bba36f | ||
|
|
5e38573aed | ||
|
|
da7b6d487c | ||
|
|
6bf4e5f4bb | ||
|
|
213f532a3c | ||
|
|
45bd88e4d2 | ||
|
|
4eb8658459 | ||
|
|
b37118fee2 | ||
|
|
026caba68a | ||
|
|
2003f8801a | ||
|
|
473bacf13a | ||
|
|
8b3f2bc61a | ||
|
|
3aa373c33c | ||
|
|
9b6263e583 | ||
|
|
3e066d08ce | ||
|
|
aa48e28162 | ||
|
|
dc660ed94a | ||
|
|
e02ecf9553 | ||
|
|
23acc0926f | ||
|
|
3fc1cd18d5 | ||
|
|
ebe29b403e | ||
|
|
dcba3312dd | ||
|
|
7fddeeec37 | ||
|
|
b5341b023f | ||
|
|
bbb7443ef2 | ||
|
|
22df347bb9 | ||
|
|
9aed6b8f35 | ||
|
|
9e7ec9f1bd | ||
|
|
373f43eac5 | ||
|
|
6c91b2791a | ||
|
|
bde62a431f | ||
|
|
615a074540 | ||
|
|
09b335ac46 | ||
|
|
2701880c77 | ||
|
|
24c8f2fb38 | ||
|
|
c3ec158ac3 | ||
|
|
b3690222d0 | ||
|
|
4d7583785b | ||
|
|
17ad1a15db | ||
|
|
4c89de9e76 | ||
|
|
5f25af41f9 | ||
|
|
3a3ee357fc | ||
|
|
08a6d27903 | ||
|
|
33f7675aac | ||
|
|
1178a0a611 | ||
|
|
96ded77bec | ||
|
|
e83ca8cb2b | ||
|
|
e507176d39 | ||
|
|
4e554b8bee | ||
|
|
bc649caa99 | ||
|
|
f7a4c7b616 | ||
|
|
2c7be5882c | ||
|
|
6107d268f7 | ||
|
|
7760699155 | ||
|
|
acf349c72e | ||
|
|
8096e5b882 | ||
|
|
873cfc7dc2 | ||
|
|
038174bc7e | ||
|
|
14881096bb | ||
|
|
ed1905d8f4 | ||
|
|
72c0a2efa3 | ||
|
|
e599d48713 | ||
|
|
7f44634d4f | ||
|
|
44170caac6 | ||
|
|
adedfadd92 | ||
|
|
6b01d0c8fe | ||
|
|
d4d96f59ab | ||
|
|
a1bf7ed6bd | ||
|
|
306e20efc9 | ||
|
|
f593f6e0a8 | ||
|
|
74646a4318 | ||
|
|
8bfcba8832 | ||
|
|
4ba5621a4d | ||
|
|
1f928324c2 | ||
|
|
3771872cd2 | ||
|
|
f359a60343 | ||
|
|
f53afd7bc9 | ||
|
|
e7dcc2ab29 | ||
|
|
0118b9e591 | ||
|
|
ffe42f9777 | ||
|
|
5a467599e9 | ||
|
|
c56e78484a | ||
|
|
1ee0d4a09e | ||
|
|
ffdce5820b | ||
|
|
9fed504f37 | ||
|
|
6c5799be0d | ||
|
|
b6747a0340 | ||
|
|
cd6df02e30 | ||
|
|
edbef6a36f | ||
|
|
b6a0016f5f | ||
|
|
e740c17837 | ||
|
|
7eb949f109 | ||
|
|
02394e310d | ||
|
|
ef83832819 | ||
|
|
a82101ab30 | ||
|
|
007c78930e | ||
|
|
300c83512c | ||
|
|
b674872c56 | ||
|
|
12a7680275 | ||
|
|
c9cf7c942b | ||
|
|
8e0326c8f8 | ||
|
|
13972119a8 | ||
|
|
acf165a119 | ||
|
|
bdff7c6069 | ||
|
|
e2e3376c87 | ||
|
|
3c613088e5 | ||
|
|
7e9ece50a0 | ||
|
|
af81b6536a | ||
|
|
4cd3ecf81f | ||
|
|
31f6b9e08e | ||
|
|
02de920cb3 | ||
|
|
84c0905297 | ||
|
|
250583f859 | ||
|
|
ee7d42196d | ||
|
|
a397de2b9f | ||
|
|
0610df9400 | ||
|
|
ed583deb31 | ||
|
|
e039048fc6 | ||
|
|
f2ea9650c4 | ||
|
|
1e89057006 | ||
|
|
019bb4842f | ||
|
|
4cb45beded | ||
|
|
8a2059f087 | ||
|
|
a6b105a2e1 | ||
|
|
6c14a6a06c | ||
|
|
cddd5d0026 | ||
|
|
78dea91e1d | ||
|
|
3df591ec52 | ||
|
|
1c4032caa6 | ||
|
|
465c422171 | ||
|
|
284e8dc6f5 | ||
|
|
3fc905c82e | ||
|
|
55a42b88ae | ||
|
|
bd26162e4a | ||
|
|
62e7e33406 | ||
|
|
2f0ddcec3f | ||
|
|
7cdfa68e0e | ||
|
|
f8939a964d | ||
|
|
7830e5dda5 | ||
|
|
6aa2f22294 | ||
|
|
50b14e3d44 | ||
|
|
e09c2192c2 | ||
|
|
b725d2cc62 | ||
|
|
abf3f617a4 | ||
|
|
1eda7e66fe | ||
|
|
b5000531c4 | ||
|
|
d4f95e752b | ||
|
|
e31c611f70 | ||
|
|
085481bee6 | ||
|
|
cf610fbe49 | ||
|
|
b681eb3cd2 | ||
|
|
f8ffdf0e81 | ||
|
|
ba38bce1da | ||
|
|
3a39b05fe4 | ||
|
|
27d2982b28 | ||
|
|
33e4d97be5 | ||
|
|
654064d7c5 | ||
|
|
aeb0da9e0d | ||
|
|
cb1a2fe067 | ||
|
|
85667ddfad | ||
|
|
7ad5016966 | ||
|
|
45c3315fd0 | ||
|
|
c5f550dd00 | ||
|
|
37c870c7ab | ||
|
|
155b768f73 | ||
|
|
09f0873f27 | ||
|
|
19a12f6c8f | ||
|
|
79a498d1d6 | ||
|
|
12a03c36f3 |
No files matched your search
@@ -0,0 +1,2 @@
|
|||||||
|
Configs/supportbot.yml filter=supportbot-token
|
||||||
|
Configs/api.yml filter=api-secrets
|
||||||
@@ -1,32 +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
|
|
||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
node_modules
|
||||||
|
|
||||||
|
# Local database, runtime data, and transcripts
|
||||||
|
Data/*.db
|
||||||
|
Data/*.db-shm
|
||||||
|
Data/*.db-wal
|
||||||
|
Data/Transcripts/*
|
||||||
|
Data/dashboard-groups.json
|
||||||
|
Data/dashboard-users.json
|
||||||
|
Data/transcript-settings.json
|
||||||
|
|
||||||
|
# Local log files
|
||||||
|
Logs/Output/*
|
||||||
|
Logs/Errors/*
|
||||||
|
Logs/Warns/*
|
||||||
|
|
||||||
|
# Dashboard UI source at repo root only (do not ignore public/dashboard/)
|
||||||
|
/dashboard/
|
||||||
|
|
||||||
|
# Local secrets — use *.example as templates
|
||||||
|
Configs/api.yml
|
||||||
+2135
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,26 @@
|
|||||||
|
const Discord = require("discord.js");
|
||||||
|
const fs = require("fs");
|
||||||
|
const yaml = require("js-yaml");
|
||||||
|
const { Command } = require('../Structures/Addon.js'); // Adjust the path as needed
|
||||||
|
const supportbot = require("../Structures/ConfigStore").supportbot;
|
||||||
|
|
||||||
|
module.exports = new Command({
|
||||||
|
name: "addon",
|
||||||
|
description: "This is a sample addon!",
|
||||||
|
options: [],
|
||||||
|
permissions: ["Administrator"],
|
||||||
|
|
||||||
|
async run(interaction) {
|
||||||
|
let disableCommand = true;
|
||||||
|
|
||||||
|
const PingEmbed = new Discord.EmbedBuilder()
|
||||||
|
.setDescription(
|
||||||
|
`This is a sample addon.\n\nPong! \`${interaction.client.ws.ping} ms\``
|
||||||
|
)
|
||||||
|
.setColor(supportbot.Embed.Colours.General);
|
||||||
|
|
||||||
|
interaction.reply({
|
||||||
|
embeds: [PingEmbed],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
// SupportBot | Emerald Services
|
||||||
|
// AI Command (Consolidated)
|
||||||
|
|
||||||
|
const fs = require("fs");
|
||||||
|
const { ApplicationCommandOptionType, ApplicationCommandType, EmbedBuilder, MessageFlags } = require("discord.js");
|
||||||
|
const yaml = require("js-yaml");
|
||||||
|
|
||||||
|
const supportbot = require("../../Structures/ConfigStore").supportbot;
|
||||||
|
const cmdconfig = require("../../Structures/ConfigStore").commands;
|
||||||
|
const supportbotai = require("../../Structures/ConfigStore").supportbotAi;
|
||||||
|
|
||||||
|
const Command = require("../../Structures/Command.js");
|
||||||
|
const AIDatabase = require("../../Structures/AIDatabase.js");
|
||||||
|
|
||||||
|
module.exports = new Command({
|
||||||
|
name: cmdconfig.AI?.Command || "ai",
|
||||||
|
description: cmdconfig.AI?.Description || "Manage SupportBot AI settings and memory.",
|
||||||
|
type: ApplicationCommandType.ChatInput,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: cmdconfig.AI?.Train?.Command || "train",
|
||||||
|
description: cmdconfig.AI?.Train?.Description || "Train the AI by adding knowledge to its database.",
|
||||||
|
type: ApplicationCommandOptionType.Subcommand,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: "content",
|
||||||
|
description: "The text/knowledge you want the bot to learn",
|
||||||
|
type: ApplicationCommandOptionType.String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: cmdconfig.AI?.Knowledge?.Command || "knowledge",
|
||||||
|
description: cmdconfig.AI?.Knowledge?.Description || "Manage the AI knowledge base.",
|
||||||
|
type: ApplicationCommandOptionType.SubcommandGroup,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: cmdconfig.AI?.Knowledge?.Clear?.Command || "clear",
|
||||||
|
description: cmdconfig.AI?.Knowledge?.Clear?.Description || "Clear all trained knowledge from the AI.",
|
||||||
|
type: ApplicationCommandOptionType.Subcommand,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: cmdconfig.AI?.Memory?.Command || "memory",
|
||||||
|
description: cmdconfig.AI?.Memory?.Description || "Manage the AI conversational memory.",
|
||||||
|
type: ApplicationCommandOptionType.SubcommandGroup,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: cmdconfig.AI?.Memory?.Clear?.Command || "clear",
|
||||||
|
description: cmdconfig.AI?.Memory?.Clear?.Description || "Clear recent conversation memory for this channel.",
|
||||||
|
type: ApplicationCommandOptionType.Subcommand,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
permissions: cmdconfig.AI?.Permission || ["Administrator"],
|
||||||
|
|
||||||
|
async run(interaction) {
|
||||||
|
if (!supportbotai.Enabled) {
|
||||||
|
return interaction.reply({
|
||||||
|
content: "SupportBot AI is currently disabled.",
|
||||||
|
flags: MessageFlags.Ephemeral,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const subcommandGroup = interaction.options.getSubcommandGroup(false);
|
||||||
|
const subcommand = interaction.options.getSubcommand(false);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (subcommand === "train" || subcommand === cmdconfig.AI?.Train?.Command) {
|
||||||
|
const content = interaction.options.getString("content");
|
||||||
|
|
||||||
|
AIDatabase.addKnowledge({
|
||||||
|
sourceType: "user_command",
|
||||||
|
sourceName: interaction.user.tag,
|
||||||
|
title: "Trained Knowledge",
|
||||||
|
content: content,
|
||||||
|
});
|
||||||
|
|
||||||
|
const SuccessEmbed = new EmbedBuilder()
|
||||||
|
.setDescription(`✅ **Knowledge Added!**\n\nThe AI has learned the following information:\n\`\`\`${content}\`\`\``)
|
||||||
|
.setColor(supportbot.Embed.Colours.Success);
|
||||||
|
|
||||||
|
return interaction.reply({ embeds: [SuccessEmbed], flags: MessageFlags.Ephemeral });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subcommandGroup === (cmdconfig.AI?.Knowledge?.Command || "knowledge")) {
|
||||||
|
if (subcommand === (cmdconfig.AI?.Knowledge?.Clear?.Command || "clear")) {
|
||||||
|
AIDatabase.clearKnowledge();
|
||||||
|
|
||||||
|
const SuccessEmbed = new EmbedBuilder()
|
||||||
|
.setDescription(`✅ **Knowledge Cleared!**\n\nAll manually trained knowledge has been wiped from the database.`)
|
||||||
|
.setColor(supportbot.Embed.Colours.Success);
|
||||||
|
|
||||||
|
return interaction.reply({ embeds: [SuccessEmbed], flags: MessageFlags.Ephemeral });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subcommandGroup === (cmdconfig.AI?.Memory?.Command || "memory")) {
|
||||||
|
if (subcommand === (cmdconfig.AI?.Memory?.Clear?.Command || "clear")) {
|
||||||
|
AIDatabase.clearConversations(interaction.channel.id);
|
||||||
|
|
||||||
|
const SuccessEmbed = new EmbedBuilder()
|
||||||
|
.setDescription(`✅ **Memory Cleared!**\n\nThe AI's recent conversation memory in this channel has been wiped. It will no longer remember previous messages sent here.`)
|
||||||
|
.setColor(supportbot.Embed.Colours.Success);
|
||||||
|
|
||||||
|
return interaction.reply({ embeds: [SuccessEmbed], flags: MessageFlags.Ephemeral });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("AI Command Error:", error);
|
||||||
|
const ErrorEmbed = new EmbedBuilder()
|
||||||
|
.setDescription(`❌ **Error:** Failed to process the AI command.`)
|
||||||
|
.setColor(supportbot.Embed.Colours.Error);
|
||||||
|
|
||||||
|
return interaction.reply({ embeds: [ErrorEmbed], flags: MessageFlags.Ephemeral });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
const Discord = require("discord.js");
|
||||||
|
const yaml = require("js-yaml");
|
||||||
|
|
||||||
|
const supportbot = require("../../Structures/ConfigStore").supportbot;
|
||||||
|
const cmdconfig = require("../../Structures/ConfigStore").commands;
|
||||||
|
|
||||||
|
const Command = require("../../Structures/Command.js");
|
||||||
|
|
||||||
|
module.exports = new Command({
|
||||||
|
name: cmdconfig.Help.Command,
|
||||||
|
description: cmdconfig.Help.Description,
|
||||||
|
type: Discord.ApplicationCommandType.ChatInput,
|
||||||
|
options: [],
|
||||||
|
permissions: cmdconfig.Help.Permission,
|
||||||
|
|
||||||
|
async run(interaction) {
|
||||||
|
const { getRole } = interaction.client;
|
||||||
|
let SupportStaff = await getRole(supportbot.Roles.StaffMember.Staff, interaction.guild);
|
||||||
|
let Admin = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild);
|
||||||
|
if (!SupportStaff || !Admin) {
|
||||||
|
return interaction.reply(
|
||||||
|
"Some roles seem to be missing!\nPlease check for errors when starting the bot."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Separate regular commands and addon commands
|
||||||
|
let botCommands = "";
|
||||||
|
let addonCommands = "";
|
||||||
|
|
||||||
|
interaction.client.commands.forEach((command) => {
|
||||||
|
if (command.permissions) {
|
||||||
|
if (command.addon) {
|
||||||
|
addonCommands += `\`${command.name}\` `;
|
||||||
|
} else {
|
||||||
|
botCommands += `\`${command.name}\` `;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create embed
|
||||||
|
const HelpEmbed1 = new Discord.EmbedBuilder()
|
||||||
|
.setTitle(supportbot.General.Name + " Commands")
|
||||||
|
.setThumbnail(interaction.user.displayAvatarURL())
|
||||||
|
.setColor(supportbot.Embed.Colours.General)
|
||||||
|
.addFields({
|
||||||
|
name: "🖥️ Commands\n",
|
||||||
|
value: botCommands || "No commands available.",
|
||||||
|
inline: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
HelpEmbed1.setFooter({
|
||||||
|
text: supportbot.Embed.Footer,
|
||||||
|
iconURL: interaction.user.displayAvatarURL(),
|
||||||
|
});
|
||||||
|
|
||||||
|
await interaction.reply({
|
||||||
|
flags: Discord.MessageFlags.Ephemeral,
|
||||||
|
embeds: [HelpEmbed1],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
// SupportBot | Emerald Services
|
||||||
|
// Info Command
|
||||||
|
|
||||||
|
const Discord = require("discord.js");
|
||||||
|
|
||||||
|
const supportbot = require("../../Structures/ConfigStore").supportbot;
|
||||||
|
const cmdconfig = require("../../Structures/ConfigStore").commands;
|
||||||
|
const msgconfig = require("../../Structures/ConfigStore").messages;
|
||||||
|
|
||||||
|
const Command = require("../../Structures/Command.js");
|
||||||
|
|
||||||
|
module.exports = new Command({
|
||||||
|
name: cmdconfig.Info.Command,
|
||||||
|
description: cmdconfig.Info.Description,
|
||||||
|
options: [],
|
||||||
|
permissions: cmdconfig.Info.Permission,
|
||||||
|
|
||||||
|
async run(interaction) {
|
||||||
|
let disableCommand = true;
|
||||||
|
|
||||||
|
const InfoButton = new Discord.ButtonBuilder()
|
||||||
|
.setLabel(msgconfig.Info.Button)
|
||||||
|
.setURL(msgconfig.Info.URL)
|
||||||
|
.setStyle("Link");
|
||||||
|
|
||||||
|
const inforow = new Discord.ActionRowBuilder().addComponents(InfoButton);
|
||||||
|
|
||||||
|
const InfoEmbed = new Discord.EmbedBuilder()
|
||||||
|
.setURL(msgconfig.Info.URL)
|
||||||
|
.setTitle(msgconfig.Info.Title)
|
||||||
|
.setDescription(msgconfig.Info.Description)
|
||||||
|
.setColor(msgconfig.Info.Colour)
|
||||||
|
|
||||||
|
interaction.reply({
|
||||||
|
embeds: [InfoEmbed],
|
||||||
|
components: [inforow],
|
||||||
|
});
|
||||||
|
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
// SupportBot | Emerald Services
|
||||||
|
// Ping Command
|
||||||
|
|
||||||
|
const fs = require("fs");
|
||||||
|
|
||||||
|
const Discord = require("discord.js");
|
||||||
|
const yaml = require("js-yaml");
|
||||||
|
const supportbot = require("../../Structures/ConfigStore").supportbot;
|
||||||
|
const cmdconfig = require("../../Structures/ConfigStore").commands;
|
||||||
|
|
||||||
|
const Command = require("../../Structures/Command.js");
|
||||||
|
|
||||||
|
module.exports = new Command({
|
||||||
|
name: cmdconfig.Ping.Command,
|
||||||
|
description: cmdconfig.Ping.Description,
|
||||||
|
options: [],
|
||||||
|
permissions: cmdconfig.Ping.Permission,
|
||||||
|
|
||||||
|
async run(interaction) {
|
||||||
|
const PingEmbed = new Discord.EmbedBuilder()
|
||||||
|
.setDescription(
|
||||||
|
`:ping_pong: **Ping:** \`${interaction.client.ws.ping} ms\``,
|
||||||
|
)
|
||||||
|
.setColor(supportbot.Embed.Colours.General);
|
||||||
|
|
||||||
|
await interaction.reply({
|
||||||
|
embeds: [PingEmbed],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,503 @@
|
|||||||
|
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 getStaffTicketStats(viewingUserId) {
|
||||||
|
const allTickets = typeof db.getAllTickets === "function" ? db.getAllTickets() : [];
|
||||||
|
const ticketsClaimed = allTickets.filter(
|
||||||
|
(t) => t.claimedBy === viewingUserId || t.claimed_by === viewingUserId,
|
||||||
|
);
|
||||||
|
const ticketsOpen = ticketsClaimed.filter(
|
||||||
|
(t) => (t.status || "").toLowerCase() === "open",
|
||||||
|
);
|
||||||
|
|
||||||
|
let totalResponseTime = 0;
|
||||||
|
let responseCount = 0;
|
||||||
|
|
||||||
|
ticketsClaimed.forEach((t) => {
|
||||||
|
const claimedAt = t.claimedAt || t.claimed_at;
|
||||||
|
const createdAt = t.createdAt || t.created_at;
|
||||||
|
if (claimedAt && createdAt) {
|
||||||
|
const diff = new Date(claimedAt).getTime() - new Date(createdAt).getTime();
|
||||||
|
if (diff > 0) {
|
||||||
|
totalResponseTime += diff;
|
||||||
|
responseCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const avgMinutes = responseCount
|
||||||
|
? Math.round(totalResponseTime / responseCount / 60000)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
return { ticketsClaimed, ticketsOpen, avgMinutes };
|
||||||
|
}
|
||||||
|
|
||||||
|
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.ticket_id || 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") {
|
||||||
|
const { ticketsClaimed, ticketsOpen, avgMinutes } = getStaffTicketStats(viewingUserId);
|
||||||
|
|
||||||
|
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") {
|
||||||
|
const { ticketsOpen } = getStaffTicketStats(viewingUserId);
|
||||||
|
|
||||||
|
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") {
|
||||||
|
const { ticketsClaimed, ticketsOpen, avgMinutes } = getStaffTicketStats(viewingUserId);
|
||||||
|
|
||||||
|
const statsContainer = buildTicketStatsContainer(
|
||||||
|
viewingUserId,
|
||||||
|
ticketsClaimed,
|
||||||
|
ticketsOpen,
|
||||||
|
avgMinutes,
|
||||||
|
);
|
||||||
|
|
||||||
|
const statsRow = new ActionRowBuilder().addComponents(
|
||||||
|
new ButtonBuilder()
|
||||||
|
.setCustomId("showOpenTickets")
|
||||||
|
.setLabel("Show Open Tickets")
|
||||||
|
.setStyle(ButtonStyle.Secondary),
|
||||||
|
new ButtonBuilder()
|
||||||
|
.setCustomId("backToProfile")
|
||||||
|
.setLabel("Back to Profile")
|
||||||
|
.setStyle(ButtonStyle.Secondary),
|
||||||
|
);
|
||||||
|
|
||||||
|
return i.update({
|
||||||
|
flags: MessageFlags.IsComponentsV2,
|
||||||
|
components: [statsContainer, statsRow],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i.customId === "backToProfile") {
|
||||||
|
const updatedProfileContainer = buildProfileContainer({
|
||||||
|
viewingUser,
|
||||||
|
viewingUserId,
|
||||||
|
bio,
|
||||||
|
timezone,
|
||||||
|
clockedIn,
|
||||||
|
isStaff,
|
||||||
|
});
|
||||||
|
|
||||||
|
const updatedButtonRows = buildProfileButtons({
|
||||||
|
isOwnProfile,
|
||||||
|
isStaff,
|
||||||
|
clockedIn,
|
||||||
|
});
|
||||||
|
|
||||||
|
return i.update({
|
||||||
|
flags: MessageFlags.IsComponentsV2,
|
||||||
|
components: [updatedProfileContainer, ...updatedButtonRows],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error executing profile command:", error);
|
||||||
|
|
||||||
|
if (interaction.deferred || interaction.replied) {
|
||||||
|
return interaction
|
||||||
|
.editReply({
|
||||||
|
content: "An error occurred while loading the profile.",
|
||||||
|
components: [],
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
return interaction
|
||||||
|
.reply({
|
||||||
|
content: "An error occurred while loading the profile.",
|
||||||
|
flags: MessageFlags.Ephemeral,
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports.clockedInUsers = clockedInUsers;
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
// SupportBot | Emerald Services
|
||||||
|
// Translate Command
|
||||||
|
|
||||||
|
const fs = require("fs");
|
||||||
|
const Discord = require("discord.js");
|
||||||
|
const yaml = require("js-yaml");
|
||||||
|
|
||||||
|
const { translate } = require("@vitalets/google-translate-api");
|
||||||
|
|
||||||
|
const supportbot = require("../../Structures/ConfigStore").supportbot;
|
||||||
|
const cmdconfig = require("../../Structures/ConfigStore").commands;
|
||||||
|
const msgconfig = require("../../Structures/ConfigStore").messages;
|
||||||
|
|
||||||
|
const Command = require("../../Structures/Command.js");
|
||||||
|
|
||||||
|
const LANGUAGES = [
|
||||||
|
{ name: "Auto Detect", value: "auto" },
|
||||||
|
{ name: "English", value: "en" },
|
||||||
|
{ name: "Spanish", value: "es" },
|
||||||
|
{ name: "French", value: "fr" },
|
||||||
|
{ name: "German", value: "de" },
|
||||||
|
{ name: "Italian", value: "it" },
|
||||||
|
{ name: "Portuguese", value: "pt" },
|
||||||
|
{ name: "Dutch", value: "nl" },
|
||||||
|
{ name: "Polish", value: "pl" },
|
||||||
|
{ name: "Russian", value: "ru" },
|
||||||
|
{ name: "Ukrainian", value: "uk" },
|
||||||
|
{ name: "Turkish", value: "tr" },
|
||||||
|
{ name: "Arabic", value: "ar" },
|
||||||
|
{ name: "Hebrew", value: "he" },
|
||||||
|
{ name: "Hindi", value: "hi" },
|
||||||
|
{ name: "Japanese", value: "ja" },
|
||||||
|
{ name: "Korean", value: "ko" },
|
||||||
|
{ name: "Chinese (Simplified)", value: "zh-cn" },
|
||||||
|
{ name: "Chinese (Traditional)", value: "zh-tw" },
|
||||||
|
{ name: "Thai", value: "th" },
|
||||||
|
{ name: "Vietnamese", value: "vi" },
|
||||||
|
{ name: "Indonesian", value: "id" },
|
||||||
|
{ name: "Greek", value: "el" },
|
||||||
|
{ name: "Swedish", value: "sv" },
|
||||||
|
{ name: "Romanian", value: "ro" },
|
||||||
|
];
|
||||||
|
|
||||||
|
function buildContainer(title, lines = [], color = null) {
|
||||||
|
const container = new Discord.ContainerBuilder();
|
||||||
|
|
||||||
|
if (color) {
|
||||||
|
container.setAccentColor(parseInt(color.replace("#", ""), 16));
|
||||||
|
}
|
||||||
|
|
||||||
|
container.addTextDisplayComponents(new Discord.TextDisplayBuilder().setContent(`## ${title}`));
|
||||||
|
|
||||||
|
container.addSeparatorComponents(new Discord.SeparatorBuilder().setDivider(true));
|
||||||
|
|
||||||
|
container.addTextDisplayComponents(new Discord.TextDisplayBuilder().setContent(lines.join("\n")));
|
||||||
|
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
|
||||||
|
function trim(text, max = 1800) {
|
||||||
|
if (text.length <= max) return text;
|
||||||
|
return text.substring(0, max) + "...";
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = new Command({
|
||||||
|
name: cmdconfig.Translate.Command,
|
||||||
|
description: cmdconfig.Translate.Description,
|
||||||
|
type: Discord.ApplicationCommandType.ChatInput,
|
||||||
|
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
type: Discord.ApplicationCommandOptionType.String,
|
||||||
|
name: "from",
|
||||||
|
description: "Language to translate FROM",
|
||||||
|
required: true,
|
||||||
|
choices: LANGUAGES,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: Discord.ApplicationCommandOptionType.String,
|
||||||
|
name: "to",
|
||||||
|
description: "Language to translate TO",
|
||||||
|
required: true,
|
||||||
|
choices: LANGUAGES.filter((l) => l.value !== "auto"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: Discord.ApplicationCommandOptionType.String,
|
||||||
|
name: "text",
|
||||||
|
description: "Text to translate",
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
permissions: cmdconfig.Translate.Permission,
|
||||||
|
|
||||||
|
async run(interaction) {
|
||||||
|
const { getRole, getChannel } = interaction.client;
|
||||||
|
|
||||||
|
const Staff = await getRole(supportbot.Roles.StaffMember.Staff, interaction.guild);
|
||||||
|
|
||||||
|
const Admin = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild);
|
||||||
|
|
||||||
|
if (!Staff || !Admin) {
|
||||||
|
return interaction.reply({
|
||||||
|
content: "Some roles seem to be missing!",
|
||||||
|
flags: Discord.MessageFlags.Ephemeral,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!interaction.member.roles.cache.has(Staff.id) &&
|
||||||
|
!interaction.member.roles.cache.has(Admin.id)
|
||||||
|
) {
|
||||||
|
return interaction.reply({
|
||||||
|
flags: Discord.MessageFlags.IsComponentsV2 | Discord.MessageFlags.Ephemeral,
|
||||||
|
components: [
|
||||||
|
buildContainer(
|
||||||
|
"Invalid Permissions",
|
||||||
|
[
|
||||||
|
msgconfig.Error.IncorrectPerms,
|
||||||
|
"",
|
||||||
|
`Required role: \`${supportbot.Roles.StaffMember.Staff}\` or \`${supportbot.Roles.StaffMember.Admin}\``,
|
||||||
|
],
|
||||||
|
supportbot.Embed.Colours.Warn,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await interaction.deferReply({ flags: Discord.MessageFlags.Ephemeral });
|
||||||
|
|
||||||
|
const fromLang = interaction.options.getString("from");
|
||||||
|
const toLang = interaction.options.getString("to");
|
||||||
|
const text = interaction.options.getString("text");
|
||||||
|
|
||||||
|
const translateLog = await getChannel(supportbot.Translate?.TranslateLog, interaction.guild);
|
||||||
|
|
||||||
|
let result;
|
||||||
|
|
||||||
|
try {
|
||||||
|
result = await translate(text, {
|
||||||
|
from: fromLang === "auto" ? undefined : fromLang,
|
||||||
|
to: toLang,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Translate error:", err);
|
||||||
|
|
||||||
|
return interaction.editReply({
|
||||||
|
flags: Discord.MessageFlags.IsComponentsV2,
|
||||||
|
components: [
|
||||||
|
buildContainer(
|
||||||
|
"Translation Failed",
|
||||||
|
["The translation service returned an error."],
|
||||||
|
supportbot.Embed.Colours.Warn,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const detectedLanguage = result.from?.language?.iso || result.from?.language || "unknown";
|
||||||
|
|
||||||
|
const container = buildContainer(
|
||||||
|
"Translation",
|
||||||
|
[
|
||||||
|
`**Requested By:** <@${interaction.user.id}>`,
|
||||||
|
`**From:** \`${fromLang === "auto" ? detectedLanguage : fromLang}\``,
|
||||||
|
`**To:** \`${toLang}\``,
|
||||||
|
"",
|
||||||
|
"**Original Text**",
|
||||||
|
trim(text),
|
||||||
|
"",
|
||||||
|
"**Translated Text**",
|
||||||
|
trim(result.text),
|
||||||
|
],
|
||||||
|
supportbot.Embed.Colours.Success,
|
||||||
|
);
|
||||||
|
|
||||||
|
await interaction.editReply({
|
||||||
|
flags: Discord.MessageFlags.IsComponentsV2,
|
||||||
|
components: [container],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (translateLog) {
|
||||||
|
const logContainer = buildContainer(
|
||||||
|
"Translation Log",
|
||||||
|
[
|
||||||
|
`User: <@${interaction.user.id}>`,
|
||||||
|
`Channel: <#${interaction.channel.id}>`,
|
||||||
|
`From: ${fromLang}`,
|
||||||
|
`To: ${toLang}`,
|
||||||
|
"",
|
||||||
|
trim(text),
|
||||||
|
"",
|
||||||
|
trim(result.text),
|
||||||
|
],
|
||||||
|
supportbot.Embed.Colours.General,
|
||||||
|
);
|
||||||
|
|
||||||
|
translateLog
|
||||||
|
.send({
|
||||||
|
flags: Discord.MessageFlags.IsComponentsV2,
|
||||||
|
components: [logContainer],
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
const Discord = require("discord.js");
|
||||||
|
const yaml = require("js-yaml");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
const supportbot = require("../../Structures/ConfigStore").supportbot;
|
||||||
|
const cmdconfig = require("../../Structures/ConfigStore").commands;
|
||||||
|
const msgconfig = require("../../Structures/ConfigStore").messages;
|
||||||
|
|
||||||
|
const Command = require("../../Structures/Command.js");
|
||||||
|
const DB = require("../../Structures/Database.js");
|
||||||
|
|
||||||
|
module.exports = new Command({
|
||||||
|
name: "suggest",
|
||||||
|
description: cmdconfig.Suggestion?.Description || "Submit a new suggestion",
|
||||||
|
type: Discord.ApplicationCommandType.ChatInput,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: "text",
|
||||||
|
description: "Your suggestion text",
|
||||||
|
type: Discord.ApplicationCommandOptionType.String,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
async run(interaction) {
|
||||||
|
const suggestion = interaction.options.getString("text");
|
||||||
|
const mode = supportbot.Suggestions.Mode || "Channel";
|
||||||
|
|
||||||
|
const embed = new Discord.EmbedBuilder()
|
||||||
|
.addFields(
|
||||||
|
{ name: "Suggestion", value: suggestion, inline: true },
|
||||||
|
{ name: "From", value: `<@${interaction.user.id}>` },
|
||||||
|
{ name: `${supportbot.Suggestions.UpvoteEmoji} ${supportbot.Suggestions.UpvoteTitle}`, value: "0", inline: true },
|
||||||
|
{ name: `${supportbot.Suggestions.DownvoteEmoji} ${supportbot.Suggestions.DownvoteTitle}`, value: "0", inline: true }
|
||||||
|
)
|
||||||
|
.setThumbnail(interaction.user.displayAvatarURL())
|
||||||
|
.setColor(supportbot.Embed.Colours.General);
|
||||||
|
|
||||||
|
const row = new Discord.ActionRowBuilder().addComponents(
|
||||||
|
new Discord.ButtonBuilder()
|
||||||
|
.setCustomId("upvote")
|
||||||
|
.setEmoji(supportbot.Suggestions.UpvoteEmoji)
|
||||||
|
.setStyle(supportbot.Suggestions.Buttons.Upvote),
|
||||||
|
new Discord.ButtonBuilder()
|
||||||
|
.setCustomId("downvote")
|
||||||
|
.setEmoji(supportbot.Suggestions.DownvoteEmoji)
|
||||||
|
.setStyle(supportbot.Suggestions.Buttons.Downvote),
|
||||||
|
new Discord.ButtonBuilder()
|
||||||
|
.setCustomId("removevote")
|
||||||
|
.setLabel(supportbot.Suggestions.Buttons.RemoveVote_Title)
|
||||||
|
.setStyle(supportbot.Suggestions.Buttons.RemoveVote)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (mode === "Forum") {
|
||||||
|
const forum = await interaction.guild.channels
|
||||||
|
.fetch(supportbot.Suggestions.ForumChannel)
|
||||||
|
.catch(() => null);
|
||||||
|
|
||||||
|
if (!forum || forum.type !== Discord.ChannelType.GuildForum) {
|
||||||
|
return interaction.reply({
|
||||||
|
embeds: [
|
||||||
|
new Discord.EmbedBuilder()
|
||||||
|
.setTitle("⚠ Missing Forum Channel")
|
||||||
|
.setDescription(msgconfig.Error.MissingChannel)
|
||||||
|
.setColor(supportbot.Embed.Colours.Error)
|
||||||
|
],
|
||||||
|
flags: Discord.MessageFlags.Ephemeral,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const appliedTags = supportbot.Suggestions.Tags?.Created
|
||||||
|
? [supportbot.Suggestions.Tags.Created]
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const thread = await forum.threads.create({
|
||||||
|
name: suggestion.slice(0, 100),
|
||||||
|
message: {
|
||||||
|
content: "\u200B",
|
||||||
|
embeds: [embed],
|
||||||
|
components: [row]
|
||||||
|
},
|
||||||
|
appliedTags,
|
||||||
|
autoArchiveDuration: Discord.ThreadAutoArchiveDuration.OneDay,
|
||||||
|
reason: "New Suggestion"
|
||||||
|
});
|
||||||
|
|
||||||
|
embed.setFooter({
|
||||||
|
text: `${supportbot.Embed.Footer} | Suggestion ID: ${thread.id}`,
|
||||||
|
iconURL: interaction.user.displayAvatarURL()
|
||||||
|
});
|
||||||
|
const starter = await thread.fetchStarterMessage();
|
||||||
|
await starter.edit({ embeds: [embed], components: [row] });
|
||||||
|
|
||||||
|
DB.add(thread.id, interaction.user.id, suggestion);
|
||||||
|
|
||||||
|
return interaction.reply({
|
||||||
|
embeds: [
|
||||||
|
new Discord.EmbedBuilder()
|
||||||
|
.setTitle(msgconfig.Suggestions.Sent_Title)
|
||||||
|
.setDescription(`${msgconfig.Suggestions.Sent}`)
|
||||||
|
.addFields({ name: 'Posted in:', value: `<#${forum.id}>` })
|
||||||
|
.setColor(supportbot.Embed.Colours.Success)
|
||||||
|
],
|
||||||
|
flags: Discord.MessageFlags.Ephemeral,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Channel Mode ----------
|
||||||
|
const channel = await interaction.guild.channels
|
||||||
|
.fetch(supportbot.Suggestions.Channel)
|
||||||
|
.catch(() => null);
|
||||||
|
|
||||||
|
if (!channel) {
|
||||||
|
return interaction.reply({
|
||||||
|
embeds: [
|
||||||
|
new Discord.EmbedBuilder()
|
||||||
|
.setTitle("⚠ Missing Suggestion Channel")
|
||||||
|
.setDescription(msgconfig.Error.MissingChannel)
|
||||||
|
.setColor(supportbot.Embed.Colours.Error)
|
||||||
|
],
|
||||||
|
flags: Discord.MessageFlags.Ephemeral,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = await channel.send({ embeds: [embed], components: [row] });
|
||||||
|
|
||||||
|
embed.setFooter({
|
||||||
|
text: `${supportbot.Embed.Footer} | Suggestion ID: ${message.id}`,
|
||||||
|
iconURL: interaction.user.displayAvatarURL()
|
||||||
|
});
|
||||||
|
await message.edit({ embeds: [embed], components: [row] });
|
||||||
|
|
||||||
|
DB.add(message.id, interaction.user.id, suggestion);
|
||||||
|
|
||||||
|
return interaction.reply({
|
||||||
|
embeds: [
|
||||||
|
new Discord.EmbedBuilder()
|
||||||
|
.setTitle(msgconfig.Suggestions.Sent_Title)
|
||||||
|
.setDescription(`${msgconfig.Suggestions.Sent}`)
|
||||||
|
.addFields({ name: 'Posted in:', value: `<#${channel.id}>` })
|
||||||
|
.setColor(supportbot.Embed.Colours.Success)
|
||||||
|
],
|
||||||
|
flags: Discord.MessageFlags.Ephemeral,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
const Discord = require("discord.js");
|
||||||
|
const fs = require("fs");
|
||||||
|
const yaml = require("js-yaml");
|
||||||
|
|
||||||
|
const supportbot = require("../../Structures/ConfigStore").supportbot;
|
||||||
|
const msgconfig = require("../../Structures/ConfigStore").messages;
|
||||||
|
const cmdconfig = require("../../Structures/ConfigStore").commands;
|
||||||
|
const SuggestionDB = require("../../Structures/Database.js");
|
||||||
|
const Command = require("../../Structures/Command.js");
|
||||||
|
|
||||||
|
module.exports = new Command({
|
||||||
|
name: cmdconfig.SuggestionAdmin.Command,
|
||||||
|
description: cmdconfig.SuggestionAdmin.Description,
|
||||||
|
type: Discord.ApplicationCommandType.ChatInput,
|
||||||
|
permissions: cmdconfig.SuggestionAdmin.Permission,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: "status",
|
||||||
|
description: "New status",
|
||||||
|
type: Discord.ApplicationCommandOptionType.String,
|
||||||
|
required: true,
|
||||||
|
choices: [
|
||||||
|
{ name: "Solved", value: "Solved" },
|
||||||
|
{ name: "Denied", value: "Denied" },
|
||||||
|
{ name: "Considering", value: "Considering" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "id",
|
||||||
|
description: "Message ID (required in Channel mode)",
|
||||||
|
type: Discord.ApplicationCommandOptionType.String,
|
||||||
|
required: false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
async run(interaction) {
|
||||||
|
const status = interaction.options.getString("status");
|
||||||
|
const idOption = interaction.options.getString("id");
|
||||||
|
const mode = supportbot.Suggestions.Mode || "Channel";
|
||||||
|
|
||||||
|
const { getRole } = interaction.client;
|
||||||
|
const staffRole = await getRole(supportbot.Roles.StaffMember.Staff, interaction.guild);
|
||||||
|
const adminRole = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild);
|
||||||
|
if (
|
||||||
|
!interaction.member.roles.cache.has(staffRole?.id) &&
|
||||||
|
!interaction.member.roles.cache.has(adminRole?.id)
|
||||||
|
) {
|
||||||
|
return interaction.reply({
|
||||||
|
embeds: [
|
||||||
|
new Discord.EmbedBuilder()
|
||||||
|
.setTitle("Invalid Permissions!")
|
||||||
|
.setDescription(
|
||||||
|
`${msgconfig.Error.IncorrectPerms}\nRequired: ${supportbot.Roles.StaffMember.Staff} or ${supportbot.Roles.StaffMember.Admin}`
|
||||||
|
)
|
||||||
|
.setColor(supportbot.Embed.Colours.Warn)
|
||||||
|
],
|
||||||
|
flags: Discord.MessageFlags.Ephemeral,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode === "Forum") {
|
||||||
|
const thread = interaction.channel;
|
||||||
|
if (!thread.isThread() || thread.parentId !== supportbot.Suggestions.ForumChannel) {
|
||||||
|
return interaction.reply({
|
||||||
|
embeds: [
|
||||||
|
new Discord.EmbedBuilder()
|
||||||
|
.setTitle("Not a Suggestion Thread")
|
||||||
|
.setDescription(msgconfig.Error.MissingChannel)
|
||||||
|
.setColor(supportbot.Embed.Colours.Error)
|
||||||
|
],
|
||||||
|
flags: Discord.MessageFlags.Ephemeral,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const tagMap = {
|
||||||
|
Solved: supportbot.Suggestions.Tags.Solved,
|
||||||
|
Denied: supportbot.Suggestions.Tags.Denied,
|
||||||
|
Considering: supportbot.Suggestions.Tags.Considering
|
||||||
|
};
|
||||||
|
const tagId = tagMap[status];
|
||||||
|
if (!tagId) {
|
||||||
|
return interaction.reply({
|
||||||
|
embeds: [
|
||||||
|
new Discord.EmbedBuilder()
|
||||||
|
.setTitle("Tag Missing")
|
||||||
|
.setDescription("No forum tag configured for this status.")
|
||||||
|
.setColor(supportbot.Embed.Colours.Error)
|
||||||
|
],
|
||||||
|
flags: Discord.MessageFlags.Ephemeral,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await thread.setAppliedTags([tagId]).catch(() => null);
|
||||||
|
SuggestionDB.setStatus(thread.id, status);
|
||||||
|
|
||||||
|
return interaction.reply({
|
||||||
|
embeds: [
|
||||||
|
new Discord.EmbedBuilder()
|
||||||
|
.setTitle("Suggestion Updated")
|
||||||
|
.setDescription(`Status set to **${status}**.`)
|
||||||
|
.setColor(supportbot.Embed.Colours.Success)
|
||||||
|
],
|
||||||
|
flags: Discord.MessageFlags.Ephemeral,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!idOption) {
|
||||||
|
return interaction.reply({
|
||||||
|
embeds: [
|
||||||
|
new Discord.EmbedBuilder()
|
||||||
|
.setTitle("Message ID Required")
|
||||||
|
.setDescription("Provide the suggestion’s message ID when in Channel mode.")
|
||||||
|
.setColor(supportbot.Embed.Colours.Error)
|
||||||
|
],
|
||||||
|
flags: Discord.MessageFlags.Ephemeral,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const channel = await interaction.guild.channels
|
||||||
|
.fetch(supportbot.Suggestions.Channel)
|
||||||
|
.catch(() => null);
|
||||||
|
if (!channel) {
|
||||||
|
return interaction.reply({
|
||||||
|
embeds: [
|
||||||
|
new Discord.EmbedBuilder()
|
||||||
|
.setTitle("Missing Suggestion Channel")
|
||||||
|
.setDescription(msgconfig.Error.MissingChannel)
|
||||||
|
.setColor(supportbot.Embed.Colours.Error)
|
||||||
|
],
|
||||||
|
flags: Discord.MessageFlags.Ephemeral,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetMsg = await channel.messages.fetch(idOption).catch(() => null);
|
||||||
|
if (!targetMsg) {
|
||||||
|
return interaction.reply({
|
||||||
|
embeds: [
|
||||||
|
new Discord.EmbedBuilder()
|
||||||
|
.setTitle("Message Not Found")
|
||||||
|
.setDescription("Could not fetch a suggestion with that ID.")
|
||||||
|
.setColor(supportbot.Embed.Colours.Error)
|
||||||
|
],
|
||||||
|
flags: Discord.MessageFlags.Ephemeral,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldEmbed = targetMsg.embeds[0];
|
||||||
|
if (!oldEmbed) {
|
||||||
|
return interaction.reply({
|
||||||
|
embeds: [
|
||||||
|
new Discord.EmbedBuilder()
|
||||||
|
.setTitle("No Embed Found")
|
||||||
|
.setDescription("The target message has no suggestion embed.")
|
||||||
|
.setColor(supportbot.Embed.Colours.Error)
|
||||||
|
],
|
||||||
|
flags: Discord.MessageFlags.Ephemeral,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const colorMap = {
|
||||||
|
Solved: supportbot.Embed.Colours.Success,
|
||||||
|
Denied: supportbot.Embed.Colours.Error,
|
||||||
|
Considering: supportbot.Embed.Colours.Warn
|
||||||
|
};
|
||||||
|
|
||||||
|
const newEmbed = Discord.EmbedBuilder.from(oldEmbed)
|
||||||
|
.setColor(colorMap[status])
|
||||||
|
.addFields({ name: "Status", value: status, inline: true });
|
||||||
|
|
||||||
|
await targetMsg.edit({ embeds: [newEmbed], components: targetMsg.components });
|
||||||
|
SuggestionDB.setStatus(idOption, status);
|
||||||
|
|
||||||
|
return interaction.reply({
|
||||||
|
embeds: [
|
||||||
|
new Discord.EmbedBuilder()
|
||||||
|
.setTitle("Suggestion Updated")
|
||||||
|
.setDescription(`Suggestion **${idOption}** marked as **${status}**.`)
|
||||||
|
.setColor(supportbot.Embed.Colours.Success)
|
||||||
|
],
|
||||||
|
flags: Discord.MessageFlags.Ephemeral,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,494 @@
|
|||||||
|
|
||||||
|
|
||||||
|
const fs = require("fs");
|
||||||
|
const Discord = require("discord.js");
|
||||||
|
const yaml = require("js-yaml");
|
||||||
|
|
||||||
|
const db = require("../../Structures/Database.js");
|
||||||
|
const TicketManager = require("../../Structures/TicketManager.js");
|
||||||
|
const Command = require("../../Structures/Command.js");
|
||||||
|
|
||||||
|
const supportbot = require("../../Structures/ConfigStore").supportbot;
|
||||||
|
const cmdconfig = require("../../Structures/ConfigStore").commands;
|
||||||
|
const msgconfig = require("../../Structures/ConfigStore").messages;
|
||||||
|
|
||||||
|
function getDepartmentConfig(departmentKey) {
|
||||||
|
return supportbot.Ticket?.DepartmentSystem?.Departments?.[departmentKey] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTicketUserId(ticket) {
|
||||||
|
return ticket?.user || ticket?.user_id || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildContainer(title, lines = []) {
|
||||||
|
const container = new Discord.ContainerBuilder();
|
||||||
|
|
||||||
|
if (supportbot.Embed?.Colours?.General) {
|
||||||
|
container.setAccentColor(
|
||||||
|
parseInt(String(supportbot.Embed.Colours.General).replace("#", ""), 16),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
container.addTextDisplayComponents(new Discord.TextDisplayBuilder().setContent(`## ${title}`));
|
||||||
|
|
||||||
|
container.addSeparatorComponents(new Discord.SeparatorBuilder().setDivider(true));
|
||||||
|
|
||||||
|
const textContent = lines.filter(Boolean).join("\n").trim();
|
||||||
|
if (textContent.length > 0) {
|
||||||
|
container.addTextDisplayComponents(
|
||||||
|
new Discord.TextDisplayBuilder().setContent(textContent),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildReviewSummaryContainer(ticket, rating, comment) {
|
||||||
|
const departmentConfig = ticket.department ? getDepartmentConfig(ticket.department) : null;
|
||||||
|
const departmentName = departmentConfig?.Name || ticket.department || null;
|
||||||
|
const departmentEmoji = departmentConfig?.Emoji || "🎫";
|
||||||
|
|
||||||
|
const claimedByEnabled = supportbot.Ticket?.ClaimTickets?.Enabled === true;
|
||||||
|
const claimedBy = claimedByEnabled && ticket.claimedBy ? `<@${ticket.claimedBy}>` : null;
|
||||||
|
|
||||||
|
const ticketUserId = getTicketUserId(ticket);
|
||||||
|
const lines = [
|
||||||
|
`**${msgconfig.ReviewSystem.ReviewEmbed.ReviewedByTitle}** ${ticketUserId ? `<@${ticketUserId}>` : "Unknown User"}`,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (claimedBy) {
|
||||||
|
lines.unshift(`**${msgconfig.ReviewSystem.ReviewEmbed.ReviewedStaffTitle}** ${claimedBy}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (supportbot.Ticket?.DepartmentSystem?.Enabled === true && departmentName) {
|
||||||
|
lines.push(`**Department** ${departmentEmoji} ${departmentName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push("");
|
||||||
|
lines.push(`**${msgconfig.ReviewSystem.ReviewEmbed.RatingTitle}**`);
|
||||||
|
lines.push(`${msgconfig.ReviewSystem.ReviewEmbed.ReviewEmoji.repeat(Number(rating))}`);
|
||||||
|
lines.push("");
|
||||||
|
lines.push(`**${msgconfig.ReviewSystem.ReviewEmbed.CommentTitle}**`);
|
||||||
|
lines.push(comment || "No Comment Provided");
|
||||||
|
|
||||||
|
const container = new Discord.ContainerBuilder();
|
||||||
|
|
||||||
|
if (msgconfig.ReviewSystem?.ReviewEmbed?.Color) {
|
||||||
|
container.setAccentColor(
|
||||||
|
parseInt(String(msgconfig.ReviewSystem.ReviewEmbed.Color).replace("#", ""), 16),
|
||||||
|
);
|
||||||
|
} else if (supportbot.Embed?.Colours?.General) {
|
||||||
|
container.setAccentColor(
|
||||||
|
parseInt(String(supportbot.Embed.Colours.General).replace("#", ""), 16),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
container.addTextDisplayComponents(
|
||||||
|
new Discord.TextDisplayBuilder().setContent(`## ${msgconfig.ReviewSystem.ReviewEmbed.Title}`),
|
||||||
|
);
|
||||||
|
|
||||||
|
container.addSeparatorComponents(new Discord.SeparatorBuilder().setDivider(true));
|
||||||
|
|
||||||
|
container.addTextDisplayComponents(new Discord.TextDisplayBuilder().setContent(lines.join("\n")));
|
||||||
|
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function collectReview(interaction, ticket) {
|
||||||
|
const ticketUserId = getTicketUserId(ticket);
|
||||||
|
if (!ticketUserId) return null;
|
||||||
|
|
||||||
|
const ratingContainer = buildContainer(msgconfig.ReviewSystem.Rate.Title, [
|
||||||
|
msgconfig.ReviewSystem.Rate.Description,
|
||||||
|
"",
|
||||||
|
]);
|
||||||
|
|
||||||
|
const ratingRow = new Discord.ActionRowBuilder().addComponents(
|
||||||
|
new Discord.StringSelectMenuBuilder()
|
||||||
|
.setCustomId(`closeReviewRating:${interaction.id}`)
|
||||||
|
.setPlaceholder("Select a rating")
|
||||||
|
.addOptions([
|
||||||
|
{ label: msgconfig.ReviewSystem.Stars.One, value: "1" },
|
||||||
|
{ label: msgconfig.ReviewSystem.Stars.Two, value: "2" },
|
||||||
|
{ label: msgconfig.ReviewSystem.Stars.Three, value: "3" },
|
||||||
|
{ label: msgconfig.ReviewSystem.Stars.Four, value: "4" },
|
||||||
|
{ label: msgconfig.ReviewSystem.Stars.Five, value: "5" },
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
ratingContainer.addActionRowComponents(ratingRow);
|
||||||
|
|
||||||
|
const ratingPrompt = await interaction.followUp({
|
||||||
|
flags: Discord.MessageFlags.IsComponentsV2,
|
||||||
|
components: [ratingContainer],
|
||||||
|
});
|
||||||
|
|
||||||
|
let rating = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const ratingInteraction = await ratingPrompt.awaitMessageComponent({
|
||||||
|
componentType: Discord.ComponentType.StringSelect,
|
||||||
|
time: 120000,
|
||||||
|
filter: (i) =>
|
||||||
|
i.customId === `closeReviewRating:${interaction.id}` && i.user.id === ticketUserId,
|
||||||
|
});
|
||||||
|
|
||||||
|
rating = ratingInteraction.values[0];
|
||||||
|
await ratingInteraction.deferUpdate().catch(() => {});
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const commentContainer = buildContainer(msgconfig.ReviewSystem.Comment.Title, [
|
||||||
|
msgconfig.ReviewSystem.Comment.Description,
|
||||||
|
"",
|
||||||
|
]);
|
||||||
|
|
||||||
|
const commentRow = new Discord.ActionRowBuilder().addComponents(
|
||||||
|
new Discord.ButtonBuilder()
|
||||||
|
.setCustomId(`closeReviewWrite:${interaction.id}`)
|
||||||
|
.setLabel("Write Comment")
|
||||||
|
.setStyle(Discord.ButtonStyle.Secondary),
|
||||||
|
new Discord.ButtonBuilder()
|
||||||
|
.setCustomId(`closeReviewSkip:${interaction.id}`)
|
||||||
|
.setLabel("Skip Comment")
|
||||||
|
.setStyle(Discord.ButtonStyle.Danger),
|
||||||
|
);
|
||||||
|
|
||||||
|
commentContainer.addActionRowComponents(commentRow);
|
||||||
|
|
||||||
|
const commentPrompt = await interaction.followUp({
|
||||||
|
flags: Discord.MessageFlags.IsComponentsV2,
|
||||||
|
components: [commentContainer],
|
||||||
|
});
|
||||||
|
|
||||||
|
let comment = "No Comment Provided";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const buttonInteraction = await commentPrompt.awaitMessageComponent({
|
||||||
|
componentType: Discord.ComponentType.Button,
|
||||||
|
time: 120000,
|
||||||
|
filter: (i) =>
|
||||||
|
(i.customId === `closeReviewWrite:${interaction.id}` ||
|
||||||
|
i.customId === `closeReviewSkip:${interaction.id}`) &&
|
||||||
|
i.user.id === ticketUserId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (buttonInteraction.customId === `closeReviewSkip:${interaction.id}`) {
|
||||||
|
await buttonInteraction.deferUpdate().catch(() => {});
|
||||||
|
return { rating, comment };
|
||||||
|
}
|
||||||
|
|
||||||
|
await buttonInteraction
|
||||||
|
.update({
|
||||||
|
flags: Discord.MessageFlags.IsComponentsV2,
|
||||||
|
components: [
|
||||||
|
buildContainer(msgconfig.ReviewSystem.Comment.Title, [
|
||||||
|
`Requested from: <@${ticketUserId}>`,
|
||||||
|
"",
|
||||||
|
"Please send your review comment in the channel now.",
|
||||||
|
"Type `no` to skip.",
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
|
||||||
|
const collected = await interaction.channel.awaitMessages({
|
||||||
|
max: 1,
|
||||||
|
time: 120000,
|
||||||
|
filter: (m) => m.author.id === ticketUserId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const message = collected.first();
|
||||||
|
|
||||||
|
if (message) {
|
||||||
|
const rawComment = message.content?.trim();
|
||||||
|
if (rawComment && rawComment.toLowerCase() !== "no") {
|
||||||
|
comment = rawComment;
|
||||||
|
}
|
||||||
|
|
||||||
|
await message.delete().catch(() => {});
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return { rating, comment };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { rating, comment };
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTranscriptAttachmentFromResult(result) {
|
||||||
|
if (!result) return null;
|
||||||
|
|
||||||
|
if (typeof result === "string") {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.filePath) return result.filePath;
|
||||||
|
if (result.attachment) return result.attachment;
|
||||||
|
if (result.path) return result.path;
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function maybeDMTranscript(interaction, ticket, transcriptResult) {
|
||||||
|
if (supportbot.Ticket?.UserManagement?.DMTranscript !== true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ticketUserId = getTicketUserId(ticket);
|
||||||
|
if (!ticketUserId) return;
|
||||||
|
|
||||||
|
const ticketUser = await interaction.client.users.fetch(ticketUserId).catch(() => null);
|
||||||
|
|
||||||
|
if (!ticketUser) return;
|
||||||
|
|
||||||
|
const attachmentPath = getTranscriptAttachmentFromResult(transcriptResult);
|
||||||
|
if (!attachmentPath) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await ticketUser.send({
|
||||||
|
content: `Here is a copy of your ticket transcript from \`${interaction.guild.name}\`.`,
|
||||||
|
files: [attachmentPath],
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to DM transcript to user:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function collectCloseConfirmation(interaction, ticket, reason) {
|
||||||
|
const ticketUserId = getTicketUserId(ticket);
|
||||||
|
|
||||||
|
const lines = [
|
||||||
|
"Are you sure you want to close this ticket?",
|
||||||
|
"",
|
||||||
|
`**Ticket:** ${interaction.channel.name}`,
|
||||||
|
`**Reason:** ${reason || "No Reason Provided."}`,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (ticketUserId) {
|
||||||
|
lines.push(`**Ticket User:** <@${ticketUserId}>`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (supportbot.Ticket?.ReviewSystem?.Enabled === true) {
|
||||||
|
lines.push("");
|
||||||
|
lines.push("A review will be requested before the ticket is closed.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmContainer = buildContainer("Close Confirmation", lines);
|
||||||
|
|
||||||
|
const confirmRow = new Discord.ActionRowBuilder().addComponents(
|
||||||
|
new Discord.ButtonBuilder()
|
||||||
|
.setCustomId(`confirmClose:${interaction.id}`)
|
||||||
|
.setLabel("Confirm Close")
|
||||||
|
.setStyle(Discord.ButtonStyle.Danger),
|
||||||
|
new Discord.ButtonBuilder()
|
||||||
|
.setCustomId(`cancelClose:${interaction.id}`)
|
||||||
|
.setLabel("Cancel")
|
||||||
|
.setStyle(Discord.ButtonStyle.Secondary),
|
||||||
|
);
|
||||||
|
|
||||||
|
confirmContainer.addActionRowComponents(confirmRow);
|
||||||
|
|
||||||
|
const confirmMessage = await interaction.editReply({
|
||||||
|
flags: Discord.MessageFlags.IsComponentsV2,
|
||||||
|
components: [confirmContainer],
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const buttonInteraction = await confirmMessage.awaitMessageComponent({
|
||||||
|
componentType: Discord.ComponentType.Button,
|
||||||
|
time: 120000,
|
||||||
|
filter: (i) =>
|
||||||
|
(i.customId === `confirmClose:${interaction.id}` ||
|
||||||
|
i.customId === `cancelClose:${interaction.id}`) &&
|
||||||
|
i.user.id === interaction.user.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (buttonInteraction.customId === `cancelClose:${interaction.id}`) {
|
||||||
|
await buttonInteraction
|
||||||
|
.update({
|
||||||
|
flags: Discord.MessageFlags.IsComponentsV2,
|
||||||
|
components: [
|
||||||
|
buildContainer("Close Cancelled", ["The ticket close process has been cancelled."]),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await buttonInteraction.deferUpdate().catch(() => {});
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
await interaction
|
||||||
|
.editReply({
|
||||||
|
flags: Discord.MessageFlags.IsComponentsV2,
|
||||||
|
components: [buildContainer("Close Cancelled", ["The close confirmation timed out."])],
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function postReview(interaction, ticket, reviewData) {
|
||||||
|
const { getChannel } = interaction.client;
|
||||||
|
const reviewChannel = await getChannel(supportbot.Ticket.ReviewSystem.Channel, interaction.guild);
|
||||||
|
|
||||||
|
if (!reviewChannel || !reviewData) return;
|
||||||
|
|
||||||
|
const reviewContainer = buildReviewSummaryContainer(
|
||||||
|
ticket,
|
||||||
|
reviewData.rating,
|
||||||
|
reviewData.comment,
|
||||||
|
);
|
||||||
|
|
||||||
|
await reviewChannel
|
||||||
|
.send({
|
||||||
|
flags: Discord.MessageFlags.IsComponentsV2,
|
||||||
|
components: [reviewContainer],
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCloseTicket(interaction, reason, ticket) {
|
||||||
|
try {
|
||||||
|
if (typeof db.updateTicketStatus === "function") {
|
||||||
|
db.updateTicketStatus(interaction.channel.id, "closed");
|
||||||
|
}
|
||||||
|
|
||||||
|
const transcriptResult = await TicketManager.createTranscript(interaction, ticket, reason);
|
||||||
|
|
||||||
|
await maybeDMTranscript(interaction, ticket, transcriptResult);
|
||||||
|
|
||||||
|
await interaction
|
||||||
|
.editReply({
|
||||||
|
flags: Discord.MessageFlags.IsComponentsV2,
|
||||||
|
components: [
|
||||||
|
buildContainer("Ticket Closed", [
|
||||||
|
`**Ticket:** ${interaction.channel.name}`,
|
||||||
|
`**Closed By:** <@${interaction.user.id}>`,
|
||||||
|
`**Reason:** ${reason || "No Reason Provided."}`,
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
|
||||||
|
await interaction.channel.delete().catch(() => {});
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error handling ticket close:", err);
|
||||||
|
|
||||||
|
if (interaction.deferred || interaction.replied) {
|
||||||
|
await interaction
|
||||||
|
.editReply({
|
||||||
|
content: "An error occurred while closing the ticket.",
|
||||||
|
components: [],
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await interaction
|
||||||
|
.reply({
|
||||||
|
content: "An error occurred while closing the ticket.",
|
||||||
|
flags: Discord.MessageFlags.Ephemeral,
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = new Command({
|
||||||
|
name: cmdconfig.CloseTicket.Command,
|
||||||
|
description: cmdconfig.CloseTicket.Description,
|
||||||
|
type: Discord.ApplicationCommandType.ChatInput,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: "reason",
|
||||||
|
description: "Ticket Close Reason",
|
||||||
|
type: Discord.ApplicationCommandOptionType.String,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
permissions: cmdconfig.CloseTicket.Permission,
|
||||||
|
|
||||||
|
async run(interaction) {
|
||||||
|
const { getRole } = interaction.client;
|
||||||
|
|
||||||
|
if (supportbot.Ticket.Close.StaffOnly) {
|
||||||
|
const SupportStaff = await getRole(supportbot.Roles.StaffMember.Staff, interaction.guild);
|
||||||
|
const Admin = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild);
|
||||||
|
|
||||||
|
if (!SupportStaff || !Admin) {
|
||||||
|
return interaction.reply({
|
||||||
|
content: "Some roles seem to be missing! Please check for errors when starting the bot.",
|
||||||
|
flags: Discord.MessageFlags.Ephemeral,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!interaction.member.roles.cache.has(SupportStaff.id) &&
|
||||||
|
!interaction.member.roles.cache.has(Admin.id)
|
||||||
|
) {
|
||||||
|
return interaction.reply({
|
||||||
|
flags: Discord.MessageFlags.IsComponentsV2 | Discord.MessageFlags.Ephemeral,
|
||||||
|
components: [
|
||||||
|
buildContainer("Invalid Permissions!", [
|
||||||
|
msgconfig.Error.IncorrectPerms,
|
||||||
|
"",
|
||||||
|
`Role Required: \`${supportbot.Roles.StaffMember.Staff}\` or \`${supportbot.Roles.StaffMember.Admin}\``,
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isThread = interaction.channel.type === Discord.ChannelType.PrivateThread;
|
||||||
|
|
||||||
|
if (
|
||||||
|
(supportbot.Ticket.TicketType === "threads" && !isThread) ||
|
||||||
|
(supportbot.Ticket.TicketType === "channels" &&
|
||||||
|
interaction.channel.type !== Discord.ChannelType.GuildText)
|
||||||
|
) {
|
||||||
|
return interaction.reply({
|
||||||
|
flags: Discord.MessageFlags.IsComponentsV2 | Discord.MessageFlags.Ephemeral,
|
||||||
|
components: [
|
||||||
|
buildContainer("Invalid Channel!", [
|
||||||
|
`This command can only be used in a ${
|
||||||
|
supportbot.Ticket.TicketType === "threads" ? "ticket thread" : "ticket channel"
|
||||||
|
}.`,
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await interaction.deferReply({ flags: Discord.MessageFlags.Ephemeral });
|
||||||
|
|
||||||
|
const ticket = db.getTicket(interaction.channel.id);
|
||||||
|
|
||||||
|
if (!ticket) {
|
||||||
|
return interaction.editReply({
|
||||||
|
flags: Discord.MessageFlags.IsComponentsV2,
|
||||||
|
components: [buildContainer("No Ticket Found!", [msgconfig.Error.NoValidTicket])],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const reason = interaction.options?.getString("reason") || "No Reason Provided.";
|
||||||
|
|
||||||
|
const confirmed = await collectCloseConfirmation(interaction, ticket, reason);
|
||||||
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (supportbot.Ticket?.ReviewSystem?.Enabled === true) {
|
||||||
|
const reviewData = await collectReview(interaction, ticket);
|
||||||
|
|
||||||
|
if (reviewData) {
|
||||||
|
await postReview(interaction, ticket, reviewData);
|
||||||
|
}
|
||||||
|
|
||||||
|
await handleCloseTicket(interaction, reason, ticket);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await handleCloseTicket(interaction, reason, ticket);
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,664 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
const yaml = require("js-yaml");
|
||||||
|
const {
|
||||||
|
EmbedBuilder,
|
||||||
|
ApplicationCommandOptionType,
|
||||||
|
ApplicationCommandType,
|
||||||
|
ChannelType,
|
||||||
|
MessageFlags,
|
||||||
|
ModalBuilder,
|
||||||
|
ActionRowBuilder,
|
||||||
|
TextInputBuilder,
|
||||||
|
TextInputStyle,
|
||||||
|
} = require("discord.js");
|
||||||
|
|
||||||
|
const supportbot = require("../../Structures/ConfigStore").supportbot;
|
||||||
|
const msgconfig = require("../../Structures/ConfigStore").messages;
|
||||||
|
const cmdconfig = require("../../Structures/ConfigStore").commands;
|
||||||
|
|
||||||
|
const TicketManager = require("../../Structures/TicketManager.js");
|
||||||
|
const Command = require("../../Structures/Command.js");
|
||||||
|
const db = require("../../Structures/Database.js");
|
||||||
|
|
||||||
|
function getDepartments() {
|
||||||
|
return supportbot.Ticket?.DepartmentSystem?.Departments || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPriorities() {
|
||||||
|
return supportbot.Ticket?.PrioritySystem?.Priorities || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
function prioritiesEnabled() {
|
||||||
|
return (
|
||||||
|
supportbot.Ticket?.TicketType !== "threads" &&
|
||||||
|
supportbot.Ticket?.PrioritySystem?.Enabled === true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function departmentsEnabled() {
|
||||||
|
return (
|
||||||
|
supportbot.Ticket?.TicketType !== "threads" &&
|
||||||
|
supportbot.Ticket?.DepartmentSystem?.Enabled === true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildTicketChannelName(ticketNumberID, priorityKey, departmentConfig) {
|
||||||
|
const defaultPrefix = supportbot.Ticket?.ChannelPrefix || supportbot.Ticket?.Channel || "ticket-";
|
||||||
|
|
||||||
|
const departmentPrefix = departmentConfig?.ChannelPrefix || defaultPrefix;
|
||||||
|
|
||||||
|
const safePrefix = String(departmentPrefix).trim() || "ticket-";
|
||||||
|
const baseName = `${safePrefix}${ticketNumberID}`;
|
||||||
|
|
||||||
|
if (!prioritiesEnabled() || !priorityKey) {
|
||||||
|
return baseName;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${baseName}-${String(priorityKey).toLowerCase()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = new Command({
|
||||||
|
name: cmdconfig.TicketManage?.Command || "ticketmanage",
|
||||||
|
description: cmdconfig.TicketManage?.Description || "Manage tickets",
|
||||||
|
type: ApplicationCommandType.ChatInput,
|
||||||
|
permissions: cmdconfig.TicketManage?.Permission || ["ManageChannels"],
|
||||||
|
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: "transfer",
|
||||||
|
description: "Transfer a ticket to another department",
|
||||||
|
type: ApplicationCommandOptionType.Subcommand,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: "department",
|
||||||
|
description: "Department to transfer this ticket to",
|
||||||
|
type: ApplicationCommandOptionType.String,
|
||||||
|
required: true,
|
||||||
|
choices: Object.entries(getDepartments())
|
||||||
|
.slice(0, 25)
|
||||||
|
.map(([key, dept]) => ({
|
||||||
|
name: dept.Name || key,
|
||||||
|
value: key,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "priority",
|
||||||
|
description: "Optional new priority",
|
||||||
|
type: ApplicationCommandOptionType.String,
|
||||||
|
required: false,
|
||||||
|
choices: Object.entries(getPriorities())
|
||||||
|
.slice(0, 25)
|
||||||
|
.map(([key, priority]) => ({
|
||||||
|
name: priority.Name || key,
|
||||||
|
value: key,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "priority",
|
||||||
|
description: "Change a ticket priority",
|
||||||
|
type: ApplicationCommandOptionType.Subcommand,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: "level",
|
||||||
|
description: "New priority level",
|
||||||
|
type: ApplicationCommandOptionType.String,
|
||||||
|
required: true,
|
||||||
|
choices: Object.entries(getPriorities())
|
||||||
|
.slice(0, 25)
|
||||||
|
.map(([key, priority]) => ({
|
||||||
|
name: priority.Name || key,
|
||||||
|
value: key,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: cmdconfig.TicketManage?.ForceAdd?.Command || "forceadd",
|
||||||
|
description:
|
||||||
|
cmdconfig.TicketManage?.ForceAdd?.Description || "Force add a user to the current ticket",
|
||||||
|
type: ApplicationCommandOptionType.Subcommand,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: "user",
|
||||||
|
description: "The user to add",
|
||||||
|
type: ApplicationCommandOptionType.User,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: cmdconfig.TicketManage?.ForceRemove?.Command || "forceremove",
|
||||||
|
description:
|
||||||
|
cmdconfig.TicketManage?.ForceRemove?.Description || "Remove a user from the current ticket",
|
||||||
|
type: ApplicationCommandOptionType.Subcommand,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: "user",
|
||||||
|
description: "The user to remove",
|
||||||
|
type: ApplicationCommandOptionType.User,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: cmdconfig.TicketManage?.Rename?.Command || "rename",
|
||||||
|
description: cmdconfig.TicketManage?.Rename?.Description || "Rename the current ticket",
|
||||||
|
type: ApplicationCommandOptionType.Subcommand,
|
||||||
|
options: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: cmdconfig.TicketManage?.Close?.Command || "close",
|
||||||
|
description: cmdconfig.TicketManage?.Close?.Description || "Close the current ticket",
|
||||||
|
type: ApplicationCommandOptionType.Subcommand,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: "reason",
|
||||||
|
description: "Reason for closing the ticket",
|
||||||
|
type: ApplicationCommandOptionType.String,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
async run(interaction) {
|
||||||
|
try {
|
||||||
|
const subcommand = interaction.options.getSubcommand(false);
|
||||||
|
const ticket = db.getTicket(interaction.channel.id);
|
||||||
|
|
||||||
|
if (!ticket) {
|
||||||
|
return interaction.reply({
|
||||||
|
content: "This channel is not a ticket.",
|
||||||
|
flags: MessageFlags.Ephemeral,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { getRole } = interaction.client;
|
||||||
|
|
||||||
|
let SupportStaff = await getRole(supportbot.Roles.StaffMember.Staff, interaction.guild);
|
||||||
|
let Admin = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild);
|
||||||
|
let Moderator = await getRole(supportbot.Roles.StaffMember.Moderator, interaction.guild);
|
||||||
|
|
||||||
|
if (!SupportStaff || !Admin || !Moderator) {
|
||||||
|
const missingRolesEmbed = new EmbedBuilder()
|
||||||
|
.setDescription(msgconfig.Error.InvalidChannel || "Required roles are missing!")
|
||||||
|
.setColor(supportbot.Embed.Colours.Warn);
|
||||||
|
|
||||||
|
return interaction.reply({
|
||||||
|
embeds: [missingRolesEmbed],
|
||||||
|
flags: MessageFlags.Ephemeral,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const NoPerms = new EmbedBuilder()
|
||||||
|
.setDescription(
|
||||||
|
msgconfig.Error.IncorrectPerms || "You do not have the correct permissions!",
|
||||||
|
)
|
||||||
|
.setColor(supportbot.Embed.Colours.Warn);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!interaction.member.roles.cache.has(Admin.id) &&
|
||||||
|
!interaction.member.roles.cache.has(Moderator.id) &&
|
||||||
|
(!supportbot.Roles?.Mod?.AllowSupportStaff || !interaction.member.roles.cache.has(SupportStaff.id))
|
||||||
|
) {
|
||||||
|
return interaction.reply({
|
||||||
|
embeds: [NoPerms],
|
||||||
|
flags: MessageFlags.Ephemeral,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subcommand === "transfer") {
|
||||||
|
if (!departmentsEnabled()) {
|
||||||
|
return interaction.reply({
|
||||||
|
content: "Departments are disabled, or ticket mode is set to threads.",
|
||||||
|
flags: MessageFlags.Ephemeral,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (interaction.channel.type !== ChannelType.GuildText) {
|
||||||
|
return interaction.reply({
|
||||||
|
content: "Ticket transfer only works for ticket channels.",
|
||||||
|
flags: MessageFlags.Ephemeral,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const departmentKey = interaction.options.getString("department");
|
||||||
|
const requestedPriority = interaction.options.getString("priority");
|
||||||
|
|
||||||
|
const departments = getDepartments();
|
||||||
|
const departmentConfig = departments[departmentKey];
|
||||||
|
|
||||||
|
if (!departmentConfig) {
|
||||||
|
return interaction.reply({
|
||||||
|
content: "That department does not exist.",
|
||||||
|
flags: MessageFlags.Ephemeral,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetCategoryId = departmentConfig.Category;
|
||||||
|
const targetRoleId = departmentConfig.Role || null;
|
||||||
|
|
||||||
|
if (!targetCategoryId) {
|
||||||
|
return interaction.reply({
|
||||||
|
content: "That department does not have a category configured.",
|
||||||
|
flags: MessageFlags.Ephemeral,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetCategory = interaction.guild.channels.cache.get(targetCategoryId);
|
||||||
|
if (!targetCategory) {
|
||||||
|
return interaction.reply({
|
||||||
|
content: "The target department category could not be found.",
|
||||||
|
flags: MessageFlags.Ephemeral,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let finalPriority = ticket.priority || null;
|
||||||
|
|
||||||
|
if (requestedPriority) {
|
||||||
|
finalPriority = requestedPriority;
|
||||||
|
} else if (!finalPriority && departmentConfig.DefaultPriority) {
|
||||||
|
finalPriority = departmentConfig.DefaultPriority;
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldDepartmentKey = ticket.department || null;
|
||||||
|
const oldDepartmentConfig = oldDepartmentKey ? departments[oldDepartmentKey] : null;
|
||||||
|
const oldRoleId = oldDepartmentConfig?.Role || null;
|
||||||
|
|
||||||
|
await interaction.channel.setParent(targetCategory.id);
|
||||||
|
|
||||||
|
if (oldRoleId && oldRoleId !== targetRoleId) {
|
||||||
|
await interaction.channel.permissionOverwrites.delete(oldRoleId).catch(() => { });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetRoleId) {
|
||||||
|
await interaction.channel.permissionOverwrites.create(targetRoleId, {
|
||||||
|
ViewChannel: true,
|
||||||
|
SendMessages: true,
|
||||||
|
ReadMessageHistory: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof db.updateTicketDepartment === "function") {
|
||||||
|
db.updateTicketDepartment(interaction.channel.id, departmentKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (finalPriority && typeof db.updateTicketPriority === "function") {
|
||||||
|
db.updateTicketPriority(interaction.channel.id, finalPriority);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ticketNumber = ticket.number || interaction.channel.name.replace(/\D/g, "");
|
||||||
|
|
||||||
|
const newChannelName = buildTicketChannelName(
|
||||||
|
ticketNumber,
|
||||||
|
finalPriority,
|
||||||
|
departmentConfig,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (interaction.channel.name !== newChannelName) {
|
||||||
|
await interaction.channel.setName(newChannelName).catch(() => { });
|
||||||
|
}
|
||||||
|
|
||||||
|
const deptEmoji = departmentConfig.Emoji || "🎫";
|
||||||
|
const deptName = departmentConfig.Name || departmentKey;
|
||||||
|
|
||||||
|
let description = `> **Department:** ${deptEmoji} ${deptName}`;
|
||||||
|
|
||||||
|
if (finalPriority && prioritiesEnabled()) {
|
||||||
|
const priorityConfig = getPriorities()[finalPriority];
|
||||||
|
const priorityEmoji = priorityConfig?.Emoji || "🟡";
|
||||||
|
const priorityName = priorityConfig?.Name || finalPriority;
|
||||||
|
description += `\n> **Priority:** ${priorityEmoji} ${priorityName}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
description += `\n> **Transferred By:** <@${interaction.user.id}>`;
|
||||||
|
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setTitle("Ticket Updated")
|
||||||
|
.setDescription(description)
|
||||||
|
.setColor(supportbot.Embed.Colours.Success);
|
||||||
|
|
||||||
|
return interaction.reply({
|
||||||
|
embeds: [embed],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subcommand === "priority") {
|
||||||
|
if (!prioritiesEnabled()) {
|
||||||
|
return interaction.reply({
|
||||||
|
content: "Priority is disabled, or ticket mode is set to threads.",
|
||||||
|
flags: MessageFlags.Ephemeral,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const level = interaction.options.getString("level");
|
||||||
|
const priorityConfig = getPriorities()[level];
|
||||||
|
|
||||||
|
if (!priorityConfig) {
|
||||||
|
return interaction.reply({
|
||||||
|
content: "That priority does not exist.",
|
||||||
|
flags: MessageFlags.Ephemeral,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof db.updateTicketPriority === "function") {
|
||||||
|
db.updateTicketPriority(interaction.channel.id, level);
|
||||||
|
}
|
||||||
|
|
||||||
|
const departments = getDepartments();
|
||||||
|
const departmentConfig = ticket.department ? departments[ticket.department] : null;
|
||||||
|
const ticketNumber = ticket.number || interaction.channel.name.replace(/\D/g, "");
|
||||||
|
|
||||||
|
const newChannelName = buildTicketChannelName(ticketNumber, level, departmentConfig);
|
||||||
|
|
||||||
|
if (interaction.channel.name !== newChannelName) {
|
||||||
|
await interaction.channel.setName(newChannelName).catch(() => { });
|
||||||
|
}
|
||||||
|
|
||||||
|
const priorityEmoji = priorityConfig.Emoji || "🟡";
|
||||||
|
const priorityName = priorityConfig.Name || level;
|
||||||
|
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setTitle("Ticket Updated")
|
||||||
|
.setDescription(
|
||||||
|
`> **Priority:** ${priorityEmoji} ${priorityName}\n> **Updated By:** <@${interaction.user.id}>`,
|
||||||
|
)
|
||||||
|
.setColor(supportbot.Embed.Colours.Success);
|
||||||
|
|
||||||
|
return interaction.reply({
|
||||||
|
embeds: [embed],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subcommand === (cmdconfig.TicketManage?.ForceAdd?.Command || "forceadd")) {
|
||||||
|
const userToAdd = interaction.options.getUser("user");
|
||||||
|
const ticketChannel = interaction.channel;
|
||||||
|
|
||||||
|
if (
|
||||||
|
(supportbot.Ticket.TicketType === "threads" && !ticketChannel.isThread()) ||
|
||||||
|
(supportbot.Ticket.TicketType === "channels" &&
|
||||||
|
ticketChannel.type !== ChannelType.GuildText)
|
||||||
|
) {
|
||||||
|
const onlyInTicket = new EmbedBuilder()
|
||||||
|
.setTitle(msgconfig.ForceAddUser.NotInTicket_Title)
|
||||||
|
.setDescription(msgconfig.ForceAddUser.NotInTicket_Description)
|
||||||
|
.setColor(supportbot.Embed.Colours.Error);
|
||||||
|
|
||||||
|
return interaction.reply({
|
||||||
|
embeds: [onlyInTicket],
|
||||||
|
flags: MessageFlags.Ephemeral,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const ticketRow = db.getTicket(ticketChannel.id);
|
||||||
|
if (!ticketRow) {
|
||||||
|
const notFound = new EmbedBuilder()
|
||||||
|
.setTitle(msgconfig.ForceAddUser.NotInTicket_Title)
|
||||||
|
.setDescription(msgconfig.ForceAddUser.NotInTicket_Description)
|
||||||
|
.setColor(supportbot.Embed.Colours.Error);
|
||||||
|
|
||||||
|
return interaction.reply({
|
||||||
|
embeds: [notFound],
|
||||||
|
flags: MessageFlags.Ephemeral,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (supportbot.Ticket.TicketType === "threads") {
|
||||||
|
const botMember = await ticketChannel.members
|
||||||
|
.fetch(interaction.client.user.id)
|
||||||
|
.catch(() => null);
|
||||||
|
|
||||||
|
if (!botMember) await ticketChannel.join();
|
||||||
|
|
||||||
|
await new Promise((res) => setTimeout(res, 1000));
|
||||||
|
await ticketChannel.members.add(userToAdd.id);
|
||||||
|
} else {
|
||||||
|
await ticketChannel.permissionOverwrites.create(userToAdd.id, {
|
||||||
|
ViewChannel: true,
|
||||||
|
SendMessages: true,
|
||||||
|
ReadMessageHistory: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
db.addUserToTicket(ticketChannel.id, userToAdd.id);
|
||||||
|
|
||||||
|
const addedEmbed = new EmbedBuilder()
|
||||||
|
.setTitle(msgconfig.ForceAddUser.Added_Title)
|
||||||
|
.setDescription(
|
||||||
|
msgconfig.ForceAddUser.Added_Description.replace("%username%", userToAdd.username),
|
||||||
|
)
|
||||||
|
.setColor(supportbot.Embed.Colours.Success);
|
||||||
|
|
||||||
|
await interaction.reply({
|
||||||
|
embeds: [addedEmbed],
|
||||||
|
flags: MessageFlags.Ephemeral,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (supportbot.Ticket?.UserManagement?.DMOnAdd !== false) {
|
||||||
|
const addedToTicketEmbed = new EmbedBuilder()
|
||||||
|
.setTitle(msgconfig.ForceAddUser.AddedToTicket_Title)
|
||||||
|
.setDescription(
|
||||||
|
msgconfig.ForceAddUser.AddedToTicket_Description.replace(
|
||||||
|
"%channel_link%",
|
||||||
|
`[${ticketChannel.name}](https://discord.com/channels/${ticketChannel.guild.id}/${ticketChannel.id})`,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.setColor(supportbot.Embed.Colours.General);
|
||||||
|
|
||||||
|
await userToAdd.send({ embeds: [addedToTicketEmbed] }).catch(() => { });
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error adding user to the ticket:", err);
|
||||||
|
|
||||||
|
const errorEmbed = new EmbedBuilder()
|
||||||
|
.setTitle(msgconfig.ForceAddUser.Error_Title)
|
||||||
|
.setDescription(msgconfig.ForceAddUser.Error_Adding)
|
||||||
|
.setColor(supportbot.Embed.Colours.Error);
|
||||||
|
|
||||||
|
await interaction.reply({
|
||||||
|
embeds: [errorEmbed],
|
||||||
|
flags: MessageFlags.Ephemeral,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subcommand === (cmdconfig.TicketManage?.ForceRemove?.Command || "forceremove")) {
|
||||||
|
const userToRemove = interaction.options.getUser("user");
|
||||||
|
const ticketChannel = interaction.channel;
|
||||||
|
|
||||||
|
if (
|
||||||
|
(supportbot.Ticket.TicketType === "threads" && !ticketChannel.isThread()) ||
|
||||||
|
(supportbot.Ticket.TicketType === "channels" &&
|
||||||
|
ticketChannel.type !== ChannelType.GuildText)
|
||||||
|
) {
|
||||||
|
const onlyInTicket = new EmbedBuilder()
|
||||||
|
.setTitle(msgconfig.RemoveUser.NotInTicket_Title)
|
||||||
|
.setDescription(msgconfig.RemoveUser.NotInTicket_Description)
|
||||||
|
.setColor(supportbot.Embed.Colours.Error);
|
||||||
|
|
||||||
|
return interaction.reply({
|
||||||
|
embeds: [onlyInTicket],
|
||||||
|
flags: MessageFlags.Ephemeral,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (supportbot.Ticket.TicketType === "threads") {
|
||||||
|
await ticketChannel.members.remove(userToRemove.id);
|
||||||
|
} else {
|
||||||
|
await ticketChannel.permissionOverwrites.delete(userToRemove.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
db.removeUserFromTicket(ticketChannel.id, userToRemove.id);
|
||||||
|
|
||||||
|
const removedEmbed = new EmbedBuilder()
|
||||||
|
.setTitle(msgconfig.RemoveUser.Removed_Title)
|
||||||
|
.setDescription(msgconfig.RemoveUser.Removed_Message)
|
||||||
|
.setColor(supportbot.Embed.Colours.Success);
|
||||||
|
|
||||||
|
await interaction.reply({
|
||||||
|
embeds: [removedEmbed],
|
||||||
|
flags: MessageFlags.Ephemeral,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (supportbot.Ticket?.UserManagement?.DMOnRemove !== false) {
|
||||||
|
const removedFromTicketEmbed = new EmbedBuilder()
|
||||||
|
.setTitle(msgconfig.RemoveUser.RemovedFromTicket_Title)
|
||||||
|
.setDescription(
|
||||||
|
msgconfig.RemoveUser.RemovedFromTicket_Description.replace(
|
||||||
|
"%channel_link%",
|
||||||
|
`[${ticketChannel.name}](https://discord.com/channels/${ticketChannel.guild.id}/${ticketChannel.id})`,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.setColor(supportbot.Embed.Colours.General);
|
||||||
|
|
||||||
|
await userToRemove.send({ embeds: [removedFromTicketEmbed] }).catch(() => { });
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error removing user from the ticket:", err);
|
||||||
|
|
||||||
|
const errorEmbed = new EmbedBuilder()
|
||||||
|
.setTitle(msgconfig.RemoveUser.Error_Title)
|
||||||
|
.setDescription(msgconfig.RemoveUser.Error_Removing)
|
||||||
|
.setColor(supportbot.Embed.Colours.Error);
|
||||||
|
|
||||||
|
await interaction.reply({
|
||||||
|
embeds: [errorEmbed],
|
||||||
|
flags: MessageFlags.Ephemeral,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subcommand === (cmdconfig.TicketManage?.Rename?.Command || "rename")) {
|
||||||
|
const modal = new ModalBuilder()
|
||||||
|
.setCustomId("renameTicketModal")
|
||||||
|
.setTitle("Rename Channel")
|
||||||
|
.addComponents(
|
||||||
|
new ActionRowBuilder().addComponents(
|
||||||
|
new TextInputBuilder()
|
||||||
|
.setCustomId("renameTicket")
|
||||||
|
.setLabel("New Channel Name")
|
||||||
|
.setStyle(TextInputStyle.Short)
|
||||||
|
.setRequired(true),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return interaction.showModal(modal);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subcommand === (cmdconfig.TicketManage?.Close?.Command || "close")) {
|
||||||
|
const reason = interaction.options.getString("reason") || "Closed by staff.";
|
||||||
|
|
||||||
|
if (supportbot.Ticket.Close.StaffOnly) {
|
||||||
|
let CloseSupportStaff = await getRole(
|
||||||
|
supportbot.Roles.StaffMember.Staff,
|
||||||
|
interaction.guild,
|
||||||
|
);
|
||||||
|
let CloseAdmin = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild);
|
||||||
|
|
||||||
|
if (!CloseSupportStaff || !CloseAdmin) {
|
||||||
|
return interaction.reply({
|
||||||
|
content:
|
||||||
|
"Some roles seem to be missing! Please check for errors when starting the bot.",
|
||||||
|
flags: MessageFlags.Ephemeral,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const noClosePerms = new EmbedBuilder()
|
||||||
|
.setTitle("Invalid Permissions!")
|
||||||
|
.setDescription(
|
||||||
|
`${msgconfig.Error.IncorrectPerms}\n\nRole Required: \`${supportbot.Roles.StaffMember.Staff}\` or \`${supportbot.Roles.StaffMember.Admin}\``,
|
||||||
|
)
|
||||||
|
.setColor(supportbot.Embed.Colours.Warn);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!interaction.member.roles.cache.has(CloseSupportStaff.id) &&
|
||||||
|
!interaction.member.roles.cache.has(CloseAdmin.id)
|
||||||
|
) {
|
||||||
|
return interaction.reply({
|
||||||
|
embeds: [noClosePerms],
|
||||||
|
flags: MessageFlags.Ephemeral,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isThread = interaction.channel.type === ChannelType.PrivateThread;
|
||||||
|
|
||||||
|
if (
|
||||||
|
(supportbot.Ticket.TicketType === "threads" && !isThread) ||
|
||||||
|
(supportbot.Ticket.TicketType === "channels" &&
|
||||||
|
interaction.channel.type !== ChannelType.GuildText)
|
||||||
|
) {
|
||||||
|
const notTicketChannel = new EmbedBuilder()
|
||||||
|
.setTitle("Invalid Channel!")
|
||||||
|
.setDescription(
|
||||||
|
`This command can only be used in a ${supportbot.Ticket.TicketType === "threads" ? "ticket thread" : "ticket channel"}.`,
|
||||||
|
)
|
||||||
|
.setColor(supportbot.Embed.Colours.Warn);
|
||||||
|
|
||||||
|
return interaction.reply({
|
||||||
|
embeds: [notTicketChannel],
|
||||||
|
flags: MessageFlags.Ephemeral,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
||||||
|
|
||||||
|
const ticketData = db.getTicket(interaction.channel.id);
|
||||||
|
|
||||||
|
if (!ticketData) {
|
||||||
|
const exists = new EmbedBuilder()
|
||||||
|
.setTitle("No Ticket Found!")
|
||||||
|
.setDescription(msgconfig.Error.NoValidTicket)
|
||||||
|
.setColor(supportbot.Embed.Colours.Warn);
|
||||||
|
|
||||||
|
return interaction.followUp({
|
||||||
|
embeds: [exists],
|
||||||
|
flags: MessageFlags.Ephemeral,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await TicketManager.createTranscript(interaction, ticketData, reason);
|
||||||
|
|
||||||
|
await interaction.followUp({
|
||||||
|
content: "Ticket closed successfully.",
|
||||||
|
flags: MessageFlags.Ephemeral,
|
||||||
|
});
|
||||||
|
|
||||||
|
await interaction.channel.delete().catch(() => { });
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error closing ticket:", err);
|
||||||
|
|
||||||
|
await interaction.followUp({
|
||||||
|
content: "An error occurred while closing the ticket.",
|
||||||
|
flags: MessageFlags.Ephemeral,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return interaction.reply({
|
||||||
|
content: "Unknown ticket management action.",
|
||||||
|
flags: MessageFlags.Ephemeral,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error in ticket manage command:", error);
|
||||||
|
|
||||||
|
if (!interaction.replied && !interaction.deferred) {
|
||||||
|
return interaction.reply({
|
||||||
|
content: "An error occurred while managing the ticket.",
|
||||||
|
flags: MessageFlags.Ephemeral,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,343 @@
|
|||||||
|
// SupportBot | Emerald Services
|
||||||
|
// Ticket Panel Command
|
||||||
|
|
||||||
|
const fs = require("fs");
|
||||||
|
const {
|
||||||
|
ApplicationCommandType,
|
||||||
|
ActionRowBuilder,
|
||||||
|
ButtonBuilder,
|
||||||
|
ButtonStyle,
|
||||||
|
MessageFlags,
|
||||||
|
ContainerBuilder,
|
||||||
|
TextDisplayBuilder,
|
||||||
|
MediaGalleryBuilder,
|
||||||
|
SeparatorBuilder,
|
||||||
|
SectionBuilder,
|
||||||
|
StringSelectMenuBuilder,
|
||||||
|
ThumbnailBuilder,
|
||||||
|
EmbedBuilder,
|
||||||
|
} = require("discord.js");
|
||||||
|
const yaml = require("js-yaml");
|
||||||
|
|
||||||
|
const panelconfig = require("../../Structures/ConfigStore").ticketPanel;
|
||||||
|
const supportbot = require("../../Structures/ConfigStore").supportbot;
|
||||||
|
const cmdconfig = require("../../Structures/ConfigStore").commands;
|
||||||
|
|
||||||
|
const Command = require("../../Structures/Command.js");
|
||||||
|
const db = require("../../Structures/Database.js");
|
||||||
|
|
||||||
|
function parseButtonStyle(style) {
|
||||||
|
switch (String(style || "1")) {
|
||||||
|
case "1":
|
||||||
|
return ButtonStyle.Primary;
|
||||||
|
case "2":
|
||||||
|
return ButtonStyle.Secondary;
|
||||||
|
case "3":
|
||||||
|
return ButtonStyle.Danger;
|
||||||
|
case "4":
|
||||||
|
return ButtonStyle.Success;
|
||||||
|
default:
|
||||||
|
return ButtonStyle.Primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPanelMessage(key, fallback) {
|
||||||
|
return panelconfig.Messages?.[key] || fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = new Command({
|
||||||
|
name: cmdconfig.TicketPanel.Command,
|
||||||
|
description: cmdconfig.TicketPanel.Description,
|
||||||
|
type: ApplicationCommandType.ChatInput,
|
||||||
|
options: [],
|
||||||
|
permissions: cmdconfig.TicketPanel.Permission,
|
||||||
|
|
||||||
|
async run(interaction) {
|
||||||
|
if (!panelconfig.Panel) {
|
||||||
|
console.log(
|
||||||
|
"\u001b[31m",
|
||||||
|
`[TICKET PANEL]`,
|
||||||
|
"\u001b[33m",
|
||||||
|
"Ticket Panel is not setup, You can set this in",
|
||||||
|
`\u001b[31m`,
|
||||||
|
"/Configs/ticket-panel.yml",
|
||||||
|
`\n `,
|
||||||
|
);
|
||||||
|
|
||||||
|
return interaction.reply({
|
||||||
|
content: getPanelMessage(
|
||||||
|
"NotConfigured",
|
||||||
|
"Ticket Panel is not set up. Please configure it in `/Configs/ticket-panel.yml`.",
|
||||||
|
),
|
||||||
|
flags: MessageFlags.Ephemeral,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { getChannel } = interaction.client;
|
||||||
|
|
||||||
|
const channel = await getChannel(supportbot.Ticket.TicketHome, interaction.guild);
|
||||||
|
|
||||||
|
if (!channel) {
|
||||||
|
console.log(`[TICKET PANEL] TicketHome channel not found. Please check your config file.`);
|
||||||
|
return interaction.reply({
|
||||||
|
content: getPanelMessage(
|
||||||
|
"InvalidChannel",
|
||||||
|
"TicketHome channel not found. Please check your config file.",
|
||||||
|
),
|
||||||
|
flags: MessageFlags.Ephemeral,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const panelRow = db.getTicketPanel();
|
||||||
|
|
||||||
|
if (panelRow) {
|
||||||
|
try {
|
||||||
|
const oldMessage = await channel.messages.fetch(panelRow.message_id);
|
||||||
|
if (oldMessage) {
|
||||||
|
await oldMessage.delete().catch(() => { });
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`[TICKET PANEL] Previous panel message not found, creating a new one.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const departmentSystem = supportbot.Ticket?.DepartmentSystem || {};
|
||||||
|
const prioritySystem = supportbot.Ticket?.PrioritySystem || {};
|
||||||
|
const usingThreads = supportbot.Ticket?.TicketType === "threads";
|
||||||
|
|
||||||
|
const departmentsEnabled = departmentSystem.Enabled === true && !usingThreads;
|
||||||
|
const prioritiesEnabled = prioritySystem.Enabled === true && !usingThreads;
|
||||||
|
const departments = departmentSystem.Departments || {};
|
||||||
|
|
||||||
|
if (usingThreads && (departmentSystem.Enabled || prioritySystem.Enabled)) {
|
||||||
|
console.log(
|
||||||
|
"[SupportBot] Departments and Priority are disabled because TicketType is set to 'threads'.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const departmentOptions = Object.entries(departments)
|
||||||
|
.filter(([_, dept]) => dept && dept.Enabled !== false)
|
||||||
|
.slice(0, 25)
|
||||||
|
.map(([key, dept]) => ({
|
||||||
|
label: dept.Name || key,
|
||||||
|
description:
|
||||||
|
dept.PanelDescription ||
|
||||||
|
dept.Description ||
|
||||||
|
panelconfig.DepartmentMenu?.DefaultDescription ||
|
||||||
|
`Open a ${String(dept.Name || key).toLowerCase()} ticket`,
|
||||||
|
value: key,
|
||||||
|
emoji:
|
||||||
|
dept.Emoji ||
|
||||||
|
panelconfig.DepartmentMenu?.DefaultEmoji ||
|
||||||
|
panelconfig.Button?.Emoji ||
|
||||||
|
undefined,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const shouldUseDepartments = departmentsEnabled && departmentOptions.length > 0;
|
||||||
|
|
||||||
|
let openerComponent;
|
||||||
|
|
||||||
|
if (shouldUseDepartments) {
|
||||||
|
openerComponent = new StringSelectMenuBuilder()
|
||||||
|
.setCustomId(panelconfig.DepartmentMenu?.CustomId || "ticketdepartmentselect")
|
||||||
|
.setPlaceholder(
|
||||||
|
panelconfig.DepartmentMenu?.Placeholder ||
|
||||||
|
departmentSystem.Placeholder ||
|
||||||
|
panelconfig.Button?.Text ||
|
||||||
|
"Choose a ticket department",
|
||||||
|
)
|
||||||
|
.addOptions(departmentOptions);
|
||||||
|
} else {
|
||||||
|
openerComponent = new ButtonBuilder()
|
||||||
|
.setCustomId(panelconfig.Button?.CustomId || "createticket")
|
||||||
|
.setLabel(panelconfig.Button?.Text || "Create Ticket")
|
||||||
|
.setStyle(parseButtonStyle(panelconfig.Button?.Color));
|
||||||
|
|
||||||
|
if (panelconfig.Button?.Emoji) {
|
||||||
|
openerComponent.setEmoji(panelconfig.Button.Emoji);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const panelContainer = new ContainerBuilder();
|
||||||
|
|
||||||
|
if (panelconfig.Style?.Color) {
|
||||||
|
panelContainer.setAccentColor(parseInt(String(panelconfig.Style.Color).replace("#", ""), 16));
|
||||||
|
}
|
||||||
|
|
||||||
|
const layout = String(panelconfig.Style?.Layout || "3");
|
||||||
|
const showImage = panelconfig.Settings?.Image === true && !!panelconfig.Style?.Image;
|
||||||
|
const showTopTitle = panelconfig.Settings?.TopTitle === true;
|
||||||
|
const showTopDivider = panelconfig.Settings?.TopDivider === true;
|
||||||
|
const showBottomTitle = panelconfig.Settings?.BottomTitle === true;
|
||||||
|
const showBottomDivider = panelconfig.Settings?.BottomDivider === true;
|
||||||
|
|
||||||
|
const panelTitle = new TextDisplayBuilder().setContent(
|
||||||
|
panelconfig.Style?.Title || "## SupportBot Ticket Creation",
|
||||||
|
);
|
||||||
|
|
||||||
|
const rawTitle = (panelconfig.Style?.Title || "SupportBot Ticket Creation").replace(/^#+\s*/, "");
|
||||||
|
|
||||||
|
const panelDesc = new TextDisplayBuilder().setContent(
|
||||||
|
panelconfig.Style?.Description || "Click on the button below to create a support ticket.",
|
||||||
|
);
|
||||||
|
|
||||||
|
if (showTopTitle) {
|
||||||
|
panelContainer.addTextDisplayComponents(panelTitle);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showTopDivider) {
|
||||||
|
panelContainer.addSeparatorComponents(new SeparatorBuilder().setDivider(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (layout === "1" || layout === "3") {
|
||||||
|
panelContainer.addTextDisplayComponents(panelDesc);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showImage && layout === "2") {
|
||||||
|
const panelImage = new MediaGalleryBuilder().addItems([
|
||||||
|
{
|
||||||
|
media: {
|
||||||
|
url: panelconfig.Style.Image,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
panelContainer.addMediaGalleryComponents(panelImage);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (layout === "1") {
|
||||||
|
const middleSection = new SectionBuilder().addTextDisplayComponents(panelDesc);
|
||||||
|
|
||||||
|
if (!shouldUseDepartments) {
|
||||||
|
middleSection.setButtonAccessory(openerComponent);
|
||||||
|
panelContainer.addSectionComponents(middleSection);
|
||||||
|
} else {
|
||||||
|
panelContainer.addSectionComponents(middleSection);
|
||||||
|
panelContainer.addActionRowComponents(
|
||||||
|
new ActionRowBuilder().addComponents(openerComponent),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showImage && layout === "3") {
|
||||||
|
const panelImage = new MediaGalleryBuilder().addItems([
|
||||||
|
{
|
||||||
|
media: {
|
||||||
|
url: panelconfig.Style.Image,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
panelContainer.addMediaGalleryComponents(panelImage);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showBottomTitle) {
|
||||||
|
panelContainer.addTextDisplayComponents(panelTitle);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showBottomDivider) {
|
||||||
|
panelContainer.addSeparatorComponents(new SeparatorBuilder().setDivider(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (layout === "2") {
|
||||||
|
const middleSection = new SectionBuilder().addTextDisplayComponents(panelDesc);
|
||||||
|
|
||||||
|
if (!shouldUseDepartments) {
|
||||||
|
middleSection.setButtonAccessory(openerComponent);
|
||||||
|
panelContainer.addSectionComponents(middleSection);
|
||||||
|
} else {
|
||||||
|
panelContainer.addSectionComponents(middleSection);
|
||||||
|
panelContainer.addActionRowComponents(
|
||||||
|
new ActionRowBuilder().addComponents(openerComponent),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (layout === "3") {
|
||||||
|
panelContainer.addActionRowComponents(new ActionRowBuilder().addComponents(openerComponent));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (layout === "4") {
|
||||||
|
// Create a classic Discord Embed for Layout 4
|
||||||
|
const oldEmbed = new EmbedBuilder()
|
||||||
|
.setTitle(rawTitle)
|
||||||
|
.setDescription(panelconfig.Style?.Description || "Click on the button below to create a support ticket.");
|
||||||
|
|
||||||
|
if (panelconfig.Style?.Color) {
|
||||||
|
oldEmbed.setColor(panelconfig.Style.Color);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showImage && panelconfig.Style?.Image) {
|
||||||
|
oldEmbed.setThumbnail(panelconfig.Style.Image);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const message = await channel.send({
|
||||||
|
embeds: [oldEmbed],
|
||||||
|
components: [new ActionRowBuilder().addComponents(openerComponent)],
|
||||||
|
});
|
||||||
|
|
||||||
|
db.saveTicketPanel(message.id, channel.id);
|
||||||
|
|
||||||
|
return interaction.reply({
|
||||||
|
content: shouldUseDepartments
|
||||||
|
? getPanelMessage(
|
||||||
|
"PanelSentWithDepartments",
|
||||||
|
"Ticket panel message with department selection has been sent!",
|
||||||
|
)
|
||||||
|
: getPanelMessage("PanelSent", "Ticket panel message has been sent!"),
|
||||||
|
flags: MessageFlags.Ephemeral,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Error sending message:", e);
|
||||||
|
return interaction.reply({
|
||||||
|
content: getPanelMessage(
|
||||||
|
"SendError",
|
||||||
|
"An error occurred while sending the ticket panel message.",
|
||||||
|
),
|
||||||
|
flags: MessageFlags.Ephemeral,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showImage && layout === "1") {
|
||||||
|
const panelImage = new MediaGalleryBuilder().addItems([
|
||||||
|
{
|
||||||
|
media: {
|
||||||
|
url: panelconfig.Style.Image,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
panelContainer.addMediaGalleryComponents(panelImage);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const message = await channel.send({
|
||||||
|
flags: MessageFlags.IsComponentsV2,
|
||||||
|
components: [panelContainer],
|
||||||
|
});
|
||||||
|
|
||||||
|
db.saveTicketPanel(message.id, channel.id);
|
||||||
|
|
||||||
|
return interaction.reply({
|
||||||
|
content: shouldUseDepartments
|
||||||
|
? getPanelMessage(
|
||||||
|
"PanelSentWithDepartments",
|
||||||
|
"Ticket panel message with department selection has been sent!",
|
||||||
|
)
|
||||||
|
: getPanelMessage("PanelSent", "Ticket panel message has been sent!"),
|
||||||
|
flags: MessageFlags.Ephemeral,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Error sending message:", e);
|
||||||
|
return interaction.reply({
|
||||||
|
content: getPanelMessage(
|
||||||
|
"SendError",
|
||||||
|
"An error occurred while sending the ticket panel message.",
|
||||||
|
),
|
||||||
|
flags: MessageFlags.Ephemeral,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,468 @@
|
|||||||
|
const {
|
||||||
|
ApplicationCommandType,
|
||||||
|
ApplicationCommandOptionType,
|
||||||
|
ActionRowBuilder,
|
||||||
|
ButtonBuilder,
|
||||||
|
EmbedBuilder,
|
||||||
|
StringSelectMenuBuilder,
|
||||||
|
ModalBuilder,
|
||||||
|
TextInputBuilder,
|
||||||
|
TextInputStyle,
|
||||||
|
ButtonStyle,
|
||||||
|
ComponentType,
|
||||||
|
MessageFlags,
|
||||||
|
} = require('discord.js');
|
||||||
|
const Command = require('../../Structures/Command.js');
|
||||||
|
const fs = require('fs');
|
||||||
|
const yaml = require('js-yaml');
|
||||||
|
|
||||||
|
const supportbot = require('../../Structures/ConfigStore').supportbot;
|
||||||
|
const cmdconfig = require("../../Structures/ConfigStore").commands;
|
||||||
|
const msgconfig = require("../../Structures/ConfigStore").messages;
|
||||||
|
|
||||||
|
function parseColor(color) {
|
||||||
|
if (!color) return 0x5865F2;
|
||||||
|
if (typeof color === 'number') return color;
|
||||||
|
if (typeof color !== 'string') return 0x5865F2;
|
||||||
|
color = color.replace('#', '');
|
||||||
|
if (/^[0-9A-F]{6}$/i.test(color)) {
|
||||||
|
return parseInt(color, 16);
|
||||||
|
}
|
||||||
|
return 0x5865F2;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = new Command({
|
||||||
|
name: cmdconfig.Embed.Command,
|
||||||
|
description: cmdconfig.Embed.Description,
|
||||||
|
type: ApplicationCommandType.ChatInput,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'channel',
|
||||||
|
description: 'Select channel to send the message',
|
||||||
|
type: ApplicationCommandOptionType.Channel,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
permissions: cmdconfig.Embed.Permission,
|
||||||
|
|
||||||
|
async run(interaction) {
|
||||||
|
const { getRole } = interaction.client;
|
||||||
|
let SupportStaff = await getRole(supportbot.Roles.StaffMember.Staff, interaction.guild);
|
||||||
|
let Admin = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild);
|
||||||
|
|
||||||
|
if (!SupportStaff || !Admin) {
|
||||||
|
return interaction.reply('Some roles seem to be missing!\nPlease check for errors when starting the bot.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!interaction.member.roles.cache.has(SupportStaff.id) &&
|
||||||
|
!interaction.member.roles.cache.has(Admin.id)) {
|
||||||
|
const NoPerms = new EmbedBuilder()
|
||||||
|
.setTitle('Invalid Permissions!')
|
||||||
|
.setDescription(`${msgconfig.Error.IncorrectPerms}\n\nRole Required: \`${supportbot.Roles.StaffMember.Staff}\` or \`${supportbot.Roles.StaffMember.Admin}\``)
|
||||||
|
.setColor(0xFF0000);
|
||||||
|
return interaction.reply({ embeds: [NoPerms], flags: MessageFlags.Ephemeral });
|
||||||
|
}
|
||||||
|
|
||||||
|
const channel = interaction.options.getChannel('channel');
|
||||||
|
|
||||||
|
const messageData = {
|
||||||
|
type: 'embed',
|
||||||
|
title: 'New Embed',
|
||||||
|
description: 'Click the buttons below to edit this embed',
|
||||||
|
color: 0x5865F2,
|
||||||
|
fields: [],
|
||||||
|
footer: null,
|
||||||
|
thumbnail: null,
|
||||||
|
image: null,
|
||||||
|
timestamp: false
|
||||||
|
};
|
||||||
|
|
||||||
|
function createEmbed() {
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setTitle(messageData.title)
|
||||||
|
.setDescription(messageData.description)
|
||||||
|
.setColor(messageData.color);
|
||||||
|
|
||||||
|
if (messageData.fields.length > 0) {
|
||||||
|
embed.addFields(messageData.fields);
|
||||||
|
}
|
||||||
|
if (messageData.footer) {
|
||||||
|
embed.setFooter({ text: messageData.footer });
|
||||||
|
}
|
||||||
|
if (messageData.thumbnail) {
|
||||||
|
embed.setThumbnail(messageData.thumbnail);
|
||||||
|
}
|
||||||
|
if (messageData.image) {
|
||||||
|
embed.setImage(messageData.image);
|
||||||
|
}
|
||||||
|
if (messageData.timestamp) {
|
||||||
|
embed.setTimestamp();
|
||||||
|
}
|
||||||
|
|
||||||
|
return embed;
|
||||||
|
}
|
||||||
|
|
||||||
|
const typeRow = new ActionRowBuilder()
|
||||||
|
.addComponents(
|
||||||
|
new StringSelectMenuBuilder()
|
||||||
|
.setCustomId('message_type')
|
||||||
|
.setPlaceholder('Select message type')
|
||||||
|
.addOptions([
|
||||||
|
{
|
||||||
|
label: 'Embed Message',
|
||||||
|
description: 'Send as an embedded message',
|
||||||
|
value: 'embed'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Text Message',
|
||||||
|
description: 'Send as a plain text message',
|
||||||
|
value: 'text'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
function createButtons(messageType) {
|
||||||
|
const buttons = [];
|
||||||
|
|
||||||
|
if (messageType === 'embed') {
|
||||||
|
const row1 = new ActionRowBuilder()
|
||||||
|
.addComponents(
|
||||||
|
new ButtonBuilder()
|
||||||
|
.setCustomId('edit_title')
|
||||||
|
.setLabel('Edit Title')
|
||||||
|
.setStyle(ButtonStyle.Secondary),
|
||||||
|
new ButtonBuilder()
|
||||||
|
.setCustomId('edit_description')
|
||||||
|
.setLabel('Edit Description')
|
||||||
|
.setStyle(ButtonStyle.Secondary),
|
||||||
|
new ButtonBuilder()
|
||||||
|
.setCustomId('edit_color')
|
||||||
|
.setLabel('Edit Color')
|
||||||
|
.setStyle(ButtonStyle.Secondary),
|
||||||
|
new ButtonBuilder()
|
||||||
|
.setCustomId('add_field')
|
||||||
|
.setLabel('Add Field')
|
||||||
|
.setStyle(ButtonStyle.Secondary)
|
||||||
|
);
|
||||||
|
|
||||||
|
const row2 = new ActionRowBuilder()
|
||||||
|
.addComponents(
|
||||||
|
new ButtonBuilder()
|
||||||
|
.setCustomId('edit_footer')
|
||||||
|
.setLabel('Edit Footer')
|
||||||
|
.setStyle(ButtonStyle.Secondary),
|
||||||
|
new ButtonBuilder()
|
||||||
|
.setCustomId('edit_image')
|
||||||
|
.setLabel('Add Image')
|
||||||
|
.setStyle(ButtonStyle.Secondary),
|
||||||
|
new ButtonBuilder()
|
||||||
|
.setCustomId('edit_thumbnail')
|
||||||
|
.setLabel('Add Thumbnail')
|
||||||
|
.setStyle(ButtonStyle.Secondary),
|
||||||
|
new ButtonBuilder()
|
||||||
|
.setCustomId('toggle_timestamp')
|
||||||
|
.setLabel('Toggle Timestamp')
|
||||||
|
.setStyle(ButtonStyle.Secondary)
|
||||||
|
);
|
||||||
|
|
||||||
|
buttons.push(row1, row2);
|
||||||
|
} else {
|
||||||
|
const textRow = new ActionRowBuilder()
|
||||||
|
.addComponents(
|
||||||
|
new ButtonBuilder()
|
||||||
|
.setCustomId('edit_description')
|
||||||
|
.setLabel('Edit Text')
|
||||||
|
.setStyle(ButtonStyle.Secondary)
|
||||||
|
);
|
||||||
|
|
||||||
|
buttons.push(textRow);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send button always shows
|
||||||
|
const sendRow = new ActionRowBuilder()
|
||||||
|
.addComponents(
|
||||||
|
new ButtonBuilder()
|
||||||
|
.setCustomId('send_message')
|
||||||
|
.setLabel('Send')
|
||||||
|
.setStyle(ButtonStyle.Success)
|
||||||
|
);
|
||||||
|
|
||||||
|
buttons.push(sendRow);
|
||||||
|
return buttons;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updatePreview(i) {
|
||||||
|
if (messageData.type === 'embed') {
|
||||||
|
await i.update({
|
||||||
|
content: 'Customize your message:',
|
||||||
|
embeds: [createEmbed()],
|
||||||
|
components: [typeRow, ...createButtons('embed')]
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await i.update({
|
||||||
|
content: messageData.description,
|
||||||
|
embeds: [],
|
||||||
|
components: [typeRow, ...createButtons('text')]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = await interaction.reply({
|
||||||
|
content: messageData.type === 'text' ? messageData.description : 'Customize your message:',
|
||||||
|
components: [typeRow, ...createButtons(messageData.type)],
|
||||||
|
embeds: messageData.type === 'embed' ? [createEmbed()] : [],
|
||||||
|
flags: MessageFlags.Ephemeral
|
||||||
|
});
|
||||||
|
|
||||||
|
const collector = message.createMessageComponentCollector({
|
||||||
|
time: 900000
|
||||||
|
});
|
||||||
|
|
||||||
|
collector.on('collect', async (i) => {
|
||||||
|
if (i.user.id !== interaction.user.id) {
|
||||||
|
return i.reply({
|
||||||
|
content: 'You cannot edit this message.',
|
||||||
|
flags: MessageFlags.Ephemeral
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (i.customId) {
|
||||||
|
case 'message_type':
|
||||||
|
messageData.type = i.values[0];
|
||||||
|
await updatePreview(i);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'edit_description':
|
||||||
|
const descModal = new ModalBuilder()
|
||||||
|
.setCustomId('desc_modal')
|
||||||
|
.setTitle(messageData.type === 'embed' ? 'Edit Description' : 'Edit Text');
|
||||||
|
|
||||||
|
descModal.addComponents(
|
||||||
|
new ActionRowBuilder()
|
||||||
|
.addComponents(
|
||||||
|
new TextInputBuilder()
|
||||||
|
.setCustomId('desc_input')
|
||||||
|
.setLabel(messageData.type === 'embed' ? 'Description' : 'Message Text')
|
||||||
|
.setStyle(TextInputStyle.Paragraph)
|
||||||
|
.setValue(messageData.description)
|
||||||
|
.setMaxLength(4000)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
await i.showModal(descModal);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'edit_title':
|
||||||
|
const titleModal = new ModalBuilder()
|
||||||
|
.setCustomId('title_modal')
|
||||||
|
.setTitle('Edit Title');
|
||||||
|
|
||||||
|
titleModal.addComponents(
|
||||||
|
new ActionRowBuilder()
|
||||||
|
.addComponents(
|
||||||
|
new TextInputBuilder()
|
||||||
|
.setCustomId('title_input')
|
||||||
|
.setLabel('Title')
|
||||||
|
.setStyle(TextInputStyle.Short)
|
||||||
|
.setValue(messageData.title)
|
||||||
|
.setMaxLength(256)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
await i.showModal(titleModal);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'edit_color':
|
||||||
|
const colorModal = new ModalBuilder()
|
||||||
|
.setCustomId('color_modal')
|
||||||
|
.setTitle('Edit Color');
|
||||||
|
|
||||||
|
colorModal.addComponents(
|
||||||
|
new ActionRowBuilder()
|
||||||
|
.addComponents(
|
||||||
|
new TextInputBuilder()
|
||||||
|
.setCustomId('color_input')
|
||||||
|
.setLabel('Color (HEX)')
|
||||||
|
.setStyle(TextInputStyle.Short)
|
||||||
|
.setPlaceholder('#FF0000')
|
||||||
|
.setMinLength(6)
|
||||||
|
.setMaxLength(7)
|
||||||
|
.setValue(messageData.color.toString(16).padStart(6, '0'))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
await i.showModal(colorModal);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'add_field':
|
||||||
|
const fieldModal = new ModalBuilder()
|
||||||
|
.setCustomId('field_modal')
|
||||||
|
.setTitle('Add Field');
|
||||||
|
|
||||||
|
fieldModal.addComponents(
|
||||||
|
new ActionRowBuilder()
|
||||||
|
.addComponents(
|
||||||
|
new TextInputBuilder()
|
||||||
|
.setCustomId('field_name')
|
||||||
|
.setLabel('Field Name')
|
||||||
|
.setStyle(TextInputStyle.Short)
|
||||||
|
.setMaxLength(256)
|
||||||
|
),
|
||||||
|
new ActionRowBuilder()
|
||||||
|
.addComponents(
|
||||||
|
new TextInputBuilder()
|
||||||
|
.setCustomId('field_value')
|
||||||
|
.setLabel('Field Value')
|
||||||
|
.setStyle(TextInputStyle.Paragraph)
|
||||||
|
.setMaxLength(1024)
|
||||||
|
),
|
||||||
|
new ActionRowBuilder()
|
||||||
|
.addComponents(
|
||||||
|
new TextInputBuilder()
|
||||||
|
.setCustomId('field_inline')
|
||||||
|
.setLabel('Inline? (true/false)')
|
||||||
|
.setStyle(TextInputStyle.Short)
|
||||||
|
.setValue('false')
|
||||||
|
.setPlaceholder('true or false')
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
await i.showModal(fieldModal);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'edit_footer':
|
||||||
|
const footerModal = new ModalBuilder()
|
||||||
|
.setCustomId('footer_modal')
|
||||||
|
.setTitle('Edit Footer');
|
||||||
|
|
||||||
|
footerModal.addComponents(
|
||||||
|
new ActionRowBuilder()
|
||||||
|
.addComponents(
|
||||||
|
new TextInputBuilder()
|
||||||
|
.setCustomId('footer_input')
|
||||||
|
.setLabel('Footer Text')
|
||||||
|
.setStyle(TextInputStyle.Short)
|
||||||
|
.setValue(messageData.footer || '')
|
||||||
|
.setMaxLength(2048)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
await i.showModal(footerModal);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'edit_image':
|
||||||
|
const imageModal = new ModalBuilder()
|
||||||
|
.setCustomId('image_modal')
|
||||||
|
.setTitle('Add Image');
|
||||||
|
|
||||||
|
imageModal.addComponents(
|
||||||
|
new ActionRowBuilder()
|
||||||
|
.addComponents(
|
||||||
|
new TextInputBuilder()
|
||||||
|
.setCustomId('image_input')
|
||||||
|
.setLabel('Image URL')
|
||||||
|
.setStyle(TextInputStyle.Short)
|
||||||
|
.setValue(messageData.image || '')
|
||||||
|
.setPlaceholder('https://example.com/image.png')
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
await i.showModal(imageModal);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'edit_thumbnail':
|
||||||
|
const thumbnailModal = new ModalBuilder()
|
||||||
|
.setCustomId('thumbnail_modal')
|
||||||
|
.setTitle('Add Thumbnail');
|
||||||
|
|
||||||
|
thumbnailModal.addComponents(
|
||||||
|
new ActionRowBuilder()
|
||||||
|
.addComponents(
|
||||||
|
new TextInputBuilder()
|
||||||
|
.setCustomId('thumbnail_input')
|
||||||
|
.setLabel('Thumbnail URL')
|
||||||
|
.setStyle(TextInputStyle.Short)
|
||||||
|
.setValue(messageData.thumbnail || '')
|
||||||
|
.setPlaceholder('https://example.com/thumbnail.png')
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
await i.showModal(thumbnailModal);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'toggle_timestamp':
|
||||||
|
messageData.timestamp = !messageData.timestamp;
|
||||||
|
await updatePreview(i);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'send_message':
|
||||||
|
try {
|
||||||
|
if (messageData.type === 'embed') {
|
||||||
|
await channel.send({ embeds: [createEmbed()] });
|
||||||
|
} else {
|
||||||
|
await channel.send(messageData.description);
|
||||||
|
}
|
||||||
|
await i.update({
|
||||||
|
content: 'Message sent successfully!',
|
||||||
|
components: [],
|
||||||
|
embeds: []
|
||||||
|
});
|
||||||
|
collector.stop();
|
||||||
|
} catch (error) {
|
||||||
|
await i.reply({
|
||||||
|
content: 'Failed to send message. Please check channel permissions.',
|
||||||
|
flags: MessageFlags.Ephemeral
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle modal submissions
|
||||||
|
interaction.client.on('interactionCreate', async (modal) => {
|
||||||
|
if (!modal.isModalSubmit()) return;
|
||||||
|
if (modal.user.id !== interaction.user.id) return;
|
||||||
|
|
||||||
|
switch (modal.customId) {
|
||||||
|
case 'title_modal':
|
||||||
|
messageData.title = modal.fields.getTextInputValue('title_input');
|
||||||
|
break;
|
||||||
|
case 'desc_modal':
|
||||||
|
messageData.description = modal.fields.getTextInputValue('desc_input');
|
||||||
|
break;
|
||||||
|
case 'color_modal':
|
||||||
|
messageData.color = parseColor(modal.fields.getTextInputValue('color_input'));
|
||||||
|
break;
|
||||||
|
case 'field_modal':
|
||||||
|
const inlineValue = modal.fields.getTextInputValue('field_inline').toLowerCase();
|
||||||
|
messageData.fields.push({
|
||||||
|
name: modal.fields.getTextInputValue('field_name'),
|
||||||
|
value: modal.fields.getTextInputValue('field_value'),
|
||||||
|
inline: inlineValue === 'true'
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'footer_modal':
|
||||||
|
messageData.footer = modal.fields.getTextInputValue('footer_input');
|
||||||
|
break;
|
||||||
|
case 'image_modal':
|
||||||
|
messageData.image = modal.fields.getTextInputValue('image_input');
|
||||||
|
break;
|
||||||
|
case 'thumbnail_modal':
|
||||||
|
messageData.thumbnail = modal.fields.getTextInputValue('thumbnail_input');
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await updatePreview(modal);
|
||||||
|
});
|
||||||
|
|
||||||
|
collector.on('end', () => {
|
||||||
|
interaction.editReply({
|
||||||
|
content: 'Message editor timed out.',
|
||||||
|
components: [],
|
||||||
|
embeds: []
|
||||||
|
}).catch(() => {});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,235 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
const { ApplicationCommandOptionType, ApplicationCommandType, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, ComponentType, MessageFlags } = require("discord.js");
|
||||||
|
const yaml = require("js-yaml");
|
||||||
|
const db = require("../../Structures/Database.js");
|
||||||
|
const supportbot = require("../../Structures/ConfigStore").supportbot;
|
||||||
|
const cmdconfig = require("../../Structures/ConfigStore").commands;
|
||||||
|
const msgconfig = require("../../Structures/ConfigStore").messages;
|
||||||
|
|
||||||
|
const Command = require("../../Structures/Command.js");
|
||||||
|
|
||||||
|
// Function to chunk array into smaller arrays
|
||||||
|
function chunkArray(arr, size) {
|
||||||
|
const chunks = [];
|
||||||
|
for (let i = 0; i < arr.length; i += size) {
|
||||||
|
chunks.push(arr.slice(i, i + size));
|
||||||
|
}
|
||||||
|
return chunks;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = new Command({
|
||||||
|
name: cmdconfig.Mod.Command,
|
||||||
|
description: cmdconfig.Mod.Description || "Moderation commands",
|
||||||
|
type: ApplicationCommandType.ChatInput,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: cmdconfig.Mod.TicketBlacklist.Command,
|
||||||
|
description: cmdconfig.Mod.TicketBlacklist.Description || "Ticket Blacklist Commands",
|
||||||
|
type: ApplicationCommandOptionType.SubcommandGroup,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: cmdconfig.Mod.TicketBlacklist.Add.Command,
|
||||||
|
description: msgconfig.Mod.TicketBlacklist.Add.Description || "Add a user to the blacklist",
|
||||||
|
type: ApplicationCommandOptionType.Subcommand,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'user',
|
||||||
|
description: 'The user to blacklist',
|
||||||
|
type: ApplicationCommandOptionType.User,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'reason',
|
||||||
|
description: 'Reason for blacklisting',
|
||||||
|
type: ApplicationCommandOptionType.String,
|
||||||
|
required: false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: cmdconfig.Mod.TicketBlacklist.Remove.Command,
|
||||||
|
description: cmdconfig.Mod.TicketBlacklist.Remove.Description || "Remove a user from the blacklist",
|
||||||
|
type: ApplicationCommandOptionType.Subcommand,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'user',
|
||||||
|
description: 'The user to remove from the blacklist',
|
||||||
|
type: ApplicationCommandOptionType.User,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'reason',
|
||||||
|
description: 'Reason for unblacklisting',
|
||||||
|
type: ApplicationCommandOptionType.String,
|
||||||
|
required: false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "view",
|
||||||
|
description: msgconfig.Mod.TicketBlacklist.View.Description || "View all blacklisted users",
|
||||||
|
type: ApplicationCommandOptionType.Subcommand
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
async run(interaction) {
|
||||||
|
const subcommandGroup = interaction.options.getSubcommandGroup();
|
||||||
|
const subcommand = interaction.options.getSubcommand();
|
||||||
|
const user = interaction.options.getUser('user');
|
||||||
|
const reason = interaction.options.getString('reason') || "No reason provided";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const blacklistedUsers = typeof db.getBlacklistedUsers === "function" ? db.getBlacklistedUsers() : [];
|
||||||
|
|
||||||
|
if (subcommand === cmdconfig.Mod.TicketBlacklist.Add.Command) {
|
||||||
|
if (db.isUserBlacklisted && db.isUserBlacklisted(user.id)) {
|
||||||
|
const alreadyBlacklistedEmbed = new EmbedBuilder()
|
||||||
|
.setDescription(msgconfig.Mod.TicketBlacklist.Add.AlreadyBlacklisted.replace("{userTag}", user.tag) || ":x: User is already blacklisted.")
|
||||||
|
.setColor(supportbot.Embed.Colours.Warn);
|
||||||
|
return interaction.reply({ embeds: [alreadyBlacklistedEmbed], flags: MessageFlags.Ephemeral });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof db.addBlacklistedUser === "function") {
|
||||||
|
db.addBlacklistedUser(user.id, reason, interaction.user.tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
const successEmbed = new EmbedBuilder()
|
||||||
|
.setDescription(msgconfig.Mod.TicketBlacklist.Add.Success.replace("{userTag}", user.tag) || ":white_check_mark: User has been blacklisted.")
|
||||||
|
.setColor(supportbot.Embed.Colours.Success);
|
||||||
|
|
||||||
|
const blacklistChannel = interaction.guild.channels.cache.get(supportbot.Ticket.Log.TicketBlacklistLog);
|
||||||
|
if (blacklistChannel) {
|
||||||
|
const blacklistLogEmbed = new EmbedBuilder()
|
||||||
|
.setTitle(msgconfig.TicketBlacklistLog.Title || "Ticket Blacklist Log")
|
||||||
|
.setColor(msgconfig.TicketBlacklistLog.Colour || supportbot.Embed.Colours.Success)
|
||||||
|
.setFooter({ text: supportbot.Embed.Footer, iconURL: interaction.user.displayAvatarURL() })
|
||||||
|
.setDescription(`> **User:** ${user.tag} (\`${user.id}\`)\n> **Actioned by:** <@${interaction.user.id}>`)
|
||||||
|
.addFields({ name: "Action", value: "\`\`\`Added to blacklist\`\`\`", inline: false })
|
||||||
|
.addFields({ name: "Reason", value: `\`\`\`${reason}\`\`\``, inline: false });
|
||||||
|
|
||||||
|
blacklistChannel.send({ embeds: [blacklistLogEmbed] });
|
||||||
|
}
|
||||||
|
|
||||||
|
return interaction.reply({
|
||||||
|
embeds: [successEmbed],
|
||||||
|
flags: MessageFlags.Ephemeral
|
||||||
|
});
|
||||||
|
|
||||||
|
} else if (subcommand === cmdconfig.Mod.TicketBlacklist.Remove.Command) {
|
||||||
|
if (db.isUserBlacklisted && !db.isUserBlacklisted(user.id)) {
|
||||||
|
const notBlacklistedEmbed = new EmbedBuilder()
|
||||||
|
.setDescription(msgconfig.Mod.TicketBlacklist.Remove.NotBlacklisted.replace("{userTag}", user.tag) || ":x: User is not blacklisted.")
|
||||||
|
.setColor(supportbot.Embed.Colours.Warn);
|
||||||
|
return interaction.reply({ embeds: [notBlacklistedEmbed], flags: MessageFlags.Ephemeral });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof db.removeBlacklistedUser === "function") {
|
||||||
|
db.removeBlacklistedUser(user.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
const removedEmbed = new EmbedBuilder()
|
||||||
|
.setDescription(msgconfig.Mod.TicketBlacklist.Remove.Success.replace("{userTag}", user.tag) || ":white_check_mark: User has been removed from the blacklist.")
|
||||||
|
.setColor(supportbot.Embed.Colours.Success);
|
||||||
|
|
||||||
|
const blacklistChannel = interaction.guild.channels.cache.get(supportbot.Ticket.Log.TicketBlacklistLog);
|
||||||
|
if (blacklistChannel) {
|
||||||
|
const blacklistLogEmbed = new EmbedBuilder()
|
||||||
|
.setTitle(msgconfig.TicketBlacklistLog.Title || "Ticket Blacklist Log")
|
||||||
|
.setColor(msgconfig.TicketBlacklistLog.Colour || supportbot.Embed.Colours.Success)
|
||||||
|
.setFooter({ text: supportbot.Embed.Footer, iconURL: interaction.user.displayAvatarURL() })
|
||||||
|
.setDescription(`> **User:** ${user.tag} (\`${user.id}\`)\n> **Actioned by:** <@${interaction.user.id}>`)
|
||||||
|
.addFields({ name: "Action", value: "\`\`\`Removed from blacklist\`\`\`", inline: false })
|
||||||
|
.addFields({ name: "Reason", value: `\`\`\`${reason}\`\`\``, inline: false });
|
||||||
|
|
||||||
|
blacklistChannel.send({ embeds: [blacklistLogEmbed] });
|
||||||
|
}
|
||||||
|
|
||||||
|
return interaction.reply({ embeds: [removedEmbed], flags: MessageFlags.Ephemeral });
|
||||||
|
|
||||||
|
} else if (subcommand === "view") {
|
||||||
|
if (blacklistedUsers.length === 0) {
|
||||||
|
const noBlacklistedUsersEmbed = new EmbedBuilder()
|
||||||
|
.setDescription(msgconfig.Mod.TicketBlacklist.View.NoBlacklistedUsers || "No users are blacklisted.")
|
||||||
|
.setColor(supportbot.Embed.Colours.Success);
|
||||||
|
return interaction.reply({ embeds: [noBlacklistedUsersEmbed], flags: MessageFlags.Ephemeral });
|
||||||
|
}
|
||||||
|
|
||||||
|
const chunkedUsers = chunkArray(blacklistedUsers, 5);
|
||||||
|
let currentPage = 0;
|
||||||
|
|
||||||
|
const createEmbed = (page) => {
|
||||||
|
return new EmbedBuilder()
|
||||||
|
.setTitle(msgconfig.Mod.TicketBlacklist.View.EmbedTitle || "Blacklisted Users")
|
||||||
|
.setDescription(chunkedUsers[page].map(id => `<@${id}>`).join("\n"))
|
||||||
|
.setColor(supportbot.Embed.Colours.Success)
|
||||||
|
.setFooter({ text: `Page ${page + 1} of ${chunkedUsers.length}` });
|
||||||
|
};
|
||||||
|
|
||||||
|
const buttons = new ActionRowBuilder()
|
||||||
|
.addComponents(
|
||||||
|
new ButtonBuilder()
|
||||||
|
.setCustomId('previous')
|
||||||
|
.setLabel('Previous')
|
||||||
|
.setStyle(ButtonStyle.Secondary)
|
||||||
|
.setDisabled(currentPage === 0),
|
||||||
|
new ButtonBuilder()
|
||||||
|
.setCustomId('next')
|
||||||
|
.setLabel('Next')
|
||||||
|
.setStyle(ButtonStyle.Secondary)
|
||||||
|
.setDisabled(currentPage === chunkedUsers.length - 1)
|
||||||
|
);
|
||||||
|
|
||||||
|
const response = await interaction.reply({
|
||||||
|
embeds: [createEmbed(currentPage)],
|
||||||
|
components: chunkedUsers.length > 1 ? [buttons] : [],
|
||||||
|
flags: MessageFlags.Ephemeral
|
||||||
|
});
|
||||||
|
|
||||||
|
if (chunkedUsers.length <= 1) return;
|
||||||
|
|
||||||
|
const collector = response.createMessageComponentCollector({
|
||||||
|
componentType: ComponentType.Button,
|
||||||
|
time: 60000
|
||||||
|
});
|
||||||
|
|
||||||
|
collector.on('collect', async (i) => {
|
||||||
|
if (i.user.id !== interaction.user.id) {
|
||||||
|
return i.reply({
|
||||||
|
content: 'You cannot use these buttons.',
|
||||||
|
flags: MessageFlags.Ephemeral
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i.customId === 'previous' && currentPage > 0) {
|
||||||
|
currentPage--;
|
||||||
|
} else if (i.customId === 'next' && currentPage < chunkedUsers.length - 1) {
|
||||||
|
currentPage++;
|
||||||
|
}
|
||||||
|
|
||||||
|
buttons.components[0].setDisabled(currentPage === 0);
|
||||||
|
buttons.components[1].setDisabled(currentPage === chunkedUsers.length - 1);
|
||||||
|
|
||||||
|
await i.update({
|
||||||
|
embeds: [createEmbed(currentPage)],
|
||||||
|
components: [buttons]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
collector.on('end', () => {
|
||||||
|
buttons.components.forEach(button => button.setDisabled(true));
|
||||||
|
interaction.editReply({
|
||||||
|
components: [buttons]
|
||||||
|
}).catch(() => {});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error in mod command:", error);
|
||||||
|
const errorEmbed = new EmbedBuilder()
|
||||||
|
.setDescription(msgconfig.Error.ActionFailed || "An error occurred while processing your request.")
|
||||||
|
.setColor(supportbot.Embed.Colours.Warn);
|
||||||
|
interaction.reply({ embeds: [errorEmbed], flags: MessageFlags.Ephemeral });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,220 @@
|
|||||||
|
const Command = require("../../Structures/Command.js");
|
||||||
|
const Discord = require("discord.js");
|
||||||
|
const db = require("../../Structures/Database.js");
|
||||||
|
|
||||||
|
const supportbot = require("../../Structures/ConfigStore").supportbot;
|
||||||
|
const cmdconfig = require("../../Structures/ConfigStore").commands;
|
||||||
|
|
||||||
|
async function loadSettings() {
|
||||||
|
return typeof db.getSettings === "function" ? db.getSettings() : {};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveSettings(settings) {
|
||||||
|
if (typeof db.saveSettings === "function") {
|
||||||
|
db.saveSettings(settings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = new Command({
|
||||||
|
name: cmdconfig.Settings.Command,
|
||||||
|
description: cmdconfig.Settings.Description,
|
||||||
|
permissions: cmdconfig.Settings.Permission,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'setstatus',
|
||||||
|
type: Discord.ApplicationCommandOptionType.Subcommand,
|
||||||
|
description: 'Set the bot status',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'status',
|
||||||
|
type: Discord.ApplicationCommandOptionType.String,
|
||||||
|
description: 'Choose the bot status',
|
||||||
|
required: true,
|
||||||
|
choices: [
|
||||||
|
{ name: 'Online 🟢', value: 'online' },
|
||||||
|
{ name: 'Idle 🌙', value: 'idle' },
|
||||||
|
{ name: 'Do Not Disturb ⛔', value: 'dnd' },
|
||||||
|
{ name: 'Invisible 👻', value: 'invisible' }
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'setactivity',
|
||||||
|
type: Discord.ApplicationCommandOptionType.Subcommand,
|
||||||
|
description: 'Set the bot activity',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'type',
|
||||||
|
type: Discord.ApplicationCommandOptionType.String,
|
||||||
|
description: 'Choose the activity type',
|
||||||
|
required: true,
|
||||||
|
choices: [
|
||||||
|
{ name: 'Playing 🎮', value: 'PLAYING' },
|
||||||
|
{ name: 'Watching 👀', value: 'WATCHING' },
|
||||||
|
{ name: 'Listening 🎧', value: 'LISTENING' },
|
||||||
|
{ name: 'Competing 🏆', value: 'COMPETING' }
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'message',
|
||||||
|
type: Discord.ApplicationCommandOptionType.String,
|
||||||
|
description: 'The activity message',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'streammode',
|
||||||
|
type: Discord.ApplicationCommandOptionType.Subcommand,
|
||||||
|
description: 'Set the bot streaming status',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'toggle',
|
||||||
|
type: Discord.ApplicationCommandOptionType.String,
|
||||||
|
description: 'Turn streaming mode on or off',
|
||||||
|
required: true,
|
||||||
|
choices: [
|
||||||
|
{ name: 'On', value: 'on' },
|
||||||
|
{ name: 'Off', value: 'off' }
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'twitchurl',
|
||||||
|
type: Discord.ApplicationCommandOptionType.String,
|
||||||
|
description: 'Twitch stream URL (Required if turning on)',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'setnickname',
|
||||||
|
type: Discord.ApplicationCommandOptionType.Subcommand,
|
||||||
|
description: 'Set the bot nickname',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'nickname',
|
||||||
|
type: Discord.ApplicationCommandOptionType.String,
|
||||||
|
description: 'The new nickname for the bot',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
async run(interaction) {
|
||||||
|
const subcommand = interaction.options.getSubcommand();
|
||||||
|
const settings = await loadSettings();
|
||||||
|
|
||||||
|
if (subcommand === 'setstatus') {
|
||||||
|
const status = interaction.options.getString('status');
|
||||||
|
|
||||||
|
try {
|
||||||
|
interaction.client.user.setPresence({
|
||||||
|
status: status,
|
||||||
|
activities: interaction.client.user.presence.activities || [],
|
||||||
|
});
|
||||||
|
|
||||||
|
settings.status = status;
|
||||||
|
await saveSettings(settings);
|
||||||
|
|
||||||
|
const embed = new Discord.EmbedBuilder()
|
||||||
|
.setDescription(`Status set to **${status.charAt(0).toUpperCase() + status.slice(1)}**`)
|
||||||
|
.setColor(supportbot.Embed.Colours.General);
|
||||||
|
|
||||||
|
return interaction.reply({ embeds: [embed], flags: Discord.MessageFlags.Ephemeral });
|
||||||
|
} catch (error) {
|
||||||
|
return interaction.reply({ content: `Failed to set status: ${error.message}`, flags: Discord.MessageFlags.Ephemeral });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subcommand === 'setactivity') {
|
||||||
|
const activityType = interaction.options.getString('type');
|
||||||
|
const activityMessage = interaction.options.getString('message');
|
||||||
|
|
||||||
|
try {
|
||||||
|
interaction.client.user.setPresence({
|
||||||
|
activities: [{ name: activityMessage, type: Discord.ActivityType[activityType] }],
|
||||||
|
status: interaction.client.user.presence.status || 'online',
|
||||||
|
});
|
||||||
|
|
||||||
|
settings.activity = { type: activityType, message: activityMessage };
|
||||||
|
await saveSettings(settings);
|
||||||
|
|
||||||
|
const embed = new Discord.EmbedBuilder()
|
||||||
|
.setDescription(`Activity set to **${activityType.toLowerCase()} ${activityMessage}**`)
|
||||||
|
.setColor(supportbot.Embed.Colours.General);
|
||||||
|
|
||||||
|
return interaction.reply({ embeds: [embed], flags: Discord.MessageFlags.Ephemeral });
|
||||||
|
} catch (error) {
|
||||||
|
return interaction.reply({ content: `Failed to set activity: ${error.message}`, flags: Discord.MessageFlags.Ephemeral });
|
||||||
|
}
|
||||||
|
} else if (subcommand === 'streammode') {
|
||||||
|
const toggle = interaction.options.getString('toggle');
|
||||||
|
const twitchUrl = interaction.options.getString('twitchurl');
|
||||||
|
|
||||||
|
if (toggle === 'on') {
|
||||||
|
if (!twitchUrl) {
|
||||||
|
return interaction.reply({ content: 'Please provide a Twitch URL to stream.', flags: Discord.MessageFlags.Ephemeral });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
interaction.client.user.setPresence({
|
||||||
|
activities: [{ name: 'Streaming', type: Discord.ActivityType.Streaming, url: twitchUrl }],
|
||||||
|
status: 'online',
|
||||||
|
});
|
||||||
|
|
||||||
|
settings.streaming = { active: true, url: twitchUrl };
|
||||||
|
await saveSettings(settings);
|
||||||
|
|
||||||
|
const embed = new Discord.EmbedBuilder()
|
||||||
|
.setDescription(`**Streaming mode enabled!**\nStreaming: [Twitch](${twitchUrl})`)
|
||||||
|
.setColor(supportbot.Embed.Colours.General);
|
||||||
|
|
||||||
|
return interaction.reply({ embeds: [embed], flags: Discord.MessageFlags.Ephemeral });
|
||||||
|
} catch (error) {
|
||||||
|
return interaction.reply({ content: `Failed to enable streaming mode: ${error.message}`, flags: Discord.MessageFlags.Ephemeral });
|
||||||
|
}
|
||||||
|
} else if (toggle === 'off') {
|
||||||
|
try {
|
||||||
|
interaction.client.user.setPresence({
|
||||||
|
activities: interaction.client.user.presence.activities.filter(a => a.type !== Discord.ActivityType.Streaming),
|
||||||
|
status: settings.status || 'online',
|
||||||
|
});
|
||||||
|
|
||||||
|
settings.streaming = { active: false };
|
||||||
|
await saveSettings(settings);
|
||||||
|
|
||||||
|
const embed = new Discord.EmbedBuilder()
|
||||||
|
.setDescription(`**Streaming mode disabled!**`)
|
||||||
|
.setColor(supportbot.Embed.Colours.General);
|
||||||
|
|
||||||
|
return interaction.reply({ embeds: [embed], flags: Discord.MessageFlags.Ephemeral });
|
||||||
|
} catch (error) {
|
||||||
|
return interaction.reply({ content: `Failed to disable streaming mode: ${error.message}`, flags: Discord.MessageFlags.Ephemeral });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (subcommand === 'setnickname') {
|
||||||
|
const nickname = interaction.options.getString('nickname');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const guild = interaction.guild;
|
||||||
|
const botMember = guild.members.cache.get(interaction.client.user.id);
|
||||||
|
await botMember.setNickname(nickname);
|
||||||
|
|
||||||
|
settings.nickname = nickname;
|
||||||
|
await saveSettings(settings);
|
||||||
|
|
||||||
|
const embed = new Discord.EmbedBuilder()
|
||||||
|
.setDescription(`Bot nickname set to **${nickname}**`)
|
||||||
|
.setColor(supportbot.Embed.Colours.General);
|
||||||
|
|
||||||
|
return interaction.reply({ embeds: [embed], flags: Discord.MessageFlags.Ephemeral });
|
||||||
|
} catch (error) {
|
||||||
|
return interaction.reply({ content: `Failed to set nickname: ${error.message}`, flags: Discord.MessageFlags.Ephemeral });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return interaction.reply({ content: 'Invalid command usage.', flags: Discord.MessageFlags.Ephemeral });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
# ___ _ ___ _
|
||||||
|
# / __> _ _ ___ ___ ___ _ _ _| |_ | . > ___ _| |_
|
||||||
|
# \__ \| | || . \| . \/ . \| '_> | | | . \/ . \ | |
|
||||||
|
# <___/`___|| _/| _/\___/|_| |_| |___/\___/ |_|
|
||||||
|
# |_| |_|
|
||||||
|
#
|
||||||
|
# SupportBot created by Emerald Services
|
||||||
|
# Installed with MIT License
|
||||||
|
#
|
||||||
|
# Discord Support: https://discord.gg/emeraldsrv
|
||||||
|
# Official Website & Marketplace: https://emeraldsrv.com
|
||||||
|
#
|
||||||
|
# Messages Configuration (api.yml)
|
||||||
|
|
||||||
|
|
||||||
|
# -------------------------------
|
||||||
|
# API Server Configuration
|
||||||
|
# -------------------------------
|
||||||
|
|
||||||
|
API:
|
||||||
|
Enabled: true
|
||||||
|
Port: 3000
|
||||||
|
|
||||||
|
SecretKey: "SECRET_KEY"
|
||||||
|
|
||||||
|
# Set to 1 when the dashboard is behind nginx, Cloudflare, Pterodactyl proxy, etc.
|
||||||
|
# so http/https is detected from X-Forwarded-Proto.
|
||||||
|
|
||||||
|
TrustProxy: false
|
||||||
|
|
||||||
|
EmeraldAPIKey: "EMERALDSRV_API_KEY"
|
||||||
|
# This is required for the one-click addon installer. Get API key from https://emeraldsrv.dev/
|
||||||
|
|
||||||
|
OAuth:
|
||||||
|
Enabled: true
|
||||||
|
ClientId: "CLIENT_ID"
|
||||||
|
ClientSecret: "CLIENT_SECRET"
|
||||||
|
# Get Client ID and Client Secret from https://discord.com/developers/applications
|
||||||
|
|
||||||
|
# Canonical callback URL (host + path). Protocol (http/https) can follow the
|
||||||
|
# browser when UseRequestOrigin is true (default).
|
||||||
|
|
||||||
|
RedirectUri: "http://localhost:3000/api/auth/discord/callback"
|
||||||
|
|
||||||
|
# Optional: register extra callback URLs in Discord and list them here.
|
||||||
|
|
||||||
|
# RedirectUris:
|
||||||
|
# - "https://bot.example.com/api/auth/discord/callback"
|
||||||
|
# UseRequestOrigin: true
|
||||||
|
# CookieSecure: false
|
||||||
|
|
||||||
|
OwnerUserIds:
|
||||||
|
- "OWNER_USER_ID"
|
||||||
|
|
||||||
|
AllowedUserIds: []
|
||||||
|
|
||||||
|
# Dashboard Features
|
||||||
|
|
||||||
|
LiveChat:
|
||||||
|
Mode: "webhook"
|
||||||
|
# "webhook" - Webhook to send messages with staff member's avatar and Discord username
|
||||||
|
# "bot" - Send messages as Bot account with discord username prefix.
|
||||||
|
# "bot_noprefix" - Send messages as Bot account without discord username prefix.
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
# ___ _ ___ _
|
||||||
|
# / __> _ _ ___ ___ ___ _ _ _| |_ | . > ___ _| |_
|
||||||
|
# \__ \| | || . \| . \/ . \| '_> | | | . \/ . \ | |
|
||||||
|
# <___/`___|| _/| _/\___/|_| |_| |___/\___/ |_|
|
||||||
|
# |_| |_|
|
||||||
|
#
|
||||||
|
# SupportBot created by Emerald Services
|
||||||
|
# Installed with MIT License
|
||||||
|
#
|
||||||
|
# Discord Support: https://discord.gg/emeraldsrv
|
||||||
|
# Official Documentation: https://docs.emeraldsrv.com
|
||||||
|
# Addons Marketplace: https://community.emeraldsrv.com
|
||||||
|
#
|
||||||
|
# Commands Configuration (commands.yml)
|
||||||
|
|
||||||
|
Help:
|
||||||
|
Command: "help"
|
||||||
|
Description: "Get a list of all the commands."
|
||||||
|
Permission: ["SendMessages"]
|
||||||
|
Enabled: true
|
||||||
|
|
||||||
|
Info:
|
||||||
|
Command: "info"
|
||||||
|
Description: "Get information about the **ServerName**."
|
||||||
|
Permission: ["SendMessages"]
|
||||||
|
Enabled: true
|
||||||
|
|
||||||
|
TicketPanel:
|
||||||
|
Command: "ticketpanel"
|
||||||
|
Description: "Create the ticket creation panel"
|
||||||
|
Permission: ["Administrator"]
|
||||||
|
Enabled: true
|
||||||
|
|
||||||
|
Profile:
|
||||||
|
Command: "profile"
|
||||||
|
Description: "Shows the profile for a discord member."
|
||||||
|
Permission: ["SendMessages"]
|
||||||
|
# Command is automatically disabled when the TicketClaims is disabled.
|
||||||
|
|
||||||
|
OpenTicket:
|
||||||
|
Command: "ticket"
|
||||||
|
Description: "Create a support ticket."
|
||||||
|
Permission: ["SendMessages"]
|
||||||
|
Enabled: true
|
||||||
|
|
||||||
|
AddUser:
|
||||||
|
Command: "add"
|
||||||
|
Description: "Add a user to a support ticket."
|
||||||
|
Permission: ["SendMessages"]
|
||||||
|
Enabled: true
|
||||||
|
|
||||||
|
CloseTicket:
|
||||||
|
Command: "close"
|
||||||
|
Description: "Close a support ticket"
|
||||||
|
Permission: ["SendMessages"]
|
||||||
|
Enabled: true
|
||||||
|
|
||||||
|
TicketManage:
|
||||||
|
Command: "ticketmanage"
|
||||||
|
Description: "Manage an existing support ticket."
|
||||||
|
Permission: ["SendMessages"]
|
||||||
|
Enabled: true
|
||||||
|
|
||||||
|
Close:
|
||||||
|
Command: "close"
|
||||||
|
Description: "Close a support ticket."
|
||||||
|
|
||||||
|
ForceAdd:
|
||||||
|
Command: "forceadd"
|
||||||
|
Description: "Add a user to a support ticket by bypassing the invite system."
|
||||||
|
|
||||||
|
ForceRemove:
|
||||||
|
Command: "forceremove"
|
||||||
|
Description: "Remove a user from a support ticket by bypassing the normal restrictions."
|
||||||
|
|
||||||
|
Rename:
|
||||||
|
Command: "rename"
|
||||||
|
Description: "Rename a support ticket by bypassing the normal restrictions."
|
||||||
|
|
||||||
|
Suggestion:
|
||||||
|
Command: "suggest"
|
||||||
|
Description: "Create a suggestion."
|
||||||
|
Permission: ["SendMessages"]
|
||||||
|
Enabled: true
|
||||||
|
|
||||||
|
SuggestionAdmin:
|
||||||
|
Command: "suggestadmin"
|
||||||
|
Description: "Update a suggestion’s status."
|
||||||
|
Permission: ['SendMessages']
|
||||||
|
# Disable "Suggestion" to disable this command.
|
||||||
|
|
||||||
|
Embed:
|
||||||
|
Command: "embed"
|
||||||
|
Description: "Build a discord embed."
|
||||||
|
Permission: ["SendMessages"]
|
||||||
|
Enabled: true
|
||||||
|
|
||||||
|
Translate:
|
||||||
|
Command: "translate"
|
||||||
|
Description: "Translate something to english"
|
||||||
|
Permission: ["SendMessages"]
|
||||||
|
Enabled: true
|
||||||
|
|
||||||
|
UserInfo:
|
||||||
|
Command: "userinfo"
|
||||||
|
Description: "Get info about a user."
|
||||||
|
Permission: ["SendMessages"]
|
||||||
|
Enabled: true
|
||||||
|
|
||||||
|
Ping:
|
||||||
|
Command: "ping"
|
||||||
|
Description: "Pong."
|
||||||
|
Permission: ["SendMessages"]
|
||||||
|
Enabled: true
|
||||||
|
|
||||||
|
# ---------------------------------------
|
||||||
|
# Bot Moderator Commands
|
||||||
|
# ---------------------------------------
|
||||||
|
|
||||||
|
Mod:
|
||||||
|
Command: "mod"
|
||||||
|
Description: "Execute a moderation action."
|
||||||
|
Permission: ["BanMembers", "KickMembers"]
|
||||||
|
Enabled: true
|
||||||
|
|
||||||
|
TicketBlacklist:
|
||||||
|
Command: "ticket-blacklist"
|
||||||
|
Description: "Blacklist a user from creating Tickets."
|
||||||
|
|
||||||
|
Add:
|
||||||
|
Command: "add"
|
||||||
|
Description: "Add a user to the blacklist."
|
||||||
|
|
||||||
|
Remove:
|
||||||
|
Command: "remove"
|
||||||
|
Description: "Remove a user from the blacklist."
|
||||||
|
|
||||||
|
View:
|
||||||
|
Command: "view"
|
||||||
|
Description: "View all blacklisted users."
|
||||||
|
|
||||||
|
# ---------------------------------------
|
||||||
|
# Bot Administrator Commands
|
||||||
|
# ---------------------------------------
|
||||||
|
|
||||||
|
Settings:
|
||||||
|
Command: "settings"
|
||||||
|
Description: "Update bot settings."
|
||||||
|
Permission: ["Administrator"]
|
||||||
|
Enabled: true
|
||||||
|
|
||||||
|
AI:
|
||||||
|
Command: "ai"
|
||||||
|
Description: "Manage SupportBot AI settings and memory."
|
||||||
|
Permission: ["Administrator"]
|
||||||
|
Enabled: true
|
||||||
|
|
||||||
|
Train:
|
||||||
|
Command: "train"
|
||||||
|
Description: "Train the AI by adding knowledge to its database."
|
||||||
|
|
||||||
|
Knowledge:
|
||||||
|
Command: "knowledge"
|
||||||
|
Description: "Manage the AI knowledge base."
|
||||||
|
Clear:
|
||||||
|
Command: "clear"
|
||||||
|
Description: "Clear all trained knowledge from the AI."
|
||||||
|
|
||||||
|
Memory:
|
||||||
|
Command: "memory"
|
||||||
|
Description: "Manage the AI conversational memory."
|
||||||
|
Clear:
|
||||||
|
Command: "clear"
|
||||||
|
Description: "Clear recent conversation memory for this channel."
|
||||||
|
|
||||||
|
# ---------------------------------------
|
||||||
|
# Command Description Configuration
|
||||||
|
# ---------------------------------------
|
||||||
|
|
||||||
|
BlacklistDesc: "Blacklist a user from creating Tickets."
|
||||||
|
TicketAddDesc: "Add a user to a support ticket."
|
||||||
|
TicketRemoveDesc: "Remove a user from a support ticket."
|
||||||
|
TranslateCommandDesc: "Translates the language provided, along with the abbreviation (2 char) long language code"
|
||||||
|
OpenTicketDesc: "Open a new support ticket."
|
||||||
|
CloseTicketDesc: "Close your support ticket."
|
||||||
|
InfoCommandDesc: "Get information about the brand."
|
||||||
|
PingCommandDesc: "Pong!"
|
||||||
|
HelpCommandDesc: "Get a list of all the commands."
|
||||||
|
EmbedCommandDesc: "Send a message as an embed."
|
||||||
|
SuggestCommandDesc: "Suggest a feature for the server."
|
||||||
|
UserInfoCommandDesc: "Get info about a user."
|
||||||
|
TicketManageDesc: "Manage an existing support ticket."
|
||||||
|
TicketManageCloseDesc: "Close a support ticket."
|
||||||
|
TicketManageForceAddDesc: "Add a user to a support ticket by bypassing the invite system."
|
||||||
|
TicketManageForceRemoveDesc: "Remove a user from a support ticket by bypassing the normal restrictions."
|
||||||
|
TicketManageForceRenameDesc: "Rename a support ticket by bypassing the normal restrictions."
|
||||||
@@ -0,0 +1,325 @@
|
|||||||
|
# ___ _ ___ _
|
||||||
|
# / __> _ _ ___ ___ ___ _ _ _| |_ | . > ___ _| |_
|
||||||
|
# \__ \| | || . \| . \/ . \| '_> | | | . \/ . \ | |
|
||||||
|
# <___/`___|| _/| _/\___/|_| |_| |___/\___/ |_|
|
||||||
|
# |_| |_|
|
||||||
|
#
|
||||||
|
# SupportBot created by Emerald Services
|
||||||
|
# Installed with MIT License
|
||||||
|
#
|
||||||
|
# Discord Support: https://discord.gg/emeraldsrv
|
||||||
|
# Official Documentation: https://docs.emeraldsrv.com
|
||||||
|
# Addons Marketplace: https://community.emeraldsrv.com
|
||||||
|
#
|
||||||
|
# Messages Configuration (messages.yml)
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
# Info Command
|
||||||
|
# ----------------------------
|
||||||
|
|
||||||
|
Info:
|
||||||
|
Title: "About Us"
|
||||||
|
Description: "SupportBot is an open-sourced support system for discord. It's completely free to use with high amounts of customization to match your own brand."
|
||||||
|
Button: "Visit Site"
|
||||||
|
URL: "https://emeraldsrv.dev"
|
||||||
|
Colour: "#45b463"
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
# Welcome/Leave Messages
|
||||||
|
# ----------------------------
|
||||||
|
|
||||||
|
Welcome:
|
||||||
|
Enabled: true
|
||||||
|
LayoutStyle: "classic"
|
||||||
|
# Available Layout Styles
|
||||||
|
# "modern" - For the new Discord v2 components
|
||||||
|
# "classic" - Use the classic Discord look.
|
||||||
|
|
||||||
|
Embed:
|
||||||
|
Colour: "#45b463"
|
||||||
|
Title: "Welcome!"
|
||||||
|
Message: ":smile: %joined_user% has just landed!"
|
||||||
|
Icon:
|
||||||
|
Mode: "USER" # USER | BOT | CUSTOM
|
||||||
|
CustomURL: "https://example.com/custom.png"
|
||||||
|
Image:
|
||||||
|
Enabled: false
|
||||||
|
URL: "https://example.com/banner.png"
|
||||||
|
Buttons:
|
||||||
|
Enabled: true
|
||||||
|
List:
|
||||||
|
- label: "Website"
|
||||||
|
style: "Link"
|
||||||
|
url: "https://emeraldsrv.dev"
|
||||||
|
emoji: "⌨️"
|
||||||
|
|
||||||
|
Modern:
|
||||||
|
Colour: "#45b463"
|
||||||
|
Title: "**Welcome to ServerName**"
|
||||||
|
Body: ":smile: %joined_user% has just landed!\n\n🔨 **Server Rules:** <#CHANNEL_ID>"
|
||||||
|
Icon:
|
||||||
|
Mode: "BOT" # USER | BOT | CUSTOM
|
||||||
|
CustomURL: "https://example.com/custom-icon.png"
|
||||||
|
Image:
|
||||||
|
Enabled: false
|
||||||
|
URL: "https://example.com/banner.png"
|
||||||
|
Buttons:
|
||||||
|
- label: "BuiltByBit"
|
||||||
|
style: "Link"
|
||||||
|
url: "https://your-website.com/how-to-play-java"
|
||||||
|
emoji: "⌨️"
|
||||||
|
- label: "GitHub"
|
||||||
|
style: "Link"
|
||||||
|
url: "https://your-website.com/how-to-play-bedrock"
|
||||||
|
emoji: "👾"
|
||||||
|
- label: "Tebex"
|
||||||
|
style: "Link"
|
||||||
|
url: "https://your-store.tebex.io/"
|
||||||
|
emoji: "🛒"
|
||||||
|
|
||||||
|
Leave:
|
||||||
|
Embed:
|
||||||
|
Colour: "#f74343"
|
||||||
|
Title: "Goodbye!"
|
||||||
|
Message: ":slight_frown: %left_user% has recently dropped out!"
|
||||||
|
Thumbnail: "BOT"
|
||||||
|
|
||||||
|
ImageEnabled: false
|
||||||
|
ImageURL: "https://animagelink.png/"
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
# Suggestion Messages
|
||||||
|
# ----------------------------
|
||||||
|
|
||||||
|
Suggestions:
|
||||||
|
Sent_Title: "Suggestion Sent!"
|
||||||
|
Sent: ":white_check_mark: **You have successfully sent a suggestion.**"
|
||||||
|
NoSelfVote: ":x: **You cannot vote on your own suggestion.**"
|
||||||
|
RemoveVoteButton: "Remove Vote"
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
# Ticket Messages
|
||||||
|
# ----------------------------
|
||||||
|
|
||||||
|
Ticket:
|
||||||
|
|
||||||
|
# Available Placeholders:
|
||||||
|
|
||||||
|
# %ticketusername% - User who created the ticket.
|
||||||
|
# <@%ticketauthor%> - Tags the user who created the ticket.
|
||||||
|
# <#%ticketid%> - Mentions the ticket channel.
|
||||||
|
|
||||||
|
# You can use Markdown formatting in this message:
|
||||||
|
# # Heading 1
|
||||||
|
# ## Heading 2
|
||||||
|
# ### Heading 3
|
||||||
|
# **Bold** text
|
||||||
|
# *Italic* text
|
||||||
|
# -# Small text
|
||||||
|
# - Bullet points
|
||||||
|
# Links: [text](https://example.com)
|
||||||
|
|
||||||
|
TicketAuthorTitle: "-# %ticketusername%'s Ticket"
|
||||||
|
TicketTitle: "## Support Ticket"
|
||||||
|
TicketMessage: "Hello <@%ticketauthor%>, Thank you for creating a support ticket\nPlease wait patiently whilst a member of our team reaches out to you."
|
||||||
|
TicketThumbnail: "https://images.emojiterra.com/google/android-pie/512px/1f39f.png"
|
||||||
|
TicketCreatedAlert: ":ticket: Ticked Created: <#%ticketid%>"
|
||||||
|
TicketExists: "❌ Ticket already exists"
|
||||||
|
TicketClaim: ":white_check_mark: Ticket claimed by" #The user who claimed the ticket will be added to the end of this message
|
||||||
|
TicketUnclaim: ":white_check_mark: Ticket unclaimed by" #The user who claimed the ticket will be added to the end of this message
|
||||||
|
|
||||||
|
TicketArchived: ":white_check_mark: Ticket archived."
|
||||||
|
TicketUnarchived: ":white_check_mark: Ticket unarchived."
|
||||||
|
TicketLocked: ":lock: Ticket is locked."
|
||||||
|
DisableInvites: "🔒 Invites have been disabled for this ticket."
|
||||||
|
EnableInvites: "🔓 Invites have been enabled for this ticket."
|
||||||
|
|
||||||
|
# Available Placeholders:
|
||||||
|
|
||||||
|
# <@%user%> - Tags the user who was added to the ticket.
|
||||||
|
|
||||||
|
Blacklisted: ":x: You are not allowed to open a ticket"
|
||||||
|
Muted: ":x: You are `Muted` and therefore not allowed to open any tickets"
|
||||||
|
InvalidSubject: "No Reason Provided!"
|
||||||
|
|
||||||
|
ConfirmClose: "Please confirm closing the ticket by clicking the button below."
|
||||||
|
CloseTimeout: "Ticket close confirmation timed out."
|
||||||
|
|
||||||
|
RenamedSuccess: "🏷️ Ticket renamed successfully!"
|
||||||
|
|
||||||
|
ClaimTickets:
|
||||||
|
ClaimTitle: "Incoming Support Request!"
|
||||||
|
ClaimMessage: "A new ticket has been created by <@!%user%>. Click the button below to claim this ticket."
|
||||||
|
ClaimMessage_Edit: "<#%channel%> was claimed by <@!%user%>."
|
||||||
|
|
||||||
|
ClaimedTitle: "Ticket Claimed!"
|
||||||
|
Claimed: "You have successfully claimed this ticket. <#%channel%>"
|
||||||
|
NoPermsToClaimTitle: "Error, no permissions!"
|
||||||
|
NoPermsToClaim: ":x: **Err!** You do not permissions to claim this ticket."
|
||||||
|
|
||||||
|
ReassigningTitle: "Reassigning!..."
|
||||||
|
ReassigningMessage: "<@%user%> did not claim the ticket. **Reassigning...**"
|
||||||
|
|
||||||
|
TicketQuestions:
|
||||||
|
Details:
|
||||||
|
Title: "Ticket Details"
|
||||||
|
Description: "Questions by <@%user%>" # Use <@%user%> to display the username.
|
||||||
|
|
||||||
|
AIMode:
|
||||||
|
Title: "SupportBot AI"
|
||||||
|
Description: "Use the buttons below to enable or disable AI chat in this ticket."
|
||||||
|
EnabledMessage: "AI Mode has been enabled for this ticket."
|
||||||
|
DisabledMessage: "AI Mode has been disabled for this ticket."
|
||||||
|
|
||||||
|
ThreadWelcomeTitle: "SupportBot AI"
|
||||||
|
ThreadWelcomeMessage: "Hi! I’m SupportBot AI. You can chat with me here about your issue."
|
||||||
|
|
||||||
|
EnableButton: "Enable AI Mode"
|
||||||
|
DisableButton: "Disable AI Mode"
|
||||||
|
|
||||||
|
TicketNotFound: "Ticket not found."
|
||||||
|
EnableError: "There was an error enabling SupportBot AI."
|
||||||
|
DisableError: "There was an error disabling SupportBot AI."
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
# Review Messages
|
||||||
|
# ----------------------------
|
||||||
|
|
||||||
|
ReviewSystem:
|
||||||
|
Rate:
|
||||||
|
Title: "Review us!"
|
||||||
|
Description: "Please rate our support for this ticket (1-5 stars)."
|
||||||
|
|
||||||
|
Stars:
|
||||||
|
One: "⭐ 1 Star"
|
||||||
|
Two: "⭐⭐ 2 Stars"
|
||||||
|
Three: "⭐⭐⭐ 3 Stars"
|
||||||
|
Four: "⭐⭐⭐⭐ 4 Stars"
|
||||||
|
Five: "⭐⭐⭐⭐⭐ 5 Stars"
|
||||||
|
|
||||||
|
Comment:
|
||||||
|
Title: "Write a comment."
|
||||||
|
Description: "Write a comment about your experience."
|
||||||
|
|
||||||
|
ReviewEmbed:
|
||||||
|
Title: "Ticket Review"
|
||||||
|
CommentTitle: "Comment"
|
||||||
|
RatingTitle: "Rating"
|
||||||
|
ReviewedStaffTitle: "Reviewed Staff:"
|
||||||
|
ReviewedByTitle: "Reviewed By:"
|
||||||
|
ReviewEmoji: "⭐"
|
||||||
|
Color: "#f7a943"
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
# Mod Command Messages
|
||||||
|
# ----------------------------
|
||||||
|
|
||||||
|
Mod:
|
||||||
|
|
||||||
|
TicketBlacklist:
|
||||||
|
Add:
|
||||||
|
Description: "Add a user to the ticket blacklist"
|
||||||
|
UserDescription: "User to blacklist"
|
||||||
|
AlreadyBlacklisted: ":x: {userTag} is already blacklisted."
|
||||||
|
Success: ":white_check_mark: {userTag} has been added to the blacklist. They can no longer use the ticket system."
|
||||||
|
|
||||||
|
Remove:
|
||||||
|
Description: "Remove a user from the ticket blacklist"
|
||||||
|
UserDescription: "User to remove from blacklist"
|
||||||
|
NotBlacklisted: ":x: {userTag} is not blacklisted."
|
||||||
|
Success: ":white_check_mark: {userTag} has been removed from the blacklist. They can now use the ticket system again."
|
||||||
|
|
||||||
|
View:
|
||||||
|
Description: "View the list of all blacklisted users"
|
||||||
|
NoBlacklistedUsers: ":information_source: There are currently no users on the blacklist."
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
# Logging Messages
|
||||||
|
# ----------------------------
|
||||||
|
|
||||||
|
TicketLog:
|
||||||
|
Title: "Ticket Log"
|
||||||
|
Colour: "#0cc559"
|
||||||
|
|
||||||
|
TicketBlacklistLog:
|
||||||
|
Title: "Ticket Blacklist Log"
|
||||||
|
Colour: "#0cc559"
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
# Misc Messages
|
||||||
|
# ----------------------------
|
||||||
|
|
||||||
|
AddUser:
|
||||||
|
NotInTicket_Title: "Ticket Invitation"
|
||||||
|
NotInTicket_Description: "This command can only be used within a ticket thread."
|
||||||
|
|
||||||
|
Invite_Title: "User Invited"
|
||||||
|
Invite_Description: "%username% has been invited to join the ticket."
|
||||||
|
|
||||||
|
DM_Title: "Ticket Invitation"
|
||||||
|
DM_Description: "You have been invited to join the ticket %channelname%. Do you accept?"
|
||||||
|
|
||||||
|
Accept_Label: "Accept"
|
||||||
|
Reject_Label: "Reject"
|
||||||
|
ViewTicket_Label: "Go to Ticket"
|
||||||
|
|
||||||
|
Accepted_Title: "Invitation Accepted"
|
||||||
|
Accepted_Message: "You have been added to the ticket."
|
||||||
|
AddedToTicket_Title: "Added to Ticket"
|
||||||
|
AddedToTicket_Description: "You have been added to the ticket %channel_link%."
|
||||||
|
|
||||||
|
# Added Message for inside the ticket, this informs everyone inside the ticket that the user has been added.
|
||||||
|
Added_Title: "User Added"
|
||||||
|
Added_Description: "Successfully added **%username%** to the ticket."
|
||||||
|
|
||||||
|
Declined_Title: "Invitation Declined"
|
||||||
|
Declined_Message: "You have declined the invitation."
|
||||||
|
|
||||||
|
NoResponse_Title: "No Response"
|
||||||
|
NoResponse_Message: "You did not respond in time. Please contact staff if you wish to join the ticket."
|
||||||
|
|
||||||
|
Error_Title: "Error"
|
||||||
|
Error_Adding: "There was an error adding you to the ticket. Please try again."
|
||||||
|
|
||||||
|
DM_Error_Title: "Error"
|
||||||
|
DM_Error_Description: "Unable to send a DM to %username%. Please ensure your DMs are enabled."
|
||||||
|
|
||||||
|
Unexpected_Error_Title: "Error"
|
||||||
|
Unexpected_Error_Description: "An unexpected error occurred: %error%"
|
||||||
|
|
||||||
|
RemoveUser:
|
||||||
|
NotInTicket_Title: "Error"
|
||||||
|
NotInTicket_Description: "This command can only be used in ticket threads."
|
||||||
|
|
||||||
|
Removed_Title: "User Removed"
|
||||||
|
Removed_Message: "The user has been successfully removed from the ticket."
|
||||||
|
|
||||||
|
RemovedFromTicket_Title: "Removed from Ticket"
|
||||||
|
RemovedFromTicket_Description: "You have been removed from the ticket channel %channel_link%."
|
||||||
|
|
||||||
|
Error_Title: "Error"
|
||||||
|
Error_Removing: "There was an error removing the user from the ticket."
|
||||||
|
|
||||||
|
ForceAddUser:
|
||||||
|
NotInTicket_Title: "Not a Ticket"
|
||||||
|
NotInTicket_Description: "This command can only be used within a ticket thread."
|
||||||
|
|
||||||
|
Added_Title: "User Added"
|
||||||
|
Added_Description: "Successfully added %username% to the ticket."
|
||||||
|
|
||||||
|
AddedToTicket_Title: "You have been added to a Ticket"
|
||||||
|
AddedToTicket_Description: "You have been added to the ticket channel [%channel_link%]."
|
||||||
|
|
||||||
|
Error_Title: "Error"
|
||||||
|
Error_Adding: "There was an error adding the user to the ticket."
|
||||||
|
|
||||||
|
Error:
|
||||||
|
IncorrectPermissions: ":x: **Error!** You do not have the correct permissions to use this command."
|
||||||
|
MissingChannel: ":x: **Error!** Required channel not found. Please ensure it is set up correctly!"
|
||||||
|
UserNotFound: ":x: **Error!** Specified user not found. Please check if they are in this server."
|
||||||
|
CommandNotAllowedHere: ":warning: This command cannot be used in this channel."
|
||||||
|
EntryAlreadyExists: ":warning: Cannot complete the action, as the entry already exists."
|
||||||
|
EntryDoesNotExist: ":warning: Cannot complete the action, as the entry does not exist."
|
||||||
|
ActionFailed: ":x: **Error!** An error occurred while processing your request. Please try again later."
|
||||||
|
InvalidChannel: ":x: **Error!** The suggestions channel has not been set up correctly."
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
# -------------------------
|
||||||
|
# SupportBot AI
|
||||||
|
#
|
||||||
|
# In order to use the SupportBot AI, You need to setup an OpenAI or Groq Developer Account.
|
||||||
|
# this can be done at https://platform.openai.com/ or https://groq.com/
|
||||||
|
# -------------------------
|
||||||
|
|
||||||
|
Enabled: true
|
||||||
|
|
||||||
|
General:
|
||||||
|
Provider: "openai"
|
||||||
|
Model: "MODEL"
|
||||||
|
Model_API_Key: "MODEL_API_KEY"
|
||||||
|
# Providers Available:
|
||||||
|
# "openai" - Use OpenAI's GPT models. Example: "gpt-5.4" "gpt-5-mini"
|
||||||
|
# "groq" - Use Groq's GroqAI models. Example: "meta-llama/llama-4-scout-17b-16e-instruct"
|
||||||
|
# "claude" - Use Anthropic's Claude models. Example: "claude-sonnet-4-6"
|
||||||
|
|
||||||
|
Tokens: 1200
|
||||||
|
|
||||||
|
Temperature: 0.4
|
||||||
|
Name: "SupportBot AI"
|
||||||
|
|
||||||
|
PastebinAPI_Key: "YOUR_PASTEBIN_API_KEY"
|
||||||
|
PastebinAPI_URL: "https://pastebin.com/api/api_post.php"
|
||||||
|
|
||||||
|
Style:
|
||||||
|
MessageType: "components_v2"
|
||||||
|
# Available Styles for AI:
|
||||||
|
# "embed" - AI responses will be sent as Discord embeds.
|
||||||
|
# "text" - AI responses will be sent as plain messages.
|
||||||
|
# "components_v2" - AI responses will be sent with the new components style.
|
||||||
|
|
||||||
|
Features:
|
||||||
|
SuggestTickets: true # Should SupportBot AI suggest creating a ticket if the user needs more help?
|
||||||
|
|
||||||
|
Channels:
|
||||||
|
AIChannel: "CHANNEL_ID"
|
||||||
|
|
||||||
|
Embed:
|
||||||
|
Color: "#45b463"
|
||||||
|
Footer: "SupportBot AI"
|
||||||
|
|
||||||
|
Memory:
|
||||||
|
Enabled: true
|
||||||
|
StoreFacts: true
|
||||||
|
# StoreFacts - Enables saving important facts the AI learns.
|
||||||
|
|
||||||
|
StoreSummaries: true
|
||||||
|
# StoreSummaries - Enables saving conversation summaries.
|
||||||
|
|
||||||
|
MaxRecentMessages: 15
|
||||||
|
# MaxRecentMessages - Number of recent messages used for context.
|
||||||
|
|
||||||
|
Scope: "guild" # channel | guild
|
||||||
|
# Available Scopes for Memory:
|
||||||
|
# "channel" - Memory is per channel only.
|
||||||
|
# "guild" - Memory is shared across the server.
|
||||||
|
|
||||||
|
CustomInstructions:
|
||||||
|
- "You are SupportBot AI made by Emerald Services"
|
||||||
|
- "You are built with DiscordJS/NodeJS and currently running on version 26.2"
|
||||||
|
- "You're current build can be found here
|
||||||
|
https://github.com/Emerald-Services/SupportBot"
|
||||||
|
- "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"
|
||||||
|
- "SupportBot includes all-new AI features such as a memory system that allows
|
||||||
|
you to remember facts and summaries of conversations, which can be used to
|
||||||
|
provide better responses."
|
||||||
|
- "SupportBot AI also has a tickets AI Mode which is enabled in
|
||||||
|
supportbot.yml, which allows you to assist users in support tickets and
|
||||||
|
provide better responses based on the context of the ticket."
|
||||||
|
- "Your main purpose is to assist users in the support server and provide
|
||||||
|
helpful responses to their questions and issues. Always be accurate and if
|
||||||
|
you do not have answer then simply ask the person to create a ticket if they
|
||||||
|
need more help."
|
||||||
|
#- "Add custom instructions here"
|
||||||
|
|
||||||
|
Messages:
|
||||||
|
ErrorResponse: "I couldn't generate a response."
|
||||||
|
AIError: "There was an error contacting the AI provider."
|
||||||
|
AIDisabled: "AI is currently disabled."
|
||||||
|
MessageTooLong: "My response was too long, so I uploaded it here: {pastebin}"
|
||||||
|
SuggestTicketText: "-# Need more help? React with 🎫 to open a ticket."
|
||||||
|
SuggestTicketReaction: "🎫"
|
||||||
@@ -0,0 +1,389 @@
|
|||||||
|
# ___ _ ___ _
|
||||||
|
# / __> _ _ ___ ___ ___ _ _ _| |_ | . > ___ _| |_
|
||||||
|
# \__ \| | || . \| . \/ . \| '_> | | | . \/ . \ | |
|
||||||
|
# <___/`___|| _/| _/\___/|_| |_| |___/\___/ |_|
|
||||||
|
# |_| |_|
|
||||||
|
#
|
||||||
|
# SupportBot created by Emerald Services
|
||||||
|
# Installed with MIT License
|
||||||
|
#
|
||||||
|
# Discord Support: https://discord.gg/emeraldsrv
|
||||||
|
# Website: https://emeraldsrv.dev
|
||||||
|
#
|
||||||
|
# SupportBot Configuration (supportbot.yml)
|
||||||
|
|
||||||
|
SupportBot_Version: "v26.2"
|
||||||
|
|
||||||
|
# -------------------------
|
||||||
|
# General
|
||||||
|
# -------------------------
|
||||||
|
|
||||||
|
General:
|
||||||
|
Name: "SupportBot"
|
||||||
|
Token: "YOUR_BOT_TOKEN_HERE"
|
||||||
|
Addons:
|
||||||
|
Enabled: true
|
||||||
|
GuildId: "YOUR_GUILD_ID_HERE"
|
||||||
|
|
||||||
|
# -------------------------
|
||||||
|
# Database Configuration
|
||||||
|
# -------------------------
|
||||||
|
|
||||||
|
Database:
|
||||||
|
# Driver options: "mysql" (works for MariaDB & MySQL) or "sqlite"
|
||||||
|
Driver: "mysql"
|
||||||
|
|
||||||
|
# Connection Settings for MariaDB / MySQL (used when Driver is "mysql" or "mariadb")
|
||||||
|
MySQL:
|
||||||
|
Host: "localhost"
|
||||||
|
Port: 3306
|
||||||
|
User: "root"
|
||||||
|
Password: "password"
|
||||||
|
Database: "supportbot"
|
||||||
|
ConnectionLimit: 10
|
||||||
|
|
||||||
|
# Connection Settings for SQLite (used when Driver is "sqlite")
|
||||||
|
SQLite:
|
||||||
|
File: "./Data/supportbot.db"
|
||||||
|
|
||||||
|
# Activity
|
||||||
|
# -------------------------
|
||||||
|
|
||||||
|
Activity:
|
||||||
|
Status: "SupportBot"
|
||||||
|
Type: "Watching"
|
||||||
|
StreamingURL: "https://www.twitch.tv/YOUR_TWITCH_NAME"
|
||||||
|
# Available Types:
|
||||||
|
# Playing
|
||||||
|
# Watching
|
||||||
|
# Listening
|
||||||
|
# Competing
|
||||||
|
# Streaming (URL required)
|
||||||
|
|
||||||
|
# You can also update the activity via Discord using: /settings
|
||||||
|
|
||||||
|
# -------------------------
|
||||||
|
# Embeds
|
||||||
|
# -------------------------
|
||||||
|
|
||||||
|
Embed:
|
||||||
|
Colours:
|
||||||
|
General: "#0cc559"
|
||||||
|
Success: "#04b545"
|
||||||
|
Error: "#d13030"
|
||||||
|
Warn: "#fca117"
|
||||||
|
|
||||||
|
Footer: "SupportBot | Developed by Emerald Development"
|
||||||
|
# Use 0x<hexcode> to change the colour of the embeds.
|
||||||
|
|
||||||
|
# -------------------------
|
||||||
|
# Roles
|
||||||
|
# -------------------------
|
||||||
|
|
||||||
|
Roles:
|
||||||
|
AutoRole:
|
||||||
|
Enabled: true
|
||||||
|
Role: "ROLE_ID"
|
||||||
|
|
||||||
|
StaffMember:
|
||||||
|
Staff: "ROLE_ID"
|
||||||
|
Admin: "ROLE_ID"
|
||||||
|
Moderator: "ROLE_ID"
|
||||||
|
|
||||||
|
Mod:
|
||||||
|
AllowSupportStaff: true
|
||||||
|
# Allow Support Staff to use the /mod command. (More in detail customisation in commands.yml)
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
# System Messages
|
||||||
|
# ----------------------------
|
||||||
|
|
||||||
|
Welcome:
|
||||||
|
Enabled: false
|
||||||
|
Channel: "CHANNEL_ID"
|
||||||
|
|
||||||
|
Leave:
|
||||||
|
Enabled: false
|
||||||
|
Channel: "CHANNEL_ID"
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
# Suggestions
|
||||||
|
# ----------------------------
|
||||||
|
|
||||||
|
Suggestions:
|
||||||
|
Mode: "Channel" # Channel or Forum
|
||||||
|
ForumChannel: "FORUM_CHANNEL_ID" # A forum channel, disabled if mode is set to Channel.
|
||||||
|
Channel: "CHANNEL_ID" # A text channel, disabled if mode is set to Forum.
|
||||||
|
|
||||||
|
UpvoteEmoji: "✅"
|
||||||
|
DownvoteEmoji: "❌"
|
||||||
|
|
||||||
|
UpvoteTitle: "Yes"
|
||||||
|
DownvoteTitle: "No"
|
||||||
|
|
||||||
|
OwnSuggestion: true
|
||||||
|
|
||||||
|
# If false this will not allow the user to vote for a suggestion they created
|
||||||
|
# If true this will allow the user to vote for a suggestion they created
|
||||||
|
|
||||||
|
ShowUsers: true
|
||||||
|
|
||||||
|
Threads:
|
||||||
|
Enabled: true
|
||||||
|
Title: "Suggestion-releated Thread"
|
||||||
|
Reason: "A thread releted to this suggestion"
|
||||||
|
|
||||||
|
Buttons:
|
||||||
|
Upvote: "2"
|
||||||
|
Downvote: "2"
|
||||||
|
RemoveVote: "4"
|
||||||
|
RemoveVote_Title: "Remove Vote"
|
||||||
|
# Available Button Styles
|
||||||
|
# 1 - Blurple
|
||||||
|
# 2 - Grey
|
||||||
|
# 3 - Green
|
||||||
|
# 4 - Red
|
||||||
|
|
||||||
|
Tags:
|
||||||
|
Created: "TAG_ID"
|
||||||
|
Considering: "TAG_ID"
|
||||||
|
Solved: "TAG_ID"
|
||||||
|
Denied: "TAG_ID"
|
||||||
|
|
||||||
|
# -------------------------
|
||||||
|
# Tickets
|
||||||
|
# -------------------------
|
||||||
|
|
||||||
|
VoiceTickets:
|
||||||
|
Name: "%username%'s VC"
|
||||||
|
Category: "CATEGORY_ID"
|
||||||
|
|
||||||
|
Ticket:
|
||||||
|
TicketType: "channels"
|
||||||
|
|
||||||
|
# Available TicketTypes:
|
||||||
|
# channels - Tickets will be created as channels in the TicketChannelsCategory
|
||||||
|
# threads - Tickets will be created as threads through the "TicketHome" channel.
|
||||||
|
|
||||||
|
UserManagement:
|
||||||
|
DMOnAdd: true # DM the user when they are added to the ticket
|
||||||
|
DMOnRemove: true # DM the user when they are removed from the ticket
|
||||||
|
DMTranscript: true # DM the user a transcript of the ticket when it is closed
|
||||||
|
|
||||||
|
TicketHome: "CHANNEL_ID"
|
||||||
|
|
||||||
|
# What is TicketHome?
|
||||||
|
# - Channel where the Ticket creation panel is sent.
|
||||||
|
# - Channel where ticket threads are made. (only if ticket threads is enabled above.)
|
||||||
|
|
||||||
|
# For TicketType: channels
|
||||||
|
TicketChannelsCategory: "CATEGORY_ID"
|
||||||
|
TicketChannelsCategory2: "CATEGORY_ID" # This will be used if the first category has reached the max channel limit.
|
||||||
|
TicketArchiveCategory: "CATEGORY_ID"
|
||||||
|
# ---
|
||||||
|
|
||||||
|
Channel: "ticket-" # The prefix for ticket channel names
|
||||||
|
TicketsPerUser: 100
|
||||||
|
TicketReason: true
|
||||||
|
# If ticket reason is set to false, It will not prompt to create a reason
|
||||||
|
|
||||||
|
TicketSubject: "description"
|
||||||
|
|
||||||
|
# Available TicketSubjects:
|
||||||
|
# "description" will display the ticket reason within the channel description
|
||||||
|
# "embed" will display the ticket reason within the ticket embed
|
||||||
|
|
||||||
|
Timeout: 5
|
||||||
|
# Set to true to allow all staff to access the ticket
|
||||||
|
|
||||||
|
Close:
|
||||||
|
StaffOnly: true
|
||||||
|
# Set to false is anyone is able to close a ticket.
|
||||||
|
|
||||||
|
Invites:
|
||||||
|
# Invites is for ticket threads.
|
||||||
|
StaffOnly: true
|
||||||
|
|
||||||
|
ClaimTickets:
|
||||||
|
Enabled: false
|
||||||
|
Channel: "CHANNEL_ID"
|
||||||
|
# Channel where the ticket assignments are sent for staff to claim.
|
||||||
|
|
||||||
|
ClaimAcceptTime: 120000
|
||||||
|
# How long the assigned user has to accept the ticket before it gets re-assigned to someone else.
|
||||||
|
# 120000 = 2 Minutes
|
||||||
|
# 180000 = 3 Minutes
|
||||||
|
# 240000 = 4 Minutes
|
||||||
|
# 300000 = 5 Minutes
|
||||||
|
# Time in Mliseconds
|
||||||
|
|
||||||
|
ButtonTitle: "Claim Ticket"
|
||||||
|
ButtonEmoji: "🎫"
|
||||||
|
Button: "1"
|
||||||
|
# Available Button Styles
|
||||||
|
# 1 - Blurple
|
||||||
|
# 2 - Grey
|
||||||
|
# 3 - Green
|
||||||
|
# 4 - Red
|
||||||
|
|
||||||
|
ReviewSystem:
|
||||||
|
Enabled: false
|
||||||
|
Channel: "CHANNEL_ID"
|
||||||
|
UseModal: true
|
||||||
|
|
||||||
|
Questions:
|
||||||
|
# Default questions asked when a ticket is created, these are used if departments are disabled.
|
||||||
|
Enabled: false
|
||||||
|
List:
|
||||||
|
- "Please provide your account email?"
|
||||||
|
- "What is the nature of your issue?"
|
||||||
|
- "Have you tried resolving the issue yourself?"
|
||||||
|
|
||||||
|
Log:
|
||||||
|
TicketDataLog: "CHANNEL_ID"
|
||||||
|
TickeDataTitle: "Ticket Data"
|
||||||
|
TicketBlacklistLog: "CHANNEL_ID"
|
||||||
|
|
||||||
|
AIMode:
|
||||||
|
Enabled: true
|
||||||
|
ThreadName: "Chat with SupportBot AI"
|
||||||
|
WelcomeMessage: "Hi! I’m SupportBot AI. You can chat with me here about your issue."
|
||||||
|
|
||||||
|
# -------------------------
|
||||||
|
# Ticket Departments
|
||||||
|
# -------------------------
|
||||||
|
|
||||||
|
DepartmentSystem:
|
||||||
|
Enabled: false
|
||||||
|
Placeholder: "Choose a ticket department"
|
||||||
|
DefaultDepartment: "general"
|
||||||
|
|
||||||
|
Departments:
|
||||||
|
general:
|
||||||
|
Name: "General Support"
|
||||||
|
Description: "Open a general support ticket"
|
||||||
|
Emoji: "🎫"
|
||||||
|
Category: "CATEGORY_ID"
|
||||||
|
Role: "ROLE_ID"
|
||||||
|
ChannelPrefix: "support-"
|
||||||
|
DefaultPriority: "normal"
|
||||||
|
Questions:
|
||||||
|
- "What do you need help with?"
|
||||||
|
- "When did the issue start?"
|
||||||
|
|
||||||
|
purchase:
|
||||||
|
Name: "Purchase Support"
|
||||||
|
Description: "Get help with store purchases"
|
||||||
|
Emoji: "💳"
|
||||||
|
Category: "CATEGORY_ID"
|
||||||
|
Role: "ROLE_ID"
|
||||||
|
ChannelPrefix: "purchase-"
|
||||||
|
DefaultPriority: "high"
|
||||||
|
Questions:
|
||||||
|
- "What is your order ID?"
|
||||||
|
- "What product did you purchase?"
|
||||||
|
- "What payment issue are you having?"
|
||||||
|
|
||||||
|
reports:
|
||||||
|
Name: "Player Report"
|
||||||
|
Description: "Report a player for breaking the rules"
|
||||||
|
Emoji: "🚨"
|
||||||
|
Category: "CATEGORY_ID"
|
||||||
|
Role: "ROLE_ID"
|
||||||
|
ChannelPrefix: "report-"
|
||||||
|
DefaultPriority: "normal"
|
||||||
|
Questions:
|
||||||
|
- "Who are you reporting?"
|
||||||
|
- "What happened?"
|
||||||
|
- "Do you have evidence?"
|
||||||
|
|
||||||
|
appeals:
|
||||||
|
Name: "Ban Appeals"
|
||||||
|
Description: "Appeal a server punishment"
|
||||||
|
Emoji: "⚖️"
|
||||||
|
Category: "CATEGORY_ID"
|
||||||
|
Role: "ROLE_ID"
|
||||||
|
ChannelPrefix: "appeals-"
|
||||||
|
DefaultPriority: "low"
|
||||||
|
Questions:
|
||||||
|
- "What punishment are you appealing?"
|
||||||
|
- "Why should it be reviewed?"
|
||||||
|
- "Anything else staff should know?"
|
||||||
|
|
||||||
|
PrioritySystem:
|
||||||
|
Enabled: false
|
||||||
|
DefaultPriority: "Normal"
|
||||||
|
AllowUsersToChooseOnOpen: true
|
||||||
|
Priorities:
|
||||||
|
low:
|
||||||
|
Name: "Low"
|
||||||
|
Emoji: "🟢"
|
||||||
|
normal:
|
||||||
|
Name: "Normal"
|
||||||
|
Emoji: "🟡"
|
||||||
|
high:
|
||||||
|
Name: "High"
|
||||||
|
Emoji: "🟠"
|
||||||
|
urgent:
|
||||||
|
Name: "Urgent"
|
||||||
|
Emoji: "🔴"
|
||||||
|
|
||||||
|
# -------------------------
|
||||||
|
# Buttons
|
||||||
|
# -------------------------
|
||||||
|
Buttons:
|
||||||
|
General:
|
||||||
|
Delete: "🗑️"
|
||||||
|
DeleteStyle: "4"
|
||||||
|
|
||||||
|
Voice:
|
||||||
|
TicketDeleteText: "Delete Voice Channel"
|
||||||
|
|
||||||
|
Tickets:
|
||||||
|
ClaimEmoji: "🧑💻"
|
||||||
|
ClaimStyle: "1"
|
||||||
|
|
||||||
|
Unarchive: "Unarchive Ticket"
|
||||||
|
Unarchive_Emoji: "♻️"
|
||||||
|
Unarchive_Style: "2"
|
||||||
|
|
||||||
|
Unlock: "Unlock Ticket"
|
||||||
|
Unlock_Emoji: "🔓"
|
||||||
|
Unlock_Style: "2"
|
||||||
|
# Available Button Styles
|
||||||
|
# 1 - Blurple
|
||||||
|
# 2 - Grey
|
||||||
|
# 3 - Green
|
||||||
|
# 4 - Red
|
||||||
|
|
||||||
|
# -------------------------
|
||||||
|
# Select Menus
|
||||||
|
# -------------------------
|
||||||
|
|
||||||
|
SelectMenus:
|
||||||
|
Tickets:
|
||||||
|
PanelEmoji: "⚙️"
|
||||||
|
|
||||||
|
CloseEmoji: "🗑️"
|
||||||
|
ArchiveEmoji: "♻️"
|
||||||
|
LockEmoji: "🔐"
|
||||||
|
RenameEmoji: "🏷️"
|
||||||
|
VCEmoji: "🔊"
|
||||||
|
|
||||||
|
EnableInvitesEmoji: "📤"
|
||||||
|
DisableInvitesEmoji: "📥"
|
||||||
|
|
||||||
|
# -------------------------
|
||||||
|
# Logging
|
||||||
|
# -------------------------
|
||||||
|
|
||||||
|
MessageDelete:
|
||||||
|
Channel: "CHANNEL_ID"
|
||||||
|
Colour: "#d92525"
|
||||||
|
|
||||||
|
MessageUpdate:
|
||||||
|
Channel: "CHANNEL_ID"
|
||||||
|
Colour: "#f2a024"
|
||||||
|
|
||||||
|
Translate:
|
||||||
|
TranslateLog: "CHANNEL_ID"
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
# ___ _ ___ _
|
||||||
|
# / __> _ _ ___ ___ ___ _ _ _| |_ | . > ___ _| |_
|
||||||
|
# \__ \| | || . \| . \/ . \| '_> | | | . \/ . \ | |
|
||||||
|
# <___/`___|| _/| _/\___/|_| |_| |___/\___/ |_|
|
||||||
|
# |_| |_|
|
||||||
|
#
|
||||||
|
# SupportBot created by Emerald Services
|
||||||
|
# Installed with MIT License
|
||||||
|
#
|
||||||
|
# Discord Support: https://discord.gg/emeraldsrv
|
||||||
|
# Official Documentation: https://docs.emeraldsrv.com
|
||||||
|
# Addons Marketplace: https://community.emeraldsrv.com
|
||||||
|
#
|
||||||
|
# Ticket Creation Panel Configuration (ticket-panel.yml)
|
||||||
|
|
||||||
|
# -------------------------------
|
||||||
|
# Ticket Panel Configuration
|
||||||
|
# -------------------------------
|
||||||
|
|
||||||
|
Panel: true
|
||||||
|
|
||||||
|
Style:
|
||||||
|
Title: "## 📬 Open Ticket"
|
||||||
|
Description: "Tap the button to open a support ticket, Our team will respond as
|
||||||
|
soon as possible."
|
||||||
|
Image: "https://cdn.1tzemerald.space/Emerald%20Services/SBPoster.png"
|
||||||
|
Color: "#45b463"
|
||||||
|
Layout: "3"
|
||||||
|
|
||||||
|
# Available Layout Styles
|
||||||
|
# "1" - Style 1 - Title & Text, Button at the top -- Image at the bottom
|
||||||
|
# "2" - Style 2 - Title & Text, Button at the bottom -- Image at the top
|
||||||
|
# "3" - Style 3 - The classic look - Title & Text at the top -- Image in the middle -- Button at the bottom
|
||||||
|
# "4" - Style 4 - Classic Embed (Title & Text, Image on right, Button at bottom)
|
||||||
|
|
||||||
|
Button:
|
||||||
|
Text: "Open"
|
||||||
|
Emoji: "🛟"
|
||||||
|
Color: "2"
|
||||||
|
CustomId: "createticket"
|
||||||
|
|
||||||
|
# Available Button Styles
|
||||||
|
# 1 - Blurple
|
||||||
|
# 2 - Grey
|
||||||
|
# 3 - Red
|
||||||
|
# 4 - Green
|
||||||
|
|
||||||
|
DepartmentMenu:
|
||||||
|
CustomId: "ticketdepartmentselect"
|
||||||
|
Placeholder: "Choose a ticket department"
|
||||||
|
DefaultDescription: "Open a support ticket"
|
||||||
|
DefaultEmoji: "🎫"
|
||||||
|
|
||||||
|
Settings:
|
||||||
|
Image: true
|
||||||
|
TopTitle: true
|
||||||
|
TopDivider: true
|
||||||
|
BottomTitle: false
|
||||||
|
BottomDivider: false
|
||||||
|
|
||||||
|
Messages:
|
||||||
|
NotConfigured: "Ticket Panel is not set up. Please configure it in
|
||||||
|
`/Configs/ticket-panel.yml`."
|
||||||
|
InvalidChannel: "TicketHome channel not found. Please check your config file."
|
||||||
|
PanelSent: "Ticket panel message has been sent!"
|
||||||
|
PanelSentWithDepartments: "Ticket panel message with department selection has been sent!"
|
||||||
|
SendError: "An error occurred while sending the ticket panel message."
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
Theme:
|
||||||
|
Background: '#0e0e0e'
|
||||||
|
Panel: '#1d1d1d'
|
||||||
|
PanelSecondary: '#222222'
|
||||||
|
PanelTertiary: '#1d2938'
|
||||||
|
Border: '#2e2e2e'
|
||||||
|
Text: '#e6edf3'
|
||||||
|
Muted: '#606060'
|
||||||
|
Accent: '#396ff8'
|
||||||
|
AccentSoft: rgba(34, 197, 94, 0.12)
|
||||||
|
AccentBorder: rgba(34, 197, 94, 0.25)
|
||||||
|
AccentText: '#8df0ad'
|
||||||
|
Link: '#396ff8'
|
||||||
|
AttachmentLink: '#396ff8'
|
||||||
|
CodeBackground: '#0e0e0e'
|
||||||
|
Typography:
|
||||||
|
FontFamily: Poppins
|
||||||
|
Header:
|
||||||
|
Title: Ticket Transcript
|
||||||
|
Subtitle: Exported conversation log for support ticket **{{ticketName}}**
|
||||||
|
Badge: Closed Transcript
|
||||||
|
ShowMetaCards: true
|
||||||
|
Meta:
|
||||||
|
ChannelLabel: Ticket Name
|
||||||
|
TicketIdLabel: Ticket ID
|
||||||
|
MessageCountLabel: Message Count
|
||||||
|
CloseReasonLabel: Close Reason
|
||||||
|
DefaultCloseReason: No Reason Provided.
|
||||||
|
Messages:
|
||||||
|
SectionTitle: Messages
|
||||||
|
EmptyState: No messages were found in this ticket.
|
||||||
|
Advanced:
|
||||||
|
CustomCss: ''
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
// Emerald Services | SupportBot
|
||||||
|
// Claude integration for SupportBot AI
|
||||||
|
|
||||||
|
const fs = require("fs");
|
||||||
|
const yaml = require("js-yaml");
|
||||||
|
const axios = require("axios");
|
||||||
|
|
||||||
|
const supportbotai = require("../../Structures/ConfigStore").supportbotAi;
|
||||||
|
|
||||||
|
function extractTextFromMessage(message) {
|
||||||
|
if (!message) return "";
|
||||||
|
|
||||||
|
if (typeof message === "string") {
|
||||||
|
return message.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.text) {
|
||||||
|
return message.text.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(message)) {
|
||||||
|
return message
|
||||||
|
.map((part) => {
|
||||||
|
if (typeof part === "string") return part;
|
||||||
|
if (part?.type === "text" && typeof part.text === "string") {
|
||||||
|
return part.text;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
})
|
||||||
|
.join("")
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function chat({ messages, temperature, maxTokens }) {
|
||||||
|
const model = supportbotai.General.Model;
|
||||||
|
const apiKey = supportbotai.General.Model_API_Key;
|
||||||
|
|
||||||
|
const systemMessages = messages.filter((m) => m.role === "system");
|
||||||
|
const systemPrompt = systemMessages.map((m) => m.content).join("\n\n");
|
||||||
|
|
||||||
|
const userAssistantMessages = messages.filter((m) => m.role !== "system");
|
||||||
|
|
||||||
|
const response = await axios.post(
|
||||||
|
"https://api.anthropic.com/v1/messages",
|
||||||
|
{
|
||||||
|
model,
|
||||||
|
system: systemPrompt || undefined,
|
||||||
|
messages: userAssistantMessages,
|
||||||
|
temperature: temperature || supportbotai.General.Temperature || 0.4,
|
||||||
|
max_tokens: maxTokens || supportbotai.General.Tokens || 800,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"x-api-key": apiKey,
|
||||||
|
"anthropic-version": "2023-06-01",
|
||||||
|
"content-type": "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const data = response.data;
|
||||||
|
const content = extractTextFromMessage(data.content);
|
||||||
|
|
||||||
|
if (!content) {
|
||||||
|
console.error("Claude raw response:", JSON.stringify(data, null, 2));
|
||||||
|
throw new Error(
|
||||||
|
`Claude returned empty response. stop_reason=${data?.stop_reason || "unknown"}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
content,
|
||||||
|
raw: data,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
chat,
|
||||||
|
};
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
// Emerald Services | SupportBot
|
||||||
|
// Groq integration for SupportBot AI
|
||||||
|
|
||||||
|
const fs = require("fs");
|
||||||
|
const yaml = require("js-yaml");
|
||||||
|
const OpenAI = require("openai");
|
||||||
|
|
||||||
|
const supportbotai = require("../../Structures/ConfigStore").supportbotAi;
|
||||||
|
|
||||||
|
const client = new OpenAI({
|
||||||
|
apiKey: supportbotai.General.Model_API_Key,
|
||||||
|
baseURL: "https://api.groq.com/openai/v1",
|
||||||
|
});
|
||||||
|
|
||||||
|
function extractTextFromMessage(message) {
|
||||||
|
if (!message) return "";
|
||||||
|
|
||||||
|
if (typeof message.content === "string") {
|
||||||
|
return message.content.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(message.content)) {
|
||||||
|
return message.content
|
||||||
|
.map((part) => {
|
||||||
|
if (typeof part === "string") return part;
|
||||||
|
if (part?.type === "text" && typeof part.text === "string") {
|
||||||
|
return part.text;
|
||||||
|
}
|
||||||
|
if (typeof part?.text?.value === "string") {
|
||||||
|
return part.text.value;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
})
|
||||||
|
.join("")
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function chat({ messages, temperature, maxTokens }) {
|
||||||
|
const model = supportbotai.General.Model;
|
||||||
|
|
||||||
|
const response = await client.chat.completions.create({
|
||||||
|
model,
|
||||||
|
messages,
|
||||||
|
temperature: temperature || supportbotai.General.Temperature || 0.4,
|
||||||
|
max_tokens: maxTokens || supportbotai.General.Tokens || 800,
|
||||||
|
});
|
||||||
|
|
||||||
|
const choice = response?.choices?.[0];
|
||||||
|
const message = choice?.message;
|
||||||
|
const content = extractTextFromMessage(message);
|
||||||
|
|
||||||
|
if (!content) {
|
||||||
|
console.error("Groq raw response:", JSON.stringify(response, null, 2));
|
||||||
|
throw new Error(
|
||||||
|
`Groq returned empty response. finish_reason=${choice?.finish_reason || "unknown"}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
content,
|
||||||
|
raw: response,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
chat,
|
||||||
|
};
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
// Emerald Services | SupportBot
|
||||||
|
// Open AI integration for SupportBot AI
|
||||||
|
|
||||||
|
const fs = require("fs");
|
||||||
|
const yaml = require("js-yaml");
|
||||||
|
const OpenAI = require("openai");
|
||||||
|
|
||||||
|
const supportbotai = require("../../Structures/ConfigStore").supportbotAi;
|
||||||
|
|
||||||
|
const client = new OpenAI({
|
||||||
|
apiKey: supportbotai.General.Model_API_Key,
|
||||||
|
});
|
||||||
|
|
||||||
|
function extractTextFromMessage(message) {
|
||||||
|
if (!message) return "";
|
||||||
|
|
||||||
|
if (typeof message.content === "string") {
|
||||||
|
return message.content.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(message.content)) {
|
||||||
|
return message.content
|
||||||
|
.map((part) => {
|
||||||
|
if (typeof part === "string") return part;
|
||||||
|
if (part?.type === "text" && typeof part.text === "string")
|
||||||
|
return part.text;
|
||||||
|
if (typeof part?.text?.value === "string") return part.text.value;
|
||||||
|
return "";
|
||||||
|
})
|
||||||
|
.join("")
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function chat({ messages, temperature, maxTokens }) {
|
||||||
|
const model = supportbotai.General.Model;
|
||||||
|
|
||||||
|
const response = await client.chat.completions.create({
|
||||||
|
model,
|
||||||
|
messages,
|
||||||
|
temperature: temperature || supportbotai.General.Temperature || 0.4,
|
||||||
|
max_completion_tokens: maxTokens || supportbotai.General.Tokens || 800,
|
||||||
|
});
|
||||||
|
|
||||||
|
const choice = response?.choices?.[0];
|
||||||
|
const message = choice?.message;
|
||||||
|
const content = extractTextFromMessage(message);
|
||||||
|
|
||||||
|
if (!content) {
|
||||||
|
console.error("OpenAI raw response:", JSON.stringify(response, null, 2));
|
||||||
|
throw new Error(
|
||||||
|
`OpenAI returned empty response. finish_reason=${choice?.finish_reason || "unknown"}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
content,
|
||||||
|
raw: response,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
chat,
|
||||||
|
};
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
const Event = require("../Structures/Event.js");
|
||||||
|
const {
|
||||||
|
getConfiguredGuildId,
|
||||||
|
syncGuildHealth,
|
||||||
|
logGuildWarning,
|
||||||
|
} = require("../Structures/GuildManager.js");
|
||||||
|
|
||||||
|
module.exports = new Event("guildCreate", async (client, guild) => {
|
||||||
|
const configuredId = getConfiguredGuildId();
|
||||||
|
|
||||||
|
if (!configuredId) {
|
||||||
|
logGuildWarning(
|
||||||
|
client,
|
||||||
|
`Joined "${guild.name}" (${guild.id}). Set General.GuildId in config to lock the bot to one server.`,
|
||||||
|
);
|
||||||
|
syncGuildHealth(client);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (guild.id === configuredId) {
|
||||||
|
syncGuildHealth(client);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logGuildWarning(
|
||||||
|
client,
|
||||||
|
`Joined unauthorized server "${guild.name}" (${guild.id}). Leave it from the dashboard or remove the invite.`,
|
||||||
|
);
|
||||||
|
|
||||||
|
syncGuildHealth(client);
|
||||||
|
});
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
const Event = require("../Structures/Event.js");
|
||||||
|
const {
|
||||||
|
getConfiguredGuildId,
|
||||||
|
syncGuildHealth,
|
||||||
|
logGuildWarning,
|
||||||
|
} = require("../Structures/GuildManager.js");
|
||||||
|
|
||||||
|
module.exports = new Event("guildDelete", async (client, guild) => {
|
||||||
|
const configuredId = getConfiguredGuildId();
|
||||||
|
|
||||||
|
if (configuredId && guild.id === configuredId) {
|
||||||
|
logGuildWarning(
|
||||||
|
client,
|
||||||
|
`Left the configured server "${guild.name}" (${guild.id}). Re-invite the bot or update General.GuildId.`,
|
||||||
|
);
|
||||||
|
} else if (!configuredId) {
|
||||||
|
logGuildWarning(client, `Left server "${guild.name}" (${guild.id}).`);
|
||||||
|
} else {
|
||||||
|
logGuildWarning(client, `Left server "${guild.name}" (${guild.id}).`);
|
||||||
|
}
|
||||||
|
|
||||||
|
syncGuildHealth(client);
|
||||||
|
});
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
const {
|
||||||
|
EmbedBuilder,
|
||||||
|
ActionRowBuilder,
|
||||||
|
ButtonBuilder,
|
||||||
|
ButtonStyle,
|
||||||
|
MessageFlags,
|
||||||
|
ContainerBuilder,
|
||||||
|
TextDisplayBuilder,
|
||||||
|
SectionBuilder,
|
||||||
|
ThumbnailBuilder,
|
||||||
|
MediaGalleryBuilder
|
||||||
|
} = require("discord.js");
|
||||||
|
const Event = require("../Structures/Event.js");
|
||||||
|
|
||||||
|
const supportbot = require("../Structures/ConfigStore").supportbot;
|
||||||
|
const msgconfig = require("../Structures/ConfigStore").messages;
|
||||||
|
|
||||||
|
function getWelcomeThumbnail(client, member, iconCfg) {
|
||||||
|
switch ((iconCfg?.Mode || "").toUpperCase()) {
|
||||||
|
case "USER":
|
||||||
|
return member.user.displayAvatarURL();
|
||||||
|
case "BOT":
|
||||||
|
return client.user.displayAvatarURL();
|
||||||
|
case "CUSTOM":
|
||||||
|
return iconCfg.CustomURL;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildModernComponents(client, member) {
|
||||||
|
const container = new ContainerBuilder();
|
||||||
|
|
||||||
|
if (msgconfig.Welcome.Modern.Colour) {
|
||||||
|
container.setAccentColor(parseInt(msgconfig.Welcome.Modern.Colour.replace("#", ""), 16));
|
||||||
|
}
|
||||||
|
|
||||||
|
const section = new SectionBuilder().addTextDisplayComponents(
|
||||||
|
new TextDisplayBuilder().setContent(
|
||||||
|
msgconfig.Welcome.Modern.Title.replace(/%joined_user%/g, member.user)
|
||||||
|
),
|
||||||
|
new TextDisplayBuilder().setContent(
|
||||||
|
msgconfig.Welcome.Modern.Body.replace(/%joined_user%/g, member.user)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const thumbUrl = getWelcomeThumbnail(client, member, msgconfig.Welcome.Modern.Icon);
|
||||||
|
if (thumbUrl) {
|
||||||
|
section.setThumbnailAccessory(
|
||||||
|
new ThumbnailBuilder()
|
||||||
|
.setURL(thumbUrl)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
container.addSectionComponents(section);
|
||||||
|
|
||||||
|
// FIXED: Using MediaGalleryBuilder instead of TextDisplayBuilder for images
|
||||||
|
if (msgconfig.Welcome.Modern.Image?.Enabled && msgconfig.Welcome.Modern.Image.URL) {
|
||||||
|
container.addMediaGalleryComponents(
|
||||||
|
new MediaGalleryBuilder().addItems([
|
||||||
|
{
|
||||||
|
media: {
|
||||||
|
url: msgconfig.Welcome.Modern.Image.URL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(msgconfig.Welcome.Modern.Buttons)) {
|
||||||
|
const row = new ActionRowBuilder();
|
||||||
|
msgconfig.Welcome.Modern.Buttons.slice(0, 5).forEach(b => {
|
||||||
|
const btn = new ButtonBuilder()
|
||||||
|
.setLabel(b.label)
|
||||||
|
.setStyle(ButtonStyle[b.style])
|
||||||
|
.setEmoji(b.emoji || null);
|
||||||
|
if (b.style === "Link") btn.setURL(b.url);
|
||||||
|
else if (b.customId) btn.setCustomId(b.customId);
|
||||||
|
row.addComponents(btn);
|
||||||
|
});
|
||||||
|
|
||||||
|
container.addActionRowComponents(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [container];
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = new Event("guildMemberAdd", async (client, member) => {
|
||||||
|
if (!supportbot.Welcome.Enabled) return;
|
||||||
|
|
||||||
|
const WelcomeChannel =
|
||||||
|
member.guild.channels.cache.get(supportbot.Welcome.Channel) ||
|
||||||
|
member.guild.channels.cache.find(c => c.name === supportbot.Welcome.Channel);
|
||||||
|
if (!WelcomeChannel) return;
|
||||||
|
|
||||||
|
const style = (msgconfig.Welcome.LayoutStyle || "classic").toLowerCase();
|
||||||
|
|
||||||
|
if (style === "classic") {
|
||||||
|
const WelcomeEmbed = new EmbedBuilder()
|
||||||
|
.setColor(msgconfig.Welcome.Embed.Colour)
|
||||||
|
.setTitle(msgconfig.Welcome.Embed.Title || null)
|
||||||
|
.setDescription(
|
||||||
|
msgconfig.Welcome.Embed.Message.replace(/%joined_user%/g, member.user)
|
||||||
|
)
|
||||||
|
.setTimestamp();
|
||||||
|
|
||||||
|
const thumb = getWelcomeThumbnail(client, member, msgconfig.Welcome.Embed.Icon);
|
||||||
|
if (thumb) WelcomeEmbed.setThumbnail(thumb);
|
||||||
|
|
||||||
|
if (msgconfig.Welcome.Embed.Image?.Enabled && msgconfig.Welcome.Embed.Image.URL) {
|
||||||
|
WelcomeEmbed.setImage(msgconfig.Welcome.Embed.Image.URL);
|
||||||
|
}
|
||||||
|
|
||||||
|
const components = [];
|
||||||
|
if (
|
||||||
|
msgconfig.Welcome.Embed.Buttons?.Enabled &&
|
||||||
|
Array.isArray(msgconfig.Welcome.Embed.Buttons.List)
|
||||||
|
) {
|
||||||
|
const row = new ActionRowBuilder();
|
||||||
|
msgconfig.Welcome.Embed.Buttons.List.slice(0, 5).forEach(b => {
|
||||||
|
const btn = new ButtonBuilder()
|
||||||
|
.setLabel(b.label)
|
||||||
|
.setStyle(ButtonStyle[b.style])
|
||||||
|
.setEmoji(b.emoji || null);
|
||||||
|
if (b.style === "Link") btn.setURL(b.url);
|
||||||
|
else if (b.customId) btn.setCustomId(b.customId);
|
||||||
|
row.addComponents(btn);
|
||||||
|
});
|
||||||
|
components.push(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
await WelcomeChannel.send({ embeds: [WelcomeEmbed], components });
|
||||||
|
} else if (style === "modern") {
|
||||||
|
const components = buildModernComponents(client, member);
|
||||||
|
if (components.length > 0) {
|
||||||
|
await WelcomeChannel.send({
|
||||||
|
components,
|
||||||
|
flags: MessageFlags.IsComponentsV2
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (supportbot.Roles.AutoRole.Enabled) {
|
||||||
|
const role =
|
||||||
|
member.guild.roles.cache.get(supportbot.Roles.AutoRole.Role) ||
|
||||||
|
member.guild.roles.cache.find(r => r.name === supportbot.Roles.AutoRole.Role);
|
||||||
|
if (role) await member.roles.add(role);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("\u001b[32m", "[+]", "\u001b[33m", `${member.user.username} joined the server!`);
|
||||||
|
});
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
const { EmbedBuilder } = require("discord.js");
|
||||||
|
const fs = require("fs");
|
||||||
|
const yaml = require("js-yaml");
|
||||||
|
const Event = require("../Structures/Event.js");
|
||||||
|
|
||||||
|
const supportbot = require("../Structures/ConfigStore").supportbot;
|
||||||
|
const msgconfig = require("../Structures/ConfigStore").messages;
|
||||||
|
|
||||||
|
module.exports = new Event("guildMemberRemove", async (client, member) => {
|
||||||
|
|
||||||
|
if (!supportbot.Leave || !supportbot.Leave.Enabled) return;
|
||||||
|
|
||||||
|
const leaveChannel =
|
||||||
|
member.guild.channels.cache.get(supportbot.Leave.Channel) ||
|
||||||
|
member.guild.channels.cache.find(c => c.name === supportbot.Leave.Channel);
|
||||||
|
|
||||||
|
if (!leaveChannel) return;
|
||||||
|
|
||||||
|
const leaveEmbed = new EmbedBuilder()
|
||||||
|
.setColor(msgconfig.Leave.Embed.Colour)
|
||||||
|
.setTitle(msgconfig.Leave.Embed.Title)
|
||||||
|
.setDescription(
|
||||||
|
msgconfig.Leave.Embed.Message.replace(/%left_user%/g, member.user.toString())
|
||||||
|
)
|
||||||
|
.setTimestamp();
|
||||||
|
|
||||||
|
if (msgconfig.Leave.Embed.Thumbnail === "BOT") {
|
||||||
|
leaveEmbed.setThumbnail(client.user.displayAvatarURL());
|
||||||
|
} else if (msgconfig.Leave.Embed.Thumbnail === "USER") {
|
||||||
|
leaveEmbed.setThumbnail(member.user.displayAvatarURL());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msgconfig.Leave.Embed.ImageEnabled) {
|
||||||
|
leaveEmbed.setImage(msgconfig.Leave.Embed.ImageURL);
|
||||||
|
}
|
||||||
|
|
||||||
|
await leaveChannel.send({ embeds: [leaveEmbed] });
|
||||||
|
|
||||||
|
console.log(`[-] ${member.user.tag} left the server`);
|
||||||
|
});
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,664 @@
|
|||||||
|
const Discord = require("discord.js");
|
||||||
|
const axios = require("axios");
|
||||||
|
|
||||||
|
const supportbotai = require("../Structures/ConfigStore").supportbotAi;
|
||||||
|
const supportbot = require("../Structures/ConfigStore").supportbot;
|
||||||
|
|
||||||
|
const Event = require("../Structures/Event.js");
|
||||||
|
const AIDatabase = require("../Structures/AIDatabase.js");
|
||||||
|
const db = require("../Structures/Database.js");
|
||||||
|
|
||||||
|
const openai = require("./Models/openai.js");
|
||||||
|
const groq = require("./Models/groq.js");
|
||||||
|
const claude = require("./Models/claude.js");
|
||||||
|
|
||||||
|
const PASTEBIN_API_KEY = supportbotai.General.PastebinAPI_Key;
|
||||||
|
const PASTEBIN_API_URL = supportbotai.General.PastebinAPI_URL;
|
||||||
|
|
||||||
|
const CHANNELS = [supportbotai.Channels.AIChannel].filter(Boolean);
|
||||||
|
|
||||||
|
async function createPaste(content) {
|
||||||
|
try {
|
||||||
|
if (!PASTEBIN_API_KEY || !PASTEBIN_API_URL) return null;
|
||||||
|
|
||||||
|
const response = await axios.post(
|
||||||
|
PASTEBIN_API_URL,
|
||||||
|
new URLSearchParams({
|
||||||
|
api_dev_key: PASTEBIN_API_KEY,
|
||||||
|
api_option: "paste",
|
||||||
|
api_paste_code: content,
|
||||||
|
api_paste_private: 1,
|
||||||
|
api_paste_expire_date: "1D",
|
||||||
|
api_paste_format: "text",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Pastebin Error:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getProvider() {
|
||||||
|
const provider = String(
|
||||||
|
supportbotai.General.Provider || "openai",
|
||||||
|
).toLowerCase();
|
||||||
|
|
||||||
|
switch (provider) {
|
||||||
|
case "openai":
|
||||||
|
return openai;
|
||||||
|
|
||||||
|
case "groq":
|
||||||
|
return groq;
|
||||||
|
|
||||||
|
case "claude":
|
||||||
|
return claude;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported AI provider: ${provider}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTicketQuestionAnswers(questionAnswers) {
|
||||||
|
if (!Array.isArray(questionAnswers) || questionAnswers.length === 0) {
|
||||||
|
return "No ticket question answers were saved.";
|
||||||
|
}
|
||||||
|
|
||||||
|
return questionAnswers
|
||||||
|
.map((item, index) => {
|
||||||
|
const question = item?.question || `Question ${index + 1}`;
|
||||||
|
const answer = item?.answer || "No answer provided";
|
||||||
|
return `${index + 1}. ${question}\nAnswer: ${answer}`;
|
||||||
|
})
|
||||||
|
.join("\n\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildTicketContext(ticket, message) {
|
||||||
|
if (!ticket) return "";
|
||||||
|
|
||||||
|
return `
|
||||||
|
Ticket Information:
|
||||||
|
Ticket ID: ${ticket.ticket_id || ticket.id || "Unknown"}
|
||||||
|
Subject: ${ticket.subject || ticket.reason || "No subject provided"}
|
||||||
|
Department: ${ticket.department || "Not set"}
|
||||||
|
Priority: ${ticket.priority || "Not set"}
|
||||||
|
Created By: ${ticket.user_id || ticket.user || "Unknown"}
|
||||||
|
Current User: ${message?.author?.tag || "Unknown"} (${message?.author?.id || "Unknown"})
|
||||||
|
|
||||||
|
Ticket Question Answers:
|
||||||
|
${formatTicketQuestionAnswers(ticket.questionAnswers)}
|
||||||
|
|
||||||
|
Use this information when helping the user with their ticket.
|
||||||
|
Do not invent missing ticket details.
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSystemPrompt(message) {
|
||||||
|
const sections = [];
|
||||||
|
|
||||||
|
if (Array.isArray(supportbotai.CustomInstructions)) {
|
||||||
|
sections.push(...supportbotai.CustomInstructions);
|
||||||
|
}
|
||||||
|
|
||||||
|
sections.push(
|
||||||
|
`I am currently powered by the ${supportbotai.General.Provider || "Unknown"} provider, running the ${supportbotai.General.Model || "Unknown"} model.`,
|
||||||
|
);
|
||||||
|
sections.push(
|
||||||
|
`The current guild is: ${message.guild?.name || "Unknown Guild"}`,
|
||||||
|
);
|
||||||
|
sections.push(
|
||||||
|
`The current channel is: #${message.channel?.name || "unknown-channel"}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return sections.join("\n- ").replace(/^/, "- ");
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildConversationFromMemory(currentMessage) {
|
||||||
|
const memoryConfig = supportbotai.Memory || {};
|
||||||
|
const memoryEnabled = memoryConfig.Enabled !== false;
|
||||||
|
const maxRecentMessages = memoryConfig.MaxRecentMessages || 15;
|
||||||
|
const memoryScope = String(memoryConfig.Scope || "guild").toLowerCase();
|
||||||
|
|
||||||
|
const conversation = [
|
||||||
|
{
|
||||||
|
role: "system",
|
||||||
|
content: buildSystemPrompt(currentMessage),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!memoryEnabled) return conversation;
|
||||||
|
|
||||||
|
const recentMessages =
|
||||||
|
typeof AIDatabase.getRecentMessagesByScope === "function"
|
||||||
|
? AIDatabase.getRecentMessagesByScope({
|
||||||
|
guildId: currentMessage.guild?.id || null,
|
||||||
|
channelId: currentMessage.channel?.id || null,
|
||||||
|
limit: maxRecentMessages,
|
||||||
|
scope: memoryScope,
|
||||||
|
})
|
||||||
|
: AIDatabase.getRecentMessages(
|
||||||
|
currentMessage.channel?.id,
|
||||||
|
maxRecentMessages,
|
||||||
|
);
|
||||||
|
|
||||||
|
const latestSummary =
|
||||||
|
memoryConfig.StoreSummaries !== false
|
||||||
|
? typeof AIDatabase.getLatestSummaryByScope === "function"
|
||||||
|
? AIDatabase.getLatestSummaryByScope({
|
||||||
|
guildId: currentMessage.guild?.id || null,
|
||||||
|
channelId: currentMessage.channel?.id || null,
|
||||||
|
scope: memoryScope,
|
||||||
|
})
|
||||||
|
: AIDatabase.getLatestSummary(currentMessage.channel?.id)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const facts =
|
||||||
|
memoryConfig.StoreFacts !== false
|
||||||
|
? typeof AIDatabase.getFactsByScope === "function"
|
||||||
|
? AIDatabase.getFactsByScope({
|
||||||
|
guildId: currentMessage.guild?.id || null,
|
||||||
|
channelId: currentMessage.channel?.id || null,
|
||||||
|
scope: memoryScope,
|
||||||
|
})
|
||||||
|
: AIDatabase.getFacts(currentMessage.channel?.id)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const userFacts =
|
||||||
|
memoryConfig.StoreFacts !== false &&
|
||||||
|
typeof AIDatabase.getUserFactsByScope === "function"
|
||||||
|
? AIDatabase.getUserFactsByScope({
|
||||||
|
guildId: currentMessage.guild?.id || null,
|
||||||
|
userId: currentMessage.author?.id || null,
|
||||||
|
scope: memoryScope,
|
||||||
|
})
|
||||||
|
: memoryConfig.StoreFacts !== false &&
|
||||||
|
typeof AIDatabase.getUserFacts === "function"
|
||||||
|
? AIDatabase.getUserFacts(
|
||||||
|
currentMessage.guild?.id,
|
||||||
|
currentMessage.author?.id,
|
||||||
|
)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const knowledgeBase = typeof AIDatabase.getKnowledge === "function"
|
||||||
|
? AIDatabase.getKnowledge(25)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
if (latestSummary?.summary) {
|
||||||
|
conversation.push({
|
||||||
|
role: "system",
|
||||||
|
content: `Shared memory summary (${memoryScope} scope):\n${latestSummary.summary}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (facts.length > 0) {
|
||||||
|
const factText = facts
|
||||||
|
.map(
|
||||||
|
(fact) => `- [${fact.fact_type}] ${fact.fact_key}: ${fact.fact_value}`,
|
||||||
|
)
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
conversation.push({
|
||||||
|
role: "system",
|
||||||
|
content: `Known shared facts (${memoryScope} scope):\n${factText}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userFacts.length > 0) {
|
||||||
|
const userFactText = userFacts
|
||||||
|
.map(
|
||||||
|
(fact) => `- [${fact.fact_type}] ${fact.fact_key}: ${fact.fact_value}`,
|
||||||
|
)
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
conversation.push({
|
||||||
|
role: "system",
|
||||||
|
content: `Known facts about this user across chats:\n${userFactText}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (knowledgeBase.length > 0) {
|
||||||
|
const knowledgeText = knowledgeBase
|
||||||
|
.map((k) => `- ${k.content}`)
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
conversation.push({
|
||||||
|
role: "system",
|
||||||
|
content: `Server Knowledge Base (use this to answer questions):\n${knowledgeText}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const msg of recentMessages) {
|
||||||
|
conversation.push({
|
||||||
|
role: msg.role === "assistant" ? "assistant" : "user",
|
||||||
|
content: msg.content,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return conversation;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTicketContextForMessage(message) {
|
||||||
|
if (!message?.channel) return null;
|
||||||
|
|
||||||
|
if (typeof db.getTicketByAIChannel === "function") {
|
||||||
|
const aiTicket = db.getTicketByAIChannel(message.channel.id);
|
||||||
|
if (aiTicket) return aiTicket;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof db.getTicket === "function") {
|
||||||
|
const directTicket = db.getTicket(message.channel.id);
|
||||||
|
if (directTicket) return directTicket;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.channel.isThread() && typeof db.getTicket === "function") {
|
||||||
|
const parentTicket = db.getTicket(message.channel.parentId);
|
||||||
|
if (parentTicket) return parentTicket;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAIMessageType() {
|
||||||
|
return String(supportbotai.Style?.MessageType || "embed").toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldSuggestTicket(message, ticket) {
|
||||||
|
if (supportbotai.Features?.SuggestTickets !== true) return false;
|
||||||
|
if (ticket) return false;
|
||||||
|
if (message.channel.isThread()) return false;
|
||||||
|
return CHANNELS.includes(message.channel.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSuggestTicketText() {
|
||||||
|
return (
|
||||||
|
supportbotai.Messages?.SuggestTicketText ||
|
||||||
|
"-# Need more help? React with 🎫 to open a ticket."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendAIResponse({
|
||||||
|
client,
|
||||||
|
message,
|
||||||
|
replyContent,
|
||||||
|
suggestTicket = false,
|
||||||
|
}) {
|
||||||
|
const messageType = getAIMessageType();
|
||||||
|
const ticketHint = suggestTicket ? `\n\n${getSuggestTicketText()}` : "";
|
||||||
|
|
||||||
|
if (messageType === "text") {
|
||||||
|
return message.reply({
|
||||||
|
content: `${replyContent}${ticketHint}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (messageType === "components_v2") {
|
||||||
|
const {
|
||||||
|
ContainerBuilder,
|
||||||
|
TextDisplayBuilder,
|
||||||
|
SeparatorBuilder,
|
||||||
|
MessageFlags,
|
||||||
|
} = Discord;
|
||||||
|
|
||||||
|
const container = new ContainerBuilder();
|
||||||
|
|
||||||
|
if (supportbot.Embed?.Colours?.General) {
|
||||||
|
container.setAccentColor(
|
||||||
|
parseInt(String(supportbot.Embed.Colours.General).replace("#", ""), 16),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const title = new TextDisplayBuilder().setContent(
|
||||||
|
`**${supportbotai.General?.Name || "SupportBot AI"}**`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const body = new TextDisplayBuilder().setContent(replyContent);
|
||||||
|
|
||||||
|
container.addTextDisplayComponents(title);
|
||||||
|
container.addSeparatorComponents(new SeparatorBuilder().setDivider(true));
|
||||||
|
container.addTextDisplayComponents(body);
|
||||||
|
|
||||||
|
if (suggestTicket) {
|
||||||
|
container.addSeparatorComponents(new SeparatorBuilder().setDivider(true));
|
||||||
|
container.addTextDisplayComponents(
|
||||||
|
new TextDisplayBuilder().setContent(getSuggestTicketText()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return message.reply({
|
||||||
|
flags: MessageFlags.IsComponentsV2,
|
||||||
|
components: [container],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const embed = new Discord.EmbedBuilder()
|
||||||
|
.setAuthor({
|
||||||
|
name: supportbotai.General.Name,
|
||||||
|
iconURL: client.user.displayAvatarURL(),
|
||||||
|
})
|
||||||
|
.setDescription(`${replyContent}${ticketHint}`)
|
||||||
|
.setColor(supportbotai.Embed.Color)
|
||||||
|
.setFooter({
|
||||||
|
text: supportbotai.Embed.Footer,
|
||||||
|
iconURL: message.author.displayAvatarURL(),
|
||||||
|
})
|
||||||
|
.setTimestamp();
|
||||||
|
|
||||||
|
return message.reply({ embeds: [embed] });
|
||||||
|
}
|
||||||
|
|
||||||
|
function rememberUserFacts(message) {
|
||||||
|
if (!message?.guild || !message?.author || !message?.content) return;
|
||||||
|
|
||||||
|
const content = message.content.trim();
|
||||||
|
|
||||||
|
const nameMatch = content.match(/^my name is\s+(.+)$/i);
|
||||||
|
if (nameMatch && typeof AIDatabase.saveUserFact === "function") {
|
||||||
|
AIDatabase.saveUserFact({
|
||||||
|
guildId: message.guild.id,
|
||||||
|
userId: message.author.id,
|
||||||
|
factType: "profile",
|
||||||
|
factKey: "name",
|
||||||
|
factValue: nameMatch[1].trim(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const nicknameMatch = content.match(/^i(?:'| a)?m\s+(.+)$/i);
|
||||||
|
if (
|
||||||
|
nicknameMatch &&
|
||||||
|
nicknameMatch[1] &&
|
||||||
|
nicknameMatch[1].length <= 32 &&
|
||||||
|
typeof AIDatabase.saveUserFact === "function"
|
||||||
|
) {
|
||||||
|
AIDatabase.saveUserFact({
|
||||||
|
guildId: message.guild.id,
|
||||||
|
userId: message.author.id,
|
||||||
|
factType: "profile",
|
||||||
|
factKey: "display_name",
|
||||||
|
factValue: nicknameMatch[1].trim(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const usesMatch = content.match(/^i use\s+(.+)$/i);
|
||||||
|
if (usesMatch && typeof AIDatabase.saveUserFact === "function") {
|
||||||
|
AIDatabase.saveUserFact({
|
||||||
|
guildId: message.guild.id,
|
||||||
|
userId: message.author.id,
|
||||||
|
factType: "preference",
|
||||||
|
factKey: "uses",
|
||||||
|
factValue: usesMatch[1].trim(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = new Event("messageCreate", async (client, message) => {
|
||||||
|
if (message.guild && typeof db.getTicket === "function") {
|
||||||
|
const activeTicket = db.getTicket(message.channel.id);
|
||||||
|
if (activeTicket && global.apiServer) {
|
||||||
|
global.apiServer.broadcast("ticket_message", {
|
||||||
|
ticketId: message.channel.id,
|
||||||
|
message: {
|
||||||
|
id: message.id,
|
||||||
|
author: {
|
||||||
|
id: message.author.id,
|
||||||
|
username: message.author.username,
|
||||||
|
globalName: message.author.globalName || message.author.username,
|
||||||
|
avatar: message.author.displayAvatarURL({ extension: "png", size: 64 }),
|
||||||
|
bot: message.author.bot
|
||||||
|
},
|
||||||
|
content: (() => {
|
||||||
|
if (message.content && message.content.trim()) return message.content;
|
||||||
|
if (Array.isArray(message.components) && message.components.length > 0) {
|
||||||
|
const texts = [];
|
||||||
|
for (const comp of message.components) {
|
||||||
|
const data = typeof comp.toJSON === "function" ? comp.toJSON() : comp;
|
||||||
|
if (data.content) texts.push(data.content);
|
||||||
|
if (Array.isArray(data.components)) {
|
||||||
|
for (const child of data.components) {
|
||||||
|
const childData = typeof child.toJSON === "function" ? child.toJSON() : child;
|
||||||
|
if (childData.content) texts.push(childData.content);
|
||||||
|
if (childData.label && (childData.type === 10 || childData.type === "TEXT_DISPLAY")) {
|
||||||
|
texts.push(childData.label);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (texts.length > 0) return texts.join("\n");
|
||||||
|
}
|
||||||
|
return message.content || "";
|
||||||
|
})(),
|
||||||
|
containerColor: (() => {
|
||||||
|
if (Array.isArray(message.components)) {
|
||||||
|
for (const comp of message.components) {
|
||||||
|
const data = typeof comp.toJSON === "function" ? comp.toJSON() : comp;
|
||||||
|
if (data.accent_color) {
|
||||||
|
return `#${data.accent_color.toString(16).padStart(6, "0")}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})(),
|
||||||
|
embeds: (message.embeds || []).map(e => ({
|
||||||
|
title: e.title || null,
|
||||||
|
description: e.description || null,
|
||||||
|
color: e.color ? `#${e.color.toString(16).padStart(6, "0")}` : "#5865F2",
|
||||||
|
author: e.author ? { name: e.author.name, iconURL: e.author.iconURL } : null,
|
||||||
|
fields: (e.fields || []).map(f => ({ name: f.name, value: f.value, inline: f.inline })),
|
||||||
|
footer: e.footer ? { text: e.footer.text, iconURL: e.footer.iconURL } : null,
|
||||||
|
thumbnail: e.thumbnail?.url ? { url: e.thumbnail.url } : null,
|
||||||
|
image: e.image?.url ? { url: e.image.url } : null,
|
||||||
|
timestamp: e.timestamp || null
|
||||||
|
})),
|
||||||
|
components: (message.components || []).map(c => {
|
||||||
|
const data = typeof c.toJSON === "function" ? c.toJSON() : c;
|
||||||
|
return {
|
||||||
|
type: data.type,
|
||||||
|
components: (data.components || []).map(sub => {
|
||||||
|
const subData = typeof sub.toJSON === "function" ? sub.toJSON() : sub;
|
||||||
|
return {
|
||||||
|
type: subData.type,
|
||||||
|
label: subData.label || subData.content || null,
|
||||||
|
style: subData.style || null,
|
||||||
|
url: subData.url || null,
|
||||||
|
customId: subData.custom_id || subData.customId || null,
|
||||||
|
emoji: subData.emoji ? (subData.emoji.name || subData.emoji.id) : null
|
||||||
|
};
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
attachments: Array.from((message.attachments || []).values()).map(a => ({
|
||||||
|
id: a.id,
|
||||||
|
name: a.name,
|
||||||
|
url: a.url,
|
||||||
|
contentType: a.contentType
|
||||||
|
})),
|
||||||
|
createdAt: message.createdTimestamp
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.author.bot || !message.guild) return;
|
||||||
|
|
||||||
|
let shouldRespond =
|
||||||
|
CHANNELS.includes(message.channelId) ||
|
||||||
|
message.mentions.users.has(client.user.id);
|
||||||
|
|
||||||
|
if (!shouldRespond && message.channel.isThread()) {
|
||||||
|
let ticket = null;
|
||||||
|
|
||||||
|
if (typeof db.getTicketByAIChannel === "function") {
|
||||||
|
ticket = db.getTicketByAIChannel(message.channel.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ticket && typeof db.getTicket === "function") {
|
||||||
|
ticket = db.getTicket(message.channel.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!ticket &&
|
||||||
|
message.channel.parentId &&
|
||||||
|
typeof db.getTicket === "function"
|
||||||
|
) {
|
||||||
|
ticket = db.getTicket(message.channel.parentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
ticket &&
|
||||||
|
ticket.aiModeEnabled &&
|
||||||
|
ticket.aiChannelId === message.channel.id
|
||||||
|
) {
|
||||||
|
shouldRespond = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!shouldRespond) return;
|
||||||
|
|
||||||
|
if (!supportbotai.Enabled) {
|
||||||
|
const embed = new Discord.EmbedBuilder()
|
||||||
|
.setAuthor({
|
||||||
|
name: supportbotai.General.Name,
|
||||||
|
iconURL: client.user.displayAvatarURL(),
|
||||||
|
})
|
||||||
|
.setDescription(
|
||||||
|
supportbotai.Messages?.AIDisabled || "AI is currently disabled.",
|
||||||
|
)
|
||||||
|
.setColor(supportbot.Embed.Colours.Error)
|
||||||
|
.setFooter({
|
||||||
|
text: supportbotai.Embed.Footer,
|
||||||
|
iconURL: message.author.displayAvatarURL(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return message.reply({ embeds: [embed] });
|
||||||
|
}
|
||||||
|
|
||||||
|
const typingInterval = setInterval(() => {
|
||||||
|
message.channel.sendTyping().catch(() => { });
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const memoryConfig = supportbotai.Memory || {};
|
||||||
|
const memoryEnabled = memoryConfig.Enabled !== false;
|
||||||
|
|
||||||
|
if (memoryEnabled) {
|
||||||
|
AIDatabase.addMessage({
|
||||||
|
guildId: message.guild.id,
|
||||||
|
channelId: message.channel.id,
|
||||||
|
userId: message.author.id,
|
||||||
|
role: "user",
|
||||||
|
content: message.content,
|
||||||
|
});
|
||||||
|
|
||||||
|
rememberUserFacts(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ticket = getTicketContextForMessage(message);
|
||||||
|
|
||||||
|
const conversation = buildConversationFromMemory(message);
|
||||||
|
|
||||||
|
if (ticket) {
|
||||||
|
conversation.push({
|
||||||
|
role: "system",
|
||||||
|
content: buildTicketContext(ticket, message),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const provider = getProvider();
|
||||||
|
|
||||||
|
const response = await provider.chat({
|
||||||
|
messages: conversation,
|
||||||
|
temperature: supportbotai.General.Temperature || 0.4,
|
||||||
|
maxTokens: supportbotai.General.Tokens || 800,
|
||||||
|
});
|
||||||
|
|
||||||
|
clearInterval(typingInterval);
|
||||||
|
|
||||||
|
if (!response?.content) {
|
||||||
|
const embed = new Discord.EmbedBuilder()
|
||||||
|
.setDescription(
|
||||||
|
supportbotai.Messages?.ErrorResponse ||
|
||||||
|
"I couldn't generate a response.",
|
||||||
|
)
|
||||||
|
.setColor(supportbotai.Embed.Color);
|
||||||
|
|
||||||
|
return message.reply({ embeds: [embed] });
|
||||||
|
}
|
||||||
|
|
||||||
|
const replyContent = response.content;
|
||||||
|
|
||||||
|
if (memoryEnabled) {
|
||||||
|
AIDatabase.addMessage({
|
||||||
|
guildId: message.guild.id,
|
||||||
|
channelId: message.channel.id,
|
||||||
|
userId: client.user.id,
|
||||||
|
role: "assistant",
|
||||||
|
content: replyContent,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (replyContent.length > 4000) {
|
||||||
|
const pasteLink = await createPaste(replyContent);
|
||||||
|
|
||||||
|
if (pasteLink) {
|
||||||
|
const embed = new Discord.EmbedBuilder()
|
||||||
|
.setDescription(
|
||||||
|
(
|
||||||
|
supportbotai.Messages?.MessageTooLong ||
|
||||||
|
"My response was too long, so I uploaded it here: {pastebin}"
|
||||||
|
).replace("{pastebin}", pasteLink),
|
||||||
|
)
|
||||||
|
.setColor(supportbotai.Embed.Color);
|
||||||
|
|
||||||
|
return message.reply({ embeds: [embed] });
|
||||||
|
}
|
||||||
|
|
||||||
|
return message.reply({
|
||||||
|
content: replyContent.slice(0, 1900),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const suggestTicket = shouldSuggestTicket(message, ticket);
|
||||||
|
|
||||||
|
const sentMessage = await sendAIResponse({
|
||||||
|
client,
|
||||||
|
message,
|
||||||
|
replyContent,
|
||||||
|
suggestTicket: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (
|
||||||
|
shouldSuggestTicket(message, ticket) &&
|
||||||
|
supportbotai.Features?.SuggestTickets === true
|
||||||
|
) {
|
||||||
|
const hintMessage = await message.channel.send(
|
||||||
|
supportbotai.Messages?.SuggestTicketText ||
|
||||||
|
"Need more help? React with 🎫 to open a ticket.",
|
||||||
|
);
|
||||||
|
|
||||||
|
await hintMessage
|
||||||
|
.react(supportbotai.Messages?.SuggestTicketReaction || "🎫")
|
||||||
|
.catch(() => { });
|
||||||
|
|
||||||
|
setTimeout(
|
||||||
|
() => {
|
||||||
|
hintMessage.delete().catch(() => { });
|
||||||
|
},
|
||||||
|
5 * 60 * 1000,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
clearInterval(typingInterval);
|
||||||
|
|
||||||
|
console.error("AI Error:", error);
|
||||||
|
|
||||||
|
const embed = new Discord.EmbedBuilder()
|
||||||
|
.setDescription(
|
||||||
|
supportbotai.Messages?.AIError ||
|
||||||
|
supportbotai.Messages?.ErrorResponse ||
|
||||||
|
"There was an error contacting the AI provider.",
|
||||||
|
)
|
||||||
|
.setColor(supportbot.Embed.Colours.Error);
|
||||||
|
|
||||||
|
message.reply({ embeds: [embed] }).catch(() => { });
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
const { EmbedBuilder } = require('discord.js');
|
||||||
|
const fs = require("fs");
|
||||||
|
const yaml = require("js-yaml");
|
||||||
|
const Event = require("../Structures/Event.js");
|
||||||
|
|
||||||
|
const supportbot = require("../Structures/ConfigStore").supportbot;
|
||||||
|
const msgconfig = require("../Structures/ConfigStore").messages;
|
||||||
|
|
||||||
|
module.exports = new Event("messageDelete", async (client, message) => {
|
||||||
|
if (!message.guild) return;
|
||||||
|
|
||||||
|
const author = message.author || (message.partial ? await message.fetch().then(m => m.author).catch(() => null) : null);
|
||||||
|
if (!author || author.bot) return;
|
||||||
|
|
||||||
|
const content = message.content || (message.attachments.size > 0 ? "Contains attachment(s)" : "*No content*");
|
||||||
|
|
||||||
|
const deleteLogChannel = message.guild.channels.cache.get(supportbot.MessageDelete.Channel) ||
|
||||||
|
message.guild.channels.cache.find(c => c.name === supportbot.MessageDelete.Channel);
|
||||||
|
|
||||||
|
const deletedMessageEmbed = new EmbedBuilder()
|
||||||
|
.setColor(supportbot.MessageDelete.Colour)
|
||||||
|
.setTitle("Message Deleted")
|
||||||
|
.setDescription(`> **Channel:** <#${message.channel.id}>\n> **Message ID:** ${message.id}\n> **Message author:** ${author.tag} (${author.id})\n> **Message Created:** <t:${Math.floor(message.createdTimestamp / 1000)}:F>`)
|
||||||
|
.addFields({ name: "Deleted Content", value: content, inline: false })
|
||||||
|
.setThumbnail(author.displayAvatarURL())
|
||||||
|
.setTimestamp();
|
||||||
|
|
||||||
|
if (message.attachments.size > 0) {
|
||||||
|
const attachment = message.attachments.first();
|
||||||
|
deletedMessageEmbed.addFields({ name: "Attachment", value: `[${attachment.name}](${attachment.proxyURL})` });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deleteLogChannel) {
|
||||||
|
deleteLogChannel.send({ embeds: [deletedMessageEmbed] });
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
const supportbot = require("../Structures/ConfigStore").supportbot;
|
||||||
|
const supportbotai = require("../Structures/ConfigStore").supportbotAi;
|
||||||
|
const cmdconfig = require("../Structures/ConfigStore").commands;
|
||||||
|
|
||||||
|
const Event = require("../Structures/Event.js");
|
||||||
|
|
||||||
|
module.exports = new Event(
|
||||||
|
"messageReactionAdd",
|
||||||
|
async (client, reaction, user) => {
|
||||||
|
try {
|
||||||
|
if (user.bot) return;
|
||||||
|
|
||||||
|
if (reaction.partial) {
|
||||||
|
await reaction.fetch().catch(() => null);
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = reaction.message;
|
||||||
|
if (!message || !message.guild) return;
|
||||||
|
|
||||||
|
const reactionEmoji = reaction.emoji?.name || reaction.emoji?.id || null;
|
||||||
|
const expectedReaction =
|
||||||
|
supportbotai.Messages?.SuggestTicketReaction || "🎫";
|
||||||
|
|
||||||
|
if (reactionEmoji !== expectedReaction) return;
|
||||||
|
if (supportbotai.Features?.SuggestTickets !== true) return;
|
||||||
|
if (message.author?.id !== client.user.id) return;
|
||||||
|
|
||||||
|
const expectedText =
|
||||||
|
supportbotai.Messages?.SuggestTicketText ||
|
||||||
|
"Need more help? React with 🎫 to open a ticket.";
|
||||||
|
|
||||||
|
if ((message.content || "").trim() !== expectedText.trim()) return;
|
||||||
|
|
||||||
|
await message.delete().catch(() => {});
|
||||||
|
|
||||||
|
const cmd = client.commands.get(cmdconfig.OpenTicket.Command);
|
||||||
|
if (!cmd) {
|
||||||
|
console.error("Open ticket command not found.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const member = await message.guild.members
|
||||||
|
.fetch(user.id)
|
||||||
|
.catch(() => null);
|
||||||
|
|
||||||
|
if (!member) return;
|
||||||
|
|
||||||
|
const fakeInteraction = {
|
||||||
|
user,
|
||||||
|
member,
|
||||||
|
guild: message.guild,
|
||||||
|
channel: message.channel,
|
||||||
|
client,
|
||||||
|
replied: true,
|
||||||
|
deferred: false,
|
||||||
|
reason: `Opened from AI suggestion in #${message.channel.name}`,
|
||||||
|
options: {
|
||||||
|
getString: () => null,
|
||||||
|
},
|
||||||
|
reply: async () => null,
|
||||||
|
};
|
||||||
|
|
||||||
|
await cmd.run(fakeInteraction);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error handling AI ticket reaction:", error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
const { EmbedBuilder } = require('discord.js');
|
||||||
|
const fs = require("fs");
|
||||||
|
const yaml = require("js-yaml");
|
||||||
|
const Event = require("../Structures/Event.js");
|
||||||
|
|
||||||
|
// Load the configuration files
|
||||||
|
const supportbot = require("../Structures/ConfigStore").supportbot;
|
||||||
|
const msgconfig = require("../Structures/ConfigStore").messages;
|
||||||
|
|
||||||
|
module.exports = new Event("messageUpdate", async (client, oldMessage, newMessage) => {
|
||||||
|
|
||||||
|
// Check if the message is from a guild and not from a bot
|
||||||
|
if (!newMessage.guild || newMessage.author.bot) return;
|
||||||
|
|
||||||
|
// Fetch old message if it's not cached (important for message content)
|
||||||
|
if (!oldMessage.content && oldMessage.partial) {
|
||||||
|
try {
|
||||||
|
oldMessage = await oldMessage.fetch();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error fetching old message:', err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback if content is missing
|
||||||
|
const oldMessageContent = oldMessage.content || (oldMessage.attachments.size > 0 ? 'Contains attachment(s)' : '*No content*');
|
||||||
|
const newMessageContent = newMessage.content || (newMessage.attachments.size > 0 ? 'Contains attachment(s)' : '*No content*');
|
||||||
|
|
||||||
|
// Get the update log channel either by ID or by name
|
||||||
|
const updateLogChannel = newMessage.guild.channels.cache.get(supportbot.MessageUpdate.Channel) ||
|
||||||
|
newMessage.guild.channels.cache.find(channel => channel.name === supportbot.MessageUpdate.Channel);
|
||||||
|
|
||||||
|
// Create the embed for the updated message
|
||||||
|
const updatedMessageEmbed = new EmbedBuilder()
|
||||||
|
.setColor(supportbot.MessageUpdate.Colour)
|
||||||
|
.setTitle("Message Updated")
|
||||||
|
.setDescription(`> **Channel:** <#${newMessage.channel.id}>\n> **Message ID:** [${newMessage.id}](https://discord.com/channels/${newMessage.guild.id}/${newMessage.channel.id}/${newMessage.id})\n> **Message author:** ${newMessage.author.tag} (${newMessage.author.id})\n> **Message Created:** <t:${Math.floor(oldMessage.createdTimestamp / 1000)}:F>`)
|
||||||
|
.addFields(
|
||||||
|
{ name: 'Old Message', value: oldMessageContent, inline: false },
|
||||||
|
{ name: 'New Message', value: newMessageContent, inline: false }
|
||||||
|
)
|
||||||
|
.setThumbnail(newMessage.author.displayAvatarURL())
|
||||||
|
.setTimestamp();
|
||||||
|
|
||||||
|
// If the new message contains an attachment, add it to the embed
|
||||||
|
if (newMessage.attachments.size > 0) {
|
||||||
|
const attachment = newMessage.attachments.first();
|
||||||
|
updatedMessageEmbed.addFields({ name: 'Attachment', value: `[${attachment.name}](${attachment.proxyURL})` });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the embed to the update log channel if the channel is found
|
||||||
|
if (updateLogChannel) {
|
||||||
|
updateLogChannel.send({
|
||||||
|
embeds: [updatedMessageEmbed]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
const Event = require("../Structures/Event.js");
|
||||||
|
const { runBotReady } = require("../Structures/BotStartup.js");
|
||||||
|
|
||||||
|
module.exports = new Event("clientReady", async (client) => {
|
||||||
|
// restartDiscordBot runs startup manually — clientReady often does not re-fire anyway
|
||||||
|
if (client.__explicitStartup) return;
|
||||||
|
await runBotReady(client, { clearConsole: true, logCommands: false });
|
||||||
|
});
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2021 Emerald Services
|
Copyright (c) 2026 Emerald Services
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
in the Software without restriction, including without limitation the rights
|
in the Software without restriction, including without limitation the rights
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
furnished to do so, subject to the following conditions:
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
@@ -1,13 +1,85 @@
|
|||||||

|
<h1 align="center">
|
||||||
|
<br>
|
||||||
## What is SupportBot?
|
<a href="https://github.com/Emerald-Services/SupportBot"><img src="https://cdn.1tzemerald.space/Emerald%20Services/SBThreadHeader.png"></a>
|
||||||
|
<br>
|
||||||
SupportBot is an open-sourced support system for discord. It's completely free to use with high amounts of customisation to match your own brand.
|
SupportBot
|
||||||
|
<br>
|
||||||
## Quick Links
|
</h1>
|
||||||
|
|
||||||
**|** [Discord](https://emeraldsrv.dev/discord) **|**
|
<div align="center">
|
||||||
[Hosting](https://emeraldsrv.dev/hosting) **|**
|
|
||||||
[Documentation](https://docs.emeraldsrv.dev) **|**
|
<a href="https://opensource.org/licenses/MIT">
|
||||||
[Download via Polymart](https://polymart.org/resource/supportbot-1-discord-ticket-bot.518) **|**
|
<img src="https://img.shields.io/badge/license-MIT-green?style=for-the-badge" alt="shield.png">
|
||||||
[Download via MC-Market](https://www.mc-market.org/resources/8940/) **|**
|
</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 | ❌ No |
|
||||||
|
| 7.x | 16.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/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 SupportBot on your own panel.
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const DB_PATH = path.join(__dirname, '../Data/ai-memory.json');
|
||||||
|
|
||||||
|
function load() {
|
||||||
|
try {
|
||||||
|
const raw = fs.readFileSync(DB_PATH, 'utf8');
|
||||||
|
return JSON.parse(raw);
|
||||||
|
} catch (e) {
|
||||||
|
return {
|
||||||
|
ai_conversations: [],
|
||||||
|
ai_summaries: [],
|
||||||
|
ai_facts: [],
|
||||||
|
ai_knowledge: [],
|
||||||
|
ai_sessions: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function save(state) {
|
||||||
|
try {
|
||||||
|
fs.mkdirSync(path.dirname(DB_PATH), { recursive: true });
|
||||||
|
fs.writeFileSync(DB_PATH, JSON.stringify(state, null, 2), 'utf8');
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to persist ai-memory:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = load();
|
||||||
|
|
||||||
|
function nextId(arr) {
|
||||||
|
return arr.length === 0 ? 1 : Math.max(...arr.map(a => a.id || 0)) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
saveUserFact({ guildId, userId, factType, factKey, factValue }) {
|
||||||
|
const existing = state.ai_facts.find(f => f.guild_id === (guildId || null) && f.user_id === (userId || null) && f.fact_type === factType && f.fact_key === factKey);
|
||||||
|
if (existing) {
|
||||||
|
existing.fact_value = factValue;
|
||||||
|
existing.updated_at = Date.now();
|
||||||
|
save(state);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = nextId(state.ai_facts);
|
||||||
|
state.ai_facts.push({ id, guild_id: guildId || null, channel_id: null, user_id: userId || null, fact_type: factType, fact_key: factKey, fact_value: factValue, created_at: Date.now(), updated_at: Date.now() });
|
||||||
|
save(state);
|
||||||
|
},
|
||||||
|
|
||||||
|
getUserFacts(guildId, userId) {
|
||||||
|
return state.ai_facts.filter(f => f.guild_id === (guildId || null) && f.user_id === (userId || null)).sort((a,b) => b.updated_at - a.updated_at);
|
||||||
|
},
|
||||||
|
|
||||||
|
getUserFactsByScope({ guildId = null, userId = null, scope = "guild" }) {
|
||||||
|
if (!userId) return [];
|
||||||
|
if (scope === "global") {
|
||||||
|
return state.ai_facts.filter(f => f.user_id === (userId || null)).sort((a,b) => b.updated_at - a.updated_at);
|
||||||
|
}
|
||||||
|
|
||||||
|
return state.ai_facts.filter(f => f.guild_id === (guildId || null) && f.user_id === (userId || null)).sort((a,b) => b.updated_at - a.updated_at);
|
||||||
|
},
|
||||||
|
|
||||||
|
addMessage({ guildId, channelId, userId, role, content }) {
|
||||||
|
const id = nextId(state.ai_conversations);
|
||||||
|
state.ai_conversations.push({ id, guild_id: guildId || null, channel_id: channelId || null, user_id: userId || null, role, content, created_at: Date.now() });
|
||||||
|
save(state);
|
||||||
|
},
|
||||||
|
|
||||||
|
getRecentMessages(channelId, limit = 15) {
|
||||||
|
const msgs = state.ai_conversations.filter(m => m.channel_id === channelId).sort((a,b) => b.created_at - a.created_at).slice(0, limit).reverse();
|
||||||
|
return msgs;
|
||||||
|
},
|
||||||
|
|
||||||
|
getRecentMessagesByScope({ guildId = null, channelId = null, limit = 15, scope = "guild" }) {
|
||||||
|
if (scope === "channel") {
|
||||||
|
return state.ai_conversations.filter(m => m.channel_id === (channelId || null)).sort((a,b) => b.created_at - a.created_at).slice(0, limit).reverse();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scope === "global") {
|
||||||
|
return state.ai_conversations.sort((a,b) => b.created_at - a.created_at).slice(0, limit).reverse();
|
||||||
|
}
|
||||||
|
|
||||||
|
return state.ai_conversations.filter(m => m.guild_id === (guildId || null)).sort((a,b) => b.created_at - a.created_at).slice(0, limit).reverse();
|
||||||
|
},
|
||||||
|
|
||||||
|
saveSummary({ guildId, channelId, summary }) {
|
||||||
|
const existing = state.ai_summaries.find(s => s.guild_id === (guildId || null) && s.channel_id === (channelId || null));
|
||||||
|
if (existing) {
|
||||||
|
existing.summary = summary;
|
||||||
|
existing.updated_at = Date.now();
|
||||||
|
save(state);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const id = nextId(state.ai_summaries);
|
||||||
|
state.ai_summaries.push({ id, guild_id: guildId || null, channel_id: channelId || null, summary, created_at: Date.now(), updated_at: Date.now() });
|
||||||
|
save(state);
|
||||||
|
},
|
||||||
|
|
||||||
|
getLatestSummary(channelId) {
|
||||||
|
const arr = state.ai_summaries.filter(s => s.channel_id === channelId).sort((a,b) => b.updated_at - a.updated_at);
|
||||||
|
return arr.length ? arr[0] : null;
|
||||||
|
},
|
||||||
|
|
||||||
|
getLatestSummaryByScope({ guildId = null, channelId = null, scope = "guild" }) {
|
||||||
|
if (scope === "channel") {
|
||||||
|
const arr = state.ai_summaries.filter(s => s.channel_id === (channelId || null)).sort((a,b) => b.updated_at - a.updated_at);
|
||||||
|
return arr.length ? arr[0] : null;
|
||||||
|
}
|
||||||
|
if (scope === "global") {
|
||||||
|
const arr = state.ai_summaries.sort((a,b) => b.updated_at - a.updated_at);
|
||||||
|
return arr.length ? arr[0] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const arr = state.ai_summaries.filter(s => s.guild_id === (guildId || null)).sort((a,b) => b.updated_at - a.updated_at);
|
||||||
|
return arr.length ? arr[0] : null;
|
||||||
|
},
|
||||||
|
|
||||||
|
saveFact({ guildId, channelId, factType, factKey, factValue }) {
|
||||||
|
const existing = state.ai_facts.find(f => f.guild_id === (guildId || null) && f.channel_id === (channelId || null) && f.fact_type === factType && f.fact_key === factKey);
|
||||||
|
if (existing) {
|
||||||
|
existing.fact_value = factValue;
|
||||||
|
existing.updated_at = Date.now();
|
||||||
|
save(state);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const id = nextId(state.ai_facts);
|
||||||
|
state.ai_facts.push({ id, guild_id: guildId || null, channel_id: channelId || null, user_id: null, fact_type: factType, fact_key: factKey, fact_value: factValue, created_at: Date.now(), updated_at: Date.now() });
|
||||||
|
save(state);
|
||||||
|
},
|
||||||
|
|
||||||
|
getFacts(channelId) {
|
||||||
|
return state.ai_facts.filter(f => f.channel_id === channelId).sort((a,b) => b.updated_at - a.updated_at);
|
||||||
|
},
|
||||||
|
|
||||||
|
getFactsByScope({ guildId = null, channelId = null, scope = "guild" }) {
|
||||||
|
if (scope === "channel") {
|
||||||
|
return state.ai_facts.filter(f => f.channel_id === (channelId || null) && f.user_id == null).sort((a,b) => b.updated_at - a.updated_at);
|
||||||
|
}
|
||||||
|
if (scope === "global") {
|
||||||
|
return state.ai_facts.filter(f => f.user_id == null).sort((a,b) => b.updated_at - a.updated_at);
|
||||||
|
}
|
||||||
|
|
||||||
|
return state.ai_facts.filter(f => f.guild_id === (guildId || null) && f.user_id == null).sort((a,b) => b.updated_at - a.updated_at);
|
||||||
|
},
|
||||||
|
|
||||||
|
addKnowledge({ sourceType, sourceName, title, content }) {
|
||||||
|
const id = nextId(state.ai_knowledge);
|
||||||
|
state.ai_knowledge.push({ id, source_type: sourceType, source_name: sourceName, title, content, created_at: Date.now(), updated_at: Date.now() });
|
||||||
|
save(state);
|
||||||
|
},
|
||||||
|
|
||||||
|
getKnowledge(limit = 25) {
|
||||||
|
return state.ai_knowledge.sort((a,b) => b.updated_at - a.updated_at).slice(0, limit);
|
||||||
|
},
|
||||||
|
|
||||||
|
clearKnowledge() {
|
||||||
|
state.ai_knowledge = [];
|
||||||
|
save(state);
|
||||||
|
},
|
||||||
|
|
||||||
|
clearConversations(channelId) {
|
||||||
|
if (!channelId) return;
|
||||||
|
state.ai_conversations = state.ai_conversations.filter(c => c.channel_id !== channelId);
|
||||||
|
save(state);
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
|
||||||
|
// SupportBot | Emerald Services
|
||||||
|
// Addon Structure
|
||||||
|
class Command {
|
||||||
|
constructor(options) {
|
||||||
|
this.name = options.name;
|
||||||
|
this.description = options.description;
|
||||||
|
this.permissions = options.permissions;
|
||||||
|
this.options = options.options || [];
|
||||||
|
this.run = options.run;
|
||||||
|
this.defaultPermission = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to dynamically update options
|
||||||
|
setOptions(options) {
|
||||||
|
this.options = options;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Event {
|
||||||
|
constructor(event, run) {
|
||||||
|
this.event = event;
|
||||||
|
this.run = run;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { Command, Event };
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
const axios = require("axios");
|
||||||
|
const { withTimeout } = require("./asyncTimeout.js");
|
||||||
|
const AdmZip = require("adm-zip");
|
||||||
|
const yaml = require("js-yaml");
|
||||||
|
|
||||||
|
const EMERALD_API = "https://emeraldsrv.dev/api/v1";
|
||||||
|
|
||||||
|
function getApiKey() {
|
||||||
|
try {
|
||||||
|
if (fs.existsSync("./Configs/api.yml")) {
|
||||||
|
const config = yaml.load(fs.readFileSync("./Configs/api.yml", "utf8"));
|
||||||
|
return config?.API?.EmeraldAPIKey || "";
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to read api.yml:", e);
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureAddonDirs() {
|
||||||
|
if (!fs.existsSync("./Addons")) {
|
||||||
|
fs.mkdirSync("./Addons", { recursive: true });
|
||||||
|
}
|
||||||
|
if (!fs.existsSync("./Addons/Configs")) {
|
||||||
|
fs.mkdirSync("./Addons/Configs", { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function listCatalogAddons() {
|
||||||
|
const key = getApiKey();
|
||||||
|
if (!key) throw new Error("EmeraldAPIKey not configured in api.yml");
|
||||||
|
|
||||||
|
const res = await withTimeout(
|
||||||
|
axios.get(`${EMERALD_API}/resources`, {
|
||||||
|
headers: { Authorization: `Bearer ${key}` },
|
||||||
|
timeout: 15_000,
|
||||||
|
}),
|
||||||
|
18_000,
|
||||||
|
"Emerald API request timed out",
|
||||||
|
);
|
||||||
|
|
||||||
|
if (res.data.status !== "success") {
|
||||||
|
throw new Error(res.data.message || "Failed to fetch catalog");
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.data.data.map(item => ({
|
||||||
|
id: item.id.toString(),
|
||||||
|
name: item.title,
|
||||||
|
slug: item.slug,
|
||||||
|
description: `Emerald Services Resource - ${item.slug}`,
|
||||||
|
price: item.price,
|
||||||
|
is_external: item.is_external,
|
||||||
|
external_store: item.external_store,
|
||||||
|
repositoryUrl: item.external_url || `https://emeraldsrv.dev`,
|
||||||
|
jsFiles: [],
|
||||||
|
configFiles: []
|
||||||
|
})).sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadFile(id) {
|
||||||
|
const key = getApiKey();
|
||||||
|
const res = await withTimeout(
|
||||||
|
axios.get(`${EMERALD_API}/download?id=${id}`, {
|
||||||
|
headers: { Authorization: `Bearer ${key}` },
|
||||||
|
responseType: "arraybuffer",
|
||||||
|
timeout: 30_000
|
||||||
|
}),
|
||||||
|
35_000,
|
||||||
|
"Addon file download timed out",
|
||||||
|
);
|
||||||
|
return Buffer.from(res.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
function listLocalAddonJs() {
|
||||||
|
ensureAddonDirs();
|
||||||
|
return fs
|
||||||
|
.readdirSync("./Addons")
|
||||||
|
.filter((f) => f.endsWith(".js") && f !== "addon.js");
|
||||||
|
}
|
||||||
|
|
||||||
|
function listLocalAddonConfigs() {
|
||||||
|
ensureAddonDirs();
|
||||||
|
const dir = "./Addons/Configs";
|
||||||
|
if (!fs.existsSync(dir)) return [];
|
||||||
|
return fs
|
||||||
|
.readdirSync(dir)
|
||||||
|
.filter((f) => f.endsWith(".yml") || f.endsWith(".yaml") || f.endsWith(".json"));
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAddonInstalled(catalogEntry) {
|
||||||
|
const localJs = listLocalAddonJs();
|
||||||
|
return localJs.some(f => f.includes(catalogEntry.slug) || f.includes(catalogEntry.id.toString()) || f.includes(catalogEntry.name.replace(/ /g, '')));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function listInstalledAddons(catalog) {
|
||||||
|
const localJs = listLocalAddonJs();
|
||||||
|
const installed = [];
|
||||||
|
|
||||||
|
for (const file of localJs) {
|
||||||
|
const name = file.replace(/\.js$/, "");
|
||||||
|
const match = catalog.find(c => c.slug === name || c.id.toString() === name || c.name.replace(/ /g, '') === name);
|
||||||
|
|
||||||
|
installed.push({
|
||||||
|
id: match ? match.id : name,
|
||||||
|
name: match ? match.name : name,
|
||||||
|
jsFiles: [file],
|
||||||
|
configFiles: listLocalAddonConfigs().filter(f => f.startsWith(name)),
|
||||||
|
custom: !match
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return installed;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function installAddonFromCatalog(addonId) {
|
||||||
|
const catalog = await listCatalogAddons();
|
||||||
|
const addon = catalog.find(c => c.id === addonId.toString());
|
||||||
|
|
||||||
|
if (!addon) {
|
||||||
|
throw new Error(`Addon not found in catalog`);
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureAddonDirs();
|
||||||
|
const content = await downloadFile(addon.id);
|
||||||
|
|
||||||
|
const zip = new AdmZip(content);
|
||||||
|
zip.extractAllTo("./Addons", true);
|
||||||
|
|
||||||
|
return {
|
||||||
|
addon: addon,
|
||||||
|
filesWritten: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function readAddonConfig(filename) {
|
||||||
|
const safe = path.basename(filename);
|
||||||
|
if (!/^[a-zA-Z0-9_.-]+\.(yml|yaml|json)$/.test(safe)) {
|
||||||
|
throw new Error("Invalid config filename");
|
||||||
|
}
|
||||||
|
|
||||||
|
const filePath = path.join("./Addons/Configs", safe);
|
||||||
|
if (!fs.existsSync(filePath)) {
|
||||||
|
throw new Error("Addon config not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const raw = fs.readFileSync(filePath, "utf8");
|
||||||
|
if (safe.endsWith(".json")) {
|
||||||
|
return { filename: safe, format: "json", data: JSON.parse(raw), raw };
|
||||||
|
}
|
||||||
|
|
||||||
|
const yaml = require("js-yaml");
|
||||||
|
return { filename: safe, format: "yaml", data: yaml.load(raw) || {}, raw };
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeAddonConfig(filename, data) {
|
||||||
|
const safe = path.basename(filename);
|
||||||
|
if (!/^[a-zA-Z0-9_.-]+\.(yml|yaml|json)$/.test(safe)) {
|
||||||
|
throw new Error("Invalid config filename");
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureAddonDirs();
|
||||||
|
const filePath = path.join("./Addons/Configs", safe);
|
||||||
|
let content;
|
||||||
|
|
||||||
|
if (safe.endsWith(".json")) {
|
||||||
|
content = `${JSON.stringify(data, null, 2)}\n`;
|
||||||
|
} else {
|
||||||
|
const yaml = require("js-yaml");
|
||||||
|
content = yaml.dump(data, { lineWidth: 120, noRefs: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync(filePath, content);
|
||||||
|
return { filename: safe, path: filePath.replace(/\\/g, "/") };
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
listCatalogAddons,
|
||||||
|
listInstalledAddons,
|
||||||
|
isAddonInstalled,
|
||||||
|
installAddonFromCatalog,
|
||||||
|
listLocalAddonConfigs,
|
||||||
|
readAddonConfig,
|
||||||
|
writeAddonConfig,
|
||||||
|
};
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
function getBotInviteUrl(clientId) {
|
||||||
|
if (!clientId) return null;
|
||||||
|
return `https://discord.com/api/oauth2/authorize?client_id=${clientId}&permissions=8&scope=bot%20applications.commands`;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { getBotInviteUrl };
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
const configStore = require("./ConfigStore.js");
|
||||||
|
const { getPrimaryGuild } = require("./GuildManager.js");
|
||||||
|
const {
|
||||||
|
applyPresence,
|
||||||
|
runBotReady,
|
||||||
|
logSlashCommandsRegistered,
|
||||||
|
} = require("./BotStartup.js");
|
||||||
|
|
||||||
|
function walk(dir, ext = ".js", fileList = []) {
|
||||||
|
if (!fs.existsSync(dir)) return fileList;
|
||||||
|
for (const file of fs.readdirSync(dir)) {
|
||||||
|
const full = path.join(dir, file);
|
||||||
|
if (fs.statSync(full).isDirectory()) {
|
||||||
|
walk(full, ext, fileList);
|
||||||
|
} else if (file.endsWith(ext)) {
|
||||||
|
fileList.push(full.replace(/\\/g, "/"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fileList;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshSlashCommands(client) {
|
||||||
|
if (!client?.application) return { count: 0 };
|
||||||
|
|
||||||
|
const cmdconfig = configStore.commands;
|
||||||
|
const commandFiles = walk("./Commands");
|
||||||
|
client.commands.clear();
|
||||||
|
|
||||||
|
for (const file of commandFiles) {
|
||||||
|
const resolved = path.resolve(file);
|
||||||
|
if (require.cache[resolved]) {
|
||||||
|
delete require.cache[resolved];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let count = 0;
|
||||||
|
for (const file of commandFiles) {
|
||||||
|
const command = require(path.resolve(file));
|
||||||
|
const matchingConfig = Object.values(cmdconfig).find(
|
||||||
|
(entry) =>
|
||||||
|
entry &&
|
||||||
|
typeof entry === "object" &&
|
||||||
|
entry.Command &&
|
||||||
|
String(entry.Command).toLowerCase() ===
|
||||||
|
String(command.name).toLowerCase(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (matchingConfig && matchingConfig.Enabled === false) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
client.commands.set(command.name, command);
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = client.commands.map((cmd) => cmd.data ?? cmd);
|
||||||
|
for (const guild of client.guilds.cache.values()) {
|
||||||
|
await guild.commands.set(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { count };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function reloadBot(client, options = {}) {
|
||||||
|
const { fileKey, refreshCommands = true } = options;
|
||||||
|
|
||||||
|
let reloaded;
|
||||||
|
if (fileKey) {
|
||||||
|
configStore.reloadFile(fileKey);
|
||||||
|
reloaded = [fileKey];
|
||||||
|
} else {
|
||||||
|
reloaded = configStore.reloadBotConfigs();
|
||||||
|
}
|
||||||
|
|
||||||
|
applyPresence(client);
|
||||||
|
|
||||||
|
let commands = null;
|
||||||
|
if (refreshCommands && (!fileKey || fileKey === "commands")) {
|
||||||
|
commands = await refreshSlashCommands(client);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
reloaded,
|
||||||
|
commands,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reload configs from disk, reconnect the Discord session, and refresh presence/commands.
|
||||||
|
* API and dashboard keep running on the same process.
|
||||||
|
*/
|
||||||
|
async function restartDiscordBot(client) {
|
||||||
|
if (!client) {
|
||||||
|
const err = new Error("Bot client not available");
|
||||||
|
err.code = "NO_CLIENT";
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
const reloaded = configStore.reloadBotConfigs();
|
||||||
|
const token = configStore.supportbot?.General?.Token;
|
||||||
|
|
||||||
|
if (!token || String(token).trim() === "" || token === "BOT_TOKEN") {
|
||||||
|
const err = new Error("Invalid or missing bot token in supportbot.yml");
|
||||||
|
err.code = "INVALID_TOKEN";
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("[BotReload] Restarting Discord bot after config change…");
|
||||||
|
client.__restarting = true;
|
||||||
|
|
||||||
|
if (client.user) {
|
||||||
|
try {
|
||||||
|
await client.destroy();
|
||||||
|
} catch (destroyErr) {
|
||||||
|
console.warn("[BotReload] destroy:", destroyErr.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
client.__explicitStartup = true;
|
||||||
|
try {
|
||||||
|
try {
|
||||||
|
await client.login(token);
|
||||||
|
} catch (loginErr) {
|
||||||
|
console.error("[BotReload] login failed:", loginErr.message);
|
||||||
|
const err = new Error(`Could not start bot: ${loginErr.message}`);
|
||||||
|
err.code = "LOGIN_FAILED";
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
// clientReady does not fire again after destroy()+login() — run startup explicitly
|
||||||
|
await runBotReady(client, { clearConsole: true, logCommands: false });
|
||||||
|
const commands = await refreshSlashCommands(client);
|
||||||
|
logSlashCommandsRegistered(client);
|
||||||
|
|
||||||
|
console.log("[BotReload] Bot restarted successfully");
|
||||||
|
return { restarted: true, reloaded, commands };
|
||||||
|
} finally {
|
||||||
|
client.__explicitStartup = false;
|
||||||
|
client.__restarting = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
applyPresence,
|
||||||
|
refreshSlashCommands,
|
||||||
|
reloadBot,
|
||||||
|
restartDiscordBot,
|
||||||
|
};
|
||||||
@@ -0,0 +1,200 @@
|
|||||||
|
const Discord = require("discord.js");
|
||||||
|
const configStore = require("./ConfigStore.js");
|
||||||
|
const {
|
||||||
|
getPrimaryGuild,
|
||||||
|
syncGuildHealth,
|
||||||
|
} = require("./GuildManager.js");
|
||||||
|
|
||||||
|
const ACTIVITY_TYPES = {
|
||||||
|
PLAYING: Discord.ActivityType.Playing,
|
||||||
|
WATCHING: Discord.ActivityType.Watching,
|
||||||
|
LISTENING: Discord.ActivityType.Listening,
|
||||||
|
COMPETING: Discord.ActivityType.Competing,
|
||||||
|
STREAMING: Discord.ActivityType.Streaming,
|
||||||
|
};
|
||||||
|
|
||||||
|
function applyPresence(client) {
|
||||||
|
if (!client?.user) return;
|
||||||
|
|
||||||
|
const activity = configStore.supportbot?.Activity;
|
||||||
|
const typeKey = activity?.Type?.toUpperCase();
|
||||||
|
const type = ACTIVITY_TYPES[typeKey] ?? Discord.ActivityType.Playing;
|
||||||
|
const name = activity?.Status || "Online";
|
||||||
|
|
||||||
|
const activityPayload = { name, type };
|
||||||
|
|
||||||
|
if (typeKey === "STREAMING" && activity?.StreamingURL) {
|
||||||
|
activityPayload.url = activity.StreamingURL;
|
||||||
|
}
|
||||||
|
|
||||||
|
client.user.setPresence({
|
||||||
|
activities: [activityPayload],
|
||||||
|
status: "online",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const c = {
|
||||||
|
reset: "\x1b[0m",
|
||||||
|
gray: "\x1b[90m",
|
||||||
|
green: "\x1b[32m",
|
||||||
|
yellow: "\x1b[33m",
|
||||||
|
bold: "\x1b[1m",
|
||||||
|
white: "\x1b[37m",
|
||||||
|
};
|
||||||
|
|
||||||
|
function line() {
|
||||||
|
console.log(
|
||||||
|
`${c.gray}────────────────────────────────────────────────────────${c.reset}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function section(title) {
|
||||||
|
console.log(`\n${c.green}${title}${c.reset}`);
|
||||||
|
console.log(`${c.gray}${"-".repeat(title.length)}${c.reset}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function info(label, value) {
|
||||||
|
console.log(`${c.gray}${label.padEnd(14)}${c.reset} ${value}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function status(label, ok, details = "") {
|
||||||
|
const icon = ok ? `${c.green}✓${c.reset}` : `${c.yellow}!${c.reset}`;
|
||||||
|
console.log(` ${icon} ${label}${details ? ` (${details})` : ""}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function logStartup(client, options = {}) {
|
||||||
|
const { clearConsole = true } = options;
|
||||||
|
const supportbot = configStore.supportbot;
|
||||||
|
|
||||||
|
if (clearConsole) {
|
||||||
|
console.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
const botName = supportbot?.General?.Name || "SupportBot";
|
||||||
|
const botVersion = supportbot?.SupportBot_Version || "Unknown";
|
||||||
|
const { getBotInviteUrl } = require("./BotInvite.js");
|
||||||
|
const inviteUrl = getBotInviteUrl(client.user.id);
|
||||||
|
|
||||||
|
line();
|
||||||
|
console.log(`${c.bold}${c.green}SupportBot${c.reset}`);
|
||||||
|
console.log(`${c.gray}Emerald Services${c.reset}`);
|
||||||
|
line();
|
||||||
|
|
||||||
|
section("System");
|
||||||
|
info("Bot Name", botName);
|
||||||
|
info("Version", botVersion);
|
||||||
|
info("Node.js", process.version);
|
||||||
|
info("Discord.js", `v${Discord.version}`);
|
||||||
|
const db = require("./Database.js");
|
||||||
|
const dbInfo = db.getDatabaseInfo ? db.getDatabaseInfo() : null;
|
||||||
|
if (dbInfo) {
|
||||||
|
const dbDisplay = dbInfo.type === "mysql"
|
||||||
|
? `${dbInfo.driver} (${dbInfo.host}:${dbInfo.port}/${dbInfo.database})`
|
||||||
|
: `${dbInfo.driver} (${dbInfo.file})`;
|
||||||
|
info("Database", dbDisplay);
|
||||||
|
}
|
||||||
|
info("Logged in as", client.user.tag);
|
||||||
|
info("Guilds", `${client.guilds.cache.size}`);
|
||||||
|
|
||||||
|
section("Links");
|
||||||
|
info("Docs", "https://emerald-services.gitbook.io/");
|
||||||
|
info("Discord", "https://dsc.gg/emerald-dev");
|
||||||
|
|
||||||
|
if (configStore.api?.API?.Enabled) {
|
||||||
|
const port = configStore.api?.API?.Port || 3000;
|
||||||
|
info("Dashboard", `http://localhost:${port}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
info("Invite", inviteUrl);
|
||||||
|
|
||||||
|
console.log("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkSetup() {
|
||||||
|
const { isSetupComplete } = require("./DashboardSetup.js");
|
||||||
|
if (!isSetupComplete()) {
|
||||||
|
const port = configStore.api?.API?.Port || 25575;
|
||||||
|
section("Setup Required");
|
||||||
|
status("Onboarding setup incomplete", false, `http://localhost:${port}/setup`);
|
||||||
|
console.log("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkConfigs(client) {
|
||||||
|
const supportbot = configStore.supportbot;
|
||||||
|
const health = syncGuildHealth(client);
|
||||||
|
|
||||||
|
if (health.status !== "ok") {
|
||||||
|
section("Guild");
|
||||||
|
status("Guild setup", false, health.message || health.status);
|
||||||
|
console.log("");
|
||||||
|
}
|
||||||
|
|
||||||
|
const guild = getPrimaryGuild(client);
|
||||||
|
if (!guild) return;
|
||||||
|
|
||||||
|
section("Config Check");
|
||||||
|
|
||||||
|
const roles = [
|
||||||
|
supportbot?.Roles?.StaffMember?.Admin,
|
||||||
|
supportbot?.Roles?.StaffMember?.Staff,
|
||||||
|
supportbot?.Roles?.AutoRole?.Role,
|
||||||
|
].filter(Boolean);
|
||||||
|
|
||||||
|
const channels = [supportbot?.Ticket?.TicketHome].filter(Boolean);
|
||||||
|
|
||||||
|
const [roleResults, channelResults] = await Promise.all([
|
||||||
|
Promise.all(roles.map((role) => client.getRole(role, guild))),
|
||||||
|
Promise.all(channels.map((channel) => client.getChannel(channel, guild))),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const missingRoles = roles.filter((_, i) => !roleResults[i]);
|
||||||
|
const missingChannels = channels.filter((_, i) => !channelResults[i]);
|
||||||
|
|
||||||
|
if (!missingRoles.length && !missingChannels.length) {
|
||||||
|
status("All configurations validated successfully", true);
|
||||||
|
} else {
|
||||||
|
if (missingRoles.length) {
|
||||||
|
status("Missing roles", false, missingRoles.join(", "));
|
||||||
|
}
|
||||||
|
if (missingChannels.length) {
|
||||||
|
status("Missing channels", false, missingChannels.join(", "));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function logSlashCommandsRegistered(client) {
|
||||||
|
const guild = getPrimaryGuild(client);
|
||||||
|
if (!guild) return;
|
||||||
|
console.log(
|
||||||
|
`\n${c.green}✓${c.reset} ${c.white}Slash Commands Registered${c.reset} ${c.gray}for ${guild.name}${c.reset}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Same work as Events/ready.js — safe to call after config reload reconnects.
|
||||||
|
*/
|
||||||
|
async function runBotReady(client, options = {}) {
|
||||||
|
const { clearConsole = true, logCommands = false } = options;
|
||||||
|
|
||||||
|
syncGuildHealth(client);
|
||||||
|
applyPresence(client);
|
||||||
|
await logStartup(client, { clearConsole });
|
||||||
|
await checkConfigs(client);
|
||||||
|
|
||||||
|
if (logCommands) {
|
||||||
|
logSlashCommandsRegistered(client);
|
||||||
|
}
|
||||||
|
|
||||||
|
checkSetup();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
applyPresence,
|
||||||
|
logStartup,
|
||||||
|
checkConfigs,
|
||||||
|
logSlashCommandsRegistered,
|
||||||
|
runBotReady,
|
||||||
|
};
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
const Discord = require("discord.js");
|
||||||
|
const { GatewayIntentBits, Partials } = require("discord.js");
|
||||||
|
const { Command, Event } = require("./Addon.js");
|
||||||
|
const configStore = require("./ConfigStore.js");
|
||||||
|
const { getPrimaryGuild } = require("./GuildManager.js");
|
||||||
|
|
||||||
|
const supportbot = configStore.supportbot;
|
||||||
|
const cmdconfig = configStore.commands;
|
||||||
|
|
||||||
|
const c = {
|
||||||
|
reset: "\x1b[0m",
|
||||||
|
gray: "\x1b[90m",
|
||||||
|
green: "\x1b[32m",
|
||||||
|
yellow: "\x1b[33m",
|
||||||
|
cyan: "\x1b[36m",
|
||||||
|
red: "\x1b[31m",
|
||||||
|
white: "\x1b[37m",
|
||||||
|
bold: "\x1b[1m",
|
||||||
|
};
|
||||||
|
|
||||||
|
function line() {
|
||||||
|
console.log(`${c.gray}────────────────────────────────────────────────────────${c.reset}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function section(title) {
|
||||||
|
console.log(`\n${c.green}${title}${c.reset}`);
|
||||||
|
console.log(`${c.gray}${"-".repeat(title.length)}${c.reset}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function walk(dir, ext = ".js", fileList = []) {
|
||||||
|
const files = fs.readdirSync(dir);
|
||||||
|
for (const file of files) {
|
||||||
|
const full = `${dir}/${file}`;
|
||||||
|
const stat = fs.statSync(full);
|
||||||
|
if (stat.isDirectory()) {
|
||||||
|
walk(full, ext, fileList);
|
||||||
|
} else if (file.endsWith(ext)) {
|
||||||
|
fileList.push(full);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fileList;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Client extends Discord.Client {
|
||||||
|
constructor() {
|
||||||
|
super({
|
||||||
|
intents: [
|
||||||
|
GatewayIntentBits.Guilds,
|
||||||
|
GatewayIntentBits.GuildMembers,
|
||||||
|
GatewayIntentBits.GuildMessages,
|
||||||
|
GatewayIntentBits.GuildMessageReactions,
|
||||||
|
GatewayIntentBits.GuildEmojisAndStickers,
|
||||||
|
GatewayIntentBits.MessageContent,
|
||||||
|
],
|
||||||
|
partials: [Partials.Message, Partials.Channel, Partials.Reaction],
|
||||||
|
});
|
||||||
|
|
||||||
|
this.commands = new Discord.Collection();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getChannel(channel, guild) {
|
||||||
|
if (!channel) return null;
|
||||||
|
const channelStr = String(channel).toLowerCase();
|
||||||
|
return guild.channels.cache.find(
|
||||||
|
(c) =>
|
||||||
|
(c.type === Discord.ChannelType.GuildText || c.type === Discord.ChannelType.GuildNews) &&
|
||||||
|
(c.id === channel || (c.name && c.name.toLowerCase() === channelStr)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getRole(role, guild) {
|
||||||
|
if (!role) return null;
|
||||||
|
const roleStr = String(role).toLowerCase();
|
||||||
|
return guild.roles.cache.find(
|
||||||
|
(r) => r.id === role || (r.name && r.name.toLowerCase() === roleStr),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCategory(category, guild) {
|
||||||
|
if (!category) return null;
|
||||||
|
const catStr = String(category).toLowerCase();
|
||||||
|
return guild.channels.cache.find(
|
||||||
|
(c) =>
|
||||||
|
c.type === Discord.ChannelType.GuildCategory &&
|
||||||
|
(c.id === category || (c.name && c.name.toLowerCase() === catStr)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async start(token) {
|
||||||
|
const commandFiles = walk("./Commands");
|
||||||
|
|
||||||
|
const allCommands = commandFiles.map((file) => ({
|
||||||
|
file,
|
||||||
|
command: require(`../${file}`),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const enabledCommands = allCommands.filter(({ command }) => {
|
||||||
|
const matchingConfig = Object.values(cmdconfig).find(
|
||||||
|
(entry) =>
|
||||||
|
entry &&
|
||||||
|
typeof entry === "object" &&
|
||||||
|
entry.Command &&
|
||||||
|
String(entry.Command).toLowerCase() === String(command.name).toLowerCase(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!matchingConfig) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return matchingConfig.Enabled !== false;
|
||||||
|
});
|
||||||
|
|
||||||
|
section("Commands");
|
||||||
|
|
||||||
|
enabledCommands.forEach(({ command, file }) => {
|
||||||
|
console.log(
|
||||||
|
`${c.green}✓${c.reset} ${c.white}${command.name}${c.reset} ${c.gray}(${path.basename(file)})${c.reset}`,
|
||||||
|
);
|
||||||
|
this.commands.set(command.name, command);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (supportbot.General.Addons.Enabled) {
|
||||||
|
const addonFiles = fs.readdirSync("./Addons").filter((file) => file.endsWith(".js"));
|
||||||
|
|
||||||
|
section("Addons");
|
||||||
|
|
||||||
|
addonFiles.forEach((file) => {
|
||||||
|
let addon = require(`../Addons/${file}`);
|
||||||
|
|
||||||
|
// Handle single or multiple addons per file
|
||||||
|
const addonArray = Array.isArray(addon) ? addon : [addon];
|
||||||
|
|
||||||
|
addonArray.forEach((item, index) => {
|
||||||
|
// If name is missing, use filename (handling index for arrays)
|
||||||
|
if (!item.name) {
|
||||||
|
item.name = index === 0 ? file.split(".")[0] : `${file.split(".")[0]}_${index}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`${c.green}✓${c.reset} ${c.white}${item.name}${c.reset}`);
|
||||||
|
|
||||||
|
if (item instanceof Command) {
|
||||||
|
this.commands.set(item.name, item);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.events && Array.isArray(item.events)) {
|
||||||
|
item.events.forEach((event) => {
|
||||||
|
if (event instanceof Event) {
|
||||||
|
this.on(event.event, (...args) => event.run(this, ...args));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's a direct Event instance
|
||||||
|
if (item instanceof Event) {
|
||||||
|
this.on(item.event, (...args) => item.run(this, ...args));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.on("clientReady", async () => {
|
||||||
|
const guild = getPrimaryGuild(this);
|
||||||
|
if (!guild) return;
|
||||||
|
await guild.commands.set(this.commands);
|
||||||
|
const { logSlashCommandsRegistered } = require("./BotStartup.js");
|
||||||
|
logSlashCommandsRegistered(this);
|
||||||
|
});
|
||||||
|
|
||||||
|
section("Events");
|
||||||
|
|
||||||
|
fs.readdirSync("./Events")
|
||||||
|
.filter((file) => file.endsWith(".js"))
|
||||||
|
.forEach((file) => {
|
||||||
|
const event = require(`../Events/${file}`);
|
||||||
|
console.log(
|
||||||
|
`${c.green}✓${c.reset} ${c.white}${event.event}${c.reset} ${c.gray}(${file})${c.reset}`,
|
||||||
|
);
|
||||||
|
this.on(event.event, (...args) => event.run(this, ...args));
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("");
|
||||||
|
|
||||||
|
if (process.argv[2] !== "test") {
|
||||||
|
await this.login(token);
|
||||||
|
} else {
|
||||||
|
console.log(`${c.red}RUNNING IN TEST MODE, IF NOT INTENDED, PLEASE USE npm start${c.reset}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Client;
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
// SupportBot | Emerald Services
|
||||||
|
// Command Structure
|
||||||
|
|
||||||
|
class Command {
|
||||||
|
constructor(options) {
|
||||||
|
this.name = options.name;
|
||||||
|
this.description = options.description;
|
||||||
|
this.permissions = options.permissions || [];
|
||||||
|
this.options = options.options || [];
|
||||||
|
this.run = options.run;
|
||||||
|
this.defaultPermission = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
module.exports = Command;
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
// Live config objects — reload mutates in place so existing references stay valid.
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
const yaml = require("js-yaml");
|
||||||
|
|
||||||
|
const CONFIG_DIR = path.join(__dirname, "../Configs");
|
||||||
|
|
||||||
|
const FILE_KEYS = [
|
||||||
|
"supportbot",
|
||||||
|
"commands",
|
||||||
|
"messages",
|
||||||
|
"ticket-panel",
|
||||||
|
"supportbot-ai",
|
||||||
|
"api",
|
||||||
|
];
|
||||||
|
|
||||||
|
function readConfig(name) {
|
||||||
|
return yaml.load(fs.readFileSync(path.join(CONFIG_DIR, `${name}.yml`), "utf8"));
|
||||||
|
}
|
||||||
|
|
||||||
|
function deepAssign(target, source) {
|
||||||
|
if (!target || typeof target !== "object" || Array.isArray(target)) {
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
if (!source || typeof source !== "object" || Array.isArray(source)) {
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key of Object.keys(source)) {
|
||||||
|
const next = source[key];
|
||||||
|
if (
|
||||||
|
next &&
|
||||||
|
typeof next === "object" &&
|
||||||
|
!Array.isArray(next) &&
|
||||||
|
target[key] &&
|
||||||
|
typeof target[key] === "object" &&
|
||||||
|
!Array.isArray(target[key])
|
||||||
|
) {
|
||||||
|
deepAssign(target[key], next);
|
||||||
|
} else {
|
||||||
|
target[key] = next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key of Object.keys(target)) {
|
||||||
|
if (!(key in source)) {
|
||||||
|
delete target[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
const store = {};
|
||||||
|
for (const name of FILE_KEYS) {
|
||||||
|
store[name] = readConfig(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
function reloadFile(fileKey) {
|
||||||
|
if (!FILE_KEYS.includes(fileKey)) {
|
||||||
|
throw new Error(`Unknown config: ${fileKey}`);
|
||||||
|
}
|
||||||
|
const fresh = readConfig(fileKey);
|
||||||
|
deepAssign(store[fileKey], fresh);
|
||||||
|
return store[fileKey];
|
||||||
|
}
|
||||||
|
|
||||||
|
function reloadBotConfigs() {
|
||||||
|
const reloaded = [];
|
||||||
|
for (const name of FILE_KEYS) {
|
||||||
|
if (name === "api") continue;
|
||||||
|
reloadFile(name);
|
||||||
|
reloaded.push(name);
|
||||||
|
}
|
||||||
|
return reloaded;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
supportbot: store.supportbot,
|
||||||
|
commands: store.commands,
|
||||||
|
messages: store.messages,
|
||||||
|
ticketPanel: store["ticket-panel"],
|
||||||
|
supportbotAi: store["supportbot-ai"],
|
||||||
|
api: store.api,
|
||||||
|
store,
|
||||||
|
reloadFile,
|
||||||
|
reloadBotConfigs,
|
||||||
|
readConfig,
|
||||||
|
};
|
||||||
@@ -0,0 +1,229 @@
|
|||||||
|
/**
|
||||||
|
* Dashboard alert registry — server-side providers for actionable banners.
|
||||||
|
*
|
||||||
|
* Register a provider to surface new alerts anywhere in the dashboard:
|
||||||
|
*
|
||||||
|
* const { dashboardAlerts } = require("./DashboardAlerts.js");
|
||||||
|
* dashboardAlerts.register("my-addon", (ctx) => {
|
||||||
|
* if (!ctx.configStore.myAddon?.Configured) {
|
||||||
|
* return [{
|
||||||
|
* id: "my-addon:setup",
|
||||||
|
* severity: "warning",
|
||||||
|
* category: "my-addon",
|
||||||
|
* title: "My addon needs setup",
|
||||||
|
* message: "Finish configuration to enable this feature.",
|
||||||
|
* action: { label: "Configure", href: "/configs?file=my-addon" },
|
||||||
|
* }];
|
||||||
|
* }
|
||||||
|
* return [];
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
const { evaluateGuildHealth } = require("./GuildManager.js");
|
||||||
|
const { evaluateAllModules } = require("./ModuleStatus.js");
|
||||||
|
|
||||||
|
const SEVERITY_ORDER = { error: 0, warning: 1, info: 2 };
|
||||||
|
|
||||||
|
const MODULE_LINKS = {
|
||||||
|
tickets: {
|
||||||
|
title: "Ticket system",
|
||||||
|
href: "/configs?file=ticket-panel",
|
||||||
|
},
|
||||||
|
supportbot: {
|
||||||
|
title: "Core bot settings",
|
||||||
|
href: "/configs?file=supportbot",
|
||||||
|
},
|
||||||
|
transcripts: {
|
||||||
|
title: "Ticket transcripts",
|
||||||
|
href: "/transcripts",
|
||||||
|
},
|
||||||
|
suggestions: {
|
||||||
|
title: "Suggestions",
|
||||||
|
href: "/configs?file=messages",
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
title: "AI assistant",
|
||||||
|
href: "/configs?file=supportbot-ai",
|
||||||
|
},
|
||||||
|
commands: {
|
||||||
|
title: "Commands",
|
||||||
|
href: "/configs?file=commands",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
class DashboardAlertRegistry {
|
||||||
|
constructor() {
|
||||||
|
/** @type {Map<string, (ctx: DashboardAlertContext) => DashboardAlert[] | Promise<DashboardAlert[]>>} */
|
||||||
|
this.providers = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} id Unique provider id (used for debugging / overrides)
|
||||||
|
* @param {(ctx: DashboardAlertContext) => DashboardAlert[] | Promise<DashboardAlert[]>} fn
|
||||||
|
*/
|
||||||
|
register(id, fn) {
|
||||||
|
this.providers.set(id, fn);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
unregister(id) {
|
||||||
|
this.providers.delete(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {DashboardAlertContext} ctx */
|
||||||
|
async evaluate(ctx) {
|
||||||
|
const alerts = [];
|
||||||
|
for (const fn of this.providers.values()) {
|
||||||
|
const result = await fn(ctx);
|
||||||
|
if (Array.isArray(result) && result.length) {
|
||||||
|
alerts.push(...result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sortAlerts(alerts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortAlerts(alerts) {
|
||||||
|
return [...alerts].sort((a, b) => {
|
||||||
|
const sa = SEVERITY_ORDER[a.severity] ?? 9;
|
||||||
|
const sb = SEVERITY_ORDER[b.severity] ?? 9;
|
||||||
|
if (sa !== sb) return sa - sb;
|
||||||
|
return a.title.localeCompare(b.title);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function guildSeverity(status) {
|
||||||
|
if (status === "offline") return "error";
|
||||||
|
return "warning";
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldSkipModuleAlert(moduleId, info) {
|
||||||
|
if (info.status === "done") return true;
|
||||||
|
if (moduleId === "ai" && info.subtitle === "AI is disabled in config") return true;
|
||||||
|
// Transcripts are automatic — empty archive is normal, not a setup step.
|
||||||
|
if (moduleId === "transcripts") return true;
|
||||||
|
// Partially configured modules are tracked on Overview — only banner when nothing is done yet.
|
||||||
|
if (info.status === "progress" && (info.passed ?? 0) > 0) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function moduleSeverity(moduleId, info) {
|
||||||
|
if (info.status === "todo") {
|
||||||
|
return moduleId === "tickets" || moduleId === "supportbot" ? "warning" : "info";
|
||||||
|
}
|
||||||
|
return "info";
|
||||||
|
}
|
||||||
|
|
||||||
|
function evaluateGuildAlerts(ctx) {
|
||||||
|
const { client } = ctx;
|
||||||
|
if (!client?.user) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: "guild:offline",
|
||||||
|
severity: "error",
|
||||||
|
category: "guild",
|
||||||
|
title: "Bot offline",
|
||||||
|
message: "The bot is not connected to Discord. Check your token and restart the server.",
|
||||||
|
action: { label: "Bot config", href: "/configs?file=supportbot" },
|
||||||
|
meta: { status: "offline" },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
const health = evaluateGuildHealth(client);
|
||||||
|
if (health.status === "ok") return [];
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: "guild:health",
|
||||||
|
severity: guildSeverity(health.status),
|
||||||
|
category: "guild",
|
||||||
|
title: "Server setup required",
|
||||||
|
message: health.message,
|
||||||
|
action: { label: "Bot config", href: "/configs?file=supportbot" },
|
||||||
|
meta: {
|
||||||
|
status: health.status,
|
||||||
|
configuredGuildId: health.configuredGuildId,
|
||||||
|
configuredGuild: health.configuredGuild,
|
||||||
|
extraGuilds: health.extraGuilds,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function evaluateModuleAlerts(ctx) {
|
||||||
|
const { configStore, transcriptCount = 0 } = ctx;
|
||||||
|
const modules = evaluateAllModules(configStore, { transcriptCount });
|
||||||
|
const alerts = [];
|
||||||
|
|
||||||
|
for (const [moduleId, info] of Object.entries(modules)) {
|
||||||
|
if (shouldSkipModuleAlert(moduleId, info)) continue;
|
||||||
|
|
||||||
|
const link = MODULE_LINKS[moduleId] || {
|
||||||
|
title: moduleId,
|
||||||
|
href: "/configs",
|
||||||
|
};
|
||||||
|
|
||||||
|
const progress =
|
||||||
|
info.total > 0 ? `${info.passed ?? 0} of ${info.total} checks complete` : null;
|
||||||
|
|
||||||
|
alerts.push({
|
||||||
|
id: `module:${moduleId}`,
|
||||||
|
severity: moduleSeverity(moduleId, info),
|
||||||
|
category: "modules",
|
||||||
|
title:
|
||||||
|
info.status === "todo"
|
||||||
|
? `${link.title} — setup required`
|
||||||
|
: `${link.title} — in progress`,
|
||||||
|
message: info.subtitle,
|
||||||
|
action: { label: "Open module", href: link.href },
|
||||||
|
meta: {
|
||||||
|
moduleId,
|
||||||
|
status: info.status,
|
||||||
|
passed: info.passed,
|
||||||
|
total: info.total,
|
||||||
|
progress,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return alerts;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dashboardAlerts = new DashboardAlertRegistry();
|
||||||
|
dashboardAlerts.register("guild", evaluateGuildAlerts);
|
||||||
|
dashboardAlerts.register("modules", evaluateModuleAlerts);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} DashboardAlertAction
|
||||||
|
* @property {string} label
|
||||||
|
* @property {string} href
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} DashboardAlert
|
||||||
|
* @property {string} id
|
||||||
|
* @property {"error"|"warning"|"info"} severity
|
||||||
|
* @property {string} category
|
||||||
|
* @property {string} title
|
||||||
|
* @property {string} message
|
||||||
|
* @property {DashboardAlertAction} [action]
|
||||||
|
* @property {Record<string, unknown>} [meta]
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} DashboardAlertContext
|
||||||
|
* @property {import("discord.js").Client} [client]
|
||||||
|
* @property {import("./ConfigStore.js")} configStore
|
||||||
|
* @property {number} [transcriptCount]
|
||||||
|
*/
|
||||||
|
|
||||||
|
async function evaluateDashboardAlerts(ctx) {
|
||||||
|
return dashboardAlerts.evaluate(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
dashboardAlerts,
|
||||||
|
evaluateDashboardAlerts,
|
||||||
|
sortAlerts,
|
||||||
|
MODULE_LINKS,
|
||||||
|
};
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
const { fullPermissions, hasPermission, canAccessConfig } = require("./DashboardPermissions.js");
|
||||||
|
|
||||||
|
function grantServiceAccess(req) {
|
||||||
|
req.dashboardService = true;
|
||||||
|
req.dashboardPermissions = fullPermissions();
|
||||||
|
req.dashboardRole = "service";
|
||||||
|
req.dashboardIsOwner = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function forbid(res, message = "Forbidden") {
|
||||||
|
return res.status(403).json({ success: false, error: message });
|
||||||
|
}
|
||||||
|
|
||||||
|
function createPermissionMiddleware(getPermissions) {
|
||||||
|
return function requirePermission(permission) {
|
||||||
|
return (req, res, next) => {
|
||||||
|
if (req.dashboardService) return next();
|
||||||
|
const perms = getPermissions(req) || req.dashboardPermissions;
|
||||||
|
if (hasPermission(perms, permission)) return next();
|
||||||
|
return forbid(res);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createConfigMiddleware(getPermissions) {
|
||||||
|
return function requireConfigAccess(mode = "view") {
|
||||||
|
return (req, res, next) => {
|
||||||
|
if (req.dashboardService) return next();
|
||||||
|
const perms = getPermissions(req) || req.dashboardPermissions;
|
||||||
|
const file = req.params?.file || req.body?.filename;
|
||||||
|
if (!file) return next();
|
||||||
|
if (!canAccessConfig(perms, file, mode)) return forbid(res);
|
||||||
|
return next();
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertConfigFilesEditable(permissions, files, res) {
|
||||||
|
if (!permissions) return forbid(res);
|
||||||
|
for (const file of files) {
|
||||||
|
if (!canAccessConfig(permissions, file, "edit")) {
|
||||||
|
return forbid(res, `No permission to edit ${file}.yml`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
grantServiceAccess,
|
||||||
|
forbid,
|
||||||
|
createPermissionMiddleware,
|
||||||
|
createConfigMiddleware,
|
||||||
|
assertConfigFilesEditable,
|
||||||
|
fullPermissions,
|
||||||
|
hasPermission,
|
||||||
|
canAccessConfig,
|
||||||
|
};
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const BRANDING_DIR = path.join(__dirname, '../Data/branding');
|
||||||
|
const BRANDING_FILE = path.join(BRANDING_DIR, 'branding.json');
|
||||||
|
const FAVICON_BASENAME = 'favicon';
|
||||||
|
|
||||||
|
const DEFAULTS = {
|
||||||
|
title: 'SupportBot',
|
||||||
|
pageTitle: 'SupportBot Dashboard',
|
||||||
|
faviconFile: null,
|
||||||
|
faviconMime: null,
|
||||||
|
updatedAt: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const ALLOWED_MIME = new Set([
|
||||||
|
'image/png',
|
||||||
|
'image/jpeg',
|
||||||
|
'image/x-icon',
|
||||||
|
'image/vnd.microsoft.icon',
|
||||||
|
'image/svg+xml',
|
||||||
|
'image/webp',
|
||||||
|
]);
|
||||||
|
|
||||||
|
const MIME_EXT = {
|
||||||
|
'image/png': '.png',
|
||||||
|
'image/jpeg': '.jpg',
|
||||||
|
'image/x-icon': '.ico',
|
||||||
|
'image/vnd.microsoft.icon': '.ico',
|
||||||
|
'image/svg+xml': '.svg',
|
||||||
|
'image/webp': '.webp',
|
||||||
|
};
|
||||||
|
|
||||||
|
function ensureDir() {
|
||||||
|
if (!fs.existsSync(BRANDING_DIR)) {
|
||||||
|
fs.mkdirSync(BRANDING_DIR, { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function readBranding() {
|
||||||
|
ensureDir();
|
||||||
|
if (!fs.existsSync(BRANDING_FILE)) {
|
||||||
|
return { ...DEFAULTS };
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const raw = JSON.parse(fs.readFileSync(BRANDING_FILE, 'utf8'));
|
||||||
|
return {
|
||||||
|
title: String(raw.title || DEFAULTS.title).slice(0, 64),
|
||||||
|
pageTitle: String(raw.pageTitle || DEFAULTS.pageTitle).slice(0, 80),
|
||||||
|
faviconFile: raw.faviconFile || null,
|
||||||
|
faviconMime: raw.faviconMime || null,
|
||||||
|
updatedAt: raw.updatedAt || null,
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return { ...DEFAULTS };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeBranding(data) {
|
||||||
|
ensureDir();
|
||||||
|
const next = {
|
||||||
|
title: String(data.title || DEFAULTS.title).slice(0, 64),
|
||||||
|
pageTitle: String(data.pageTitle || DEFAULTS.pageTitle).slice(0, 80),
|
||||||
|
faviconFile: data.faviconFile ?? null,
|
||||||
|
faviconMime: data.faviconMime ?? null,
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
};
|
||||||
|
fs.writeFileSync(BRANDING_FILE, JSON.stringify(next, null, 2), 'utf8');
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
function publicPayload(branding) {
|
||||||
|
const hasFavicon = Boolean(branding.faviconFile);
|
||||||
|
return {
|
||||||
|
title: branding.title,
|
||||||
|
pageTitle: branding.pageTitle,
|
||||||
|
hasFavicon,
|
||||||
|
faviconUrl: hasFavicon ? '/api/branding/favicon' : null,
|
||||||
|
updatedAt: branding.updatedAt,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateBranding({ title, pageTitle }) {
|
||||||
|
const current = readBranding();
|
||||||
|
return writeBranding({
|
||||||
|
...current,
|
||||||
|
title: title !== undefined ? title : current.title,
|
||||||
|
pageTitle: pageTitle !== undefined ? pageTitle : current.pageTitle,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeExistingFavicons() {
|
||||||
|
if (!fs.existsSync(BRANDING_DIR)) return;
|
||||||
|
for (const file of fs.readdirSync(BRANDING_DIR)) {
|
||||||
|
if (file.startsWith(`${FAVICON_BASENAME}.`)) {
|
||||||
|
fs.unlinkSync(path.join(BRANDING_DIR, file));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveFaviconFromBase64(dataUrl) {
|
||||||
|
const match = String(dataUrl || '').match(
|
||||||
|
/^data:([\w/+.-]+);base64,([A-Za-z0-9+/=]+)$/,
|
||||||
|
);
|
||||||
|
if (!match) {
|
||||||
|
throw new Error('Invalid image data. Upload a PNG, ICO, SVG, or WebP file.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const mime = match[1];
|
||||||
|
if (!ALLOWED_MIME.has(mime)) {
|
||||||
|
throw new Error('Unsupported image type. Use PNG, ICO, SVG, JPEG, or WebP.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const buffer = Buffer.from(match[2], 'base64');
|
||||||
|
if (buffer.length > 512 * 1024) {
|
||||||
|
throw new Error('Favicon must be 512 KB or smaller.');
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureDir();
|
||||||
|
removeExistingFavicons();
|
||||||
|
|
||||||
|
const ext = MIME_EXT[mime] || '.png';
|
||||||
|
const filename = `${FAVICON_BASENAME}${ext}`;
|
||||||
|
const filePath = path.join(BRANDING_DIR, filename);
|
||||||
|
fs.writeFileSync(filePath, buffer);
|
||||||
|
|
||||||
|
const current = readBranding();
|
||||||
|
return writeBranding({
|
||||||
|
...current,
|
||||||
|
faviconFile: filename,
|
||||||
|
faviconMime: mime,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearFavicon() {
|
||||||
|
removeExistingFavicons();
|
||||||
|
const current = readBranding();
|
||||||
|
return writeBranding({
|
||||||
|
...current,
|
||||||
|
faviconFile: null,
|
||||||
|
faviconMime: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFaviconPath() {
|
||||||
|
const branding = readBranding();
|
||||||
|
if (!branding.faviconFile) return null;
|
||||||
|
const filePath = path.join(BRANDING_DIR, branding.faviconFile);
|
||||||
|
if (!fs.existsSync(filePath)) return null;
|
||||||
|
return { filePath, mime: branding.faviconMime || 'image/png' };
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetBranding() {
|
||||||
|
clearFavicon();
|
||||||
|
return writeBranding({ ...DEFAULTS });
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
readBranding,
|
||||||
|
writeBranding,
|
||||||
|
publicPayload,
|
||||||
|
updateBranding,
|
||||||
|
saveFaviconFromBase64,
|
||||||
|
clearFavicon,
|
||||||
|
getFaviconPath,
|
||||||
|
resetBranding,
|
||||||
|
DEFAULTS,
|
||||||
|
};
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
// SupportBot | Emerald Services
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
const { fullPermissions, normalizePermissions } = require("./DashboardPermissions.js");
|
||||||
|
|
||||||
|
const GROUPS_FILE = path.join(__dirname, "../Data/dashboard-groups.json");
|
||||||
|
|
||||||
|
const DEFAULT_GROUPS = [
|
||||||
|
{
|
||||||
|
id: "owner",
|
||||||
|
name: "Owner",
|
||||||
|
description: "Full system owner access to all features and configs",
|
||||||
|
color: "#10B981", // Emerald Green
|
||||||
|
discordRoleId: null,
|
||||||
|
isSystem: true,
|
||||||
|
permissions: fullPermissions(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "admin",
|
||||||
|
name: "Admin",
|
||||||
|
description: "Full administrative access including user and group management",
|
||||||
|
color: "#3B82F6", // Blue
|
||||||
|
discordRoleId: null,
|
||||||
|
isSystem: true,
|
||||||
|
permissions: fullPermissions(false, false),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "support",
|
||||||
|
name: "Support Staff",
|
||||||
|
description: "Access to Overview, Live Tickets, and Transcripts",
|
||||||
|
color: "#8B5CF6", // Purple
|
||||||
|
discordRoleId: null,
|
||||||
|
isSystem: false,
|
||||||
|
permissions: {
|
||||||
|
overview: true,
|
||||||
|
tickets: true,
|
||||||
|
logs: false,
|
||||||
|
transcripts: true,
|
||||||
|
settings: { view: false, update: false },
|
||||||
|
configs: {
|
||||||
|
supportbot: { view: false, edit: false },
|
||||||
|
"ticket-panel": { view: false, edit: false },
|
||||||
|
commands: { view: false, edit: false },
|
||||||
|
messages: { view: false, edit: false },
|
||||||
|
"supportbot-ai": { view: false, edit: false },
|
||||||
|
api: { view: false, edit: false },
|
||||||
|
},
|
||||||
|
users: { view: false, manage: false },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "viewer",
|
||||||
|
name: "Viewer",
|
||||||
|
description: "Read-only access to dashboard and logs",
|
||||||
|
color: "#6B7280", // Gray
|
||||||
|
discordRoleId: null,
|
||||||
|
isSystem: false,
|
||||||
|
permissions: {
|
||||||
|
overview: true,
|
||||||
|
tickets: true,
|
||||||
|
logs: true,
|
||||||
|
transcripts: true,
|
||||||
|
settings: { view: true, update: false },
|
||||||
|
configs: {
|
||||||
|
supportbot: { view: true, edit: false },
|
||||||
|
"ticket-panel": { view: true, edit: false },
|
||||||
|
commands: { view: true, edit: false },
|
||||||
|
messages: { view: true, edit: false },
|
||||||
|
"supportbot-ai": { view: true, edit: false },
|
||||||
|
api: { view: true, edit: false },
|
||||||
|
},
|
||||||
|
users: { view: false, manage: false },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function ensureStorage() {
|
||||||
|
const dir = path.dirname(GROUPS_FILE);
|
||||||
|
if (!fs.existsSync(dir)) {
|
||||||
|
fs.mkdirSync(dir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fs.existsSync(GROUPS_FILE)) {
|
||||||
|
const initialData = {
|
||||||
|
version: 1,
|
||||||
|
groups: DEFAULT_GROUPS,
|
||||||
|
};
|
||||||
|
fs.writeFileSync(GROUPS_FILE, JSON.stringify(initialData, null, 2), "utf8");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadStore() {
|
||||||
|
ensureStorage();
|
||||||
|
try {
|
||||||
|
const raw = fs.readFileSync(GROUPS_FILE, "utf8");
|
||||||
|
const parsed = JSON.parse(raw);
|
||||||
|
if (parsed && Array.isArray(parsed.groups)) {
|
||||||
|
return parsed.groups;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[GroupStore] Error loading groups file:", err.message);
|
||||||
|
}
|
||||||
|
return DEFAULT_GROUPS;
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveStore(groups) {
|
||||||
|
ensureStorage();
|
||||||
|
const payload = {
|
||||||
|
version: 1,
|
||||||
|
groups,
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
};
|
||||||
|
fs.writeFileSync(GROUPS_FILE, JSON.stringify(payload, null, 2), "utf8");
|
||||||
|
}
|
||||||
|
|
||||||
|
function listGroups() {
|
||||||
|
return loadStore();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getGroup(id) {
|
||||||
|
const groups = loadStore();
|
||||||
|
return groups.find((g) => g.id === id) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createGroup(payload = {}) {
|
||||||
|
const groups = loadStore();
|
||||||
|
const name = (payload.name || "New Group").trim();
|
||||||
|
const slug = name.toLowerCase().replace(/[^a-z0-9_-]+/g, "_");
|
||||||
|
let newId = slug || `group_${Date.now()}`;
|
||||||
|
|
||||||
|
let counter = 1;
|
||||||
|
while (groups.some((g) => g.id === newId)) {
|
||||||
|
newId = `${slug}_${counter++}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newGroup = {
|
||||||
|
id: newId,
|
||||||
|
name,
|
||||||
|
description: (payload.description || "").trim(),
|
||||||
|
color: payload.color || "#10B981",
|
||||||
|
discordRoleId: payload.discordRoleId ? String(payload.discordRoleId).trim() : null,
|
||||||
|
isSystem: false,
|
||||||
|
permissions: normalizePermissions(payload.permissions),
|
||||||
|
createdAt: Date.now(),
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
groups.push(newGroup);
|
||||||
|
saveStore(groups);
|
||||||
|
return newGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateGroup(id, payload = {}) {
|
||||||
|
if (id === "owner") {
|
||||||
|
throw new Error('System group "Owner" cannot be modified.');
|
||||||
|
}
|
||||||
|
const groups = loadStore();
|
||||||
|
const index = groups.findIndex((g) => g.id === id);
|
||||||
|
if (index === -1) {
|
||||||
|
throw new Error(`Group with ID "${id}" not found.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const existing = groups[index];
|
||||||
|
const updatedGroup = {
|
||||||
|
...existing,
|
||||||
|
name: payload.name !== undefined ? payload.name.trim() : existing.name,
|
||||||
|
description: payload.description !== undefined ? payload.description.trim() : existing.description,
|
||||||
|
color: payload.color || existing.color,
|
||||||
|
discordRoleId: payload.discordRoleId !== undefined ? (payload.discordRoleId ? String(payload.discordRoleId).trim() : null) : existing.discordRoleId,
|
||||||
|
permissions: payload.permissions ? normalizePermissions(payload.permissions) : existing.permissions,
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
groups[index] = updatedGroup;
|
||||||
|
saveStore(groups);
|
||||||
|
return updatedGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteGroup(id) {
|
||||||
|
const groups = loadStore();
|
||||||
|
const group = groups.find((g) => g.id === id);
|
||||||
|
if (!group) {
|
||||||
|
throw new Error(`Group with ID "${id}" not found.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (group.isSystem) {
|
||||||
|
throw new Error(`System group "${group.name}" cannot be deleted.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const filtered = groups.filter((g) => g.id !== id);
|
||||||
|
saveStore(filtered);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
listGroups,
|
||||||
|
getGroup,
|
||||||
|
createGroup,
|
||||||
|
updateGroup,
|
||||||
|
deleteGroup,
|
||||||
|
DEFAULT_GROUPS,
|
||||||
|
};
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
const CONFIG_FILES = [
|
||||||
|
"supportbot",
|
||||||
|
"ticket-panel",
|
||||||
|
"commands",
|
||||||
|
"messages",
|
||||||
|
"supportbot-ai",
|
||||||
|
"api",
|
||||||
|
];
|
||||||
|
|
||||||
|
const ROLES = ["owner", "admin", "moderator", "editor", "viewer", "custom"];
|
||||||
|
|
||||||
|
function configMap(view, edit) {
|
||||||
|
const configs = {};
|
||||||
|
for (const file of CONFIG_FILES) {
|
||||||
|
configs[file] = { view: Boolean(view), edit: Boolean(edit) };
|
||||||
|
}
|
||||||
|
return configs;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fullPermissions(rawYaml = true, userManage = true) {
|
||||||
|
return {
|
||||||
|
overview: true,
|
||||||
|
tickets: true,
|
||||||
|
logs: true,
|
||||||
|
transcripts: true,
|
||||||
|
rawYaml: Boolean(rawYaml),
|
||||||
|
settings: { view: true, update: true },
|
||||||
|
configs: configMap(true, true),
|
||||||
|
users: { view: true, manage: Boolean(userManage) },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function permissionsFromRole(role, customPermissions) {
|
||||||
|
switch (role) {
|
||||||
|
case "owner":
|
||||||
|
return fullPermissions(true, true);
|
||||||
|
case "admin":
|
||||||
|
return fullPermissions(false, false);
|
||||||
|
case "moderator":
|
||||||
|
return {
|
||||||
|
overview: true,
|
||||||
|
tickets: true,
|
||||||
|
logs: true,
|
||||||
|
transcripts: true,
|
||||||
|
rawYaml: false,
|
||||||
|
settings: { view: true, update: false },
|
||||||
|
configs: configMap(true, false),
|
||||||
|
users: { view: false, manage: false },
|
||||||
|
};
|
||||||
|
case "editor":
|
||||||
|
return {
|
||||||
|
overview: true,
|
||||||
|
tickets: true,
|
||||||
|
logs: true,
|
||||||
|
transcripts: false,
|
||||||
|
rawYaml: false,
|
||||||
|
settings: { view: true, update: false },
|
||||||
|
configs: configMap(true, true),
|
||||||
|
users: { view: false, manage: false },
|
||||||
|
};
|
||||||
|
case "viewer":
|
||||||
|
return {
|
||||||
|
overview: true,
|
||||||
|
tickets: true,
|
||||||
|
logs: true,
|
||||||
|
transcripts: true,
|
||||||
|
rawYaml: false,
|
||||||
|
settings: { view: true, update: false },
|
||||||
|
configs: configMap(true, false),
|
||||||
|
users: { view: false, manage: false },
|
||||||
|
};
|
||||||
|
case "custom":
|
||||||
|
return normalizePermissions(customPermissions);
|
||||||
|
default:
|
||||||
|
return permissionsFromRole("viewer");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizePermissions(input) {
|
||||||
|
const base = permissionsFromRole("viewer");
|
||||||
|
if (!input || typeof input !== "object") return base;
|
||||||
|
|
||||||
|
return {
|
||||||
|
overview: input.overview ?? base.overview,
|
||||||
|
tickets: input.tickets ?? input.overview ?? base.tickets,
|
||||||
|
logs: input.logs ?? base.logs,
|
||||||
|
transcripts: input.transcripts ?? base.transcripts,
|
||||||
|
rawYaml: Boolean(input.rawYaml ?? input.configs?.rawYaml ?? base.rawYaml),
|
||||||
|
settings: {
|
||||||
|
view: input.settings?.view ?? base.settings.view,
|
||||||
|
update: input.settings?.update ?? base.settings.update,
|
||||||
|
},
|
||||||
|
configs: CONFIG_FILES.reduce((acc, file) => {
|
||||||
|
acc[file] = {
|
||||||
|
view: input.configs?.[file]?.view ?? base.configs[file].view,
|
||||||
|
edit: input.configs?.[file]?.edit ?? base.configs[file].edit,
|
||||||
|
};
|
||||||
|
return acc;
|
||||||
|
}, {}),
|
||||||
|
users: {
|
||||||
|
view: input.users?.view ?? base.users.view,
|
||||||
|
manage: input.users?.manage ?? base.users.manage,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasPermission(permissions, check) {
|
||||||
|
if (!permissions) return false;
|
||||||
|
|
||||||
|
if (check === "overview") return Boolean(permissions.overview);
|
||||||
|
if (check === "tickets") return Boolean(permissions.tickets ?? permissions.overview);
|
||||||
|
if (check === "logs") return Boolean(permissions.logs);
|
||||||
|
if (check === "transcripts") return Boolean(permissions.transcripts);
|
||||||
|
if (check === "configs.raw_yaml" || check === "configs.rawYaml" || check === "rawYaml") {
|
||||||
|
return Boolean(permissions.rawYaml ?? permissions.configs?.rawYaml ?? false);
|
||||||
|
}
|
||||||
|
if (check === "settings.view") return Boolean(permissions.settings?.view);
|
||||||
|
if (check === "settings.update") return Boolean(permissions.settings?.update);
|
||||||
|
if (check === "users.view") return Boolean(permissions.users?.view);
|
||||||
|
if (check === "users.manage") return Boolean(permissions.users?.manage);
|
||||||
|
|
||||||
|
const configView = check.match(/^configs\.([a-z0-9-]+)\.view$/);
|
||||||
|
if (configView) {
|
||||||
|
const file = configView[1];
|
||||||
|
return Boolean(permissions.configs?.[file]?.view);
|
||||||
|
}
|
||||||
|
|
||||||
|
const configEdit = check.match(/^configs\.([a-z0-9-]+)\.edit$/);
|
||||||
|
if (configEdit) {
|
||||||
|
const file = configEdit[1];
|
||||||
|
return Boolean(permissions.configs?.[file]?.edit);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function canAccessConfig(permissions, file, mode = "view") {
|
||||||
|
if (!CONFIG_FILES.includes(file)) return false;
|
||||||
|
return hasPermission(permissions, `configs.${file}.${mode}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
CONFIG_FILES,
|
||||||
|
ROLES,
|
||||||
|
fullPermissions,
|
||||||
|
permissionsFromRole,
|
||||||
|
normalizePermissions,
|
||||||
|
hasPermission,
|
||||||
|
canAccessConfig,
|
||||||
|
};
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
const crypto = require("crypto");
|
||||||
|
|
||||||
|
const SESSION_COOKIE = "sb_dashboard";
|
||||||
|
const SESSION_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
|
function signSession(payload, secret) {
|
||||||
|
const body = Buffer.from(JSON.stringify(payload)).toString("base64url");
|
||||||
|
const sig = crypto
|
||||||
|
.createHmac("sha256", secret)
|
||||||
|
.update(body)
|
||||||
|
.digest("base64url");
|
||||||
|
return `${body}.${sig}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function verifySession(token, secret) {
|
||||||
|
if (!token || typeof token !== "string") return null;
|
||||||
|
const dot = token.lastIndexOf(".");
|
||||||
|
if (dot <= 0) return null;
|
||||||
|
|
||||||
|
const body = token.slice(0, dot);
|
||||||
|
const sig = token.slice(dot + 1);
|
||||||
|
const expected = crypto
|
||||||
|
.createHmac("sha256", secret)
|
||||||
|
.update(body)
|
||||||
|
.digest("base64url");
|
||||||
|
|
||||||
|
if (sig.length !== expected.length) return null;
|
||||||
|
if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload = JSON.parse(
|
||||||
|
Buffer.from(body, "base64url").toString("utf8"),
|
||||||
|
);
|
||||||
|
if (!payload?.userId || !payload?.exp) return null;
|
||||||
|
if (Date.now() > payload.exp) return null;
|
||||||
|
return payload;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSessionPayload(user) {
|
||||||
|
return {
|
||||||
|
userId: user.id,
|
||||||
|
username: user.username,
|
||||||
|
globalName: user.global_name || null,
|
||||||
|
avatar: user.avatar || null,
|
||||||
|
exp: Date.now() + SESSION_MAX_AGE_MS,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function sessionCookieOptions(maxAgeMs = SESSION_MAX_AGE_MS, opts = {}) {
|
||||||
|
const secure =
|
||||||
|
opts.secure !== undefined
|
||||||
|
? Boolean(opts.secure)
|
||||||
|
: process.env.NODE_ENV === "production";
|
||||||
|
return {
|
||||||
|
httpOnly: true,
|
||||||
|
secure,
|
||||||
|
sameSite: "lax",
|
||||||
|
maxAge: maxAgeMs,
|
||||||
|
path: "/",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
SESSION_COOKIE,
|
||||||
|
SESSION_MAX_AGE_MS,
|
||||||
|
signSession,
|
||||||
|
verifySession,
|
||||||
|
createSessionPayload,
|
||||||
|
sessionCookieOptions,
|
||||||
|
};
|
||||||
@@ -0,0 +1,510 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
const axios = require("axios");
|
||||||
|
const YAML = require("yaml");
|
||||||
|
const configStore = require("./ConfigStore.js");
|
||||||
|
const { CALLBACK_PATH } = require("./DiscordOAuth.js");
|
||||||
|
|
||||||
|
const DISCORD_API = "https://discord.com/api/v10";
|
||||||
|
const CONFIG_DIR = path.join(__dirname, "../Configs");
|
||||||
|
const API_FILE = path.join(CONFIG_DIR, "api.yml");
|
||||||
|
const SUPPORTBOT_FILE = path.join(CONFIG_DIR, "supportbot.yml");
|
||||||
|
|
||||||
|
const PLACEHOLDER_PATTERNS = [
|
||||||
|
/DISCORD_CLIENT_ID/i,
|
||||||
|
/DISCORD_CLIENT_SECRET/i,
|
||||||
|
/YOUR_DISCORD_USER_ID/i,
|
||||||
|
/CHANGE_ME/i,
|
||||||
|
/BOT_TOKEN/i,
|
||||||
|
/PUT_RANDOM_STRING/i,
|
||||||
|
];
|
||||||
|
|
||||||
|
function isEmpty(value) {
|
||||||
|
if (value == null) return true;
|
||||||
|
if (typeof value === "boolean") return false;
|
||||||
|
if (typeof value === "number") return false;
|
||||||
|
return String(value).trim() === "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPlaceholder(value) {
|
||||||
|
if (isEmpty(value)) return true;
|
||||||
|
const s = String(value).trim();
|
||||||
|
return PLACEHOLDER_PATTERNS.some((p) => p.test(s));
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDiscordSnowflake(value) {
|
||||||
|
return /^\d{17,20}$/.test(String(value).trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidBotToken(value) {
|
||||||
|
if (isPlaceholder(value)) return false;
|
||||||
|
const s = String(value).trim();
|
||||||
|
return s.length >= 50 && s.includes(".");
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidSecretKey(value) {
|
||||||
|
if (isPlaceholder(value)) return false;
|
||||||
|
return String(value).trim().length >= 16;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readApiRoot() {
|
||||||
|
if (!fs.existsSync(API_FILE)) return null;
|
||||||
|
try {
|
||||||
|
const raw = YAML.parse(fs.readFileSync(API_FILE, "utf8"));
|
||||||
|
return raw?.API ?? raw ?? null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function readSupportbotRoot() {
|
||||||
|
if (!fs.existsSync(SUPPORTBOT_FILE)) return {};
|
||||||
|
try {
|
||||||
|
return YAML.parse(fs.readFileSync(SUPPORTBOT_FILE, "utf8")) || {};
|
||||||
|
} catch {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOwnerIds(oauth) {
|
||||||
|
const ids = [];
|
||||||
|
for (const id of oauth?.OwnerUserIds || []) {
|
||||||
|
const s = String(id).trim();
|
||||||
|
if (s) ids.push(s);
|
||||||
|
}
|
||||||
|
if (ids.length === 0) {
|
||||||
|
for (const id of oauth?.AllowedUserIds || []) {
|
||||||
|
const s = String(id).trim();
|
||||||
|
if (s) ids.push(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSteps(apiRoot, supportbot) {
|
||||||
|
const oauth = apiRoot?.OAuth || {};
|
||||||
|
const token = supportbot?.General?.Token;
|
||||||
|
const dbConfig = supportbot?.Database || {};
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: "botToken",
|
||||||
|
title: "Bot token",
|
||||||
|
description: "From the Discord Developer Portal → your application → Bot → Reset Token.",
|
||||||
|
complete: isValidBotToken(token),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "database",
|
||||||
|
title: "Database storage",
|
||||||
|
description: "Choose SQLite or MySQL database engine for storing bot data.",
|
||||||
|
complete: Boolean(dbConfig.Driver),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "secretKey",
|
||||||
|
title: "Dashboard session secret",
|
||||||
|
description: "Random string used to sign login cookies. Keep it private.",
|
||||||
|
complete: isValidSecretKey(apiRoot?.SecretKey),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "oauth",
|
||||||
|
title: "Discord OAuth app",
|
||||||
|
description: "Same application as your bot — Client ID and Client Secret from OAuth2.",
|
||||||
|
complete:
|
||||||
|
!isPlaceholder(oauth.ClientId) &&
|
||||||
|
!isPlaceholder(oauth.ClientSecret) &&
|
||||||
|
isDiscordSnowflake(oauth.ClientId),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "redirectUri",
|
||||||
|
title: "OAuth redirect URL",
|
||||||
|
description: `Add this exact URL under OAuth2 → Redirects in the Discord portal.`,
|
||||||
|
complete: isValidRedirectUriConfigured(oauth.RedirectUri),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "owners",
|
||||||
|
title: "Dashboard owners",
|
||||||
|
description: "Discord user IDs that can sign in and manage the dashboard (at least one).",
|
||||||
|
complete: getOwnerIds(oauth).length > 0 && getOwnerIds(oauth).every(isDiscordSnowflake),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "emeraldApi",
|
||||||
|
title: "Emerald API",
|
||||||
|
description: "Optional. Used for the one-click addon installer.",
|
||||||
|
complete: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidRedirectUriConfigured(uri) {
|
||||||
|
if (isPlaceholder(uri)) return false;
|
||||||
|
try {
|
||||||
|
const u = new URL(String(uri).trim());
|
||||||
|
if (!["http:", "https:"].includes(u.protocol)) return false;
|
||||||
|
return u.pathname.replace(/\/$/, "") === CALLBACK_PATH.replace(/\/$/, "");
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSetupComplete() {
|
||||||
|
const apiRoot = readApiRoot();
|
||||||
|
if (!apiRoot?.Enabled) return false;
|
||||||
|
const oauth = apiRoot.OAuth || {};
|
||||||
|
if (!oauth.Enabled) return false;
|
||||||
|
const steps = buildSteps(apiRoot, readSupportbotRoot());
|
||||||
|
return steps.every((s) => s.complete);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSetupStatus(req) {
|
||||||
|
const apiRoot = readApiRoot() || {};
|
||||||
|
const steps = buildSteps(apiRoot, readSupportbotRoot());
|
||||||
|
const complete = steps.every((s) => s.complete);
|
||||||
|
|
||||||
|
let suggestedRedirectUri = null;
|
||||||
|
if (req) {
|
||||||
|
const proto =
|
||||||
|
req.get("x-forwarded-proto")?.split(",")[0]?.trim() ||
|
||||||
|
req.protocol ||
|
||||||
|
"http";
|
||||||
|
const host = req.get("x-forwarded-host")?.split(",")[0]?.trim() || req.get("host");
|
||||||
|
if (host) {
|
||||||
|
suggestedRedirectUri = `${proto}://${host}${CALLBACK_PATH}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!suggestedRedirectUri) {
|
||||||
|
suggestedRedirectUri = `http://localhost:${apiRoot.Port || 3000}${CALLBACK_PATH}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
complete,
|
||||||
|
steps,
|
||||||
|
suggestedRedirectUri,
|
||||||
|
oauthCallbackPath: CALLBACK_PATH,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function validateBotToken(token) {
|
||||||
|
const trimmed = String(token || "").trim();
|
||||||
|
if (!isValidBotToken(trimmed)) {
|
||||||
|
return { ok: false, error: "Enter a valid bot token from the Discord Developer Portal." };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [userRes, appRes] = await Promise.all([
|
||||||
|
axios.get(`${DISCORD_API}/users/@me`, {
|
||||||
|
headers: { Authorization: `Bot ${trimmed}` },
|
||||||
|
timeout: 12_000,
|
||||||
|
}),
|
||||||
|
axios.get(`${DISCORD_API}/oauth2/applications/@me`, {
|
||||||
|
headers: { Authorization: `Bot ${trimmed}` },
|
||||||
|
timeout: 12_000,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
bot: {
|
||||||
|
id: userRes.data.id,
|
||||||
|
username: userRes.data.username,
|
||||||
|
discriminator: userRes.data.discriminator,
|
||||||
|
},
|
||||||
|
applicationId: String(appRes.data.id),
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
const status = err.response?.status;
|
||||||
|
if (status === 401) {
|
||||||
|
return { ok: false, error: "Discord rejected this bot token (401 Unauthorized)." };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: err.response?.data?.message || err.message || "Could not verify bot token.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function validateOAuthCredentials(clientId, clientSecret, botToken) {
|
||||||
|
const id = String(clientId || "").trim();
|
||||||
|
const secret = String(clientSecret || "").trim();
|
||||||
|
|
||||||
|
if (!isDiscordSnowflake(id)) {
|
||||||
|
return { ok: false, error: "Client ID must be a numeric Discord application ID." };
|
||||||
|
}
|
||||||
|
if (isPlaceholder(secret) || secret.length < 20) {
|
||||||
|
return { ok: false, error: "Enter the OAuth Client Secret from the Discord portal." };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (botToken) {
|
||||||
|
const botCheck = await validateBotToken(botToken);
|
||||||
|
if (botCheck.ok && botCheck.applicationId && botCheck.applicationId !== id) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: `Client ID does not match the bot application (${botCheck.applicationId}). Use the same app's credentials.`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await axios.post(
|
||||||
|
`${DISCORD_API}/oauth2/token`,
|
||||||
|
new URLSearchParams({ grant_type: "client_credentials", scope: "identify" }),
|
||||||
|
{
|
||||||
|
auth: { username: id, password: secret },
|
||||||
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||||
|
timeout: 12_000,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return { ok: true, clientId: id };
|
||||||
|
} catch (err) {
|
||||||
|
const status = err.response?.status;
|
||||||
|
if (botToken) {
|
||||||
|
const botCheck = await validateBotToken(botToken);
|
||||||
|
if (botCheck.ok && botCheck.applicationId === id) {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
clientId: id,
|
||||||
|
warning:
|
||||||
|
"Could not verify the client secret with Discord (token endpoint). ID matches your bot — double-check the secret in the portal.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (status === 401 || status === 400) {
|
||||||
|
return { ok: false, error: "Invalid Client ID or Client Secret." };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: err.response?.data?.error_description || err.message || "OAuth validation failed.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateRedirectUri(uri) {
|
||||||
|
const trimmed = String(uri || "").trim();
|
||||||
|
if (!isValidRedirectUriConfigured(trimmed)) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: `Redirect URL must end with ${CALLBACK_PATH} and use http or https.`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { ok: true, redirectUri: trimmed };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function validateOwnerUserIds(ownerIds, botToken) {
|
||||||
|
const ids = (Array.isArray(ownerIds) ? ownerIds : [ownerIds])
|
||||||
|
.map((id) => String(id).trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
if (ids.length === 0) {
|
||||||
|
return { ok: false, error: "Add at least one Discord user ID." };
|
||||||
|
}
|
||||||
|
|
||||||
|
const invalid = ids.filter((id) => !isDiscordSnowflake(id));
|
||||||
|
if (invalid.length) {
|
||||||
|
return { ok: false, error: `Invalid user ID: ${invalid[0]}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!botToken || !isValidBotToken(botToken)) {
|
||||||
|
return { ok: true, ownerUserIds: ids, warning: "User IDs format OK (bot token not verified yet)." };
|
||||||
|
}
|
||||||
|
|
||||||
|
const users = [];
|
||||||
|
try {
|
||||||
|
for (const id of ids) {
|
||||||
|
const res = await axios.get(`${DISCORD_API}/users/${id}`, {
|
||||||
|
headers: { Authorization: `Bot ${String(botToken).trim()}` },
|
||||||
|
timeout: 12_000,
|
||||||
|
});
|
||||||
|
users.push({
|
||||||
|
id: res.data.id,
|
||||||
|
username: res.data.username,
|
||||||
|
globalName: res.data.global_name || null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return { ok: true, ownerUserIds: ids, users };
|
||||||
|
} catch (err) {
|
||||||
|
if (err.response?.status === 404) {
|
||||||
|
return { ok: false, error: "One or more user IDs were not found. Check Developer Mode → Copy User ID." };
|
||||||
|
}
|
||||||
|
if (err.response?.status === 403) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: "Bot could not look up users. Ensure the bot is in a server with those users, or verify IDs manually.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { ok: false, error: err.message || "Failed to verify owner user IDs." };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateSecretKey(secretKey) {
|
||||||
|
if (!isValidSecretKey(secretKey)) {
|
||||||
|
return { ok: false, error: "Use at least 16 characters (not CHANGE_ME)." };
|
||||||
|
}
|
||||||
|
return { ok: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function validateDatabase(dbDriver, dbHost, dbPort, dbUser, dbPassword, dbDatabase) {
|
||||||
|
const driver = String(dbDriver || "sqlite").toLowerCase();
|
||||||
|
if (driver === "mysql" || driver === "mariadb") {
|
||||||
|
const host = String(dbHost || "").trim();
|
||||||
|
const port = Number(dbPort) || 3306;
|
||||||
|
const user = String(dbUser || "").trim();
|
||||||
|
const password = String(dbPassword || "");
|
||||||
|
const database = String(dbDatabase || "").trim();
|
||||||
|
|
||||||
|
if (isEmpty(host)) return { ok: false, error: "MySQL Host is required." };
|
||||||
|
if (isEmpty(user)) return { ok: false, error: "MySQL User is required." };
|
||||||
|
if (isEmpty(database)) return { ok: false, error: "MySQL Database Name is required." };
|
||||||
|
|
||||||
|
try {
|
||||||
|
const mysql = require("mysql2/promise");
|
||||||
|
const connection = await mysql.createConnection({
|
||||||
|
host,
|
||||||
|
port,
|
||||||
|
user,
|
||||||
|
password,
|
||||||
|
database,
|
||||||
|
connectTimeout: 5000,
|
||||||
|
});
|
||||||
|
await connection.end();
|
||||||
|
return { ok: true, driver: "mysql" };
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: `MySQL Connection Failed: ${err.message || err.code || "Could not connect to database host."}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ok: true, driver: "sqlite" };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function validateSetupPayload(payload) {
|
||||||
|
const results = {};
|
||||||
|
|
||||||
|
if (payload.botToken != null) {
|
||||||
|
results.botToken = await validateBotToken(payload.botToken);
|
||||||
|
}
|
||||||
|
if (payload.dbDriver != null) {
|
||||||
|
results.database = await validateDatabase(
|
||||||
|
payload.dbDriver,
|
||||||
|
payload.dbHost,
|
||||||
|
payload.dbPort,
|
||||||
|
payload.dbUser,
|
||||||
|
payload.dbPassword,
|
||||||
|
payload.dbDatabase,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (payload.secretKey != null) {
|
||||||
|
results.secretKey = validateSecretKey(payload.secretKey);
|
||||||
|
}
|
||||||
|
if (payload.clientId != null && payload.clientSecret != null) {
|
||||||
|
results.oauth = await validateOAuthCredentials(
|
||||||
|
payload.clientId,
|
||||||
|
payload.clientSecret,
|
||||||
|
payload.botToken,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (payload.redirectUri != null) {
|
||||||
|
results.redirectUri = validateRedirectUri(payload.redirectUri);
|
||||||
|
}
|
||||||
|
if (payload.ownerUserIds != null) {
|
||||||
|
results.owners = await validateOwnerUserIds(payload.ownerUserIds, payload.botToken);
|
||||||
|
}
|
||||||
|
if (payload.emeraldApiKey != null) {
|
||||||
|
const key = String(payload.emeraldApiKey).trim();
|
||||||
|
if (key === "") {
|
||||||
|
results.emeraldApi = { ok: true };
|
||||||
|
} else if (key.length < 10) {
|
||||||
|
results.emeraldApi = { ok: false, error: "Invalid API key." };
|
||||||
|
} else {
|
||||||
|
results.emeraldApi = { ok: true };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const allOk = Object.values(results).every((r) => r?.ok);
|
||||||
|
return { ok: allOk, results };
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureApiFile() {
|
||||||
|
if (fs.existsSync(API_FILE)) return;
|
||||||
|
const example = path.join(CONFIG_DIR, "api.yml.example");
|
||||||
|
if (fs.existsSync(example)) {
|
||||||
|
fs.copyFileSync(example, API_FILE);
|
||||||
|
configStore.reloadFile("api");
|
||||||
|
} else {
|
||||||
|
throw new Error("Configs/api.yml is missing. Copy Configs/api.yml.example to api.yml.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applySetup(payload) {
|
||||||
|
ensureApiFile();
|
||||||
|
|
||||||
|
const ownerIds = (Array.isArray(payload.ownerUserIds)
|
||||||
|
? payload.ownerUserIds
|
||||||
|
: String(payload.ownerUserIds || "").split(/[\s,]+/)
|
||||||
|
)
|
||||||
|
.map((id) => String(id).trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
const apiDoc = YAML.parseDocument(fs.readFileSync(API_FILE, "utf8"));
|
||||||
|
if (!apiDoc.get("API")) apiDoc.set("API", {});
|
||||||
|
|
||||||
|
apiDoc.setIn(["API", "Enabled"], true);
|
||||||
|
apiDoc.setIn(["API", "SecretKey"], String(payload.secretKey).trim());
|
||||||
|
if (payload.emeraldApiKey != null) {
|
||||||
|
apiDoc.setIn(["API", "EmeraldAPIKey"], String(payload.emeraldApiKey).trim());
|
||||||
|
}
|
||||||
|
if (payload.port != null && payload.port !== "") {
|
||||||
|
apiDoc.setIn(["API", "Port"], Number(payload.port) || 3000);
|
||||||
|
}
|
||||||
|
if (!apiDoc.getIn(["API", "OAuth"])) apiDoc.setIn(["API", "OAuth"], {});
|
||||||
|
|
||||||
|
apiDoc.setIn(["API", "OAuth", "Enabled"], true);
|
||||||
|
apiDoc.setIn(["API", "OAuth", "ClientId"], String(payload.clientId).trim());
|
||||||
|
apiDoc.setIn(["API", "OAuth", "ClientSecret"], String(payload.clientSecret).trim());
|
||||||
|
apiDoc.setIn(["API", "OAuth", "RedirectUri"], String(payload.redirectUri).trim());
|
||||||
|
apiDoc.setIn(["API", "OAuth", "OwnerUserIds"], ownerIds);
|
||||||
|
apiDoc.setIn(["API", "OAuth", "AllowedUserIds"], []);
|
||||||
|
|
||||||
|
fs.writeFileSync(API_FILE, apiDoc.toString());
|
||||||
|
|
||||||
|
const sbDoc = YAML.parseDocument(fs.readFileSync(SUPPORTBOT_FILE, "utf8"));
|
||||||
|
if (payload.botToken != null && String(payload.botToken).trim() !== "") {
|
||||||
|
sbDoc.setIn(["General", "Token"], String(payload.botToken).trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payload.dbDriver != null) {
|
||||||
|
const driver = String(payload.dbDriver).toLowerCase() === "mysql" ? "mysql" : "sqlite";
|
||||||
|
sbDoc.setIn(["Database", "Driver"], driver);
|
||||||
|
if (driver === "mysql") {
|
||||||
|
sbDoc.setIn(["Database", "MySQL", "Host"], String(payload.dbHost || "127.0.0.1").trim());
|
||||||
|
sbDoc.setIn(["Database", "MySQL", "Port"], Number(payload.dbPort) || 3306);
|
||||||
|
sbDoc.setIn(["Database", "MySQL", "User"], String(payload.dbUser || "").trim());
|
||||||
|
sbDoc.setIn(["Database", "MySQL", "Password"], String(payload.dbPassword || ""));
|
||||||
|
sbDoc.setIn(["Database", "MySQL", "Database"], String(payload.dbDatabase || "").trim());
|
||||||
|
} else {
|
||||||
|
sbDoc.setIn(["Database", "SQLite", "File"], "./Data/supportbot.db");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync(SUPPORTBOT_FILE, sbDoc.toString());
|
||||||
|
|
||||||
|
configStore.reloadFile("api");
|
||||||
|
configStore.reloadFile("supportbot");
|
||||||
|
|
||||||
|
return {
|
||||||
|
ownerUserIds: ownerIds,
|
||||||
|
api: configStore.api?.API || configStore.api,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
isSetupComplete,
|
||||||
|
getSetupStatus,
|
||||||
|
validateBotToken,
|
||||||
|
validateOAuthCredentials,
|
||||||
|
validateRedirectUri,
|
||||||
|
validateOwnerUserIds,
|
||||||
|
validateSecretKey,
|
||||||
|
validateSetupPayload,
|
||||||
|
applySetup,
|
||||||
|
};
|
||||||
@@ -0,0 +1,311 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
const {
|
||||||
|
CONFIG_FILES,
|
||||||
|
ROLES,
|
||||||
|
fullPermissions,
|
||||||
|
permissionsFromRole,
|
||||||
|
normalizePermissions,
|
||||||
|
hasPermission,
|
||||||
|
canAccessConfig,
|
||||||
|
} = require("./DashboardPermissions.js");
|
||||||
|
const { discordAvatarUrl } = require("./DiscordOAuth.js");
|
||||||
|
|
||||||
|
const STORE_PATH = path.join(__dirname, "../Data/dashboard-users.json");
|
||||||
|
|
||||||
|
function ensureDataDir() {
|
||||||
|
const dir = path.dirname(STORE_PATH);
|
||||||
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
function emptyStore() {
|
||||||
|
return { version: 1, users: {} };
|
||||||
|
}
|
||||||
|
|
||||||
|
function readStore() {
|
||||||
|
ensureDataDir();
|
||||||
|
if (!fs.existsSync(STORE_PATH)) return emptyStore();
|
||||||
|
try {
|
||||||
|
const raw = JSON.parse(fs.readFileSync(STORE_PATH, "utf8"));
|
||||||
|
if (!raw.users || typeof raw.users !== "object") return emptyStore();
|
||||||
|
return raw;
|
||||||
|
} catch {
|
||||||
|
return emptyStore();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeStore(store) {
|
||||||
|
ensureDataDir();
|
||||||
|
fs.writeFileSync(STORE_PATH, JSON.stringify(store, null, 2), "utf8");
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOwnerIds(oauth) {
|
||||||
|
const owners = new Set();
|
||||||
|
for (const id of oauth?.OwnerUserIds || []) owners.add(String(id));
|
||||||
|
for (const id of oauth?.AllowedUserIds || []) owners.add(String(id));
|
||||||
|
return owners;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isYamlOwner(userId, oauth) {
|
||||||
|
return getOwnerIds(oauth).has(String(userId));
|
||||||
|
}
|
||||||
|
|
||||||
|
function bootstrapFromOAuth(oauth) {
|
||||||
|
const store = readStore();
|
||||||
|
const owners = getOwnerIds(oauth);
|
||||||
|
let changed = false;
|
||||||
|
|
||||||
|
for (const id of owners) {
|
||||||
|
if (!store.users[id]) {
|
||||||
|
store.users[id] = {
|
||||||
|
id,
|
||||||
|
username: null,
|
||||||
|
globalName: null,
|
||||||
|
avatar: null,
|
||||||
|
role: "owner",
|
||||||
|
permissions: null,
|
||||||
|
enabled: true,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
createdBy: "system",
|
||||||
|
};
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changed) writeStore(store);
|
||||||
|
return store;
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncDiscordProfile(userId, profile) {
|
||||||
|
const store = readStore();
|
||||||
|
const entry = store.users[userId];
|
||||||
|
if (!entry) return;
|
||||||
|
|
||||||
|
entry.username = profile.username ?? entry.username;
|
||||||
|
entry.globalName = profile.globalName ?? entry.globalName;
|
||||||
|
entry.avatar = profile.avatar ?? entry.avatar;
|
||||||
|
entry.updatedAt = Date.now();
|
||||||
|
writeStore(store);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { getGroup } = require("./DashboardGroupStore.js");
|
||||||
|
|
||||||
|
function resolvePermissions(entry, oauth, userId) {
|
||||||
|
if (isYamlOwner(userId, oauth)) {
|
||||||
|
return { permissions: fullPermissions(), role: "owner", isOwner: true, groupId: "owner" };
|
||||||
|
}
|
||||||
|
if (!entry?.enabled) return null;
|
||||||
|
|
||||||
|
const groupId = entry.groupId || entry.role || "support";
|
||||||
|
const group = getGroup(groupId);
|
||||||
|
|
||||||
|
if (entry.role === "custom" && entry.permissions) {
|
||||||
|
return { permissions: normalizePermissions(entry.permissions), role: "custom", isOwner: false, groupId };
|
||||||
|
}
|
||||||
|
|
||||||
|
const permissions = group ? group.permissions : permissionsFromRole(entry.role || "viewer");
|
||||||
|
return { permissions, role: entry.role || groupId, isOwner: false, groupId };
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAccess(userId, oauth) {
|
||||||
|
bootstrapFromOAuth(oauth);
|
||||||
|
const store = readStore();
|
||||||
|
const id = String(userId);
|
||||||
|
|
||||||
|
if (isYamlOwner(id, oauth)) {
|
||||||
|
const entry = store.users[id];
|
||||||
|
return {
|
||||||
|
entry: entry || { id, role: "owner" },
|
||||||
|
permissions: fullPermissions(),
|
||||||
|
role: "owner",
|
||||||
|
isOwner: true,
|
||||||
|
yamlOwner: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const entry = store.users[id];
|
||||||
|
const resolved = resolvePermissions(entry, oauth, id);
|
||||||
|
if (!resolved) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
entry,
|
||||||
|
permissions: resolved.permissions,
|
||||||
|
role: resolved.role,
|
||||||
|
isOwner: false,
|
||||||
|
yamlOwner: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function canLogin(userId, oauth) {
|
||||||
|
return Boolean(getAccess(userId, oauth));
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatPublicUser(entry, access, discordProfile) {
|
||||||
|
const id = String(entry?.id || discordProfile?.id);
|
||||||
|
const avatarHash = entry?.avatar ?? discordProfile?.avatar ?? null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
username: entry?.username || discordProfile?.username || "Unknown",
|
||||||
|
globalName: entry?.globalName ?? discordProfile?.global_name ?? null,
|
||||||
|
avatar: avatarHash
|
||||||
|
? discordAvatarUrl({ id, avatar: avatarHash })
|
||||||
|
: discordProfile
|
||||||
|
? discordAvatarUrl(discordProfile)
|
||||||
|
: discordAvatarUrl({ id, avatar: null }),
|
||||||
|
role: access.role,
|
||||||
|
isOwner: access.isOwner,
|
||||||
|
permissions: access.permissions,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function listUsers(oauth, discordClient) {
|
||||||
|
bootstrapFromOAuth(oauth);
|
||||||
|
const store = readStore();
|
||||||
|
const yamlOwners = getOwnerIds(oauth);
|
||||||
|
|
||||||
|
let updated = false;
|
||||||
|
|
||||||
|
const usersList = Object.values(store.users).map((entry) => {
|
||||||
|
if (discordClient?.users?.cache) {
|
||||||
|
const cached = discordClient.users.cache.get(String(entry.id));
|
||||||
|
if (cached && (!entry.username || !entry.avatar)) {
|
||||||
|
entry.username = cached.username || entry.username;
|
||||||
|
entry.globalName = cached.globalName || cached.username || entry.globalName;
|
||||||
|
entry.avatar = cached.avatar || entry.avatar;
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const access = resolvePermissions(entry, oauth, entry.id) || {
|
||||||
|
permissions: permissionsFromRole("viewer"),
|
||||||
|
role: entry.role,
|
||||||
|
isOwner: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: entry.id,
|
||||||
|
username: entry.username || (yamlOwners.has(String(entry.id)) ? "Owner" : "User"),
|
||||||
|
globalName: entry.globalName || entry.username || null,
|
||||||
|
avatar: entry.avatar
|
||||||
|
? discordAvatarUrl({ id: entry.id, avatar: entry.avatar })
|
||||||
|
: discordAvatarUrl({ id: entry.id, avatar: null }),
|
||||||
|
role: yamlOwners.has(String(entry.id)) ? "owner" : entry.role || entry.groupId || "support",
|
||||||
|
groupId: entry.groupId || entry.role || "support",
|
||||||
|
permissions:
|
||||||
|
entry.role === "custom"
|
||||||
|
? normalizePermissions(entry.permissions)
|
||||||
|
: access.permissions,
|
||||||
|
enabled: entry.enabled !== false,
|
||||||
|
isOwner: yamlOwners.has(String(entry.id)),
|
||||||
|
yamlOwner: yamlOwners.has(String(entry.id)),
|
||||||
|
createdAt: entry.createdAt,
|
||||||
|
updatedAt: entry.updatedAt,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
if (updated) {
|
||||||
|
writeStore(store);
|
||||||
|
}
|
||||||
|
|
||||||
|
return usersList;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addUser(payload, actorId, oauth) {
|
||||||
|
const id = String(payload.id || "").trim();
|
||||||
|
if (!/^\d{17,20}$/.test(id)) {
|
||||||
|
throw new Error("Invalid Discord user ID");
|
||||||
|
}
|
||||||
|
|
||||||
|
const role = payload.role || payload.groupId || "support";
|
||||||
|
const groupId = payload.groupId || payload.role || "support";
|
||||||
|
|
||||||
|
const store = readStore();
|
||||||
|
if (store.users[id]) throw new Error("User already exists");
|
||||||
|
|
||||||
|
store.users[id] = {
|
||||||
|
id,
|
||||||
|
username: payload.username || null,
|
||||||
|
globalName: payload.globalName || null,
|
||||||
|
avatar: null,
|
||||||
|
role,
|
||||||
|
groupId,
|
||||||
|
permissions:
|
||||||
|
role === "custom" ? normalizePermissions(payload.permissions) : null,
|
||||||
|
enabled: payload.enabled !== false,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
createdBy: String(actorId),
|
||||||
|
};
|
||||||
|
|
||||||
|
writeStore(store);
|
||||||
|
return listUsers(oauth).find((u) => u.id === id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateUser(id, payload, actorId, oauth) {
|
||||||
|
const store = readStore();
|
||||||
|
const entry = store.users[String(id)];
|
||||||
|
if (!entry) throw new Error("User not found");
|
||||||
|
|
||||||
|
if (isYamlOwner(id, oauth) && payload.enabled === false) {
|
||||||
|
throw new Error("Cannot disable an owner defined in api.yml");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payload.groupId) {
|
||||||
|
entry.groupId = payload.groupId;
|
||||||
|
if (!payload.role) entry.role = payload.groupId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payload.role) {
|
||||||
|
entry.role = payload.role;
|
||||||
|
if (!payload.groupId) entry.groupId = payload.role;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payload.enabled !== undefined) entry.enabled = Boolean(payload.enabled);
|
||||||
|
if (payload.username !== undefined) entry.username = payload.username;
|
||||||
|
if (payload.globalName !== undefined) entry.globalName = payload.globalName;
|
||||||
|
|
||||||
|
if (payload.permissions !== undefined) {
|
||||||
|
entry.permissions = normalizePermissions(payload.permissions);
|
||||||
|
if (entry.role !== "custom") entry.role = "custom";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.role === "custom" && payload.permissions === undefined && payload.role !== "custom") {
|
||||||
|
entry.permissions = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.updatedAt = Date.now();
|
||||||
|
writeStore(store);
|
||||||
|
return listUsers(oauth).find((u) => u.id === String(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeUser(id, oauth) {
|
||||||
|
if (isYamlOwner(id, oauth)) {
|
||||||
|
throw new Error("Remove this user from OwnerUserIds in api.yml instead");
|
||||||
|
}
|
||||||
|
|
||||||
|
const store = readStore();
|
||||||
|
if (!store.users[String(id)]) throw new Error("User not found");
|
||||||
|
delete store.users[String(id)];
|
||||||
|
writeStore(store);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
CONFIG_FILES,
|
||||||
|
STORE_PATH,
|
||||||
|
getOwnerIds,
|
||||||
|
isYamlOwner,
|
||||||
|
bootstrapFromOAuth,
|
||||||
|
syncDiscordProfile,
|
||||||
|
getAccess,
|
||||||
|
canLogin,
|
||||||
|
formatPublicUser,
|
||||||
|
listUsers,
|
||||||
|
addUser,
|
||||||
|
updateUser,
|
||||||
|
removeUser,
|
||||||
|
hasPermission,
|
||||||
|
canAccessConfig,
|
||||||
|
ROLES,
|
||||||
|
};
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,233 @@
|
|||||||
|
const crypto = require("crypto");
|
||||||
|
const axios = require("axios");
|
||||||
|
|
||||||
|
const DISCORD_API = "https://discord.com/api";
|
||||||
|
const OAUTH_STATE_COOKIE = "sb_oauth_state";
|
||||||
|
const OAUTH_REDIRECT_COOKIE = "sb_oauth_redirect";
|
||||||
|
const OAUTH_STATE_MAX_AGE_MS = 10 * 60 * 1000;
|
||||||
|
const CALLBACK_PATH = "/api/auth/discord/callback";
|
||||||
|
|
||||||
|
function discordAvatarUrl(user) {
|
||||||
|
if (user.avatar) {
|
||||||
|
const ext = user.avatar.startsWith("a_") ? "gif" : "png";
|
||||||
|
return `https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}.${ext}?size=128`;
|
||||||
|
}
|
||||||
|
const index = Number((BigInt(user.id) >> 22n) % 6n);
|
||||||
|
return `https://cdn.discordapp.com/embed/avatars/${index}.png`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTrustProxySetting(apiConfig) {
|
||||||
|
if (apiConfig?.TrustProxy === false) return false;
|
||||||
|
if (typeof apiConfig?.TrustProxy === "number") return apiConfig.TrustProxy;
|
||||||
|
if (apiConfig?.TrustProxy === true) return 1;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRequestProto(req, apiConfig) {
|
||||||
|
if (apiConfig?.TrustProxy !== false) {
|
||||||
|
const forwarded = req.get("x-forwarded-proto");
|
||||||
|
if (forwarded) {
|
||||||
|
return forwarded.split(",")[0].trim().toLowerCase();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (req.protocol || "http").toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRequestHost(req) {
|
||||||
|
const forwarded = req.get("x-forwarded-host");
|
||||||
|
if (forwarded) {
|
||||||
|
return forwarded.split(",")[0].trim();
|
||||||
|
}
|
||||||
|
return req.get("host") || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRequestOrigin(req, apiConfig) {
|
||||||
|
const proto = getRequestProto(req, apiConfig);
|
||||||
|
const host = getRequestHost(req);
|
||||||
|
if (!host) return null;
|
||||||
|
return `${proto}://${host}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeRedirectUri(uri) {
|
||||||
|
try {
|
||||||
|
const u = new URL(uri);
|
||||||
|
u.hash = "";
|
||||||
|
u.search = "";
|
||||||
|
return u.toString();
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function redirectUriMatchesHostPath(a, b) {
|
||||||
|
try {
|
||||||
|
const ua = new URL(a);
|
||||||
|
const ub = new URL(b);
|
||||||
|
return ua.host === ub.host && ua.pathname === ub.pathname;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectConfiguredRedirectUris(oauth) {
|
||||||
|
const list = [];
|
||||||
|
if (oauth?.RedirectUri) {
|
||||||
|
const normalized = normalizeRedirectUri(oauth.RedirectUri);
|
||||||
|
if (normalized) list.push(normalized);
|
||||||
|
}
|
||||||
|
if (Array.isArray(oauth?.RedirectUris)) {
|
||||||
|
for (const entry of oauth.RedirectUris) {
|
||||||
|
const normalized = normalizeRedirectUri(entry);
|
||||||
|
if (normalized && !list.includes(normalized)) {
|
||||||
|
list.push(normalized);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pick the OAuth redirect_uri for this request. When UseRequestOrigin is enabled
|
||||||
|
* (default), uses the same host/path as RedirectUri but the protocol the user
|
||||||
|
* actually used (http vs https). Register every variant in the Discord portal.
|
||||||
|
*/
|
||||||
|
function resolveOAuthRedirectUri(req, oauth, apiConfig) {
|
||||||
|
const configured = collectConfiguredRedirectUris(oauth);
|
||||||
|
const fallback = configured[0] || oauth?.RedirectUri || "";
|
||||||
|
|
||||||
|
const useRequestOrigin = oauth?.UseRequestOrigin !== false;
|
||||||
|
if (!useRequestOrigin || !req) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
const origin = getRequestOrigin(req, apiConfig);
|
||||||
|
if (!origin) return fallback;
|
||||||
|
|
||||||
|
const dynamic = `${origin}${CALLBACK_PATH}`;
|
||||||
|
const normalizedDynamic = normalizeRedirectUri(dynamic);
|
||||||
|
if (!normalizedDynamic) return fallback;
|
||||||
|
|
||||||
|
if (configured.includes(normalizedDynamic)) {
|
||||||
|
return normalizedDynamic;
|
||||||
|
}
|
||||||
|
|
||||||
|
const matchesConfigured = configured.some((uri) =>
|
||||||
|
redirectUriMatchesHostPath(uri, normalizedDynamic),
|
||||||
|
);
|
||||||
|
if (matchesConfigured) {
|
||||||
|
return normalizedDynamic;
|
||||||
|
}
|
||||||
|
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSecureRequest(req, oauth, apiConfig) {
|
||||||
|
if (oauth?.CookieSecure === true) return true;
|
||||||
|
if (oauth?.CookieSecure === false) return false;
|
||||||
|
if (req) {
|
||||||
|
if (typeof req.secure === "boolean") return req.secure;
|
||||||
|
return getRequestProto(req, apiConfig) === "https";
|
||||||
|
}
|
||||||
|
return process.env.NODE_ENV === "production";
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildAuthorizeUrl(oauth, state, redirectUri) {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
client_id: oauth.ClientId,
|
||||||
|
redirect_uri: redirectUri,
|
||||||
|
response_type: "code",
|
||||||
|
scope: "identify",
|
||||||
|
state,
|
||||||
|
prompt: "none",
|
||||||
|
});
|
||||||
|
return `${DISCORD_API}/oauth2/authorize?${params.toString()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function exchangeCode(oauth, code, redirectUri) {
|
||||||
|
const body = new URLSearchParams({
|
||||||
|
client_id: oauth.ClientId,
|
||||||
|
client_secret: oauth.ClientSecret,
|
||||||
|
grant_type: "authorization_code",
|
||||||
|
code,
|
||||||
|
redirect_uri: redirectUri,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data } = await axios.post(`${DISCORD_API}/oauth2/token`, body, {
|
||||||
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||||
|
timeout: 15000,
|
||||||
|
});
|
||||||
|
|
||||||
|
return data.access_token;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchDiscordUser(accessToken) {
|
||||||
|
const { data } = await axios.get(`${DISCORD_API}/users/@me`, {
|
||||||
|
headers: { Authorization: `Bearer ${accessToken}` },
|
||||||
|
timeout: 15000,
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createOAuthState() {
|
||||||
|
return crypto.randomBytes(24).toString("hex");
|
||||||
|
}
|
||||||
|
|
||||||
|
function oauthStateCookieOptions(req, oauth, apiConfig) {
|
||||||
|
return {
|
||||||
|
httpOnly: true,
|
||||||
|
secure: isSecureRequest(req, oauth, apiConfig),
|
||||||
|
sameSite: "lax",
|
||||||
|
maxAge: OAUTH_STATE_MAX_AGE_MS,
|
||||||
|
path: "/",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function oauthRedirectCookieOptions(req, oauth, apiConfig) {
|
||||||
|
return oauthStateCookieOptions(req, oauth, apiConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isOAuthConfigured(oauth) {
|
||||||
|
return Boolean(
|
||||||
|
oauth?.Enabled &&
|
||||||
|
oauth.ClientId &&
|
||||||
|
oauth.ClientSecret &&
|
||||||
|
oauth.RedirectUri,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isUserAllowed(userId, oauth) {
|
||||||
|
const allowed = oauth?.AllowedUserIds;
|
||||||
|
if (!Array.isArray(allowed) || allowed.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return allowed.map(String).includes(String(userId));
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatPublicUser(user) {
|
||||||
|
return {
|
||||||
|
id: user.id,
|
||||||
|
username: user.username,
|
||||||
|
globalName: user.global_name || null,
|
||||||
|
avatar: discordAvatarUrl(user),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
OAUTH_STATE_COOKIE,
|
||||||
|
OAUTH_REDIRECT_COOKIE,
|
||||||
|
OAUTH_STATE_MAX_AGE_MS,
|
||||||
|
CALLBACK_PATH,
|
||||||
|
getTrustProxySetting,
|
||||||
|
resolveOAuthRedirectUri,
|
||||||
|
isSecureRequest,
|
||||||
|
buildAuthorizeUrl,
|
||||||
|
exchangeCode,
|
||||||
|
fetchDiscordUser,
|
||||||
|
createOAuthState,
|
||||||
|
oauthStateCookieOptions,
|
||||||
|
oauthRedirectCookieOptions,
|
||||||
|
isOAuthConfigured,
|
||||||
|
isUserAllowed,
|
||||||
|
formatPublicUser,
|
||||||
|
discordAvatarUrl,
|
||||||
|
};
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// SupportBot | Emerald Services
|
||||||
|
// Event Structure
|
||||||
|
|
||||||
|
class Event {
|
||||||
|
constructor(event, run) {
|
||||||
|
this.event = event;
|
||||||
|
this.run = run;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Event;
|
||||||
@@ -0,0 +1,277 @@
|
|||||||
|
const { ChannelType } = require("discord.js");
|
||||||
|
const configStore = require("./ConfigStore.js");
|
||||||
|
const { withTimeout } = require("./asyncTimeout.js");
|
||||||
|
|
||||||
|
const RESOURCES_CACHE_MS = 90 * 1000;
|
||||||
|
/** Bump when resource payload shape changes (e.g. added emojis). */
|
||||||
|
const RESOURCES_CACHE_VERSION = 2;
|
||||||
|
let resourcesCache = { guildId: null, data: null, at: 0, version: 0 };
|
||||||
|
let resourcesInflight = null;
|
||||||
|
|
||||||
|
function getConfiguredGuildId() {
|
||||||
|
const id = configStore.supportbot?.General?.GuildId;
|
||||||
|
if (!id || id === "GUILD_ID" || String(id).trim() === "") return null;
|
||||||
|
return String(id).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setConfiguredGuildId(guildId) {
|
||||||
|
if (!configStore.supportbot.General) {
|
||||||
|
configStore.supportbot.General = {};
|
||||||
|
}
|
||||||
|
configStore.supportbot.General.GuildId = guildId ? String(guildId) : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPrimaryGuild(client) {
|
||||||
|
if (!client) return null;
|
||||||
|
|
||||||
|
const configuredId = getConfiguredGuildId();
|
||||||
|
if (configuredId) {
|
||||||
|
const match = client.guilds.cache.get(configuredId);
|
||||||
|
if (match) return match;
|
||||||
|
}
|
||||||
|
|
||||||
|
return client.guilds.cache.first() || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function evaluateGuildHealth(client) {
|
||||||
|
const configuredId = getConfiguredGuildId();
|
||||||
|
const guilds = [...client.guilds.cache.values()];
|
||||||
|
|
||||||
|
if (!configuredId) {
|
||||||
|
return {
|
||||||
|
status: "missing_config",
|
||||||
|
configuredGuildId: null,
|
||||||
|
message:
|
||||||
|
"No guild ID is set. Add your Discord server ID under Bot config → General.",
|
||||||
|
configuredGuild: null,
|
||||||
|
extraGuilds: guilds.map(formatGuildSummary),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const configuredGuild = client.guilds.cache.get(configuredId) || null;
|
||||||
|
const extraGuilds = guilds
|
||||||
|
.filter((g) => g.id !== configuredId)
|
||||||
|
.map(formatGuildSummary);
|
||||||
|
|
||||||
|
if (!configuredGuild) {
|
||||||
|
return {
|
||||||
|
status: "not_in_guild",
|
||||||
|
configuredGuildId: configuredId,
|
||||||
|
message: `The bot is not in the configured server (${configuredId}). Invite it or update the guild ID.`,
|
||||||
|
configuredGuild: null,
|
||||||
|
extraGuilds: guilds.map(formatGuildSummary),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extraGuilds.length > 0) {
|
||||||
|
return {
|
||||||
|
status: "extra_guilds",
|
||||||
|
configuredGuildId: configuredId,
|
||||||
|
message: `The bot is in ${extraGuilds.length} extra server(s). Leave them so only your configured server remains.`,
|
||||||
|
configuredGuild: formatGuildSummary(configuredGuild),
|
||||||
|
extraGuilds,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: "ok",
|
||||||
|
configuredGuildId: configuredId,
|
||||||
|
message: null,
|
||||||
|
configuredGuild: formatGuildSummary(configuredGuild),
|
||||||
|
extraGuilds: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatGuildSummary(guild) {
|
||||||
|
return {
|
||||||
|
id: guild.id,
|
||||||
|
name: guild.name,
|
||||||
|
memberCount: guild.memberCount ?? null,
|
||||||
|
icon: guild.iconURL?.({ size: 128 }) ?? null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncGuildHealth(client) {
|
||||||
|
if (!client?.user) {
|
||||||
|
client.guildHealth = {
|
||||||
|
status: "offline",
|
||||||
|
configuredGuildId: getConfiguredGuildId(),
|
||||||
|
message: "Bot is not connected to Discord.",
|
||||||
|
configuredGuild: null,
|
||||||
|
extraGuilds: [],
|
||||||
|
};
|
||||||
|
return client.guildHealth;
|
||||||
|
}
|
||||||
|
|
||||||
|
client.guildHealth = evaluateGuildHealth(client);
|
||||||
|
return client.guildHealth;
|
||||||
|
}
|
||||||
|
|
||||||
|
function logGuildWarning(client, message) {
|
||||||
|
const name = client?.user?.username || "Bot";
|
||||||
|
console.warn(`\x1b[33m[Guild]\x1b[0m ${name}: ${message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function leaveGuild(client, guildId) {
|
||||||
|
const guild = client.guilds.cache.get(String(guildId));
|
||||||
|
if (!guild) {
|
||||||
|
throw new Error("Bot is not in that server.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const configuredId = getConfiguredGuildId();
|
||||||
|
if (configuredId && guild.id === configuredId) {
|
||||||
|
throw new Error("Cannot leave the configured server. Change the guild ID first.");
|
||||||
|
}
|
||||||
|
|
||||||
|
await guild.leave();
|
||||||
|
syncGuildHealth(client);
|
||||||
|
return syncGuildHealth(client);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildResourcesFromGuild(guild) {
|
||||||
|
const roles = [...guild.roles.cache.values()]
|
||||||
|
.filter((r) => r.id !== guild.id)
|
||||||
|
.sort((a, b) => b.position - a.position)
|
||||||
|
.map((r) => ({
|
||||||
|
id: r.id,
|
||||||
|
name: r.name,
|
||||||
|
color: r.hexColor || null,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const channels = [...guild.channels.cache.values()]
|
||||||
|
.filter(
|
||||||
|
(c) =>
|
||||||
|
c.type === ChannelType.GuildText ||
|
||||||
|
c.type === ChannelType.GuildVoice ||
|
||||||
|
c.type === ChannelType.GuildAnnouncement ||
|
||||||
|
c.type === ChannelType.GuildStageVoice ||
|
||||||
|
c.type === ChannelType.GuildForum,
|
||||||
|
)
|
||||||
|
.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
|
.map((c) => ({
|
||||||
|
id: c.id,
|
||||||
|
name: c.name,
|
||||||
|
type: c.type,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const categories = [...guild.channels.cache.values()]
|
||||||
|
.filter((c) => c.type === ChannelType.GuildCategory)
|
||||||
|
.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
|
.map((c) => ({
|
||||||
|
id: c.id,
|
||||||
|
name: c.name,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const emojis = [...guild.emojis.cache.values()]
|
||||||
|
.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
|
.map((e) => {
|
||||||
|
const animated = Boolean(e.animated);
|
||||||
|
const ext = animated ? "gif" : "png";
|
||||||
|
return {
|
||||||
|
id: e.id,
|
||||||
|
name: e.name,
|
||||||
|
animated,
|
||||||
|
url:
|
||||||
|
e.imageURL({ extension: ext, size: 64 }) ||
|
||||||
|
`https://cdn.discordapp.com/emojis/${e.id}.${ext}?size=64`,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
guildId: guild.id,
|
||||||
|
guildName: guild.name,
|
||||||
|
guildIcon: guild.iconURL({ extension: "png", size: 64 }) || null,
|
||||||
|
roles,
|
||||||
|
channels,
|
||||||
|
categories,
|
||||||
|
emojis,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchGuildResourcesFromDiscord(client, guildId) {
|
||||||
|
const id = guildId || getConfiguredGuildId();
|
||||||
|
if (!id) return null;
|
||||||
|
|
||||||
|
const guild = client.guilds.cache.get(id);
|
||||||
|
if (!guild) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await withTimeout(
|
||||||
|
Promise.all([guild.channels.fetch(), guild.roles.fetch()]),
|
||||||
|
12_000,
|
||||||
|
"Discord channel/role fetch timed out",
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("[Guild] Failed to fetch channels/roles:", err.message);
|
||||||
|
if (guild.roles.cache.size === 0 && guild.channels.cache.size === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await withTimeout(guild.emojis.fetch(), 12_000, "Discord emoji fetch timed out");
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("[Guild] Failed to fetch guild emojis:", err.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildResourcesFromGuild(guild);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchGuildResources(client, guildId) {
|
||||||
|
const id = guildId || getConfiguredGuildId();
|
||||||
|
if (!id || !client?.user) return null;
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
if (
|
||||||
|
resourcesCache.data &&
|
||||||
|
resourcesCache.version === RESOURCES_CACHE_VERSION &&
|
||||||
|
resourcesCache.guildId === id &&
|
||||||
|
now - resourcesCache.at < RESOURCES_CACHE_MS
|
||||||
|
) {
|
||||||
|
return resourcesCache.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resourcesInflight) {
|
||||||
|
try {
|
||||||
|
return await resourcesInflight;
|
||||||
|
} catch {
|
||||||
|
/* fall through to retry */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resourcesInflight = (async () => {
|
||||||
|
const data = await fetchGuildResourcesFromDiscord(client, id);
|
||||||
|
if (data) {
|
||||||
|
resourcesCache = {
|
||||||
|
guildId: id,
|
||||||
|
data,
|
||||||
|
at: Date.now(),
|
||||||
|
version: RESOURCES_CACHE_VERSION,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
})();
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await resourcesInflight;
|
||||||
|
} finally {
|
||||||
|
resourcesInflight = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearGuildResourcesCache() {
|
||||||
|
resourcesCache = { guildId: null, data: null, at: 0, version: 0 };
|
||||||
|
resourcesInflight = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getConfiguredGuildId,
|
||||||
|
setConfiguredGuildId,
|
||||||
|
getPrimaryGuild,
|
||||||
|
evaluateGuildHealth,
|
||||||
|
syncGuildHealth,
|
||||||
|
logGuildWarning,
|
||||||
|
leaveGuild,
|
||||||
|
fetchGuildResources,
|
||||||
|
clearGuildResourcesCache,
|
||||||
|
};
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
const LOG_ROOT = path.resolve(__dirname, "../Logs");
|
||||||
|
const LOG_TYPES = ["Output", "Warn", "Error"];
|
||||||
|
|
||||||
|
function todayDate() {
|
||||||
|
return new Date().toISOString().split("T")[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLogPath(type, date = todayDate()) {
|
||||||
|
return path.join(LOG_ROOT, type, `${type}-${date}.log`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Prefer today's file; fall back to the newest log in the folder. */
|
||||||
|
function resolveLogPath(type) {
|
||||||
|
const todayPath = getLogPath(type);
|
||||||
|
if (fs.existsSync(todayPath)) return todayPath;
|
||||||
|
|
||||||
|
const dir = path.join(LOG_ROOT, type);
|
||||||
|
if (!fs.existsSync(dir)) return todayPath;
|
||||||
|
|
||||||
|
const files = fs
|
||||||
|
.readdirSync(dir)
|
||||||
|
.filter((f) => f.endsWith(".log"))
|
||||||
|
.sort()
|
||||||
|
.reverse();
|
||||||
|
|
||||||
|
if (!files.length) return todayPath;
|
||||||
|
return path.join(dir, files[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stripAnsi(text) {
|
||||||
|
return String(text).replace(/\x1b\[[0-9;]*m/g, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseLogLine(line, type) {
|
||||||
|
const trimmed = line.trimEnd();
|
||||||
|
if (!trimmed) return null;
|
||||||
|
|
||||||
|
const match = trimmed.match(/^\[([^\]]+)\]\s?(.*)$/);
|
||||||
|
if (match) {
|
||||||
|
return {
|
||||||
|
type,
|
||||||
|
timestamp: match[1],
|
||||||
|
message: stripAnsi(match[2]),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type,
|
||||||
|
timestamp: null,
|
||||||
|
message: stripAnsi(trimmed),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function readFromByte(filePath, fromByte) {
|
||||||
|
if (!fs.existsSync(filePath)) {
|
||||||
|
return { lines: [], position: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const stat = fs.statSync(filePath);
|
||||||
|
if (stat.size === 0) {
|
||||||
|
return { lines: [], position: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const start = Math.max(0, Math.min(fromByte, stat.size));
|
||||||
|
if (start >= stat.size) {
|
||||||
|
return { lines: [], position: stat.size };
|
||||||
|
}
|
||||||
|
|
||||||
|
const fd = fs.openSync(filePath, "r");
|
||||||
|
const buffer = Buffer.alloc(stat.size - start);
|
||||||
|
fs.readSync(fd, buffer, 0, buffer.length, start);
|
||||||
|
fs.closeSync(fd);
|
||||||
|
|
||||||
|
const text = buffer.toString("utf8");
|
||||||
|
const lines = text.split(/\r?\n/).filter((l) => l.length > 0);
|
||||||
|
|
||||||
|
return { lines, position: stat.size };
|
||||||
|
}
|
||||||
|
|
||||||
|
function readLastLines(filePath, maxLines) {
|
||||||
|
if (!fs.existsSync(filePath)) {
|
||||||
|
return { lines: [], position: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const stat = fs.statSync(filePath);
|
||||||
|
if (stat.size === 0) {
|
||||||
|
return { lines: [], position: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const chunkSize = Math.min(stat.size, 512 * 1024);
|
||||||
|
const start = Math.max(0, stat.size - chunkSize);
|
||||||
|
const fd = fs.openSync(filePath, "r");
|
||||||
|
const buffer = Buffer.alloc(chunkSize);
|
||||||
|
fs.readSync(fd, buffer, 0, chunkSize, start);
|
||||||
|
fs.closeSync(fd);
|
||||||
|
|
||||||
|
const lines = buffer
|
||||||
|
.toString("utf8")
|
||||||
|
.split(/\r?\n/)
|
||||||
|
.filter((l) => l.length > 0);
|
||||||
|
const slice = lines.slice(-maxLines);
|
||||||
|
|
||||||
|
return { lines: slice, position: stat.size };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string[]} types
|
||||||
|
* @param {Record<string, number>} cursor byte offsets per type
|
||||||
|
* @param {number} initialLines lines to load when cursor is empty
|
||||||
|
*/
|
||||||
|
function fetchLogs(types, cursor = {}, initialLines = 300) {
|
||||||
|
const validTypes = types.filter((t) => LOG_TYPES.includes(t));
|
||||||
|
const entries = [];
|
||||||
|
const nextCursor = { ...cursor };
|
||||||
|
|
||||||
|
for (const type of validTypes) {
|
||||||
|
const filePath = resolveLogPath(type);
|
||||||
|
const prevPos = cursor[type] ?? 0;
|
||||||
|
let lines = [];
|
||||||
|
let position = 0;
|
||||||
|
|
||||||
|
if (!fs.existsSync(filePath)) {
|
||||||
|
nextCursor[type] = 0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prevPos === 0 && initialLines > 0) {
|
||||||
|
const tail = readLastLines(filePath, initialLines);
|
||||||
|
lines = tail.lines;
|
||||||
|
position = tail.position;
|
||||||
|
} else if (prevPos > 0) {
|
||||||
|
const chunk = readFromByte(filePath, prevPos);
|
||||||
|
lines = chunk.lines;
|
||||||
|
position = chunk.position;
|
||||||
|
} else {
|
||||||
|
// No tail requested and no cursor — skip content, anchor at EOF
|
||||||
|
position = fs.statSync(filePath).size;
|
||||||
|
}
|
||||||
|
|
||||||
|
nextCursor[type] = position;
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const entry = parseLogLine(line, type);
|
||||||
|
if (entry) entries.push(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
entries.sort((a, b) => {
|
||||||
|
const ta = a.timestamp ? Date.parse(a.timestamp) : 0;
|
||||||
|
const tb = b.timestamp ? Date.parse(b.timestamp) : 0;
|
||||||
|
return ta - tb;
|
||||||
|
});
|
||||||
|
|
||||||
|
return { entries, cursor: nextCursor };
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLogMeta() {
|
||||||
|
const date = todayDate();
|
||||||
|
return LOG_TYPES.map((type) => {
|
||||||
|
const filePath = resolveLogPath(type);
|
||||||
|
const exists = fs.existsSync(filePath) && fs.statSync(filePath).size > 0;
|
||||||
|
return {
|
||||||
|
type,
|
||||||
|
date,
|
||||||
|
file: exists ? path.basename(filePath) : null,
|
||||||
|
size: exists ? fs.statSync(filePath).size : 0,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
LOG_ROOT,
|
||||||
|
LOG_TYPES,
|
||||||
|
fetchLogs,
|
||||||
|
getLogMeta,
|
||||||
|
getLogPath,
|
||||||
|
resolveLogPath,
|
||||||
|
};
|
||||||
@@ -0,0 +1,270 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
const PLACEHOLDER_PATTERNS = [
|
||||||
|
/ROLE_ID/i,
|
||||||
|
/CHANNEL_ID/i,
|
||||||
|
/CATEGORY_ID/i,
|
||||||
|
/TAG_ID/i,
|
||||||
|
/FORUM_CHANNEL_ID/i,
|
||||||
|
/BOT_TOKEN/i,
|
||||||
|
/MODEL_API_KEY/i,
|
||||||
|
/PASTEBIN_API_KEY/i,
|
||||||
|
/YOUR_/i,
|
||||||
|
/PUT_RANDOM_STRING/i,
|
||||||
|
/example\.com/i,
|
||||||
|
];
|
||||||
|
|
||||||
|
function isEmpty(value) {
|
||||||
|
if (value == null) return true;
|
||||||
|
if (typeof value === "boolean") return false;
|
||||||
|
if (typeof value === "number") return false;
|
||||||
|
return String(value).trim() === "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPlaceholder(value) {
|
||||||
|
if (isEmpty(value)) return true;
|
||||||
|
const s = String(value).trim();
|
||||||
|
return PLACEHOLDER_PATTERNS.some((p) => p.test(s));
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDiscordId(value) {
|
||||||
|
return /^\d{17,20}$/.test(String(value).trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
function isConfiguredId(value) {
|
||||||
|
return isDiscordId(value) && !isPlaceholder(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidToken(value) {
|
||||||
|
if (isPlaceholder(value)) return false;
|
||||||
|
const s = String(value).trim();
|
||||||
|
return s.length >= 50 && s.includes(".");
|
||||||
|
}
|
||||||
|
|
||||||
|
function scoreChecks(checks) {
|
||||||
|
const total = checks.length;
|
||||||
|
const passed = checks.filter((c) => c.ok).length;
|
||||||
|
|
||||||
|
if (total === 0) {
|
||||||
|
return { status: "done", subtitle: "Ready", passed: 0, total: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (passed === 0) {
|
||||||
|
const hint = checks.find((c) => !c.ok)?.hint ?? "Not configured yet";
|
||||||
|
return { status: "todo", subtitle: hint, passed, total };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (passed === total) {
|
||||||
|
return { status: "done", subtitle: "Configured and ready", passed, total };
|
||||||
|
}
|
||||||
|
|
||||||
|
const next = checks.find((c) => !c.ok);
|
||||||
|
return {
|
||||||
|
status: "progress",
|
||||||
|
subtitle: next?.hint ?? `${passed} of ${total} steps complete`,
|
||||||
|
passed,
|
||||||
|
total,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function evaluateTickets(supportbot, ticketPanel) {
|
||||||
|
const ticket = supportbot?.Ticket ?? {};
|
||||||
|
const ticketType = String(ticket.TicketType || "channels").toLowerCase();
|
||||||
|
|
||||||
|
return scoreChecks([
|
||||||
|
{
|
||||||
|
ok: ticketPanel?.Panel === true,
|
||||||
|
hint: "Enable the ticket panel",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ok: isConfiguredId(ticket.TicketHome),
|
||||||
|
hint: "Set TicketHome channel ID",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ok:
|
||||||
|
ticketType !== "channels" ||
|
||||||
|
isConfiguredId(ticket.TicketChannelsCategory),
|
||||||
|
hint: "Set ticket category ID",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ok: !isPlaceholder(ticketPanel?.Button?.Text),
|
||||||
|
hint: "Configure panel button text",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function evaluateSupportbot(supportbot) {
|
||||||
|
const roles = supportbot?.Roles?.StaffMember ?? {};
|
||||||
|
const welcome = supportbot?.Welcome ?? {};
|
||||||
|
|
||||||
|
return scoreChecks([
|
||||||
|
{ ok: isValidToken(supportbot?.General?.Token), hint: "Add your bot token" },
|
||||||
|
{
|
||||||
|
ok:
|
||||||
|
isConfiguredId(roles.Staff) ||
|
||||||
|
isConfiguredId(roles.Admin) ||
|
||||||
|
isConfiguredId(roles.Moderator),
|
||||||
|
hint: "Set staff role IDs",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ok: !welcome.Enabled || isConfiguredId(welcome.Channel),
|
||||||
|
hint: "Set welcome channel ID",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ok: !isPlaceholder(supportbot?.Activity?.Status),
|
||||||
|
hint: "Set bot activity status",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function evaluateTranscripts(supportbot, transcriptCount) {
|
||||||
|
const ticketReady = isConfiguredId(supportbot?.Ticket?.TicketHome);
|
||||||
|
|
||||||
|
if (!ticketReady) {
|
||||||
|
// Ticket setup is tracked by the tickets module — avoid a misleading transcripts alert.
|
||||||
|
return {
|
||||||
|
status: "done",
|
||||||
|
subtitle: "Available after ticket channels are configured",
|
||||||
|
passed: 0,
|
||||||
|
total: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (transcriptCount > 0) {
|
||||||
|
return {
|
||||||
|
status: "done",
|
||||||
|
subtitle: `${transcriptCount} transcript${transcriptCount === 1 ? "" : "s"} saved`,
|
||||||
|
passed: 1,
|
||||||
|
total: 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: "done",
|
||||||
|
subtitle: "No transcripts yet — they are created when tickets close",
|
||||||
|
passed: 1,
|
||||||
|
total: 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function evaluateSuggestions(supportbot) {
|
||||||
|
const sug = supportbot?.Suggestions ?? {};
|
||||||
|
const mode = String(sug.Mode || "Channel").toLowerCase();
|
||||||
|
|
||||||
|
const channelOk =
|
||||||
|
mode === "forum"
|
||||||
|
? isConfiguredId(sug.ForumChannel)
|
||||||
|
: isConfiguredId(sug.Channel);
|
||||||
|
|
||||||
|
return scoreChecks([
|
||||||
|
{
|
||||||
|
ok: channelOk,
|
||||||
|
hint:
|
||||||
|
mode === "forum"
|
||||||
|
? "Set suggestions forum channel"
|
||||||
|
: "Set suggestions channel ID",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ok: !isPlaceholder(sug.UpvoteEmoji) && !isPlaceholder(sug.DownvoteEmoji),
|
||||||
|
hint: "Configure voting emojis",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function evaluateAi(supportbotAi) {
|
||||||
|
if (supportbotAi?.Enabled !== true) {
|
||||||
|
return {
|
||||||
|
status: "progress",
|
||||||
|
subtitle: "AI is disabled in config",
|
||||||
|
passed: 0,
|
||||||
|
total: 3,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return scoreChecks([
|
||||||
|
{
|
||||||
|
ok: !isPlaceholder(supportbotAi?.General?.Model_API_Key),
|
||||||
|
hint: "Add your model API key",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ok: isConfiguredId(supportbotAi?.Channels?.AIChannel),
|
||||||
|
hint: "Set AI channel ID",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ok: !isPlaceholder(supportbotAi?.General?.Model),
|
||||||
|
hint: "Choose an AI model",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function evaluateCommands(commands) {
|
||||||
|
const entries = Object.entries(commands || {}).filter(
|
||||||
|
([, v]) => v && typeof v === "object" && "Enabled" in v,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!entries.length) {
|
||||||
|
return { status: "todo", subtitle: "No commands configured", passed: 0, total: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const enabled = entries.filter(([, v]) => v.Enabled !== false).length;
|
||||||
|
const total = entries.length;
|
||||||
|
const ratio = enabled / total;
|
||||||
|
|
||||||
|
if (ratio >= 0.85) {
|
||||||
|
return {
|
||||||
|
status: "done",
|
||||||
|
subtitle: `${enabled} of ${total} commands enabled`,
|
||||||
|
passed: enabled,
|
||||||
|
total,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (enabled === 0) {
|
||||||
|
return {
|
||||||
|
status: "todo",
|
||||||
|
subtitle: "Enable slash commands",
|
||||||
|
passed: 0,
|
||||||
|
total,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: "progress",
|
||||||
|
subtitle: `${enabled} of ${total} commands enabled`,
|
||||||
|
passed: enabled,
|
||||||
|
total,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function countTranscripts() {
|
||||||
|
const dir = path.resolve(__dirname, "../Data/Transcripts");
|
||||||
|
if (!fs.existsSync(dir)) return 0;
|
||||||
|
return fs
|
||||||
|
.readdirSync(dir)
|
||||||
|
.filter((f) => f.endsWith("-transcript.html")).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function evaluateAllModules(configStore, options = {}) {
|
||||||
|
const supportbot = configStore.supportbot;
|
||||||
|
const ticketPanel = configStore.ticketPanel;
|
||||||
|
const supportbotAi = configStore.supportbotAi;
|
||||||
|
const commands = configStore.commands;
|
||||||
|
const transcriptCount =
|
||||||
|
options.transcriptCount ?? countTranscripts();
|
||||||
|
|
||||||
|
return {
|
||||||
|
tickets: evaluateTickets(supportbot, ticketPanel),
|
||||||
|
supportbot: evaluateSupportbot(supportbot),
|
||||||
|
transcripts: evaluateTranscripts(supportbot, transcriptCount),
|
||||||
|
suggestions: evaluateSuggestions(supportbot),
|
||||||
|
ai: evaluateAi(supportbotAi),
|
||||||
|
commands: evaluateCommands(commands),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
evaluateAllModules,
|
||||||
|
isPlaceholder,
|
||||||
|
isConfiguredId,
|
||||||
|
};
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
const notificationStore = require("./NotificationStore.js");
|
||||||
|
const { fetchUpdateInfo } = require("./UpdateChecker.js");
|
||||||
|
|
||||||
|
function notifyBotRestart(success, errorMessage) {
|
||||||
|
if (success) {
|
||||||
|
return notificationStore.add({
|
||||||
|
type: "bot",
|
||||||
|
title: "Bot restarted",
|
||||||
|
message:
|
||||||
|
"Discord bot reconnected with your latest config. API and dashboard stayed online.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return notificationStore.add({
|
||||||
|
type: "error",
|
||||||
|
title: "Bot failed to restart",
|
||||||
|
message: errorMessage || "Check the logs for details.",
|
||||||
|
href: "/logs",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function notifyUpdateAvailable(info) {
|
||||||
|
return notificationStore.add({
|
||||||
|
id: "update-available",
|
||||||
|
type: "update",
|
||||||
|
title: "Update available",
|
||||||
|
message: `Version ${info.latest} is ready (you are on ${info.current}).`,
|
||||||
|
href: "/settings",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function notifyUpdateInstalled(message) {
|
||||||
|
return notificationStore.add({
|
||||||
|
type: "update",
|
||||||
|
title: "Update installed",
|
||||||
|
message:
|
||||||
|
message ||
|
||||||
|
"Files were updated. Restart your server (npm start) to finish applying changes.",
|
||||||
|
href: "/settings",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearUpdateAvailable() {
|
||||||
|
notificationStore.dismiss("update-available");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runUpdateCheck() {
|
||||||
|
const info = await fetchUpdateInfo();
|
||||||
|
if (info.updateAvailable) {
|
||||||
|
notifyUpdateAvailable(info);
|
||||||
|
} else {
|
||||||
|
clearUpdateAvailable();
|
||||||
|
}
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
notifyBotRestart,
|
||||||
|
notifyUpdateAvailable,
|
||||||
|
notifyUpdateInstalled,
|
||||||
|
clearUpdateAvailable,
|
||||||
|
runUpdateCheck,
|
||||||
|
};
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
const MAX_NOTIFICATIONS = 50;
|
||||||
|
|
||||||
|
/** @type {Map<string, object>} */
|
||||||
|
const byId = new Map();
|
||||||
|
/** @type {string[]} */
|
||||||
|
let order = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {object} input
|
||||||
|
* @param {string} [input.id] Stable id replaces existing (e.g. update-available)
|
||||||
|
*/
|
||||||
|
function add(input) {
|
||||||
|
const id =
|
||||||
|
input.id ||
|
||||||
|
`n-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||||
|
|
||||||
|
const item = {
|
||||||
|
id,
|
||||||
|
type: input.type || "info",
|
||||||
|
title: input.title || "Notification",
|
||||||
|
message: input.message || "",
|
||||||
|
href: input.href || null,
|
||||||
|
createdAt: input.createdAt || Date.now(),
|
||||||
|
read: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (byId.has(id)) {
|
||||||
|
order = order.filter((x) => x !== id);
|
||||||
|
}
|
||||||
|
|
||||||
|
byId.set(id, item);
|
||||||
|
order.unshift(id);
|
||||||
|
|
||||||
|
while (order.length > MAX_NOTIFICATIONS) {
|
||||||
|
const removed = order.pop();
|
||||||
|
if (removed) byId.delete(removed);
|
||||||
|
}
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
function list() {
|
||||||
|
return order.map((id) => byId.get(id)).filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
function unreadCount() {
|
||||||
|
let n = 0;
|
||||||
|
for (const item of byId.values()) {
|
||||||
|
if (!item.read) n++;
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
function markRead(ids) {
|
||||||
|
if (ids === "all") {
|
||||||
|
for (const item of byId.values()) {
|
||||||
|
item.read = true;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const id of ids) {
|
||||||
|
const item = byId.get(id);
|
||||||
|
if (item) item.read = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function dismiss(id) {
|
||||||
|
if (!byId.has(id)) return false;
|
||||||
|
byId.delete(id);
|
||||||
|
order = order.filter((x) => x !== id);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clear() {
|
||||||
|
byId.clear();
|
||||||
|
order = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
add,
|
||||||
|
list,
|
||||||
|
unreadCount,
|
||||||
|
markRead,
|
||||||
|
dismiss,
|
||||||
|
clear,
|
||||||
|
};
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
// SupportBot | Emerald Services
|
||||||
|
// Ticket ID Structure
|
||||||
|
|
||||||
|
const db = require("./Database");
|
||||||
|
|
||||||
|
class TicketNumberID {
|
||||||
|
static async pad() {
|
||||||
|
let num = await this.get();
|
||||||
|
let n = num + "";
|
||||||
|
await this.set(num + 1);
|
||||||
|
return n.length >= 4 ? n : new Array(4 - n.length + 1).join("0") + n;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async get() {
|
||||||
|
const settings = db.getSettings ? db.getSettings() : {};
|
||||||
|
if (typeof settings.ticket_counter === "number") {
|
||||||
|
return settings.ticket_counter;
|
||||||
|
}
|
||||||
|
|
||||||
|
const row = db.getTicketPanel ? db.getTicketPanel() : null;
|
||||||
|
if (row && typeof row.message_id === "string" && row.message_id.startsWith("panel-")) {
|
||||||
|
const parsed = parseInt(row.message_id.replace("panel-", ""), 10);
|
||||||
|
if (!isNaN(parsed)) return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (row && typeof row.id === "number" && row.id < 1000000) {
|
||||||
|
return row.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async set(value) {
|
||||||
|
const settings = db.getSettings ? db.getSettings() : {};
|
||||||
|
settings.ticket_counter = value;
|
||||||
|
if (db.saveSettings) db.saveSettings(settings);
|
||||||
|
if (db.saveTicketPanel) db.saveTicketPanel(`panel-${value}`, "none");
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async reset() {
|
||||||
|
return await this.set(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = TicketNumberID;
|
||||||
@@ -0,0 +1,293 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
const Discord = require("discord.js");
|
||||||
|
const yaml = require("js-yaml");
|
||||||
|
const db = require("./Database.js");
|
||||||
|
|
||||||
|
const supportbot = require("./ConfigStore").supportbot;
|
||||||
|
const msgconfig = require("./ConfigStore").messages;
|
||||||
|
const { createTranscriptHTML } = require("./TranscriptTemplate.js");
|
||||||
|
|
||||||
|
function escapeHtml(str = "") {
|
||||||
|
return String(str)
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/"/g, """);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseMarkdown(content = "") {
|
||||||
|
let safe = escapeHtml(content);
|
||||||
|
|
||||||
|
safe = safe.replace(/```([\s\S]*?)```/g, "<pre><code>$1</code></pre>");
|
||||||
|
safe = safe.replace(/`(.*?)`/g, "<code>$1</code>");
|
||||||
|
safe = safe.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>");
|
||||||
|
safe = safe.replace(/\*(.*?)\*/g, "<em>$1</em>");
|
||||||
|
safe = safe.replace(/_(.*?)_/g, "<em>$1</em>");
|
||||||
|
safe = safe.replace(
|
||||||
|
/\[(.*?)\]\((.*?)\)/g,
|
||||||
|
'<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>',
|
||||||
|
);
|
||||||
|
|
||||||
|
return safe.replace(/\n/g, "<br>");
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(iso) {
|
||||||
|
try {
|
||||||
|
return new Date(iso).toLocaleString("en-GB", {
|
||||||
|
dateStyle: "medium",
|
||||||
|
timeStyle: "short",
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return iso;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFileTypeInfo(att) {
|
||||||
|
const fileName = String(att.name || "attachment").toLowerCase();
|
||||||
|
|
||||||
|
if (/\.(png|jpg|jpeg|gif|webp|bmp|svg)$/i.test(fileName)) {
|
||||||
|
return "image";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/\.(mp4|webm|mov|m4v)$/i.test(fileName)) {
|
||||||
|
return "video";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/\.(mp3|wav|ogg|m4a|flac)$/i.test(fileName)) {
|
||||||
|
return "audio";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "file";
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderAttachment(att) {
|
||||||
|
const url = escapeHtml(att.url || "");
|
||||||
|
const name = escapeHtml(att.name || "attachment");
|
||||||
|
const type = getFileTypeInfo(att);
|
||||||
|
|
||||||
|
if (type === "image") {
|
||||||
|
return `
|
||||||
|
<div class="attachment">
|
||||||
|
<a href="${url}" target="_blank" rel="noopener noreferrer" class="attachment-file">📎 ${name}</a>
|
||||||
|
<div class="attachment-preview attachment-image">
|
||||||
|
<img src="${url}" alt="${name}" loading="lazy">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === "video") {
|
||||||
|
return `
|
||||||
|
<div class="attachment">
|
||||||
|
<a href="${url}" target="_blank" rel="noopener noreferrer" class="attachment-file">📎 ${name}</a>
|
||||||
|
<div class="attachment-preview">
|
||||||
|
<video controls preload="metadata">
|
||||||
|
<source src="${url}">
|
||||||
|
</video>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === "audio") {
|
||||||
|
return `
|
||||||
|
<div class="attachment">
|
||||||
|
<a href="${url}" target="_blank" rel="noopener noreferrer" class="attachment-file">📎 ${name}</a>
|
||||||
|
<div class="attachment-preview">
|
||||||
|
<audio controls preload="metadata">
|
||||||
|
<source src="${url}">
|
||||||
|
</audio>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="attachment">
|
||||||
|
<a href="${url}" target="_blank" rel="noopener noreferrer" class="attachment-file">📎 ${name}</a>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderEmbeds(embeds = []) {
|
||||||
|
return embeds
|
||||||
|
.map((embed) => {
|
||||||
|
const fields = (embed.fields || [])
|
||||||
|
.map(
|
||||||
|
(field) => `
|
||||||
|
<div class="embed-field">
|
||||||
|
<div class="embed-field-name">${escapeHtml(field.name || "Field")}</div>
|
||||||
|
<div class="embed-field-value">${parseMarkdown(field.value || "")}</div>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
)
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="discord-embed">
|
||||||
|
${embed.title ? `<div class="discord-embed-title">${escapeHtml(embed.title)}</div>` : ""}
|
||||||
|
${embed.description ? `<div class="discord-embed-description">${parseMarkdown(embed.description)}</div>` : ""}
|
||||||
|
${fields}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
})
|
||||||
|
.join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createTranscript(interaction, ticketInput, reasonInput) {
|
||||||
|
let ticketRecord = typeof ticketInput === "object" && ticketInput !== null ? ticketInput : null;
|
||||||
|
let closeReason = typeof ticketInput === "string" ? ticketInput : reasonInput;
|
||||||
|
|
||||||
|
if (!ticketRecord && typeof db.getTicket === "function") {
|
||||||
|
ticketRecord = db.getTicket(interaction.channel.id) || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const { getChannel } = interaction.client;
|
||||||
|
|
||||||
|
const transcriptChannel = await getChannel(
|
||||||
|
supportbot.Ticket.Log.TicketDataLog,
|
||||||
|
interaction.guild,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!transcriptChannel) {
|
||||||
|
throw new Error("Transcript log channel missing or inaccessible.");
|
||||||
|
}
|
||||||
|
|
||||||
|
let allMessages = [];
|
||||||
|
let lastId = null;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const options = { limit: 100 };
|
||||||
|
if (lastId) options.before = lastId;
|
||||||
|
|
||||||
|
const messages = await interaction.channel.messages.fetch(options);
|
||||||
|
if (messages.size === 0) break;
|
||||||
|
|
||||||
|
allMessages = [...allMessages, ...messages.values()];
|
||||||
|
lastId = messages.last().id;
|
||||||
|
|
||||||
|
if (messages.size < 100) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
allMessages.sort((a, b) => a.createdTimestamp - b.createdTimestamp);
|
||||||
|
|
||||||
|
const transcriptData = allMessages.map((msg) => ({
|
||||||
|
content: msg.content || "",
|
||||||
|
username: msg.author.username,
|
||||||
|
userId: msg.author.id,
|
||||||
|
avatar: msg.author.displayAvatarURL({ extension: "png", size: 128 }),
|
||||||
|
timestamp: msg.createdAt.toISOString(),
|
||||||
|
attachments: Array.from(msg.attachments.values()).map((att) => ({
|
||||||
|
url: att.url,
|
||||||
|
name: att.name || "attachment",
|
||||||
|
contentType: att.contentType || null,
|
||||||
|
size: att.size || 0,
|
||||||
|
})),
|
||||||
|
embeds: msg.embeds.map((embed) => ({
|
||||||
|
title: embed.title || "",
|
||||||
|
description: embed.description || "",
|
||||||
|
fields: embed.fields || [],
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (typeof db.updateTicketStatus === "function") {
|
||||||
|
db.updateTicketStatus(interaction.channel.id, "closed");
|
||||||
|
}
|
||||||
|
|
||||||
|
let ticketUserId = ticketRecord?.user_id || ticketRecord?.user || ticketRecord?.author_id;
|
||||||
|
|
||||||
|
if (!ticketUserId && interaction.channel.permissionOverwrites) {
|
||||||
|
const memberOverwrite = interaction.channel.permissionOverwrites.cache.find(
|
||||||
|
(pow) => pow.type === 1 && pow.id !== interaction.client.user.id
|
||||||
|
);
|
||||||
|
if (memberOverwrite) {
|
||||||
|
ticketUserId = memberOverwrite.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ticketUserId && allMessages.length > 0) {
|
||||||
|
const firstNonBotMsg = allMessages.find((m) => !m.author.bot);
|
||||||
|
if (firstNonBotMsg) {
|
||||||
|
ticketUserId = firstNonBotMsg.author.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const tUser =
|
||||||
|
(ticketUserId ? interaction.client.users.cache.get(ticketUserId) : null) ||
|
||||||
|
(ticketUserId ? await interaction.client.users.fetch(ticketUserId).catch(() => null) : null);
|
||||||
|
|
||||||
|
const transcriptEmbed = new Discord.EmbedBuilder()
|
||||||
|
.setTitle(msgconfig.TicketLog.Title)
|
||||||
|
.setColor(msgconfig.TicketLog.Colour)
|
||||||
|
.setFooter({
|
||||||
|
text: supportbot.Embed.Footer,
|
||||||
|
iconURL: interaction.user.displayAvatarURL(),
|
||||||
|
})
|
||||||
|
.setDescription(
|
||||||
|
`> **Ticket:** ${interaction.channel.name} (\`${interaction.channel.id}\`)\n` +
|
||||||
|
`> **User:** ${tUser ? `<@${tUser.id}> (\`${tUser.tag}\`)` : "Unknown User"}\n` +
|
||||||
|
`> **Closed by:** <@${interaction.user.id}>\n` +
|
||||||
|
`> **Message Count:** ${transcriptData.length}`,
|
||||||
|
)
|
||||||
|
.addFields({
|
||||||
|
name: "Reason",
|
||||||
|
value: `\`\`\`${closeReason || "No Reason Provided."}\`\`\``,
|
||||||
|
inline: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const configStore = require("./ConfigStore");
|
||||||
|
const transcriptStore = require("./TranscriptStore");
|
||||||
|
const apiConfig = configStore.api?.API;
|
||||||
|
const tSettings = transcriptStore.getSettings();
|
||||||
|
|
||||||
|
if (apiConfig?.Enabled !== false) {
|
||||||
|
const port = apiConfig?.Port || 3000;
|
||||||
|
const isPublic = Boolean(tSettings?.publicAccess?.enabled);
|
||||||
|
const transcriptUrl = `http://localhost:${port}/transcripts/${interaction.channel.id}`;
|
||||||
|
|
||||||
|
transcriptEmbed.addFields({
|
||||||
|
name: isPublic ? "Public Web Transcript" : "Dashboard Transcript",
|
||||||
|
value: `[${isPublic ? "Click here to view public transcript" : "Click here to view in Dashboard"}](${transcriptUrl})`,
|
||||||
|
inline: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("[TicketManager] Could not add web transcript URL:", err.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
const transcriptDir = path.join(process.cwd(), "./Data/Transcripts");
|
||||||
|
if (!fs.existsSync(transcriptDir)) {
|
||||||
|
fs.mkdirSync(transcriptDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const htmlFileName = `${interaction.channel.id}-transcript.html`;
|
||||||
|
const htmlPath = path.join(transcriptDir, htmlFileName);
|
||||||
|
|
||||||
|
const html = createTranscriptHTML(
|
||||||
|
{
|
||||||
|
id: interaction.channel.id,
|
||||||
|
name: interaction.channel.name,
|
||||||
|
messages: transcriptData,
|
||||||
|
},
|
||||||
|
closeReason,
|
||||||
|
);
|
||||||
|
|
||||||
|
fs.writeFileSync(htmlPath, html, "utf8");
|
||||||
|
|
||||||
|
await transcriptChannel.send({
|
||||||
|
embeds: [transcriptEmbed],
|
||||||
|
files: [
|
||||||
|
new Discord.AttachmentBuilder(htmlPath, {
|
||||||
|
name: `SPOILER_${htmlFileName}`,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
return { htmlPath };
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
createTranscript,
|
||||||
|
};
|
||||||
@@ -0,0 +1,223 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const TRANSCRIPT_DIR = path.join(__dirname, '../Data/Transcripts');
|
||||||
|
const SETTINGS_FILE = path.join(__dirname, '../Data/transcript-settings.json');
|
||||||
|
|
||||||
|
const DEFAULT_SETTINGS = {
|
||||||
|
autoDelete: {
|
||||||
|
enabled: false,
|
||||||
|
afterDays: 30,
|
||||||
|
},
|
||||||
|
publicAccess: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let purgeTimer = null;
|
||||||
|
|
||||||
|
function ensureTranscriptDir() {
|
||||||
|
if (!fs.existsSync(TRANSCRIPT_DIR)) {
|
||||||
|
fs.mkdirSync(TRANSCRIPT_DIR, { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSettings() {
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(SETTINGS_FILE)) {
|
||||||
|
return JSON.parse(JSON.stringify(DEFAULT_SETTINGS));
|
||||||
|
}
|
||||||
|
const raw = JSON.parse(fs.readFileSync(SETTINGS_FILE, 'utf8'));
|
||||||
|
const days = Number(raw?.autoDelete?.afterDays);
|
||||||
|
return {
|
||||||
|
autoDelete: {
|
||||||
|
enabled: Boolean(raw?.autoDelete?.enabled),
|
||||||
|
afterDays:
|
||||||
|
Number.isFinite(days) && days >= 1
|
||||||
|
? Math.min(Math.floor(days), 3650)
|
||||||
|
: DEFAULT_SETTINGS.autoDelete.afterDays,
|
||||||
|
},
|
||||||
|
publicAccess: {
|
||||||
|
enabled: Boolean(raw?.publicAccess?.enabled),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return JSON.parse(JSON.stringify(DEFAULT_SETTINGS));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveSettings(patch) {
|
||||||
|
const current = getSettings();
|
||||||
|
const next = {
|
||||||
|
autoDelete: {
|
||||||
|
enabled:
|
||||||
|
patch?.autoDelete?.enabled !== undefined
|
||||||
|
? Boolean(patch.autoDelete.enabled)
|
||||||
|
: current.autoDelete.enabled,
|
||||||
|
afterDays:
|
||||||
|
patch?.autoDelete?.afterDays !== undefined
|
||||||
|
? Math.min(
|
||||||
|
Math.max(Math.floor(Number(patch.autoDelete.afterDays) || 1), 1),
|
||||||
|
3650,
|
||||||
|
)
|
||||||
|
: current.autoDelete.afterDays,
|
||||||
|
},
|
||||||
|
publicAccess: {
|
||||||
|
enabled:
|
||||||
|
patch?.publicAccess?.enabled !== undefined
|
||||||
|
? Boolean(patch.publicAccess.enabled)
|
||||||
|
: current.publicAccess.enabled,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const dir = path.dirname(SETTINGS_FILE);
|
||||||
|
if (!fs.existsSync(dir)) {
|
||||||
|
fs.mkdirSync(dir, { recursive: true });
|
||||||
|
}
|
||||||
|
fs.writeFileSync(SETTINGS_FILE, JSON.stringify(next, null, 2), 'utf8');
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeId(rawId) {
|
||||||
|
return String(rawId).replace(/[^0-9]/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function readTranscriptFile(rawId) {
|
||||||
|
const id = normalizeId(rawId);
|
||||||
|
if (!id) return null;
|
||||||
|
ensureTranscriptDir();
|
||||||
|
const filePath = path.join(TRANSCRIPT_DIR, `${id}-transcript.html`);
|
||||||
|
if (!fs.existsSync(filePath)) return null;
|
||||||
|
const content = fs.readFileSync(filePath, 'utf8');
|
||||||
|
const titleMatch = content.match(/<title>\s*Transcript\s*-\s*([^<]+)\s*<\/title>/i);
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
filePath,
|
||||||
|
filename: `${id}-transcript.html`,
|
||||||
|
content,
|
||||||
|
ticketName: titleMatch ? titleMatch[1].trim() : null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function listTranscripts() {
|
||||||
|
ensureTranscriptDir();
|
||||||
|
if (!fs.existsSync(TRANSCRIPT_DIR)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return fs
|
||||||
|
.readdirSync(TRANSCRIPT_DIR)
|
||||||
|
.filter((f) => f.endsWith('-transcript.html'))
|
||||||
|
.map((f) => {
|
||||||
|
const id = f.replace('-transcript.html', '');
|
||||||
|
const stats = fs.statSync(path.join(TRANSCRIPT_DIR, f));
|
||||||
|
const parsed = readTranscriptFile(id);
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
filename: f,
|
||||||
|
createdAt: stats.mtime.toISOString(),
|
||||||
|
size: stats.size,
|
||||||
|
ticketName: parsed?.ticketName ?? null,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteTranscript(rawId) {
|
||||||
|
const id = normalizeId(rawId);
|
||||||
|
if (!id) return false;
|
||||||
|
const filePath = path.join(TRANSCRIPT_DIR, `${id}-transcript.html`);
|
||||||
|
if (!fs.existsSync(filePath)) return false;
|
||||||
|
fs.unlinkSync(filePath);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function purgeExpiredTranscripts() {
|
||||||
|
const settings = getSettings();
|
||||||
|
if (!settings.autoDelete.enabled) {
|
||||||
|
return { deleted: 0, skipped: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureTranscriptDir();
|
||||||
|
const maxAgeMs = settings.autoDelete.afterDays * 24 * 60 * 60 * 1000;
|
||||||
|
const cutoff = Date.now() - maxAgeMs;
|
||||||
|
let deleted = 0;
|
||||||
|
|
||||||
|
for (const f of fs.readdirSync(TRANSCRIPT_DIR)) {
|
||||||
|
if (!f.endsWith('-transcript.html')) continue;
|
||||||
|
const filePath = path.join(TRANSCRIPT_DIR, f);
|
||||||
|
const stats = fs.statSync(filePath);
|
||||||
|
if (stats.mtime.getTime() < cutoff) {
|
||||||
|
fs.unlinkSync(filePath);
|
||||||
|
deleted += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { deleted, skipped: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
function notFoundHtml(message = 'Transcript not found') {
|
||||||
|
return `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Transcript</title>
|
||||||
|
<style>body{font-family:system-ui,sans-serif;padding:2rem;background:#0b0f14;color:#e6edf3;margin:0}</style></head>
|
||||||
|
<body><h1>${message}</h1></body></html>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function servePublicTranscript(req, res) {
|
||||||
|
const settings = getSettings();
|
||||||
|
if (!settings.publicAccess.enabled) {
|
||||||
|
return res.status(404).type('html').send(notFoundHtml('Transcript not found'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const transcript = readTranscriptFile(req.params.id);
|
||||||
|
if (!transcript) {
|
||||||
|
return res.status(404).type('html').send(notFoundHtml('Transcript not found'));
|
||||||
|
}
|
||||||
|
|
||||||
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
||||||
|
res.setHeader('Cache-Control', 'public, max-age=120');
|
||||||
|
return res.send(transcript.content);
|
||||||
|
}
|
||||||
|
|
||||||
|
function startAutoDeleteScheduler(intervalMs = 60 * 60 * 1000) {
|
||||||
|
if (purgeTimer) return;
|
||||||
|
|
||||||
|
const run = () => {
|
||||||
|
try {
|
||||||
|
const result = purgeExpiredTranscripts();
|
||||||
|
if (result.deleted > 0) {
|
||||||
|
console.log(
|
||||||
|
`[Transcripts] Auto-deleted ${result.deleted} transcript(s) older than ${getSettings().autoDelete.afterDays} day(s)`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Transcripts] Auto-delete failed:', err.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
run();
|
||||||
|
purgeTimer = setInterval(run, intervalMs);
|
||||||
|
if (typeof purgeTimer.unref === 'function') {
|
||||||
|
purgeTimer.unref();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPublicUrl(req, id) {
|
||||||
|
const settings = getSettings();
|
||||||
|
if (!settings.publicAccess.enabled) return null;
|
||||||
|
const host = req.get('host');
|
||||||
|
const proto = req.get('x-forwarded-proto') || req.protocol || 'http';
|
||||||
|
return `${proto}://${host}/transcripts/${id}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
TRANSCRIPT_DIR,
|
||||||
|
DEFAULT_SETTINGS,
|
||||||
|
getSettings,
|
||||||
|
saveSettings,
|
||||||
|
readTranscriptFile,
|
||||||
|
listTranscripts,
|
||||||
|
deleteTranscript,
|
||||||
|
purgeExpiredTranscripts,
|
||||||
|
servePublicTranscript,
|
||||||
|
startAutoDeleteScheduler,
|
||||||
|
getPublicUrl,
|
||||||
|
};
|
||||||
@@ -0,0 +1,721 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
const yaml = require("js-yaml");
|
||||||
|
const {
|
||||||
|
normalizeFontFamily,
|
||||||
|
resolveTypography,
|
||||||
|
} = require("./googleFonts.js");
|
||||||
|
|
||||||
|
const TEMPLATE_PATH = path.join(__dirname, "../Configs/transcript-template.yml");
|
||||||
|
|
||||||
|
let cachedTemplate = null;
|
||||||
|
|
||||||
|
function escapeHtml(str = "") {
|
||||||
|
return String(str)
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/"/g, """);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseMarkdown(content = "") {
|
||||||
|
let safe = escapeHtml(content);
|
||||||
|
|
||||||
|
safe = safe.replace(/```([\s\S]*?)```/g, "<pre><code>$1</code></pre>");
|
||||||
|
safe = safe.replace(/`(.*?)`/g, "<code>$1</code>");
|
||||||
|
safe = safe.replace(/\|\|(.*?)\|\|/g, '<span class="spoiler">$1</span>');
|
||||||
|
safe = safe.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>");
|
||||||
|
safe = safe.replace(/\*(.*?)\*/g, "<em>$1</em>");
|
||||||
|
safe = safe.replace(/_(.*?)_/g, "<em>$1</em>");
|
||||||
|
safe = safe.replace(/~~(.*?)~~/g, "<del>$1</del>");
|
||||||
|
safe = safe.replace(/__(.*?)__/g, "<u>$1</u>");
|
||||||
|
safe = safe.replace(/<@!?(\d+)>/g, '<span class="mention">@User</span>');
|
||||||
|
safe = safe.replace(/<@&(\d+)>/g, '<span class="mention">@Role</span>');
|
||||||
|
safe = safe.replace(
|
||||||
|
/\[(.*?)\]\((.*?)\)/g,
|
||||||
|
'<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>',
|
||||||
|
);
|
||||||
|
|
||||||
|
return safe.replace(/\n/g, "<br>");
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(iso) {
|
||||||
|
try {
|
||||||
|
return new Date(iso).toLocaleString("en-GB", {
|
||||||
|
dateStyle: "medium",
|
||||||
|
timeStyle: "short",
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return iso;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFileTypeInfo(att) {
|
||||||
|
const fileName = String(att.name || "attachment").toLowerCase();
|
||||||
|
|
||||||
|
if (/\.(png|jpg|jpeg|gif|webp|bmp|svg)$/i.test(fileName)) {
|
||||||
|
return "image";
|
||||||
|
}
|
||||||
|
if (/\.(mp4|webm|mov|m4v)$/i.test(fileName)) {
|
||||||
|
return "video";
|
||||||
|
}
|
||||||
|
if (/\.(mp3|wav|ogg|m4a|flac)$/i.test(fileName)) {
|
||||||
|
return "audio";
|
||||||
|
}
|
||||||
|
return "file";
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderAttachment(att) {
|
||||||
|
const url = escapeHtml(att.url || "");
|
||||||
|
const name = escapeHtml(att.name || "attachment");
|
||||||
|
const type = getFileTypeInfo(att);
|
||||||
|
|
||||||
|
if (type === "image") {
|
||||||
|
return `
|
||||||
|
<div class="attachment">
|
||||||
|
<a href="${url}" target="_blank" rel="noopener noreferrer" class="attachment-file">📎 ${name}</a>
|
||||||
|
<div class="attachment-preview attachment-image">
|
||||||
|
<img src="${url}" alt="${name}" loading="lazy">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === "video") {
|
||||||
|
return `
|
||||||
|
<div class="attachment">
|
||||||
|
<a href="${url}" target="_blank" rel="noopener noreferrer" class="attachment-file">📎 ${name}</a>
|
||||||
|
<div class="attachment-preview">
|
||||||
|
<video controls preload="metadata">
|
||||||
|
<source src="${url}">
|
||||||
|
</video>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === "audio") {
|
||||||
|
return `
|
||||||
|
<div class="attachment">
|
||||||
|
<a href="${url}" target="_blank" rel="noopener noreferrer" class="attachment-file">📎 ${name}</a>
|
||||||
|
<div class="attachment-preview">
|
||||||
|
<audio controls preload="metadata">
|
||||||
|
<source src="${url}">
|
||||||
|
</audio>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="attachment">
|
||||||
|
<a href="${url}" target="_blank" rel="noopener noreferrer" class="attachment-file">📎 ${name}</a>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderEmbeds(embeds = []) {
|
||||||
|
return embeds
|
||||||
|
.map((embed) => {
|
||||||
|
const color = embed.color || "#5865F2";
|
||||||
|
const fields = (embed.fields || [])
|
||||||
|
.map(
|
||||||
|
(field) => `
|
||||||
|
<div class="embed-field ${field.inline ? 'inline-field' : ''}">
|
||||||
|
<div class="embed-field-name">${escapeHtml(field.name || "Field")}</div>
|
||||||
|
<div class="embed-field-value">${parseMarkdown(field.value || "")}</div>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
)
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
const author = embed.author ? `
|
||||||
|
<div class="embed-author">
|
||||||
|
${embed.author.iconURL ? `<img src="${escapeHtml(embed.author.iconURL)}" class="embed-author-icon" />` : ''}
|
||||||
|
<span>${escapeHtml(embed.author.name || '')}</span>
|
||||||
|
</div>
|
||||||
|
` : '';
|
||||||
|
|
||||||
|
const thumbnail = embed.thumbnail?.url ? `
|
||||||
|
<img src="${escapeHtml(embed.thumbnail.url)}" class="embed-thumbnail" />
|
||||||
|
` : '';
|
||||||
|
|
||||||
|
const image = embed.image?.url ? `
|
||||||
|
<div class="embed-image"><img src="${escapeHtml(embed.image.url)}" /></div>
|
||||||
|
` : '';
|
||||||
|
|
||||||
|
const footer = (embed.footer || embed.timestamp) ? `
|
||||||
|
<div class="embed-footer">
|
||||||
|
${embed.footer?.iconURL ? `<img src="${escapeHtml(embed.footer.iconURL)}" class="embed-footer-icon" />` : ''}
|
||||||
|
${embed.footer?.text ? `<span>${escapeHtml(embed.footer.text)}</span>` : ''}
|
||||||
|
</div>
|
||||||
|
` : '';
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="discord-embed" style="border-left-color: ${escapeHtml(color)}; border-left-width: 4px; border-left-style: solid; position: relative;">
|
||||||
|
${author}
|
||||||
|
${thumbnail}
|
||||||
|
${embed.title ? `<div class="discord-embed-title">${parseMarkdown(embed.title)}</div>` : ""}
|
||||||
|
${embed.description ? `<div class="discord-embed-description">${parseMarkdown(embed.description)}</div>` : ""}
|
||||||
|
${fields ? `<div class="embed-fields-grid">${fields}</div>` : ""}
|
||||||
|
${image}
|
||||||
|
${footer}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
})
|
||||||
|
.join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function deepMerge(base, patch) {
|
||||||
|
const out = structuredClone(base);
|
||||||
|
for (const [key, val] of Object.entries(patch || {})) {
|
||||||
|
if (
|
||||||
|
val &&
|
||||||
|
typeof val === "object" &&
|
||||||
|
!Array.isArray(val) &&
|
||||||
|
out[key] &&
|
||||||
|
typeof out[key] === "object" &&
|
||||||
|
!Array.isArray(out[key])
|
||||||
|
) {
|
||||||
|
out[key] = deepMerge(out[key], val);
|
||||||
|
} else {
|
||||||
|
out[key] = val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDefaultTemplate() {
|
||||||
|
return {
|
||||||
|
Theme: {
|
||||||
|
Background: "#0b0f14",
|
||||||
|
Panel: "#111821",
|
||||||
|
PanelSecondary: "#182230",
|
||||||
|
PanelTertiary: "#1d2938",
|
||||||
|
Border: "#283548",
|
||||||
|
Text: "#e6edf3",
|
||||||
|
Muted: "#9fb0c3",
|
||||||
|
Accent: "#22c55e",
|
||||||
|
AccentSoft: "rgba(34, 197, 94, 0.12)",
|
||||||
|
AccentBorder: "rgba(34, 197, 94, 0.25)",
|
||||||
|
AccentText: "#8df0ad",
|
||||||
|
Link: "#8fd9ff",
|
||||||
|
AttachmentLink: "#8fd9a8",
|
||||||
|
CodeBackground: "#0f1720",
|
||||||
|
},
|
||||||
|
Typography: {
|
||||||
|
FontFamily: "Inter",
|
||||||
|
},
|
||||||
|
Header: {
|
||||||
|
Title: "Ticket Transcript",
|
||||||
|
Subtitle:
|
||||||
|
"Exported conversation log for support ticket **{{ticketName}}**",
|
||||||
|
Badge: "Closed Transcript",
|
||||||
|
ShowMetaCards: true,
|
||||||
|
},
|
||||||
|
Meta: {
|
||||||
|
ChannelLabel: "Channel",
|
||||||
|
TicketIdLabel: "Ticket ID",
|
||||||
|
MessageCountLabel: "Message Count",
|
||||||
|
CloseReasonLabel: "Close Reason",
|
||||||
|
DefaultCloseReason: "No Reason Provided.",
|
||||||
|
},
|
||||||
|
Messages: {
|
||||||
|
SectionTitle: "Messages",
|
||||||
|
EmptyState: "No messages were found in this ticket.",
|
||||||
|
},
|
||||||
|
Advanced: {
|
||||||
|
CustomCss: "",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeTemplate(raw) {
|
||||||
|
const merged = deepMerge(getDefaultTemplate(), raw || {});
|
||||||
|
if (merged.Typography) {
|
||||||
|
merged.Typography.FontFamily = normalizeFontFamily(
|
||||||
|
merged.Typography.FontFamily,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return merged;
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadTemplateFromDisk() {
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(TEMPLATE_PATH)) {
|
||||||
|
const defaults = getDefaultTemplate();
|
||||||
|
fs.mkdirSync(path.dirname(TEMPLATE_PATH), { recursive: true });
|
||||||
|
fs.writeFileSync(TEMPLATE_PATH, yaml.dump(defaults), "utf8");
|
||||||
|
return defaults;
|
||||||
|
}
|
||||||
|
const parsed = yaml.load(fs.readFileSync(TEMPLATE_PATH, "utf8"));
|
||||||
|
return normalizeTemplate(parsed);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[TranscriptTemplate] Failed to load template:", err.message);
|
||||||
|
return getDefaultTemplate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTemplate() {
|
||||||
|
if (!cachedTemplate) {
|
||||||
|
cachedTemplate = loadTemplateFromDisk();
|
||||||
|
}
|
||||||
|
return structuredClone(cachedTemplate);
|
||||||
|
}
|
||||||
|
|
||||||
|
function reloadTemplate() {
|
||||||
|
cachedTemplate = loadTemplateFromDisk();
|
||||||
|
return getTemplate();
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveTemplate(data) {
|
||||||
|
const merged = normalizeTemplate(data);
|
||||||
|
fs.mkdirSync(path.dirname(TEMPLATE_PATH), { recursive: true });
|
||||||
|
fs.writeFileSync(TEMPLATE_PATH, yaml.dump(merged), "utf8");
|
||||||
|
cachedTemplate = merged;
|
||||||
|
return merged;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyPlaceholders(text, ctx) {
|
||||||
|
return String(text || "")
|
||||||
|
.replace(/\{\{ticketName\}\}/g, ctx.name || "")
|
||||||
|
.replace(/\{\{ticketId\}\}/g, ctx.id || "")
|
||||||
|
.replace(/\{\{messageCount\}\}/g, String(ctx.messageCount ?? 0))
|
||||||
|
.replace(/\{\{reason\}\}/g, ctx.reason || "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatHeaderText(text, ctx) {
|
||||||
|
const withPlaceholders = applyPlaceholders(text, ctx);
|
||||||
|
const escaped = escapeHtml(withPlaceholders);
|
||||||
|
return escaped.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>");
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildStyles(template) {
|
||||||
|
const t = template.Theme;
|
||||||
|
const { fontFamilyCss } = resolveTypography(template.Typography?.FontFamily);
|
||||||
|
const custom = String(template.Advanced?.CustomCss || "").trim();
|
||||||
|
|
||||||
|
return `
|
||||||
|
:root {
|
||||||
|
--bg: ${t.Background};
|
||||||
|
--panel: ${t.Panel};
|
||||||
|
--panel-2: ${t.PanelSecondary};
|
||||||
|
--panel-3: ${t.PanelTertiary};
|
||||||
|
--border: ${t.Border};
|
||||||
|
--text: ${t.Text};
|
||||||
|
--muted: ${t.Muted};
|
||||||
|
--accent: ${t.Accent};
|
||||||
|
--accent-soft: ${t.AccentSoft};
|
||||||
|
--accent-border: ${t.AccentBorder};
|
||||||
|
--accent-text: ${t.AccentText};
|
||||||
|
--link: ${t.Link};
|
||||||
|
--attachment-link: ${t.AttachmentLink};
|
||||||
|
--code: ${t.CodeBackground};
|
||||||
|
}
|
||||||
|
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
font-family: ${fontFamilyCss};
|
||||||
|
}
|
||||||
|
|
||||||
|
body { padding: 32px 20px; }
|
||||||
|
|
||||||
|
.page { max-width: 1100px; margin: 0 auto; }
|
||||||
|
|
||||||
|
.header {
|
||||||
|
background: var(--panel);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 24px;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-top {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 16px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-wrap h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.8rem;
|
||||||
|
line-height: 1.2;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-wrap p {
|
||||||
|
margin: 8px 0 0;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.98rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
background: var(--accent-soft);
|
||||||
|
color: var(--accent-text);
|
||||||
|
border: 1px solid var(--accent-border);
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 700;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-grid {
|
||||||
|
margin-top: 18px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-card {
|
||||||
|
background: var(--panel-2);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-label {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.82rem;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-value {
|
||||||
|
font-size: 0.98rem;
|
||||||
|
font-weight: 600;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messages-wrap {
|
||||||
|
background: var(--panel);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messages-header {
|
||||||
|
padding: 18px 20px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
background: var(--panel-2);
|
||||||
|
font-weight: 800;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
display: flex;
|
||||||
|
gap: 14px;
|
||||||
|
padding: 18px 20px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.message:last-child { border-bottom: none; }
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
width: 42px;
|
||||||
|
height: 42px;
|
||||||
|
min-width: 42px;
|
||||||
|
border-radius: 50%;
|
||||||
|
object-fit: cover;
|
||||||
|
background: var(--panel-3);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-body { min-width: 0; flex: 1; }
|
||||||
|
|
||||||
|
.message-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.username {
|
||||||
|
font-weight: 800;
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 0.98rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userid {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timestamp {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.82rem;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 0.96rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content:empty { display: none; }
|
||||||
|
|
||||||
|
.discord-embed {
|
||||||
|
margin-top: 12px;
|
||||||
|
background: var(--panel-2);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-left: 4px solid var(--accent);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discord-embed-title { font-weight: 800; margin-bottom: 6px; }
|
||||||
|
.discord-embed-description { color: var(--text); line-height: 1.5; }
|
||||||
|
|
||||||
|
.embed-author { display: flex; align-items: center; gap: 8px; font-weight: 700; font-size: 0.9em; margin-bottom: 6px; }
|
||||||
|
.embed-author-icon { width: 18px; height: 18px; border-radius: 50%; }
|
||||||
|
.embed-fields-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 10px; margin-top: 10px; padding-top: 10px; border-top: 1px solid rgba(255,255,255,0.06); }
|
||||||
|
.embed-thumbnail { position: absolute; top: 12px; right: 12px; width: 44px; height: 44px; border-radius: 6px; object-fit: cover; }
|
||||||
|
.embed-image img { width: 100%; max-height: 300px; object-fit: cover; border-radius: 8px; margin-top: 10px; }
|
||||||
|
.embed-footer { display: flex; align-items: center; gap: 6px; font-size: 0.8em; color: var(--muted); margin-top: 8px; padding-top: 6px; border-top: 1px solid rgba(255,255,255,0.04); }
|
||||||
|
.embed-footer-icon { width: 14px; height: 14px; border-radius: 50%; }
|
||||||
|
.mention { background: rgba(88, 101, 242, 0.2); color: #5865F2; padding: 2px 6px; border-radius: 4px; font-weight: 600; font-size: 0.9em; }
|
||||||
|
.spoiler { background: #202225; color: transparent; border-radius: 4px; padding: 0 4px; cursor: pointer; }
|
||||||
|
.spoiler:hover { color: inherit; background: rgba(255,255,255,0.1); }
|
||||||
|
|
||||||
|
.embed-field {
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.embed-field-name { font-weight: 700; margin-bottom: 4px; font-size: 0.85em; text-transform: uppercase; color: var(--muted); }
|
||||||
|
.embed-field-value { color: var(--text); line-height: 1.5; }
|
||||||
|
|
||||||
|
.attachment {
|
||||||
|
margin-top: 12px;
|
||||||
|
background: var(--panel-2);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-file {
|
||||||
|
display: inline-block;
|
||||||
|
color: var(--attachment-link);
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 700;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-file:hover { text-decoration: underline; }
|
||||||
|
|
||||||
|
.attachment-preview { margin-top: 10px; }
|
||||||
|
|
||||||
|
.attachment-preview img,
|
||||||
|
.attachment-preview video {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 560px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
display: block;
|
||||||
|
background: #0a0f15;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-preview audio {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 560px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
background: var(--code);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
background: var(--code);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow-x: auto;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre code { background: transparent; border: none; padding: 0; }
|
||||||
|
|
||||||
|
a { color: var(--link); }
|
||||||
|
|
||||||
|
.empty-state { padding: 24px 20px; color: var(--muted); }
|
||||||
|
|
||||||
|
@media (max-width: 700px) {
|
||||||
|
body { padding: 18px 12px; }
|
||||||
|
.header, .messages-wrap { border-radius: 12px; }
|
||||||
|
.message { padding: 14px; }
|
||||||
|
.timestamp { width: 100%; margin-left: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
${custom}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSampleTicket() {
|
||||||
|
return {
|
||||||
|
id: "123456789012345678",
|
||||||
|
name: "ticket-support-demo",
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
username: "SupportBot",
|
||||||
|
userId: "100000000000000001",
|
||||||
|
avatar: "https://cdn.discordapp.com/embed/avatars/0.png",
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
content: "Hello! Thanks for opening a ticket. How can we help?",
|
||||||
|
embeds: [],
|
||||||
|
attachments: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
username: "Member",
|
||||||
|
userId: "100000000000000002",
|
||||||
|
avatar: "https://cdn.discordapp.com/embed/avatars/1.png",
|
||||||
|
timestamp: new Date(Date.now() - 60000).toISOString(),
|
||||||
|
content: "I need help with my purchase — order **#48291**.",
|
||||||
|
embeds: [
|
||||||
|
{
|
||||||
|
title: "Order details",
|
||||||
|
description: "Payment received · awaiting delivery",
|
||||||
|
fields: [{ name: "Status", value: "Pending" }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
attachments: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createTranscriptHTML(ticket, reason, templateInput) {
|
||||||
|
const template = templateInput
|
||||||
|
? normalizeTemplate(templateInput)
|
||||||
|
: getTemplate();
|
||||||
|
const header = template.Header;
|
||||||
|
const meta = template.Meta;
|
||||||
|
const messagesCfg = template.Messages;
|
||||||
|
const closeReason = reason || meta.DefaultCloseReason;
|
||||||
|
const ctx = {
|
||||||
|
name: ticket.name,
|
||||||
|
id: ticket.id,
|
||||||
|
messageCount: ticket.messages.length,
|
||||||
|
reason: closeReason,
|
||||||
|
};
|
||||||
|
|
||||||
|
const { headLinks } = resolveTypography(template.Typography?.FontFamily);
|
||||||
|
|
||||||
|
const metaSection = header.ShowMetaCards
|
||||||
|
? `
|
||||||
|
<div class="meta-grid">
|
||||||
|
<div class="meta-card">
|
||||||
|
<div class="meta-label">${escapeHtml(meta.ChannelLabel)}</div>
|
||||||
|
<div class="meta-value">${escapeHtml(ticket.name)}</div>
|
||||||
|
</div>
|
||||||
|
<div class="meta-card">
|
||||||
|
<div class="meta-label">${escapeHtml(meta.TicketIdLabel)}</div>
|
||||||
|
<div class="meta-value">${escapeHtml(ticket.id)}</div>
|
||||||
|
</div>
|
||||||
|
<div class="meta-card">
|
||||||
|
<div class="meta-label">${escapeHtml(meta.MessageCountLabel)}</div>
|
||||||
|
<div class="meta-value">${ticket.messages.length}</div>
|
||||||
|
</div>
|
||||||
|
<div class="meta-card">
|
||||||
|
<div class="meta-label">${escapeHtml(meta.CloseReasonLabel)}</div>
|
||||||
|
<div class="meta-value">${escapeHtml(closeReason)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: "";
|
||||||
|
|
||||||
|
return `<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Transcript - ${escapeHtml(ticket.name)}</title>
|
||||||
|
${headLinks}
|
||||||
|
<style>${buildStyles(template)}</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="page">
|
||||||
|
<section class="header">
|
||||||
|
<div class="header-top">
|
||||||
|
<div class="title-wrap">
|
||||||
|
<h1>${escapeHtml(header.Title)}</h1>
|
||||||
|
<p>${formatHeaderText(header.Subtitle, ctx)}</p>
|
||||||
|
</div>
|
||||||
|
<div class="badge">${escapeHtml(header.Badge)}</div>
|
||||||
|
</div>
|
||||||
|
${metaSection}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="messages-wrap">
|
||||||
|
<div class="messages-header">${escapeHtml(messagesCfg.SectionTitle)}</div>
|
||||||
|
${
|
||||||
|
ticket.messages.length
|
||||||
|
? ticket.messages
|
||||||
|
.map(
|
||||||
|
(msg) => `
|
||||||
|
<article class="message">
|
||||||
|
<img src="${escapeHtml(msg.avatar || "")}" alt="Avatar" class="avatar">
|
||||||
|
<div class="message-body">
|
||||||
|
<div class="message-header">
|
||||||
|
<span class="username">${escapeHtml(msg.username || "Unknown User")}</span>
|
||||||
|
<span class="userid">${escapeHtml(msg.userId || "Unknown ID")}</span>
|
||||||
|
<span class="timestamp">${formatDate(msg.timestamp)}</span>
|
||||||
|
</div>
|
||||||
|
<div class="content">${parseMarkdown(msg.content || "")}</div>
|
||||||
|
${renderEmbeds(msg.embeds || [])}
|
||||||
|
${(msg.attachments || []).map(renderAttachment).join("")}
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
`,
|
||||||
|
)
|
||||||
|
.join("")
|
||||||
|
: `<div class="empty-state">${escapeHtml(messagesCfg.EmptyState)}</div>`
|
||||||
|
}
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPreview(templateInput) {
|
||||||
|
const sample = getSampleTicket();
|
||||||
|
return createTranscriptHTML(
|
||||||
|
sample,
|
||||||
|
"Resolved — thank you for contacting support.",
|
||||||
|
templateInput,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
TEMPLATE_PATH,
|
||||||
|
getDefaultTemplate,
|
||||||
|
getTemplate,
|
||||||
|
reloadTemplate,
|
||||||
|
saveTemplate,
|
||||||
|
normalizeTemplate,
|
||||||
|
getSampleTicket,
|
||||||
|
createTranscriptHTML,
|
||||||
|
renderPreview,
|
||||||
|
};
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
const REPOSITORIES = [
|
||||||
|
{ id: "bot", name: "SupportBot", repo: "Emerald-Services/SupportBot", branch: "release" },
|
||||||
|
{ id: "dashboard", name: "SupportBot Dashboard", repo: "Emerald-Services/SupportBot-Dashboard", branch: "release" }
|
||||||
|
];
|
||||||
|
|
||||||
|
function compareVersions(a, b) {
|
||||||
|
const pa = String(a)
|
||||||
|
.replace(/^v/i, "")
|
||||||
|
.split(".")
|
||||||
|
.map((n) => parseInt(n, 10) || 0);
|
||||||
|
const pb = String(b)
|
||||||
|
.replace(/^v/i, "")
|
||||||
|
.split(".")
|
||||||
|
.map((n) => parseInt(n, 10) || 0);
|
||||||
|
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
||||||
|
if ((pa[i] || 0) > (pb[i] || 0)) return 1;
|
||||||
|
if ((pa[i] || 0) < (pb[i] || 0)) return -1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchUpdateInfo() {
|
||||||
|
const axios = require("axios");
|
||||||
|
const pkgPath = path.join(__dirname, "../package.json");
|
||||||
|
const currentPkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
||||||
|
const currentVersion = currentPkg.version;
|
||||||
|
|
||||||
|
const checks = [];
|
||||||
|
|
||||||
|
for (const target of REPOSITORIES) {
|
||||||
|
let installedVersion = target.id === "bot" ? currentPkg.version : (currentPkg.dashboardVersion || "1.0.0");
|
||||||
|
let latest = installedVersion;
|
||||||
|
try {
|
||||||
|
const pkgUrl = target.id === 'dashboard'
|
||||||
|
? `https://raw.githubusercontent.com/${target.repo}/${target.branch}/dashboard/package.json`
|
||||||
|
: `https://raw.githubusercontent.com/${target.repo}/${target.branch}/package.json`;
|
||||||
|
|
||||||
|
const remoteRes = await axios.get(pkgUrl, {
|
||||||
|
timeout: 15000,
|
||||||
|
headers: { "User-Agent": "SupportBot-Updater" },
|
||||||
|
});
|
||||||
|
if (remoteRes.data && remoteRes.data.version) {
|
||||||
|
latest = remoteRes.data.version;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`[Updater] Failed to check ${target.repo} for updates.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateAvailable = compareVersions(latest, installedVersion) > 0;
|
||||||
|
|
||||||
|
checks.push({
|
||||||
|
id: target.id,
|
||||||
|
name: target.name,
|
||||||
|
current: installedVersion,
|
||||||
|
latest,
|
||||||
|
updateAvailable,
|
||||||
|
repository: `https://github.com/${target.repo}`,
|
||||||
|
branch: target.branch,
|
||||||
|
zipUrl: `https://github.com/${target.repo}/archive/refs/heads/${target.branch}.zip`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return checks;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
compareVersions,
|
||||||
|
fetchUpdateInfo,
|
||||||
|
REPOSITORIES,
|
||||||
|
};
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
function withTimeout(promise, ms, message) {
|
||||||
|
let timer;
|
||||||
|
const timeout = new Promise((_, reject) => {
|
||||||
|
timer = setTimeout(() => {
|
||||||
|
reject(new Error(message || `Timed out after ${ms}ms`));
|
||||||
|
}, ms);
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.race([promise, timeout]).finally(() => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { withTimeout };
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
/** Curated Google Fonts for transcript HTML (keep in sync with dashboard/src/lib/google-fonts.ts). */
|
||||||
|
|
||||||
|
const SYSTEM_FALLBACK =
|
||||||
|
'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif';
|
||||||
|
|
||||||
|
const GOOGLE_FONTS = [
|
||||||
|
{ id: "Inter", label: "Inter" },
|
||||||
|
{ id: "Roboto", label: "Roboto" },
|
||||||
|
{ id: "Open Sans", label: "Open Sans" },
|
||||||
|
{ id: "Lato", label: "Lato" },
|
||||||
|
{ id: "Montserrat", label: "Montserrat" },
|
||||||
|
{ id: "Poppins", label: "Poppins" },
|
||||||
|
{ id: "Nunito", label: "Nunito" },
|
||||||
|
{ id: "Raleway", label: "Raleway" },
|
||||||
|
{ id: "Work Sans", label: "Work Sans" },
|
||||||
|
{ id: "Source Sans 3", label: "Source Sans 3" },
|
||||||
|
{ id: "DM Sans", label: "DM Sans" },
|
||||||
|
{ id: "Manrope", label: "Manrope" },
|
||||||
|
{ id: "Outfit", label: "Outfit" },
|
||||||
|
{ id: "Rubik", label: "Rubik" },
|
||||||
|
{ id: "Ubuntu", label: "Ubuntu" },
|
||||||
|
{ id: "Fira Sans", label: "Fira Sans" },
|
||||||
|
{ id: "PT Sans", label: "PT Sans" },
|
||||||
|
{ id: "Noto Sans", label: "Noto Sans" },
|
||||||
|
{ id: "Oswald", label: "Oswald" },
|
||||||
|
{ id: "Merriweather", label: "Merriweather" },
|
||||||
|
{ id: "Playfair Display", label: "Playfair Display" },
|
||||||
|
{ id: "Libre Baskerville", label: "Libre Baskerville" },
|
||||||
|
{ id: "JetBrains Mono", label: "JetBrains Mono" },
|
||||||
|
{ id: "Fira Code", label: "Fira Code" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const GOOGLE_FONT_IDS = new Set(GOOGLE_FONTS.map((f) => f.id));
|
||||||
|
|
||||||
|
function normalizeFontFamily(fontFamily) {
|
||||||
|
if (!fontFamily || typeof fontFamily !== "string") return "Inter";
|
||||||
|
const trimmed = fontFamily.trim();
|
||||||
|
if (trimmed === "system") return "system";
|
||||||
|
if (!trimmed.includes(",")) {
|
||||||
|
return GOOGLE_FONT_IDS.has(trimmed) ? trimmed : "Inter";
|
||||||
|
}
|
||||||
|
const match = trimmed.match(/^["']?([^"',]+)["']?/);
|
||||||
|
const first = match?.[1]?.trim();
|
||||||
|
if (first && GOOGLE_FONT_IDS.has(first)) return first;
|
||||||
|
return "Inter";
|
||||||
|
}
|
||||||
|
|
||||||
|
function googleFontsCssUrl(familyIds) {
|
||||||
|
const families = familyIds
|
||||||
|
.map((id) => {
|
||||||
|
const encoded = encodeURIComponent(id).replace(/%20/g, "+");
|
||||||
|
return `family=${encoded}:wght@400;600;700;800`;
|
||||||
|
})
|
||||||
|
.join("&");
|
||||||
|
return `https://fonts.googleapis.com/css2?${families}&display=swap`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveTypography(fontFamily) {
|
||||||
|
const id = normalizeFontFamily(fontFamily);
|
||||||
|
|
||||||
|
if (id === "system") {
|
||||||
|
return {
|
||||||
|
fontId: "system",
|
||||||
|
fontFamilyCss: SYSTEM_FALLBACK,
|
||||||
|
headLinks: "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = googleFontsCssUrl([id]);
|
||||||
|
return {
|
||||||
|
fontId: id,
|
||||||
|
fontFamilyCss: `"${id}", ${SYSTEM_FALLBACK}`,
|
||||||
|
headLinks: `<link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link href="${url}" rel="stylesheet">`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
GOOGLE_FONTS,
|
||||||
|
GOOGLE_FONT_IDS,
|
||||||
|
SYSTEM_FALLBACK,
|
||||||
|
normalizeFontFamily,
|
||||||
|
googleFontsCssUrl,
|
||||||
|
resolveTypography,
|
||||||
|
};
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
// SupportBot, Created by Emerald Services
|
|
||||||
// Help Command
|
|
||||||
|
|
||||||
const Discord = require("discord.js");
|
|
||||||
const fs = require("fs");
|
|
||||||
|
|
||||||
const yaml = require('js-yaml');
|
|
||||||
|
|
||||||
// location for the addons configuration, to keep it simple we generate a settings folder within the addons directory.
|
|
||||||
const example = yaml.load(fs.readFileSync('./addons/settings/exampleaddon.yml', 'utf8'));
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
name: example.ExampleAddonCommand,
|
|
||||||
description: example.ExampleAddonDesc,
|
|
||||||
|
|
||||||
execute(message, args) {
|
|
||||||
if (supportbot.DeleteMessages == "true") message.delete();
|
|
||||||
message.channel.send(example.ExampleAddonMessage)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Vendored
BIN
Binary file not shown.
@@ -1,9 +0,0 @@
|
|||||||
#
|
|
||||||
# SupportBot, Created by Emerald Services
|
|
||||||
# Example Addon
|
|
||||||
#
|
|
||||||
|
|
||||||
ExampleAddonCommand: "exampleaddon"
|
|
||||||
ExampleAddonDesc: "This is the default/example addon for SupportBot."
|
|
||||||
|
|
||||||
ExampleAddonMessage: "> :white_check_mark: This is the **default** addon for SupportBot. You can download addons from our website, https://emeraldservices.xyz/"
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
// SupportBot, Created by Emerald Services
|
|
||||||
// Add User
|
|
||||||
|
|
||||||
const Discord = require("discord.js");
|
|
||||||
const fs = require("fs");
|
|
||||||
|
|
||||||
const yaml = require('js-yaml');
|
|
||||||
const supportbot = yaml.load(fs.readFileSync('./supportbot-config.yml', 'utf8'));
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
name: supportbot.AddUser,
|
|
||||||
description: supportbot.AddUserDesc,
|
|
||||||
|
|
||||||
execute(message, args) {
|
|
||||||
if (supportbot.DeleteMessages) message.delete();
|
|
||||||
|
|
||||||
let SupportStaff = message.guild.roles.cache.find(SupportTeam => SupportTeam.name === supportbot.Staff) || message.guild.roles.cache.find(SupportTeam => SupportTeam.id === supportbot.Staff)
|
|
||||||
let Admins = message.guild.roles.cache.find(AdminUser => AdminUser.name === supportbot.Admin) || message.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 (message.member.roles.cache.has(SupportStaff.id) || message.member.roles.cache.has(Admins.id)) {
|
|
||||||
if (!message.channel.name.startsWith( `${supportbot.TicketChannel}-` )) {
|
|
||||||
const Exists = new Discord.MessageEmbed()
|
|
||||||
.setTitle("No Ticket Found!")
|
|
||||||
.setDescription(`${supportbot.NoValidTicket}`)
|
|
||||||
.setColor(supportbot.WarningColour);
|
|
||||||
message.channel.send({ embed: Exists });
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let rUser = message.guild.member(message.mentions.users.first() || message.guild.members.cache.get(args[0]));
|
|
||||||
const UserNotExist = new Discord.MessageEmbed()
|
|
||||||
.setTitle("User Not Found!")
|
|
||||||
.setDescription(`${supportbot.UserNotFound}\n\nTry Again:\`${supportbot.Prefix}${supportbot.AddUser} <user#0000>\``)
|
|
||||||
.setColor(supportbot.ErrorColour)
|
|
||||||
|
|
||||||
if (!rUser) return message.channel.send({ embed: UserNotExist });
|
|
||||||
|
|
||||||
message.channel.updateOverwrite(rUser, {
|
|
||||||
VIEW_CHANNEL: true,
|
|
||||||
CREATE_INVITE: false,
|
|
||||||
SEND_MESSAGES: true,
|
|
||||||
READ_MESSAGES: true
|
|
||||||
});
|
|
||||||
|
|
||||||
const Complete = new Discord.MessageEmbed()
|
|
||||||
.setTitle("User Added!")
|
|
||||||
.setDescription(supportbot.AddedUser.replace(/%user%/g, rUser.id))
|
|
||||||
.setTimestamp()
|
|
||||||
.setColor(supportbot.EmbedColour)
|
|
||||||
message.channel.send({ embed: Complete });
|
|
||||||
} else {
|
|
||||||
|
|
||||||
return message.channel.send({ embed: NoPerms });
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
// SupportBot, Created by Emerald Services
|
|
||||||
// Announce
|
|
||||||
|
|
||||||
const Discord = require("discord.js");
|
|
||||||
const fs = require("fs");
|
|
||||||
|
|
||||||
const yaml = require('js-yaml');
|
|
||||||
const { execute } = require("./help");
|
|
||||||
const supportbot = yaml.load(fs.readFileSync('./supportbot-config.yml', 'utf8'));
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
name: supportbot.AnnounceCommand,
|
|
||||||
description: supportbot.AnnounceDesc,
|
|
||||||
|
|
||||||
execute(message, args) {
|
|
||||||
|
|
||||||
if (supportbot.DeleteMessages) message.delete();
|
|
||||||
|
|
||||||
let locateChannel = message.guild.channels.cache.find(AnnouncementChannel => AnnouncementChannel.name === supportbot.AnnouncementChannel) || message.guild.channels.cache.find(AnnouncementChannel => AnnouncementChannel.id === supportbot.AnnouncementChannel)
|
|
||||||
|
|
||||||
const errornochannel = new Discord.MessageEmbed()
|
|
||||||
.setTitle("Invalid Channel")
|
|
||||||
.setDescription(`${supportbot.InvalidChannel}\n\nChannel Required: \`${supportbot.AnnouncementChannel}`)
|
|
||||||
.setColor(supportbot.ErrorColour);
|
|
||||||
|
|
||||||
if(!locateChannel) return message.channel.send({ embed: errornochannel });
|
|
||||||
|
|
||||||
let Admins = message.guild.roles.cache.find(adminRole => adminRole.name === supportbot.Admin) || message.guild.roles.cache.find(adminRole => adminRole.id === supportbot.Admin)
|
|
||||||
|
|
||||||
const NoPerms = new Discord.MessageEmbed()
|
|
||||||
.setTitle("Invalid Permissions!")
|
|
||||||
.setDescription(`${supportbot.IncorrectPerms}\n\nRole Required: \`${Admins.name}\``)
|
|
||||||
.setColor(supportbot.WarningColour)
|
|
||||||
|
|
||||||
if (!message.member.roles.cache.has(Admins.id))
|
|
||||||
return message.channel.send({ embed: NoPerms });
|
|
||||||
|
|
||||||
|
|
||||||
const embed = new Discord.MessageEmbed()
|
|
||||||
.setDescription(`> **${supportbot.AnnouncementStarter}**`)
|
|
||||||
.setColor(supportbot.EmbedColour)
|
|
||||||
message.channel.send({ embed: embed });
|
|
||||||
|
|
||||||
let announcement = []
|
|
||||||
message.channel.awaitMessages(response => response.content.length > 2, {
|
|
||||||
max: 1,
|
|
||||||
time: 500000,
|
|
||||||
errors: ['time'],
|
|
||||||
}).then((collected) => {
|
|
||||||
announcement.push(collected.map(r => r.content));
|
|
||||||
|
|
||||||
locateChannel.send('@everyone')
|
|
||||||
const AnnouncementEmbed = new Discord.MessageEmbed()
|
|
||||||
.setTitle(supportbot.AnnouncementTitle)
|
|
||||||
.setThumbnail(supportbot.AnnouncementIcon)
|
|
||||||
.setDescription(announcement)
|
|
||||||
.setColor(supportbot.EmbedColour)
|
|
||||||
.setFooter(supportbot.EmbedFooter, message.author.displayAvatarURL());
|
|
||||||
|
|
||||||
locateChannel.send({ embed: AnnouncementEmbed }).then(async function(msg) {
|
|
||||||
const AnnouncementComplete = new Discord.MessageEmbed()
|
|
||||||
.setColor(supportbot.SuccessColour)
|
|
||||||
.setDescription(`:white_check_mark: You have successfully created an announcement. <#${locateChannel.id}>`)
|
|
||||||
message.channel.send({ embed: AnnouncementComplete })
|
|
||||||
})
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
-108
@@ -1,108 +0,0 @@
|
|||||||
// SupportBot, Created by Emerald Services
|
|
||||||
// Say Command
|
|
||||||
|
|
||||||
const Discord = require("discord.js");
|
|
||||||
const fs = require("fs");
|
|
||||||
|
|
||||||
const yaml = require('js-yaml');
|
|
||||||
const { execute } = require("./help");
|
|
||||||
const supportbot = yaml.load(fs.readFileSync('./supportbot-config.yml', 'utf8'));
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
name: supportbot.BanUser,
|
|
||||||
description: supportbot.BanDesc,
|
|
||||||
|
|
||||||
execute(message, args, member) {
|
|
||||||
if (supportbot.DeleteMessages) message.delete();
|
|
||||||
|
|
||||||
let SupportStaff = message.guild.roles.cache.find(adminRole => adminRole.name === supportbot.Admin) || message.guild.roles.cache.find(adminRole => adminRole.id === supportbot.Admin)
|
|
||||||
|
|
||||||
const NoPerms = new Discord.MessageEmbed()
|
|
||||||
.setTitle("Invalid Permissions!")
|
|
||||||
.setDescription(`${supportbot.IncorrectPerms}\n\nRole Required: \`${SupportStaff.name}\``)
|
|
||||||
.setColor(supportbot.WarningColour)
|
|
||||||
|
|
||||||
if (!message.member.roles.cache.has(SupportStaff.id))
|
|
||||||
return message.channel.send({ embed: NoPerms });
|
|
||||||
|
|
||||||
let locateChannel = message.guild.channels.cache.find(ModLogChannel => ModLogChannel.name === supportbot.ModLogChannel) || message.guild.channels.cache.find(ModLogChannel => ModLogChannel.id === supportbot.ModLogChannel)
|
|
||||||
|
|
||||||
const errornochannel = new Discord.MessageEmbed()
|
|
||||||
.setTitle("Invalid Channel")
|
|
||||||
.setDescription(`${supportbot.InvalidChannel}\n\nChannel Required: \`${supportbot.ModLogChannel}\``)
|
|
||||||
.setColor(supportbot.ErrorColour);
|
|
||||||
|
|
||||||
if(!locateChannel) return message.channel.send({ embed: errornochannel });
|
|
||||||
|
|
||||||
const user = message.mentions.members.first() || message.guild.members.cache.get(args[0]);
|
|
||||||
|
|
||||||
const embed1 = new Discord.MessageEmbed()
|
|
||||||
.setDescription("> :x: Please provide a reason to ban.\n\n`Try Again!`")
|
|
||||||
.setColor(supportbot.ErrorColour)
|
|
||||||
|
|
||||||
if (!args[0]) return message.channel.send({
|
|
||||||
embed: embed1
|
|
||||||
});
|
|
||||||
|
|
||||||
const embed2 = new Discord.MessageEmbed()
|
|
||||||
.setDescription("> :detective: **Hmm!** Can\'t seem tp find this user.\n\n`Are they in the server?`")
|
|
||||||
.setColor(supportbot.EmbedColour)
|
|
||||||
|
|
||||||
if (!user) return message.channel.send({
|
|
||||||
embed: embed2
|
|
||||||
})
|
|
||||||
|
|
||||||
const embed3 = new Discord.MessageEmbed()
|
|
||||||
.setDescription("> :x: **Err No!** Cannot punish this user!\n\n`Reason: Their permissions and or roles are greater than mine.`")
|
|
||||||
.setColor(supportbot.EmbedColour)
|
|
||||||
|
|
||||||
if(!user.bannable) return message.channel.send({
|
|
||||||
embed: embed3
|
|
||||||
});
|
|
||||||
|
|
||||||
const embed4 = new Discord.MessageEmbed()
|
|
||||||
.setDescription("> :joy: **OML!** Did you really just try to ban yourself!")
|
|
||||||
.setColor(supportbot.EmbedColour)
|
|
||||||
|
|
||||||
if(user.id === message.author.id) return message.channel.send({
|
|
||||||
embed: embed4
|
|
||||||
});
|
|
||||||
|
|
||||||
let reason = args.slice(1).join(" ");
|
|
||||||
|
|
||||||
if(!reason) reason = 'No Reason Provided.';
|
|
||||||
|
|
||||||
const embed5 = new Discord.MessageEmbed()
|
|
||||||
.setDescription("> :thinking: **Hmm** Something seems to have gone wrong.\n\n`Check your system for errors!`")
|
|
||||||
.setColor(supportbot.EmbedColour)
|
|
||||||
|
|
||||||
user.ban({ days: supportbot.DaysBannedFor, reason: `${reason}` }).catch(err => {
|
|
||||||
message.channel.send({ embed: embed5 })
|
|
||||||
console.log(err)
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
const successEmbed = new Discord.MessageEmbed()
|
|
||||||
.setDescription(`> :white_check_mark: **OOF!** ${user} has been banned!`)
|
|
||||||
.setColor(supportbot.SuccessColour)
|
|
||||||
message.channel.send({
|
|
||||||
embed: successEmbed
|
|
||||||
})
|
|
||||||
|
|
||||||
const logEmbed = new Discord.MessageEmbed()
|
|
||||||
.setTitle(supportbot.BanTitle)
|
|
||||||
.setThumbnail(supportbot.ModThumbnail)
|
|
||||||
.addField('Banned User:', user)
|
|
||||||
.addField('Staff Member', message.author)
|
|
||||||
.addField('Reason:', reason)
|
|
||||||
.setFooter(supportbot.EmbedFooter)
|
|
||||||
.setColor(supportbot.BanLogColour)
|
|
||||||
.setTimestamp()
|
|
||||||
|
|
||||||
locateChannel.send({ embed: logEmbed })
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
// SupportBot, Created by Emerald Services
|
|
||||||
// Blacklist Command
|
|
||||||
|
|
||||||
const Discord = require("discord.js");
|
|
||||||
const fs = require("fs");
|
|
||||||
|
|
||||||
const yaml = require('js-yaml');
|
|
||||||
const { execute } = require("./help");
|
|
||||||
const supportbot = yaml.load(fs.readFileSync('./supportbot-config.yml', 'utf8'));
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
name: supportbot.BlacklistUser,
|
|
||||||
description: supportbot.BlacklistDesc,
|
|
||||||
|
|
||||||
execute(message, args) {
|
|
||||||
if (supportbot.DeleteMessages) message.delete();
|
|
||||||
|
|
||||||
let SupportStaff = message.guild.roles.cache.find(adminRole => adminRole.name === supportbot.Admin) || message.guild.roles.cache.find(adminRole => adminRole.id === supportbot.Admin)
|
|
||||||
|
|
||||||
const NoPerms = new Discord.MessageEmbed()
|
|
||||||
.setTitle("Invalid Permissions!")
|
|
||||||
.setDescription(`${supportbot.IncorrectPerms}\n\nRole Required: \`${SupportStaff.name}\``)
|
|
||||||
.setColor(supportbot.WarningColour)
|
|
||||||
|
|
||||||
if (!message.member.roles.cache.has(SupportStaff.id)) {
|
|
||||||
return message.channel.send({ embed: NoPerms });
|
|
||||||
}
|
|
||||||
|
|
||||||
let locateChannel = message.guild.channels.cache.find(ModLogChannel => ModLogChannel.name === supportbot.ModLogChannel) || message.guild.channels.cache.find(ModLogChannel => ModLogChannel.id === supportbot.ModLogChannel)
|
|
||||||
|
|
||||||
const user = message.mentions.members.first() || message.guild.members.cache.get(args[0]);
|
|
||||||
|
|
||||||
const errornochannel = new Discord.MessageEmbed()
|
|
||||||
.setTitle("Invalid Channel")
|
|
||||||
.setDescription(`${supportbot.InvalidChannel}\n\nChannel Required: \`${supportbot.ModLogChannel}\``)
|
|
||||||
.setColor(supportbot.ErrorColour);
|
|
||||||
|
|
||||||
if(!locateChannel) return message.channel.send({ embed: errornochannel });
|
|
||||||
|
|
||||||
const embed1 = new Discord.MessageEmbed()
|
|
||||||
.setDescription("> :x: Please mention or write an ID\n\n`Try Again!`")
|
|
||||||
.setColor(supportbot.ErrorColour)
|
|
||||||
|
|
||||||
if (!args[0]) return message.channel.send({
|
|
||||||
embed: embed1
|
|
||||||
});
|
|
||||||
|
|
||||||
const embed2 = new Discord.MessageEmbed()
|
|
||||||
.setDescription("> :detective: **Hmm!** Can\'t seem tp find this user.\n\n`Are they in the server?`")
|
|
||||||
.setColor(supportbot.EmbedColour)
|
|
||||||
|
|
||||||
if (!user) return message.channel.send({
|
|
||||||
embed: embed2
|
|
||||||
});
|
|
||||||
|
|
||||||
let role = message.guild.roles.cache.find(role => role.name === supportbot.TicketBlackListRole) || message.guild.roles.cache.find(role => role.id === supportbot.TicketBlackListRole)
|
|
||||||
|
|
||||||
const embed3 = new Discord.MessageEmbed()
|
|
||||||
.setDescription("> :detective: **Hmm!** Can\'t seem to find the blacklist role.\n\n`Did you created it?")
|
|
||||||
.setColor(supportbot.ErrorColour)
|
|
||||||
|
|
||||||
|
|
||||||
if (!role) return message.channel.send({
|
|
||||||
embed: embed3
|
|
||||||
});
|
|
||||||
|
|
||||||
const embed4 = new Discord.MessageEmbed()
|
|
||||||
.setDescription(`${user} is already blacklisted`)
|
|
||||||
.setColor(supportbot.ErrorColour)
|
|
||||||
|
|
||||||
if(user.roles.cache.find(role => role.name === supportbot.TicketBlackListRole) || user.roles.cache.find(role => role.id === supportbot.TicketBlackListRole)) {
|
|
||||||
return message.channel.send({
|
|
||||||
embed: embed4
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
user.roles.add(role);
|
|
||||||
|
|
||||||
const embed5 = new Discord.MessageEmbed()
|
|
||||||
.setDescription(`Blacklisted ${user} successfully from creating a ticket`)
|
|
||||||
.setColor(supportbot.EmbedColour)
|
|
||||||
|
|
||||||
message.channel.send({
|
|
||||||
embed: embed5
|
|
||||||
});
|
|
||||||
|
|
||||||
const logEmbed = new Discord.MessageEmbed()
|
|
||||||
.setTitle(supportbot.BlacklistTitle)
|
|
||||||
.setThumbnail(supportbot.ModThumbnail)
|
|
||||||
.addField('Banned User:', user)
|
|
||||||
.addField('Staff Member', message.author)
|
|
||||||
.setFooter(supportbot.EmbedFooter)
|
|
||||||
.setColor(supportbot.BlacklistColour)
|
|
||||||
.setTimestamp()
|
|
||||||
|
|
||||||
locateChannel.send({ embed: logEmbed });
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
// SupportBot, Created by Emerald Services
|
|
||||||
// Say Command
|
|
||||||
|
|
||||||
const Discord = require("discord.js");
|
|
||||||
const fs = require("fs");
|
|
||||||
|
|
||||||
const yaml = require('js-yaml');
|
|
||||||
const { execute } = require("./help");
|
|
||||||
const supportbot = yaml.load(fs.readFileSync('./supportbot-config.yml', 'utf8'));
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
name: supportbot.BotSay,
|
|
||||||
description: supportbot.BotSayDesc,
|
|
||||||
|
|
||||||
execute(message, args) {
|
|
||||||
if (supportbot.DeleteMessages) message.delete();
|
|
||||||
|
|
||||||
let SupportStaff = message.guild.roles.cache.find(adminRole => adminRole.name === supportbot.Staff) || message.guild.roles.cache.find(adminRole => adminRole.id === supportbot.Staff)
|
|
||||||
|
|
||||||
const NoPerms = new Discord.MessageEmbed()
|
|
||||||
.setTitle("Invalid Permissions!")
|
|
||||||
.setDescription(`${supportbot.IncorrectPerms}\n\nRole Required: \`${SupportStaff.name}\``)
|
|
||||||
.setColor(supportbot.WarningColour)
|
|
||||||
|
|
||||||
if (!message.member.roles.cache.has(SupportStaff.id))
|
|
||||||
return message.channel.send({ embed: NoPerms });
|
|
||||||
|
|
||||||
|
|
||||||
const embed = new Discord.MessageEmbed()
|
|
||||||
.setDescription(`> **What would you like me to say!**`)
|
|
||||||
.setColor(supportbot.EmbedColour)
|
|
||||||
message.channel.send({ embed: embed });
|
|
||||||
|
|
||||||
let announcement = []
|
|
||||||
message.channel.awaitMessages(response => response.content.length > 2, {
|
|
||||||
max: 1,
|
|
||||||
time: 500000,
|
|
||||||
errors: ['time'],
|
|
||||||
}).then((collected) => {
|
|
||||||
announcement.push(collected.map(r => r.content));
|
|
||||||
|
|
||||||
const AnnouncementEmbed = new Discord.MessageEmbed()
|
|
||||||
.setDescription(announcement)
|
|
||||||
.setColor(supportbot.EmbedColour)
|
|
||||||
|
|
||||||
message.channel.send({ embed: AnnouncementEmbed })
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
// SupportBot, Created by Emerald Services
|
|
||||||
// Close Command
|
|
||||||
|
|
||||||
const Discord = require("discord.js");
|
|
||||||
const fs = require("fs");
|
|
||||||
|
|
||||||
const yaml = require('js-yaml');
|
|
||||||
const { execute } = require("./help");
|
|
||||||
const supportbot = yaml.load(fs.readFileSync('./supportbot-config.yml', 'utf8'));
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
name: supportbot.CloseTicket,
|
|
||||||
description: supportbot.CloseTicketDesc,
|
|
||||||
|
|
||||||
execute(message, args) {
|
|
||||||
if (supportbot.DeleteMessages) message.delete();
|
|
||||||
|
|
||||||
if (!message.channel.name.startsWith( `${supportbot.TicketChannel}-` )) {
|
|
||||||
const Exists = new Discord.MessageEmbed()
|
|
||||||
.setTitle("No Ticket Found!")
|
|
||||||
.setDescription(`${supportbot.NoValidTicket}`)
|
|
||||||
.setColor(supportbot.WarningColour);
|
|
||||||
message.channel.send({ embed: Exists });
|
|
||||||
|
|
||||||
return;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
if (supportbot.CloseConfirmation) {
|
|
||||||
const CloseTicketRequest = new Discord.MessageEmbed()
|
|
||||||
.setTitle(`**${supportbot.ClosingTicket}**`)
|
|
||||||
.setDescription(`Please confirm by repeating the following word.. \`${supportbot.ClosingConfirmation_Word}\` `)
|
|
||||||
.setColor(supportbot.EmbedColour);
|
|
||||||
|
|
||||||
message.channel.send(CloseTicketRequest).then(m => {
|
|
||||||
message.channel.awaitMessages( response => response.content.toLowerCase() === supportbot.ClosingConfirmation_Word, {
|
|
||||||
max: 1,
|
|
||||||
time: 20000,
|
|
||||||
errors: [ 'time' ],
|
|
||||||
|
|
||||||
|
|
||||||
}).then( collected => {
|
|
||||||
let logChannel = message.guild.channels.cache.find(channel => channel.name === supportbot.TranscriptLog) || message.guild.channels.cache.find(channel => channel.id === supportbot.TranscriptLog)
|
|
||||||
let user = message.author;
|
|
||||||
let reason = args.join(" ") || "No Reason Provided.";
|
|
||||||
|
|
||||||
let name = message.channel.name;
|
|
||||||
let ticketChannel = message.channel;
|
|
||||||
|
|
||||||
message.channel.send(`**${supportbot.ClosingTicket}**`)
|
|
||||||
|
|
||||||
.then(() => {
|
|
||||||
const logEmbed = new Discord.MessageEmbed()
|
|
||||||
.setTitle(supportbot.TranscriptTitle)
|
|
||||||
.setColor(supportbot.EmbedColour)
|
|
||||||
.setFooter(supportbot.EmbedFooter)
|
|
||||||
.addField("Closed By", message.author.tag)
|
|
||||||
.addField("Reason", reason);
|
|
||||||
|
|
||||||
message.channel.messages.fetch({ limit: 100 }).then( msgs => {
|
|
||||||
let html = '';
|
|
||||||
|
|
||||||
msgs = msgs.sort( ( a, b ) => a.createdTimestamp - b.createdTimestamp );
|
|
||||||
html += `<strong>Server Name:</strong> ${message.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(logEmbed).catch(err => {
|
|
||||||
message.reply(err)
|
|
||||||
})
|
|
||||||
|
|
||||||
logChannel.send( new Discord.MessageAttachment( Buffer.from( html ), `${name}.html` ) );
|
|
||||||
|
|
||||||
message.channel.delete();
|
|
||||||
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
}).catch( () => {
|
|
||||||
m.edit('The request to close the ticket has timed out.').then( m2 => m2.delete( 3000 ) );
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!supportbot.CloseConfirmation) {
|
|
||||||
|
|
||||||
let logChannel = message.guild.channels.cache.find(channel => channel.name === supportbot.TranscriptLog) || message.guild.channels.cache.find(channel => channel.id === supportbot.TranscriptLog)
|
|
||||||
let user = message.author;
|
|
||||||
let reason = args.join(" ") || "No Reason Provided.";
|
|
||||||
|
|
||||||
let name = message.channel.name;
|
|
||||||
let ticketChannel = message.channel;
|
|
||||||
|
|
||||||
message.channel.send(`**${supportbot.ClosingTicket}**`)
|
|
||||||
|
|
||||||
.then(() => {
|
|
||||||
const logEmbed = new Discord.MessageEmbed()
|
|
||||||
.setTitle(supportbot.TranscriptTitle)
|
|
||||||
.setColor(supportbot.EmbedColour)
|
|
||||||
.setFooter(supportbot.EmbedFooter)
|
|
||||||
.addField("Closed By", message.author.tag)
|
|
||||||
.addField("Reason", reason);
|
|
||||||
|
|
||||||
message.channel.messages.fetch({ limit: 100 }).then( msgs => {
|
|
||||||
let html = '';
|
|
||||||
|
|
||||||
msgs = msgs.sort( ( a, b ) => a.createdTimestamp - b.createdTimestamp );
|
|
||||||
html += `<strong>Server Name:</strong> ${message.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(logEmbed).catch(err => {
|
|
||||||
message.reply(err)
|
|
||||||
})
|
|
||||||
|
|
||||||
logChannel.send( new Discord.MessageAttachment( Buffer.from( html ), `${name}.html` ) );
|
|
||||||
|
|
||||||
message.channel.delete();
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
}).catch( () => {
|
|
||||||
m.edit('The request to close the ticket has timed out.').then( m2 => m2.delete( 3000 ) );
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
// SupportBot, Created by Emerald Services
|
|
||||||
// Help Command
|
|
||||||
|
|
||||||
const Discord = require("discord.js");
|
|
||||||
const fs = require("fs");
|
|
||||||
|
|
||||||
const yaml = require('js-yaml');
|
|
||||||
const supportbot = yaml.load(fs.readFileSync('./supportbot-config.yml', 'utf8'));
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
name: supportbot.HelpCommand,
|
|
||||||
description: supportbot.HelpDesc,
|
|
||||||
|
|
||||||
execute(message, args) {
|
|
||||||
if (supportbot.DeleteMessages) message.delete();
|
|
||||||
|
|
||||||
let SupportStaff = message.guild.roles.cache.find(adminRole => adminRole.name === supportbot.Staff) || message.guild.roles.cache.find(staffRole => staffRole.id === supportbot.Staff);
|
|
||||||
let AdminStaff = message.guild.roles.cache.find(adminRole => adminRole.name === supportbot.Admin) || message.guild.roles.cache.find(adminRole => adminRole.id === supportbot.Admin);
|
|
||||||
|
|
||||||
if (supportbot.HelpMenu) {
|
|
||||||
let generalCommands = "";
|
|
||||||
generalCommands += `**${supportbot.Prefix}${supportbot.HelpCommand}** ${supportbot.HelpDesc}\n`;
|
|
||||||
generalCommands += `**${supportbot.Prefix}${supportbot.LinksCommand}** ${supportbot.LinksDesc}\n`;
|
|
||||||
generalCommands += `**${supportbot.Prefix}${supportbot.SuggestCommand}** ${supportbot.SuggestionDesc}\n`;
|
|
||||||
generalCommands += `**${supportbot.Prefix}${supportbot.PingCommand}** ${supportbot.PingDesc}\n`;
|
|
||||||
|
|
||||||
let supportCommands = "";
|
|
||||||
supportCommands += `**${supportbot.Prefix}${supportbot.NewTicket} [reason]** ${supportbot.NewTicketDesc}\n`
|
|
||||||
supportCommands += `**${supportbot.Prefix}${supportbot.CloseTicket} [reason]** ${supportbot.CloseTicketDesc}\n`
|
|
||||||
|
|
||||||
let staffCommands = "";
|
|
||||||
staffCommands += `**${supportbot.Prefix}${supportbot.AddUser} <user#0000>** ${supportbot.AddUserDesc}\n`
|
|
||||||
staffCommands += `**${supportbot.Prefix}${supportbot.RemoveUser} <user#0000>** ${supportbot.RemoveUserDesc}\n`
|
|
||||||
staffCommands += `**${supportbot.Prefix}${supportbot.BotSay}** ${supportbot.BotSayDesc}\n`
|
|
||||||
staffCommands += `**${supportbot.Prefix}${supportbot.PurgeMessage}** ${supportbot.PurgeDesc}\n`
|
|
||||||
staffCommands += `**${supportbot.Prefix}${supportbot.ChatLog}** ${supportbot.ChatLogDesc}\n`
|
|
||||||
|
|
||||||
let adminCommands = "";
|
|
||||||
adminCommands += `**${supportbot.Prefix}${supportbot.AnnounceCommand}** ${supportbot.AnnounceDesc} \`Admin Only\`\n`
|
|
||||||
adminCommands += `**${supportbot.Prefix}${supportbot.BanUser}** ${supportbot.BanDesc} \`Admin Only\` \n`
|
|
||||||
adminCommands += `**${supportbot.Prefix}${supportbot.KickUser}** ${supportbot.KickDesc} \`Admin Only\` \n`
|
|
||||||
adminCommands += `**${supportbot.Prefix}${supportbot.BlacklistUser}** ${supportbot.BlacklistDesc} \`Admin Only\` \n`
|
|
||||||
|
|
||||||
const HelpEmbed1 = new Discord.MessageEmbed()
|
|
||||||
.setTitle(supportbot.Bot_Name)
|
|
||||||
.setThumbnail(message.author.displayAvatarURL())
|
|
||||||
|
|
||||||
.addFields(
|
|
||||||
{ name: '🖥️ General Commands\n', value: `${generalCommands}\n`, inline: false },
|
|
||||||
{ name: '🎫 Support Commands\n', value: `${supportCommands}\n`, inline: false },
|
|
||||||
)
|
|
||||||
|
|
||||||
.setColor(supportbot.EmbedColour)
|
|
||||||
.setFooter(supportbot.EmbedFooter, message.author.displayAvatarURL());
|
|
||||||
|
|
||||||
if (message.member.roles.cache.has(SupportStaff.id)) {
|
|
||||||
HelpEmbed1.addFields(
|
|
||||||
{ name: '🔐 Staff Commands\n', value: `${staffCommands}\n`, inline: false },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.member.roles.cache.has(AdminStaff.id)) {
|
|
||||||
HelpEmbed1.addFields(
|
|
||||||
{ name: '🔐 Admin Commands\n', value: `${adminCommands}\n`, inline: false },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (supportbot.SendHelpPage === "dm") {
|
|
||||||
message.author.send({
|
|
||||||
embed: HelpEmbed1
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (supportbot.SendHelpPage === "channel") {
|
|
||||||
message.channel.send({
|
|
||||||
embed: HelpEmbed1
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
// SupportBot, Created by Emerald Services
|
|
||||||
// Say Command
|
|
||||||
|
|
||||||
const Discord = require("discord.js");
|
|
||||||
const fs = require("fs");
|
|
||||||
|
|
||||||
const yaml = require('js-yaml');
|
|
||||||
const { execute } = require("./help");
|
|
||||||
const supportbot = yaml.load(fs.readFileSync('./supportbot-config.yml', 'utf8'));
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
name: supportbot.KickUser,
|
|
||||||
description: supportbot.KickDesc,
|
|
||||||
|
|
||||||
execute(message, args, member) {
|
|
||||||
if (supportbot.DeleteMessages) message.delete();
|
|
||||||
|
|
||||||
let SupportStaff = message.guild.roles.cache.find(adminRole => adminRole.name === supportbot.Admin) || message.guild.roles.cache.find(adminRole => adminRole.id === supportbot.Admin)
|
|
||||||
|
|
||||||
const NoPerms = new Discord.MessageEmbed()
|
|
||||||
.setTitle("Invalid Permissions!")
|
|
||||||
.setDescription(`${supportbot.IncorrectPerms}\n\nRole Required: \`${SupportStaff.name}\``)
|
|
||||||
.setColor(supportbot.WarningColour)
|
|
||||||
|
|
||||||
if (!message.member.roles.cache.has(SupportStaff.id))
|
|
||||||
return message.channel.send({ embed: NoPerms });
|
|
||||||
|
|
||||||
let locateChannel = message.guild.channels.cache.find(ModLogChannel => ModLogChannel.name === supportbot.ModLogChannel) || message.guild.channels.cache.find(ModLogChannel => ModLogChannel.id === supportbot.ModLogChannel)
|
|
||||||
|
|
||||||
const errornochannel = new Discord.MessageEmbed()
|
|
||||||
.setTitle("Invalid Channel")
|
|
||||||
.setDescription(`${supportbot.InvalidChannel}\n\nChannel Required: \`${supportbot.ModLogChannel}\``)
|
|
||||||
.setColor(supportbot.ErrorColour);
|
|
||||||
|
|
||||||
if(!locateChannel) return message.channel.send({ embed: errornochannel });
|
|
||||||
|
|
||||||
const user = message.mentions.members.first() || message.guild.members.cache.get(args[0]);
|
|
||||||
|
|
||||||
const embed1 = new Discord.MessageEmbed()
|
|
||||||
.setDescription("> :x: Please provide a reason to kick.\n\n`Try Again!`")
|
|
||||||
.setColor(supportbot.ErrorColour)
|
|
||||||
|
|
||||||
if (!args[0]) return message.channel.send({
|
|
||||||
embed: embed1
|
|
||||||
});
|
|
||||||
|
|
||||||
const embed2 = new Discord.MessageEmbed()
|
|
||||||
.setDescription("> :detective: **Hmm!** Can\'t seem tp find this user.\n\n`Are they in the server?`")
|
|
||||||
.setColor(supportbot.EmbedColour)
|
|
||||||
|
|
||||||
if (!user) return message.channel.send({
|
|
||||||
embed: embed2
|
|
||||||
})
|
|
||||||
|
|
||||||
const embed3 = new Discord.MessageEmbed()
|
|
||||||
.setDescription("> :x: **Err No!** Cannot punish this user!\n\n`Reason: Their permissions and or roles are greater than mine.`")
|
|
||||||
.setColor(supportbot.EmbedColour)
|
|
||||||
|
|
||||||
if(!user.kickable) return message.channel.send({
|
|
||||||
embed: embed3
|
|
||||||
});
|
|
||||||
|
|
||||||
const embed4 = new Discord.MessageEmbed()
|
|
||||||
.setDescription("> :joy: **OML!** Did you really just try to kick yourself!")
|
|
||||||
.setColor(supportbot.EmbedColour)
|
|
||||||
|
|
||||||
if(user.id === message.author.id) return message.channel.send({
|
|
||||||
embed: embed4
|
|
||||||
});
|
|
||||||
|
|
||||||
let reason = args.slice(1).join(" ");
|
|
||||||
|
|
||||||
if(!reason) reason = 'No Reason Provided.';
|
|
||||||
|
|
||||||
const embed5 = new Discord.MessageEmbed()
|
|
||||||
.setDescription("> :thinking: **Hmm** Something seems to have gone wrong.\n\n`Check your system for errors!`")
|
|
||||||
.setColor(supportbot.EmbedColour)
|
|
||||||
|
|
||||||
user.kick({ reason: `${reason}` }).catch(err => {
|
|
||||||
message.channel.send({ embed: embed5 })
|
|
||||||
console.log(err)
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
const successEmbed = new Discord.MessageEmbed()
|
|
||||||
.setDescription(`> :white_check_mark: **OOF!** ${user} has been kicked!`)
|
|
||||||
.setColor(supportbot.SuccessColour)
|
|
||||||
|
|
||||||
message.channel.send({
|
|
||||||
embed: successEmbed
|
|
||||||
})
|
|
||||||
|
|
||||||
const logEmbed = new Discord.MessageEmbed()
|
|
||||||
.setTitle(supportbot.KickTitle)
|
|
||||||
.setThumbnail(supportbot.ModThumbnail)
|
|
||||||
.addField('Kicked User:', user)
|
|
||||||
.addField('Staff Member', message.author)
|
|
||||||
.addField('Reason:', reason)
|
|
||||||
.setFooter(supportbot.EmbedFooter)
|
|
||||||
.setColor(supportbot.KickLogColour)
|
|
||||||
.setTimestamp()
|
|
||||||
|
|
||||||
locateChannel.send({ embed: logEmbed })
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
// SupportBot, Created by Emerald Services
|
|
||||||
// Links Command
|
|
||||||
|
|
||||||
const Discord = require("discord.js");
|
|
||||||
const fs = require("fs");
|
|
||||||
|
|
||||||
const yaml = require('js-yaml');
|
|
||||||
const { execute } = require("./help");
|
|
||||||
const supportbot = yaml.load(fs.readFileSync('./supportbot-config.yml', 'utf8'));
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
name: supportbot.LinksCommand,
|
|
||||||
description: supportbot.LinksDesc,
|
|
||||||
|
|
||||||
execute(message, args) {
|
|
||||||
if (supportbot.DeleteMessages) message.delete();
|
|
||||||
|
|
||||||
const LinksEmbed = new Discord.MessageEmbed()
|
|
||||||
.setColor(supportbot.EmbedColour)
|
|
||||||
|
|
||||||
let botLinks = supportbot.LINKS;
|
|
||||||
|
|
||||||
let links = '';
|
|
||||||
|
|
||||||
for ( let name in botLinks ) {
|
|
||||||
links += `[${name}](${botLinks[ name ]})\n`;
|
|
||||||
};
|
|
||||||
|
|
||||||
LinksEmbed.addField(supportbot.LinksTitle, links)
|
|
||||||
|
|
||||||
message.channel.send({ embed: LinksEmbed });
|
|
||||||
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
// SupportBot, Created by Emerald Services
|
|
||||||
// Ping Command
|
|
||||||
|
|
||||||
const Discord = require("discord.js");
|
|
||||||
const fs = require("fs");
|
|
||||||
|
|
||||||
const yaml = require('js-yaml');
|
|
||||||
const supportbot = yaml.load(fs.readFileSync('./supportbot-config.yml', 'utf8'));
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
name: supportbot.PingCommand,
|
|
||||||
description: supportbot.PingDesc,
|
|
||||||
|
|
||||||
execute(message, args) {
|
|
||||||
if (supportbot.DeleteMessages) message.delete();
|
|
||||||
|
|
||||||
let ping = Date.now() - message.createdTimestamp + " ms";
|
|
||||||
|
|
||||||
const PingCommandEmbed = new Discord.MessageEmbed()
|
|
||||||
.setDescription(`🏓 **Pong!** \`${Date.now() - message.createdTimestamp}ms\``)
|
|
||||||
.setColor(supportbot.EmbedColour)
|
|
||||||
|
|
||||||
message.channel.send({ embed: PingCommandEmbed });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
// SupportBot, Created by Emerald Services
|
|
||||||
// Say Command
|
|
||||||
|
|
||||||
const Discord = require("discord.js");
|
|
||||||
const fs = require("fs");
|
|
||||||
|
|
||||||
const yaml = require('js-yaml');
|
|
||||||
const { execute } = require("./help");
|
|
||||||
const supportbot = yaml.load(fs.readFileSync('./supportbot-config.yml', 'utf8'));
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
name: supportbot.PurgeMessage,
|
|
||||||
description: supportbot.PurgeDesc,
|
|
||||||
|
|
||||||
execute(message, args, member) {
|
|
||||||
if (supportbot.DeleteMessages) message.delete();
|
|
||||||
|
|
||||||
let SupportStaff = message.guild.roles.cache.find(adminRole => adminRole.name === supportbot.Staff) || message.guild.roles.cache.find(adminRole => adminRole.id === supportbot.Staff)
|
|
||||||
|
|
||||||
const NoPerms = new Discord.MessageEmbed()
|
|
||||||
.setTitle("Invalid Permissions!")
|
|
||||||
.setDescription(`${supportbot.IncorrectPerms}\n\nRole Required: \`${SupportStaff.name}\``)
|
|
||||||
.setColor(supportbot.WarningColour)
|
|
||||||
|
|
||||||
if (!message.member.roles.cache.has(SupportStaff.id))
|
|
||||||
return message.channel.send({ embed: NoPerms });
|
|
||||||
|
|
||||||
let locateChannel = message.guild.channels.cache.find(ModLogChannel => ModLogChannel.name === supportbot.ModLogChannel) || message.guild.channels.cache.find(ModLogChannel => ModLogChannel.id === supportbot.ModLogChannel)
|
|
||||||
|
|
||||||
const errornochannel = new Discord.MessageEmbed()
|
|
||||||
.setTitle("Invalid Channel")
|
|
||||||
.setDescription(`${supportbot.InvalidChannel}\n\nChannel Required: \`${supportbot.ModLogChannel}\``)
|
|
||||||
.setColor(supportbot.ErrorColour);
|
|
||||||
|
|
||||||
if(!locateChannel) return message.channel.send({ embed: errornochannel });
|
|
||||||
|
|
||||||
const embed1 = new Discord.MessageEmbed()
|
|
||||||
.setDescription(`> :thinking: **Hmm!** Please enter a valid number of message to purge!\n\n\`${supportbot.Prefix}${supportbot.PurgeMessage} <message count>\` `)
|
|
||||||
.setColor(supportbot.WarningColour)
|
|
||||||
|
|
||||||
if (!args[0]) return message.channel.send({
|
|
||||||
embed: embed1
|
|
||||||
});
|
|
||||||
|
|
||||||
const messagePrune = parseInt(args[0])
|
|
||||||
|
|
||||||
const embed2 = new Discord.MessageEmbed()
|
|
||||||
.setDescription("> :person_facepalming: **Grr!** Can you count? That ain't a number :joy:")
|
|
||||||
.setColor(supportbot.ErrorColour)
|
|
||||||
|
|
||||||
if (isNaN(messagePrune)) return message.reply({
|
|
||||||
embed: embed2
|
|
||||||
})
|
|
||||||
|
|
||||||
message.channel.bulkDelete(messagePrune).then(() => {
|
|
||||||
|
|
||||||
const embed3 = new Discord.MessageEmbed()
|
|
||||||
.setDescription(`> :recycle: \`${args[0]} messages\` **have been purged!** `)
|
|
||||||
.setColor(supportbot.ErrorColour)
|
|
||||||
message.channel.send({
|
|
||||||
embed: embed3
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
const logEmbed = new Discord.MessageEmbed()
|
|
||||||
.setTitle(supportbot.PruneLogTitle)
|
|
||||||
.setThumbnail(supportbot.PruneThumbnail)
|
|
||||||
.addField('Messages Purged', `\`${args[0]} messages\``)
|
|
||||||
.addField('Staff Member', message.author)
|
|
||||||
.setFooter(supportbot.EmbedFooter)
|
|
||||||
.setColor(supportbot.PruneColour)
|
|
||||||
.setTimestamp()
|
|
||||||
|
|
||||||
locateChannel.send({ embed: logEmbed })
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
// SupportBot, Created by Emerald Services
|
|
||||||
// Remove User
|
|
||||||
|
|
||||||
const Discord = require("discord.js");
|
|
||||||
const fs = require("fs");
|
|
||||||
|
|
||||||
const yaml = require('js-yaml');
|
|
||||||
const supportbot = yaml.load(fs.readFileSync('./supportbot-config.yml', 'utf8'));
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
name: supportbot.RemoveUser,
|
|
||||||
description: supportbot.RemoveUserDesc,
|
|
||||||
|
|
||||||
execute(message, args) {
|
|
||||||
if (supportbot.DeleteMessages) message.delete();
|
|
||||||
|
|
||||||
let SupportStaff = message.guild.roles.cache.find(SupportTeam => SupportTeam.name === supportbot.Staff) || message.guild.roles.cache.find(SupportTeam => SupportTeam.id === supportbot.Staff)
|
|
||||||
let Admins = message.guild.roles.cache.find(AdminUser => AdminUser.name === supportbot.Admin) || message.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 (message.member.roles.cache.has(SupportStaff.id) || message.member.roles.cache.has(Admins.id)) {
|
|
||||||
if (!message.channel.name.startsWith( `${supportbot.TicketChannel}-` )) {
|
|
||||||
const Exists = new Discord.MessageEmbed()
|
|
||||||
.setTitle("No Ticket Found!")
|
|
||||||
.setDescription(`${supportbot.NoValidTicket}`)
|
|
||||||
.setColor(supportbot.WarningColour);
|
|
||||||
message.channel.send({ embed: Exists });
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let rUser = message.guild.member(message.mentions.users.first() || message.guild.members.cache.get(args[0]));
|
|
||||||
const UserNotExist = new Discord.MessageEmbed()
|
|
||||||
.setTitle("User Not Found!")
|
|
||||||
.setDescription(`${supportbot.UserNotFound}\n\nTry Again:\`${supportbot.Prefix}${supportbot.RemoveUser} <user#0000>\``)
|
|
||||||
.setColor(supportbot.ErrorColour)
|
|
||||||
|
|
||||||
if (!rUser) return message.channel.send({ embed: UserNotExist });
|
|
||||||
|
|
||||||
message.channel.updateOverwrite(rUser, {
|
|
||||||
VIEW_CHANNEL: false,
|
|
||||||
CREATE_INVITE: false,
|
|
||||||
SEND_MESSAGES: false,
|
|
||||||
READ_MESSAGES: false
|
|
||||||
});
|
|
||||||
|
|
||||||
const Complete = new Discord.MessageEmbed()
|
|
||||||
.setTitle("User Removed!")
|
|
||||||
.setDescription(supportbot.RemovedUser.replace(/%user%/g, rUser.id))
|
|
||||||
.setTimestamp()
|
|
||||||
.setColor(supportbot.EmbedColour)
|
|
||||||
message.channel.send({ embed: Complete });
|
|
||||||
} else {
|
|
||||||
return message.channel.send({ embed: NoPerms });
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
// SupportBot, Created by Emerald Services
|
|
||||||
// Suggest Command
|
|
||||||
|
|
||||||
const Discord = require("discord.js");
|
|
||||||
const fs = require("fs");
|
|
||||||
|
|
||||||
const yaml = require('js-yaml');
|
|
||||||
const supportbot = yaml.load(fs.readFileSync('./supportbot-config.yml', 'utf8'));
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
name: supportbot.SuggestCommand,
|
|
||||||
description: supportbot.SuggestionDesc,
|
|
||||||
|
|
||||||
execute(message, args) {
|
|
||||||
if (supportbot.DeleteMessages) message.delete();
|
|
||||||
|
|
||||||
let locateChannel = message.guild.channels.cache.find(SuggestionChannel => SuggestionChannel.name === supportbot.SuggestionChannel) || message.guild.channels.cache.find(SuggestionChannel => SuggestionChannel.id === supportbot.SuggestionChannel)
|
|
||||||
|
|
||||||
const errornochannel = new Discord.MessageEmbed()
|
|
||||||
.setTitle("Invalid Channel")
|
|
||||||
.setDescription(`${supportbot.InvalidChannel}\n\nChannel Required: \`${supportbot.SuggestionChannel}\``)
|
|
||||||
.setColor(supportbot.ErrorColour);
|
|
||||||
|
|
||||||
if(!locateChannel) return message.channel.send({ embed: errornochannel });
|
|
||||||
|
|
||||||
const embed = new Discord.MessageEmbed()
|
|
||||||
.setDescription(`> **${supportbot.SuggestionStarter}**`)
|
|
||||||
.setColor(supportbot.EmbedColour)
|
|
||||||
message.channel.send({ embed: embed });
|
|
||||||
|
|
||||||
let suggestion = []
|
|
||||||
message.channel.awaitMessages(response => response.content.length > 2, {
|
|
||||||
max: 1,
|
|
||||||
time: 500000,
|
|
||||||
errors: ['time'],
|
|
||||||
}).then((collected) => {
|
|
||||||
suggestion.push(collected.map(r => r.content));
|
|
||||||
|
|
||||||
const SuggestionMessage = new Discord.MessageEmbed()
|
|
||||||
.setColor(supportbot.EmbedColour)
|
|
||||||
.setFooter(supportbot.EmbedFooter)
|
|
||||||
|
|
||||||
.addField(supportbot.SuggestionTitle, suggestion, true)
|
|
||||||
.addField("From", `<@${message.author.id}>`, true)
|
|
||||||
|
|
||||||
if (supportbot.SuggestionMessage_Icon === "BOT") {
|
|
||||||
SuggestionMessage.setThumbnail(bot.user.displayAvatarURL())
|
|
||||||
}
|
|
||||||
|
|
||||||
if (supportbot.SuggestionMessage_Icon === "USER") {
|
|
||||||
SuggestionMessage.setThumbnail(message.author.displayAvatarURL())
|
|
||||||
}
|
|
||||||
|
|
||||||
if (supportbot.SuggestionMessage_Icon === "CUSTOM") {
|
|
||||||
SuggestionMessage.setThumbnail(supportbot.SuggestionIcon_URL)
|
|
||||||
}
|
|
||||||
|
|
||||||
locateChannel.send({ embed: SuggestionMessage }).then(async function(msg) {
|
|
||||||
msg.react(supportbot.SuggestReact_1).then(() => msg.react(supportbot.SuggestReact_2));
|
|
||||||
});
|
|
||||||
|
|
||||||
const SuggestionComplete = new Discord.MessageEmbed()
|
|
||||||
.setColor(supportbot.SuccessColour)
|
|
||||||
.setDescription(`:white_check_mark: Your suggestion has been successfully created. <#${locateChannel.id}>`)
|
|
||||||
message.channel.send({ embed: SuggestionComplete })
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,230 +0,0 @@
|
|||||||
// SupportBot, Created by Emerald Services
|
|
||||||
// Ticket Command
|
|
||||||
|
|
||||||
const Discord = require("discord.js");
|
|
||||||
const bot = new Discord.Client()
|
|
||||||
|
|
||||||
const fs = require("fs")
|
|
||||||
const yaml = require('js-yaml');
|
|
||||||
|
|
||||||
const supportbot = yaml.load(fs.readFileSync('./supportbot-config.yml', 'utf8'));
|
|
||||||
const TicketNumberID = require('../utils/TicketNumber.js');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
name: supportbot.NewTicket,
|
|
||||||
description: supportbot.NewTicketDesc,
|
|
||||||
|
|
||||||
async execute(message, args) {
|
|
||||||
if (!message.channel.name === supportbot.ReactionChannel || !message.channel.id === supportbot.ReactionChannel) {
|
|
||||||
if (supportbot.DeleteMessages) message.delete();
|
|
||||||
}
|
|
||||||
|
|
||||||
let reactionUser = message.guild.members.cache.get(message.author.id)
|
|
||||||
|
|
||||||
if (reactionUser.roles.cache.find(role => role.name === supportbot.TicketBlackListRole) || reactionUser.roles.cache.find(role => role.id === supportbot.TicketBlackListRole)) {
|
|
||||||
return message.channel.send(`<@${message.author.id}> ${supportbot.TicketBlackListMessage}`).then(msg => {
|
|
||||||
msg.delete({timeout: 3500})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ticket ID
|
|
||||||
let ticketNumberID = TicketNumberID.pad(message.guild.id);
|
|
||||||
|
|
||||||
// Ticket Subject
|
|
||||||
const TicketSubject = args || supportbot.NoTicketSubject;
|
|
||||||
|
|
||||||
const channel = await message.guild.channels.create(`${supportbot.TicketChannel}-${ticketNumberID}`);
|
|
||||||
|
|
||||||
if (message.channel.name === supportbot.ReactionChannel || message.channel.id === supportbot.ReactionChannel) {
|
|
||||||
const CreatedTicket = new Discord.MessageEmbed()
|
|
||||||
.setDescription(supportbot.TicketCreatedAlert.replace(/%ticketauthor%/g, message.author.id).replace(/%ticketid%/g, channel.id).replace(/%ticketusername%/g, message.author.username))
|
|
||||||
.setColor(supportbot.EmbedColour)
|
|
||||||
message.channel.send({embed: CreatedTicket}).then((r) => {
|
|
||||||
r.delete({ timeout:5000 })
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
const CreatedTicket = new Discord.MessageEmbed()
|
|
||||||
.setDescription(supportbot.TicketCreatedAlert.replace(/%ticketauthor%/g, message.author.id).replace(/%ticketid%/g, channel.id).replace(/%ticketusername%/g, message.author.username))
|
|
||||||
.setColor(supportbot.EmbedColour)
|
|
||||||
message.channel.send({embed: CreatedTicket});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ticket Category
|
|
||||||
let TicketCategory = message.guild.channels.cache.find(category => category.name === supportbot.TicketCategory) || message.guild.channels.cache.find(category => category.id === supportbot.TicketCategory)
|
|
||||||
|
|
||||||
// Locate Roles
|
|
||||||
let StaffMember = message.guild.roles.cache.find(SupportTeam => SupportTeam.name === supportbot.Staff) || message.guild.roles.cache.find(SupportTeam => SupportTeam.id === supportbot.Staff)
|
|
||||||
let ServerAdmins = message.guild.roles.cache.find(AdminUser => AdminUser.name === supportbot.Admin) || message.guild.roles.cache.find(AdminUser => AdminUser.id === supportbot.Admin)
|
|
||||||
|
|
||||||
if (TicketCategory) {
|
|
||||||
channel.setParent(TicketCategory.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
channel.updateOverwrite(message.guild.id, {
|
|
||||||
SEND_MESSAGES: false,
|
|
||||||
VIEW_CHANNEL: false
|
|
||||||
})
|
|
||||||
|
|
||||||
channel.updateOverwrite(message.author, {
|
|
||||||
SEND_MESSAGES: true,
|
|
||||||
VIEW_CHANNEL: true
|
|
||||||
})
|
|
||||||
|
|
||||||
channel.updateOverwrite(StaffMember, {
|
|
||||||
SEND_MESSAGES: true,
|
|
||||||
VIEW_CHANNEL: true
|
|
||||||
})
|
|
||||||
|
|
||||||
channel.updateOverwrite(ServerAdmins, {
|
|
||||||
SEND_MESSAGES: true,
|
|
||||||
VIEW_CHANNEL: true
|
|
||||||
})
|
|
||||||
|
|
||||||
// Ticket Created, Message Sent
|
|
||||||
|
|
||||||
if (supportbot.AllowTicketMentions) {
|
|
||||||
channel.send(`<@${message.author.id}>`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const TicketMessage = new Discord.MessageEmbed()
|
|
||||||
.setTitle(supportbot.Ticket_Title.replace(/%ticketauthor%/g, message.author.id).replace(/%ticketid%/g, channel.id).replace(/%ticketusername%/g, message.author.username))
|
|
||||||
.setDescription(supportbot.TicketMessage.replace(/%ticketauthor%/g, message.author.id).replace(/%ticketid%/g, channel.id).replace(/%ticketusername%/g, message.author.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.') {
|
|
||||||
channel.setTopic(`Reason: ${TicketSubject} - User ID: ${message.author.id} - Ticket: ${channel.name}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
const reactionMessage = await channel.send({
|
|
||||||
embed: TicketMessage
|
|
||||||
}).then( async function(ticketmsg) {
|
|
||||||
await ticketmsg.react(supportbot.TicketLockReaction)
|
|
||||||
await ticketmsg.react(supportbot.TicketDeleteReaction)
|
|
||||||
|
|
||||||
const collector = ticketmsg.createReactionCollector(
|
|
||||||
(reaction, user) => message.guild.roles.cache.find((staff) => staff.name === supportbot.Staff),
|
|
||||||
{ dispose: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
collector.on('collect', (reaction, user) => {
|
|
||||||
switch (reaction.emoji.name) {
|
|
||||||
case supportbot.TicketLockReaction:
|
|
||||||
|
|
||||||
channel.updateOverwrite(message.author, {
|
|
||||||
SEND_MESSAGES: false,
|
|
||||||
VIEW_CHANNEL: true
|
|
||||||
});
|
|
||||||
|
|
||||||
const LockTicketEmbed = new Discord.MessageEmbed()
|
|
||||||
.setDescription(supportbot.TicketLockMessage)
|
|
||||||
.setColor(supportbot.EmbedColour)
|
|
||||||
channel.send({embed: LockTicketEmbed})
|
|
||||||
break;
|
|
||||||
|
|
||||||
case supportbot.TicketDeleteReaction:
|
|
||||||
const DeleteTicketEmbed = new Discord.MessageEmbed()
|
|
||||||
.setDescription(supportbot.TicketDeleteMessage)
|
|
||||||
.setColor(supportbot.EmbedColour)
|
|
||||||
channel.send({embed: DeleteTicketEmbed})
|
|
||||||
|
|
||||||
let logChannel = message.guild.channels.cache.find(channel => channel.name === supportbot.TranscriptLog) || message.guild.channels.cache.find(channel => channel.id === supportbot.TranscriptLog)
|
|
||||||
let user = message.author;
|
|
||||||
let reason = args.join(" ") || "No Reason Provided.";
|
|
||||||
|
|
||||||
let name = message.channel.name;
|
|
||||||
let ticketChannel = message.channel;
|
|
||||||
|
|
||||||
message.channel.send(`**${supportbot.ClosingTicket}**`)
|
|
||||||
|
|
||||||
.then(() => {
|
|
||||||
const logEmbed = new Discord.MessageEmbed()
|
|
||||||
.setTitle(supportbot.TranscriptTitle)
|
|
||||||
.setColor(supportbot.EmbedColour)
|
|
||||||
.setFooter(supportbot.EmbedFooter)
|
|
||||||
.addField("Closed By", message.author.tag)
|
|
||||||
.addField("Reason", reason);
|
|
||||||
|
|
||||||
message.channel.messages.fetch({ limit: 100 }).then( msgs => {
|
|
||||||
let html = '';
|
|
||||||
|
|
||||||
msgs = msgs.sort( ( a, b ) => a.createdTimestamp - b.createdTimestamp );
|
|
||||||
html += `<strong>Server Name:</strong> ${message.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(logEmbed).catch(err => {
|
|
||||||
message.reply(err)
|
|
||||||
})
|
|
||||||
|
|
||||||
const TranscriptSavedEmbed = new Discord.MessageEmbed()
|
|
||||||
.setDescription(supportbot.TranscriptSavedMessage)
|
|
||||||
.setColor(supportbot.SuccessColour)
|
|
||||||
channel.send({embed: TranscriptSavedEmbed})
|
|
||||||
|
|
||||||
logChannel.send( new Discord.MessageAttachment( Buffer.from( html ), `${name}.html` ) );
|
|
||||||
setTimeout(() => channel.delete(), supportbot.TicketDeleteTime);
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
}).catch( () => {
|
|
||||||
m.edit('The request to close the ticket has timed out.').then( m2 => m2.delete( 3000 ) );
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
fs.writeFileSync("./storage/SupportTickets.json", '{\n "ticket": "' + channel.name + '", \n "id":' + message.author.id + "\n}", (err) => {
|
|
||||||
if (!err) return;
|
|
||||||
console.error(err)
|
|
||||||
})
|
|
||||||
// Ticket Logs
|
|
||||||
|
|
||||||
const errornochannel = new Discord.MessageEmbed()
|
|
||||||
.setTitle("SupportBot Error!")
|
|
||||||
.setDescription(`:x: **Error!** Channel not Found, This command cannot be executed proberbly as their is no channel within this server.\nThis is configurable via **supportbot-config.yml**\n\nChannel Required: \`${supportbot.TicketLog}\`\n\nError Code: \`SB-03\``)
|
|
||||||
.setColor(supportbot.ErrorColour);
|
|
||||||
|
|
||||||
const TicketLogs = new Discord.MessageEmbed()
|
|
||||||
.setTitle(supportbot.TicketLog_Title)
|
|
||||||
.setThumbnail(supportbot.Ticket_Thumbnail)
|
|
||||||
.addFields(
|
|
||||||
{ name: 'Ticket', value: `<#${channel.id}>` },
|
|
||||||
{ name: 'User', value: `<@${message.author.id}>` },
|
|
||||||
)
|
|
||||||
.setColor(supportbot.EmbedColour)
|
|
||||||
.setFooter(supportbot.EmbedFooter)
|
|
||||||
|
|
||||||
let locateChannel = message.guild.channels.cache.find(LocateChannel => LocateChannel.name === supportbot.TicketLog) || message.guild.channels.cache.find(LocateChannel => LocateChannel.id === supportbot.TicketLog)
|
|
||||||
|
|
||||||
if(!locateChannel) return message.channel.send({ embed: errornochannel });
|
|
||||||
|
|
||||||
locateChannel.send({ embed: TicketLogs });
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
// SupportBot, Created by Emerald Services
|
|
||||||
// Announce
|
|
||||||
|
|
||||||
const Discord = require("discord.js");
|
|
||||||
const fs = require("fs");
|
|
||||||
|
|
||||||
const yaml = require('js-yaml');
|
|
||||||
const { execute } = require("./help");
|
|
||||||
const supportbot = yaml.load(fs.readFileSync('./supportbot-config.yml', 'utf8'));
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
name: supportbot.ChatLog,
|
|
||||||
description: supportbot.ChatLogDesc,
|
|
||||||
|
|
||||||
execute(message, args) {
|
|
||||||
if (supportbot.DeleteMessages) message.delete();
|
|
||||||
|
|
||||||
let name = message.channel.name;
|
|
||||||
|
|
||||||
message.channel.messages.fetch({ limit: 100 }).then( msgs => {
|
|
||||||
let html = '';
|
|
||||||
|
|
||||||
msgs = msgs.sort( ( a, b ) => a.createdTimestamp - b.createdTimestamp );
|
|
||||||
html += `<strong>--- Channel Transcript ---</strong><br>`;
|
|
||||||
html += `<strong>Server Name:</strong> ${message.guild.name}<br>`;
|
|
||||||
html += `<strong>Channel:</strong> ${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>`;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const TranscriptSavedEmbed = new Discord.MessageEmbed()
|
|
||||||
.setDescription(supportbot.ChatLogSavedMessage)
|
|
||||||
.setColor(supportbot.SuccessColour)
|
|
||||||
message.channel.send({embed: TranscriptSavedEmbed})
|
|
||||||
|
|
||||||
const errornochannel = new Discord.MessageEmbed()
|
|
||||||
.setTitle("SupportBot Error!")
|
|
||||||
.setDescription(`:x: **Error!** Channel not Found, This command cannot be executed proberbly as their is no channel within this server.\nThis is configurable via **supportbot-config.yml**\n\nChannel Required: \`${supportbot.TicketLog}\`\n\nError Code: \`SB-03\``)
|
|
||||||
.setColor(supportbot.ErrorColour);
|
|
||||||
|
|
||||||
const ChatLogs = new Discord.MessageEmbed()
|
|
||||||
.setTitle(supportbot.ChatLog_Title)
|
|
||||||
.setThumbnail(supportbot.ChatLog_Thumbnail)
|
|
||||||
.addFields(
|
|
||||||
{ name: 'Channel', value: `<#${message.channel.id}>` },
|
|
||||||
{ name: 'User', value: `<@${message.author.id}>` },
|
|
||||||
)
|
|
||||||
.setColor(supportbot.EmbedColour)
|
|
||||||
.setFooter(supportbot.EmbedFooter)
|
|
||||||
|
|
||||||
let locateChannel = message.guild.channels.cache.find(LocateChannel => LocateChannel.name === supportbot.ChatLogChannel) || message.guild.channels.cache.find(LocateChannel => LocateChannel.id === supportbot.ChatLogChannel)
|
|
||||||
|
|
||||||
if(!locateChannel) return message.channel.send({ embed: errornochannel });
|
|
||||||
|
|
||||||
locateChannel.send( new Discord.MessageAttachment( Buffer.from( html ), `${name}.html` ) );
|
|
||||||
locateChannel.send({ embed: ChatLogs });
|
|
||||||
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
// SupportBot 6.0, Created by Emerald Services
|
|
||||||
// Ready Event
|
|
||||||
|
|
||||||
const Discord = require("discord.js");
|
|
||||||
const fs = require("fs");
|
|
||||||
|
|
||||||
const yaml = require('js-yaml');
|
|
||||||
const supportbot = yaml.load(fs.readFileSync('./supportbot-config.yml', 'utf8'));
|
|
||||||
|
|
||||||
module.exports = async (bot, member) => {
|
|
||||||
|
|
||||||
if (supportbot.SystemMessages) {
|
|
||||||
const SystemChannel = member.guild.channels.cache.find(channel => channel.name === supportbot.SystemMessage_Channel) || member.guild.channels.cache.get(supportbot.SystemMessage_Channel)
|
|
||||||
|
|
||||||
if (!SystemChannel) return;
|
|
||||||
|
|
||||||
console.log(`[${supportbot.Bot_Name}]: ${member.user.username} has joined ${member.guild.name}!`)
|
|
||||||
console.log(supportbot.AutoRole_Role)
|
|
||||||
|
|
||||||
const role = member.guild.roles.cache.find(role => role.name === supportbot.AutoRole_Role) || member.guild.roles.cache.find(role => role.id === supportbot.AutoRole_Role)
|
|
||||||
|
|
||||||
member.roles.add(role).catch(console.error);
|
|
||||||
|
|
||||||
if (supportbot.SystemMessage_Type === "embed") {
|
|
||||||
|
|
||||||
const GuildAddMember = new Discord.MessageEmbed()
|
|
||||||
.setTitle(supportbot.Welcome_Title)
|
|
||||||
.setDescription(supportbot.WelcomeMessage.replace(/%member%/g, `<@!${member.user.id}>`).replace(/%guildname%/g, member.guild.name))
|
|
||||||
.setColor(supportbot.EmbedColour)
|
|
||||||
|
|
||||||
if (supportbot.SystemMessage_Icon === "BOT") {
|
|
||||||
GuildAddMember.setThumbnail(bot.user.displayAvatarURL())
|
|
||||||
}
|
|
||||||
|
|
||||||
if (supportbot.SystemMessage_Icon === "USER") {
|
|
||||||
GuildAddMember.setThumbnail(member.user.displayAvatarURL())
|
|
||||||
}
|
|
||||||
|
|
||||||
if (supportbot.SystemMessage_Icon === "CUSTOM") {
|
|
||||||
SuggestionMessage.setThumbnail(supportbot.SystemMessageIcon_URL)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (supportbot.SystemMessage_EmbedFooter) {
|
|
||||||
GuildAddMember.setFooter(supportbot.EmbedFooter)
|
|
||||||
}
|
|
||||||
|
|
||||||
SystemChannel.send({ embed: GuildAddMember });
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
if (supportbot.SystemMessage_Type === "normal") {
|
|
||||||
SystemChannel.send(supportbot.WelcomeMessage.replace(/%member%/g, member.user.username).replace(/%guildname%/g, member.guild.name))
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
// SupportBot 6.0, Created by Emerald Services
|
|
||||||
// Ready Event
|
|
||||||
|
|
||||||
const Discord = require("discord.js");
|
|
||||||
const fs = require("fs");
|
|
||||||
|
|
||||||
const yaml = require('js-yaml');
|
|
||||||
const supportbot = yaml.load(fs.readFileSync('./supportbot-config.yml', 'utf8'));
|
|
||||||
|
|
||||||
module.exports = async (bot, member) => {
|
|
||||||
|
|
||||||
if (supportbot.SystemMessages) {
|
|
||||||
const SystemChannel = member.guild.channels.cache.find(channel => channel.name === supportbot.SystemMessage_Channel) || member.guild.channels.cache.get(supportbot.SystemMessage_Channel)
|
|
||||||
|
|
||||||
if (!SystemChannel) return;
|
|
||||||
|
|
||||||
console.log(`[${supportbot.Bot_Name}]: ${member.user.username} has left ${member.guild.name}!`)
|
|
||||||
|
|
||||||
if (supportbot.SystemMessage_Type === "embed") {
|
|
||||||
const GuildRemoveMember = new Discord.MessageEmbed()
|
|
||||||
.setTitle(supportbot.Leave_Title)
|
|
||||||
.setDescription(supportbot.LeaveMessage.replace(/%member%/g, member.user.username).replace(/%guildname%/g, member.guild.name))
|
|
||||||
.setColor(supportbot.EmbedColour)
|
|
||||||
|
|
||||||
if (supportbot.SystemMessage_Icon === "BOT") {
|
|
||||||
GuildRemoveMember.setThumbnail(bot.user.displayAvatarURL())
|
|
||||||
}
|
|
||||||
|
|
||||||
if (supportbot.SystemMessage_Icon === "USER") {
|
|
||||||
GuildRemoveMember.setThumbnail(member.user.displayAvatarURL())
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (supportbot.SystemMessage_EmbedFooter === "true") {
|
|
||||||
GuildRemoveMember.setFooter(supportbot.EmbedFooter)
|
|
||||||
}
|
|
||||||
|
|
||||||
SystemChannel.send({ embed: GuildRemoveMember });
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
if (supportbot.SystemMessage_Type === "normal") {
|
|
||||||
SystemChannel.send(supportbot.LeaveMessage.replace(/%member%/g, member.user.username).replace(/%guildname%/g, member.guild.name))
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
// SupportBot 6.0, Created by Emerald Services
|
|
||||||
// Message Event
|
|
||||||
|
|
||||||
const Discord = require("discord.js");
|
|
||||||
const fs = require("fs");
|
|
||||||
|
|
||||||
const yaml = require('js-yaml');
|
|
||||||
const supportbot = yaml.load(fs.readFileSync('./supportbot-config.yml', 'utf8'));
|
|
||||||
|
|
||||||
module.exports = async (bot, message) => {
|
|
||||||
if(message.author.bot) return;
|
|
||||||
if(message.channel.type === "dm") return;
|
|
||||||
if (message.content.indexOf(supportbot.Prefix) !== 0) return;
|
|
||||||
|
|
||||||
let messageArray = message.content.split(" ");
|
|
||||||
const args = message.content.slice(supportbot.Prefix.length).trim().split(/ +/g);
|
|
||||||
const command = args.shift().toLowerCase();
|
|
||||||
|
|
||||||
console.log(`\u001b[32m`, `[${supportbot.Bot_Name}]`, `\u001b[36m`, `${message.author.tag}`, `\u001b[33m`, `has executed the command`, `\u001b[36m`,`${supportbot.Prefix}${command}`)
|
|
||||||
|
|
||||||
const cmd = bot.commands.get(command);
|
|
||||||
if(!cmd) return;
|
|
||||||
cmd.execute(message, args);
|
|
||||||
};
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
// SupportBot 6.0, Created by Emerald Services
|
|
||||||
// Message Reaction Event
|
|
||||||
|
|
||||||
const Discord = require("discord.js");
|
|
||||||
const fs = require("fs");
|
|
||||||
|
|
||||||
const yaml = require('js-yaml');
|
|
||||||
const supportbot = yaml.load(fs.readFileSync('./supportbot-config.yml', 'utf8'));
|
|
||||||
const tickets = yaml.load(fs.readFileSync('./storage/ticketreaction.yml', 'utf8'));
|
|
||||||
|
|
||||||
module.exports = async (client, reaction, user) => {
|
|
||||||
|
|
||||||
let message = {
|
|
||||||
guild: reaction.message.channel.guild,
|
|
||||||
author: user,
|
|
||||||
content: "N/A",
|
|
||||||
channel: reaction.message.channel
|
|
||||||
}
|
|
||||||
|
|
||||||
if (supportbot.ReactionTickets) {
|
|
||||||
if (reaction.message.id === tickets.ReactionMessage_ID) {
|
|
||||||
if (reaction.emoji.name == supportbot.ReactionEmoji || reaction.emoji.id == supportbot.ReactionEmoji) {
|
|
||||||
await reaction.users.remove(user.id)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const cmd = client.commands.get(supportbot.NewTicket);
|
|
||||||
if(!cmd) return;
|
|
||||||
|
|
||||||
cmd.execute(message, message.content);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
const Discord = require("discord.js");
|
|
||||||
const fs = require("fs");
|
|
||||||
|
|
||||||
const yaml = require('js-yaml');
|
|
||||||
const supportbot = yaml.load(fs.readFileSync('./supportbot-config.yml', 'utf8'));
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
module.exports = async (bot, message, packet) => {
|
|
||||||
if (!['MESSAGE_REACTION_ADD', 'MESSAGE_REACTION_REMOVE'].includes(packet.t)) return;
|
|
||||||
|
|
||||||
const channel = bot.channels.cache.get(packet.d.channel_id);
|
|
||||||
if (channel.messages.has(packet.d.message_id)) return;
|
|
||||||
|
|
||||||
channel.messages.fetch(packet.d.message_id).then(message => {
|
|
||||||
const emoji = packet.d.emoji.id ? `${packet.d.emoji.name}:${packet.d.emoji.id}` : packet.d.emoji.name;
|
|
||||||
const reaction = message.reactions.get(emoji);
|
|
||||||
if (reaction) reaction.users.set(packet.d.user_id, bot.users.get(packet.d.user_id));
|
|
||||||
if (packet.t === 'MESSAGE_REACTION_ADD') {
|
|
||||||
bot.emit('messageReactionAdd', reaction, bot.users.get(packet.d.user_id));
|
|
||||||
}
|
|
||||||
if (packet.t === 'MESSAGE_REACTION_REMOVE') {
|
|
||||||
bot.emit('messageReactionRemove', reaction, bot.users.get(packet.d.user_id));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
// SupportBot 6.0, Created by Emerald Services
|
|
||||||
// Ready Event
|
|
||||||
|
|
||||||
const Discord = require("discord.js");
|
|
||||||
const fs = require("fs");
|
|
||||||
|
|
||||||
const yaml = require('js-yaml');
|
|
||||||
const supportbot = yaml.load(fs.readFileSync('./supportbot-config.yml', 'utf8'));
|
|
||||||
const tickets = yaml.load(fs.readFileSync('./storage/ticketreaction.yml', 'utf8'));
|
|
||||||
|
|
||||||
|
|
||||||
module.exports = async (bot) => {
|
|
||||||
bot.user.setActivity(supportbot.BotActivity, {
|
|
||||||
type: supportbot.ActivityType,
|
|
||||||
url: supportbot.StreamingURL
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
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.Bot_Name} v${supportbot.SupportBot_Version}`, `\u001b[36m`, `Connected to Discord`)
|
|
||||||
console.log(`\u001b[33m`, `Documentation:`, `\u001b[36m`, `https://docs.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 (supportbot.ReactionTickets === true) {
|
|
||||||
let chan1 = bot.channels.cache.find(channel => channel.name === supportbot.ReactionChannel)
|
|
||||||
|
|
||||||
if (!chan1) {
|
|
||||||
console.log('\u001b[33m', "[WARN] Ticket reaction panel is not setup, You can do so via the configuration file!")
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
chan1.messages.fetch(tickets.ReactionMessage_ID).catch(r => {
|
|
||||||
let embed = new Discord.MessageEmbed()
|
|
||||||
.setTitle(supportbot.ReactionMessage.replace(/%reaction_emoji%/g, supportbot.ReactionEmoji))
|
|
||||||
.setColor(supportbot.SuccessColour)
|
|
||||||
.setFooter(supportbot.EmbedFooter);
|
|
||||||
|
|
||||||
if (supportbot.ReactionPanel_Description === true) {
|
|
||||||
embed.setDescription(supportbot.ReactionDescription)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (supportbot.ReactionPanel_Thumbnail === true) {
|
|
||||||
embed.setThumbnail(supportbot.ReactionThumbnail)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (supportbot.ReactionPanel_Image === true) {
|
|
||||||
embed.setImage(supportbot.ReactionImage)
|
|
||||||
}
|
|
||||||
|
|
||||||
bot.channels.cache.find(channel => channel.name === supportbot.ReactionChannel).send({
|
|
||||||
embed: embed
|
|
||||||
}).then(r => {
|
|
||||||
let data = {
|
|
||||||
"ReactionMessage_ID": `${r.id}`
|
|
||||||
}
|
|
||||||
let yamlStr = yaml.dump(data);
|
|
||||||
fs.writeFileSync('./storage/ticketreaction.yml', yamlStr, 'utf8');
|
|
||||||
|
|
||||||
r.react(supportbot.ReactionEmoji);
|
|
||||||
}).catch(e => {
|
|
||||||
console.log(e)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
};
|
|
||||||
@@ -1,105 +1,101 @@
|
|||||||
// ___ _ ___ _
|
// ___ _ ___ _
|
||||||
// / __> _ _ ___ ___ ___ _ _ _| |_ | . > ___ _| |_
|
// / __> _ _ ___ ___ ___ _ _ _| |_ | . > ___ _| |_
|
||||||
// \__ \| | || . \| . \/ . \| '_> | | | . \/ . \ | |
|
// \__ \| | || . \| . \/ . \| '_> | | | . \/ . \ | |
|
||||||
// <___/`___|| _/| _/\___/|_| |_| |___/\___/ |_|
|
// <___/`___|| _/| _/\___/|_| |_| |___/\___/ |_|
|
||||||
// |_| |_|
|
// |_| |_|
|
||||||
//
|
//
|
||||||
// SupportBot created by Emerald Services
|
// SupportBot created by Emerald Services
|
||||||
// Installed with MIT License
|
// Installed with MIT License
|
||||||
//
|
//
|
||||||
// Discord Support: https://emeraldsrv.dev/discord
|
// Discord Support: https://emeraldsrv.dev/discord
|
||||||
|
// Community Resources: https://community.emeraldsrv.dev
|
||||||
|
|
||||||
const Discord = require("discord.js");
|
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const bot = new Discord.Client();
|
const path = require("path");
|
||||||
|
|
||||||
bot.commands = new Discord.Collection();
|
const configStore = require("./Structures/ConfigStore.js");
|
||||||
|
|
||||||
const yaml = require('js-yaml');
|
const Client = require("./Structures/Client.js");
|
||||||
const supportbot = yaml.load(fs.readFileSync('./supportbot-config.yml', 'utf8'));
|
const client = new Client({
|
||||||
|
intents: ["Guilds", "GuildMembers", "GuildMessages", "MessageContent"],
|
||||||
const { promisify } = require("util");
|
|
||||||
const readdir = promisify(require("fs").readdir);
|
|
||||||
|
|
||||||
String.prototype.toProperCase = function () {
|
|
||||||
return this.replace(
|
|
||||||
/([^\W_]+[^\s-]*) */g,
|
|
||||||
(txt) => txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase()
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const init = async () => {
|
|
||||||
const evtFiles = await readdir("./events/");
|
|
||||||
|
|
||||||
console.info(`\u001b[36m`, `➢ SupportBot Events`);
|
|
||||||
console.info(` `);
|
|
||||||
|
|
||||||
evtFiles.forEach((file, i) => {
|
|
||||||
const eventName = file.split(".")[0];
|
|
||||||
const event = require(`./events/${file}`);
|
|
||||||
console.info(`\u001b[32m`, `[${supportbot.Bot_Name}]:`, `\u001b[36m`, `#${i + 1} Event Loaded: ${eventName.toProperCase()} `);
|
|
||||||
|
|
||||||
bot.on(eventName, event.bind(null, bot));
|
|
||||||
delete require.cache[require.resolve(`./events/${file}`)];
|
|
||||||
});
|
|
||||||
|
|
||||||
console.info(`\u001b[32m`, `[${supportbot.Bot_Name}]:`, `\u001b[36m`, `Successfully loaded ${evtFiles.length} events.`);
|
|
||||||
console.log(`\u001b[31m`, `――――――――――――――――――――――――――――――――――――――――――――`)
|
|
||||||
console.log(` `)
|
|
||||||
};
|
|
||||||
|
|
||||||
init();
|
|
||||||
|
|
||||||
fs.readdir("./commands/", (err, files) => {
|
|
||||||
|
|
||||||
console.info(`\u001b[36m`, `➢ SupportBot Commands`);
|
|
||||||
console.info(` `);
|
|
||||||
|
|
||||||
if (err) console.info(err, "error");
|
|
||||||
|
|
||||||
let jsfiles = files.filter((f) => f.split(".").pop() === "js");
|
|
||||||
|
|
||||||
if (jsfiles.length <= 0) {
|
|
||||||
console.log(`[${supportbot.Bot_Name}]: No commands found. Try to Re-download the resource`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
jsfiles.forEach((f, i) => {
|
|
||||||
let props = require(`./commands/${f}`);
|
|
||||||
|
|
||||||
console.info(`\u001b[32m`, `[${supportbot.Bot_Name}]:`, `\u001b[36m`, `#${i + 1} Command Loaded: ${props.name.toProperCase()}`);
|
|
||||||
bot.commands.set(props.name, props);
|
|
||||||
});
|
|
||||||
|
|
||||||
console.info(`\u001b[32m`, `[${supportbot.Bot_Name}]:`, `\u001b[36m`, `Loaded ${jsfiles.length} commands!`);
|
|
||||||
console.log(`\u001b[31m`, `――――――――――――――――――――――――――――――――――――――――――――`)
|
|
||||||
console.log(` `)
|
|
||||||
});
|
});
|
||||||
|
|
||||||
fs.readdir("./addons/", (err, files) => {
|
const { isSetupComplete } = require("./Structures/DashboardSetup.js");
|
||||||
|
const APIServer = require("./API/server.js");
|
||||||
console.info(`\u001b[36m`, `➢ SupportBot Addons`);
|
|
||||||
console.info(` `);
|
|
||||||
|
|
||||||
if (err) console.info(err, "error");
|
|
||||||
|
|
||||||
let jsfiles = files.filter((f) => f.split(".").pop() === "js");
|
|
||||||
|
|
||||||
if (jsfiles.length <= 0) {
|
const api = new APIServer(client);
|
||||||
console.log(`[${supportbot.Bot_Name}]: No addons found. You can download addons from https://emeraldsrv.dev`);
|
client.apiServer = api;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
jsfiles.forEach((f, i) => {
|
|
||||||
let props = require(`./addons/${f}`);
|
|
||||||
|
|
||||||
console.info(`\u001b[32m`, `[${supportbot.Bot_Name}]:`, `\u001b[36m`, `#${i + 1} Addon Loaded: ${props.name.toProperCase()}`);
|
|
||||||
bot.commands.set(props.name, props);
|
|
||||||
});
|
|
||||||
|
|
||||||
console.info(`\u001b[32m`, `[${supportbot.Bot_Name}]:`, `\u001b[36m`, `Loaded ${jsfiles.length} addons!`);
|
const port = api.config?.Port || 25575;
|
||||||
console.log(`\u001b[31m`, `――――――――――――――――――――――――――――――――――――――――――――`)
|
if (api.config?.Enabled) {
|
||||||
console.log(` `)
|
api.start(port);
|
||||||
|
if (!isSetupComplete()) {
|
||||||
|
console.log(`\n\x1b[33m\x1b[1m[Setup Required] Go to http://localhost:${port}/setup to set up your bot and dashboard.\x1b[0m\n`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn("[Dashboard] API disabled in Configs/api.yml");
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = configStore.supportbot?.General?.Token;
|
||||||
|
const isPlaceholderToken = !token || token.includes("BOT_TOKEN") || token.includes("CHANGE_ME") || token.length < 30;
|
||||||
|
|
||||||
|
if (isPlaceholderToken) {
|
||||||
|
console.warn(`\x1b[33m[SupportBot] Bot token is not configured yet. Complete setup at http://localhost:${port}/setup\x1b[0m`);
|
||||||
|
} else {
|
||||||
|
client.start(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
// SupportBot - New Logging System
|
||||||
|
|
||||||
|
const logTypes = ["Output", "Warn", "Error"];
|
||||||
|
|
||||||
|
if (!fs.existsSync("./Logs")) {
|
||||||
|
fs.mkdirSync("./Logs");
|
||||||
|
}
|
||||||
|
logTypes.forEach((type) => {
|
||||||
|
const dir = `./Logs/${type}`;
|
||||||
|
if (!fs.existsSync(dir)) {
|
||||||
|
fs.mkdirSync(dir);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
bot.login(supportbot.Token);
|
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);
|
||||||
|
});
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
SupportBot Language Compatibility
|
|
||||||
|
|
||||||
In order to switch languages simple remove the current existig configuration file and replace it with one of the files here renamnig the file to "supportbot-config.yml" from (for example) "supportbot-config_es.yml" to home directory of SupportBot.
|
|
||||||
|
|
||||||
-> Delete the existing supportbot-config.yml file
|
|
||||||
-> Download the new file for whatever language
|
|
||||||
-> Upload the file to SupportBot and rename it to supportbot-config.yml
|
|
||||||
-> Job Done, restart the bot :wink:
|
|
||||||
Loaded 100 of 112 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user