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 |
No files matched your search
@@ -0,0 +1,2 @@
|
||||
Configs/supportbot.yml filter=supportbot-token
|
||||
Configs/api.yml filter=api-secrets
|
||||
@@ -1,40 +0,0 @@
|
||||
---
|
||||
name: "\U0001F41B Bug report"
|
||||
about: Create a report to help us improve
|
||||
title: "[BUG]"
|
||||
labels: Bug
|
||||
assignees: ""
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
|
||||
- OS: [e.g. iOS]
|
||||
- Browser [e.g. chrome, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Smartphone (please complete the following information):**
|
||||
|
||||
- Device: [e.g. iPhone6]
|
||||
- OS: [e.g. iOS8.1]
|
||||
- Browser [e.g. stock browser, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
@@ -1,19 +0,0 @@
|
||||
---
|
||||
name: "❓Suggest an Idea?"
|
||||
about: Suggest an idea for SupportBot?
|
||||
title: "[SUGGESTION]"
|
||||
labels: Suggestion
|
||||
assignees: ""
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
@@ -1,28 +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: [dev, release]
|
||||
pull_request:
|
||||
branches: [dev, release]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [16.x, 17.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 test
|
||||
+21
-1
@@ -1 +1,21 @@
|
||||
node_modules/
|
||||
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],
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -1,45 +0,0 @@
|
||||
// SupportBot | Emerald Services
|
||||
// Example (default) Addon
|
||||
|
||||
const fs = require("fs");
|
||||
|
||||
const Discord = require("discord.js");
|
||||
const yaml = require("js-yaml");
|
||||
const supportbot = yaml.load(
|
||||
fs.readFileSync("./Configs/supportbot.yml", "utf8")
|
||||
);
|
||||
const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8"));
|
||||
|
||||
// Your Addon Config file! Saved in /Addons/Configs/YourAddonConfig.yml
|
||||
// const config = yaml.load(fs.readFileSync('./Addons/Configs/YourAddonConfig.yml', 'utf8'));
|
||||
|
||||
const { Command, Event } = require("../Structures/Addon");
|
||||
|
||||
module.exports.commands = [
|
||||
/*
|
||||
// EXAMPLE COMMAND
|
||||
new Command({
|
||||
name: "hello",
|
||||
description: "hello",
|
||||
options: [],
|
||||
permissions: ["SEND_MESSAGES"],
|
||||
async run(interaction) {
|
||||
const Embed = new Discord.MessageEmbed()
|
||||
.setDescription("Hello World!")
|
||||
.setColor(supportbot.InfoColour);
|
||||
|
||||
interaction.reply({
|
||||
embeds: [Embed],
|
||||
});
|
||||
},
|
||||
}),
|
||||
*/
|
||||
];
|
||||
module.exports.events = [
|
||||
/*
|
||||
// EXAMPLE EVENT
|
||||
new Event("ready", async (client) => {
|
||||
console.log("Bot started!");
|
||||
}),
|
||||
*/
|
||||
];
|
||||
@@ -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 });
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -1,92 +0,0 @@
|
||||
// SupportBot | Emerald Services
|
||||
// Ticket Add Command
|
||||
|
||||
const fs = require("fs");
|
||||
|
||||
const Discord = require("discord.js");
|
||||
const yaml = require("js-yaml");
|
||||
const supportbot = yaml.load(
|
||||
fs.readFileSync("./Configs/supportbot.yml", "utf8")
|
||||
);
|
||||
const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8"));
|
||||
|
||||
const Command = require("../Structures/Command.js");
|
||||
|
||||
module.exports = new Command({
|
||||
name: cmdconfig.TicketAdd,
|
||||
description: cmdconfig.TicketAddDesc,
|
||||
options: [
|
||||
{
|
||||
name: "user",
|
||||
description: "User to add",
|
||||
type: "USER",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
permissions: ["SEND_MESSAGES"],
|
||||
|
||||
async run(interaction) {
|
||||
const { getRole, getChannel, getCategory } = interaction.client;
|
||||
let SupportStaff = await getRole(supportbot.Staff, interaction.guild);
|
||||
let Admin = await getRole(supportbot.Admin, interaction.guild);
|
||||
if (!SupportStaff || !Admin)
|
||||
return interaction.reply(
|
||||
"Some roles seem to be missing!\nPlease check for errors when starting the bot."
|
||||
);
|
||||
|
||||
const NoPerms = new Discord.MessageEmbed()
|
||||
.setTitle("Invalid Permissions!")
|
||||
.setDescription(
|
||||
`${supportbot.IncorrectPerms}\n\nRole Required: \`${supportbot.Staff}\` or \`${supportbot.Admin}\``
|
||||
)
|
||||
.setColor(supportbot.WarningColour);
|
||||
|
||||
if (
|
||||
!interaction.member.roles.cache.has(SupportStaff.id) &&
|
||||
!interaction.member.roles.cache.has(Admin.id)
|
||||
)
|
||||
return interaction.reply({ embeds: [NoPerms] });
|
||||
|
||||
let TicketData = await JSON.parse(
|
||||
fs.readFileSync("./Data/TicketData.json", "utf8")
|
||||
);
|
||||
let ticket = await TicketData.tickets.findIndex(
|
||||
(t) => t.id == interaction.channel.id
|
||||
);
|
||||
if (ticket == -1) {
|
||||
const Exists = new Discord.MessageEmbed()
|
||||
.setTitle("No Ticket Found!")
|
||||
.setDescription(`${supportbot.NoValidTicket}`)
|
||||
.setColor(supportbot.WarningColour);
|
||||
return interaction.reply({ embeds: [Exists] });
|
||||
}
|
||||
|
||||
let uMember = interaction.options.getUser("user");
|
||||
const UserNotExist = new Discord.MessageEmbed()
|
||||
.setTitle("User Not Found!")
|
||||
.setDescription(
|
||||
`${supportbot.UserNotFound}\n\nTry Again:\`/${cmdconfig.TicketAdd} <user#0000>\``
|
||||
)
|
||||
.setColor(supportbot.ErrorColour);
|
||||
|
||||
if (!uMember) return interaction.reply({ embeds: [UserNotExist] });
|
||||
|
||||
interaction.channel.permissionOverwrites.edit(uMember.id, {
|
||||
VIEW_CHANNEL: true,
|
||||
});
|
||||
const Complete = new Discord.MessageEmbed()
|
||||
.setTitle("User Added!")
|
||||
.setDescription(supportbot.AddedUser.replace(/%user%/g, uMember.id))
|
||||
.setTimestamp()
|
||||
.setColor(supportbot.EmbedColour);
|
||||
interaction.reply({ embeds: [Complete] });
|
||||
TicketData.tickets[ticket].subUsers.push(uMember.id);
|
||||
fs.writeFileSync(
|
||||
"./Data/TicketData.json",
|
||||
JSON.stringify(TicketData, null, 4),
|
||||
(err) => {
|
||||
if (err) console.error(err);
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
@@ -1,180 +0,0 @@
|
||||
// SupportBot | Emerald Services
|
||||
// Close Ticket Command
|
||||
|
||||
const fs = require("fs");
|
||||
|
||||
const Discord = require("discord.js");
|
||||
const yaml = require("js-yaml");
|
||||
const supportbot = yaml.load(
|
||||
fs.readFileSync("./Configs/supportbot.yml", "utf8")
|
||||
);
|
||||
const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8"));
|
||||
|
||||
const Command = require("../Structures/Command.js");
|
||||
|
||||
module.exports = new Command({
|
||||
name: cmdconfig.CloseTicket,
|
||||
description: cmdconfig.CloseTicketDesc,
|
||||
options: [
|
||||
{
|
||||
name: "reason",
|
||||
description: "Ticket Close Reason",
|
||||
type: "STRING",
|
||||
},
|
||||
],
|
||||
permissions: ["SEND_MESSAGES"],
|
||||
|
||||
async run(interaction) {
|
||||
const { getRole, getChannel, getCategory } = interaction.client;
|
||||
if (supportbot.StaffOnly) {
|
||||
let SupportStaff = await getRole(supportbot.Staff, interaction.guild);
|
||||
let Admin = await getRole(supportbot.Admin, interaction.guild);
|
||||
if (!SupportStaff || !Admin)
|
||||
return interaction.reply(
|
||||
"Some roles seem to be missing!\nPlease check for errors when starting the bot."
|
||||
);
|
||||
const NoPerms = new Discord.MessageEmbed()
|
||||
.setTitle("Invalid Permissions!")
|
||||
.setDescription(
|
||||
`${supportbot.IncorrectPerms}\n\nRole Required: \`${supportbot.Staff}\` or \`${supportbot.Admin}\``
|
||||
)
|
||||
.setColor(supportbot.WarningColour);
|
||||
|
||||
if (
|
||||
!interaction.member.roles.cache.has(SupportStaff.id) &&
|
||||
!interaction.member.roles.cache.has(Admin.id)
|
||||
)
|
||||
return interaction.reply({ embeds: [NoPerms] });
|
||||
}
|
||||
await interaction.deferReply();
|
||||
let tickets = await JSON.parse(
|
||||
fs.readFileSync("./Data/TicketData.json", "utf8")
|
||||
);
|
||||
let TicketData = await tickets.tickets.findIndex(
|
||||
(t) => t.id == interaction.channel.id
|
||||
);
|
||||
let ticket = tickets.tickets[TicketData];
|
||||
if (TicketData == -1) {
|
||||
const Exists = new Discord.MessageEmbed()
|
||||
.setTitle("No Ticket Found!")
|
||||
.setDescription(`${supportbot.NoValidTicket}`)
|
||||
.setColor(supportbot.WarningColour);
|
||||
return interaction.followUp({ embeds: [Exists] });
|
||||
}
|
||||
let tUser = interaction.client.users.cache.get(ticket.user);
|
||||
let transcriptChannel = await getChannel(
|
||||
supportbot.TranscriptLog,
|
||||
interaction.guild
|
||||
);
|
||||
let logChannel = await getChannel(supportbot.TicketLog, interaction.guild);
|
||||
let reason =
|
||||
(await interaction.options?.getString("reason")) || "No Reason Provided.";
|
||||
|
||||
if (!transcriptChannel || !logChannel)
|
||||
return interaction.followUp("Some Channels seem to be missing!");
|
||||
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);
|
||||
await interaction.followUp({ embeds: [CloseTicketRequest] });
|
||||
let filter = (m) =>
|
||||
m.content.toLowerCase() == supportbot.ClosingConfirmation_Word &&
|
||||
m.author.id == interaction.user.id;
|
||||
await interaction.channel.awaitMessages({
|
||||
filter,
|
||||
max: 1,
|
||||
time: 20000,
|
||||
errors: ["time"],
|
||||
});
|
||||
}
|
||||
try {
|
||||
tickets.tickets[TicketData].open = false;
|
||||
fs.writeFileSync(
|
||||
"./Data/TicketData.json",
|
||||
JSON.stringify(tickets, null, 4),
|
||||
(err) => {
|
||||
if (err) console.error(err);
|
||||
}
|
||||
);
|
||||
await interaction.followUp(`**${supportbot.ClosingTicket}**`);
|
||||
const transcriptEmbed = new Discord.MessageEmbed()
|
||||
.setTitle(supportbot.TranscriptTitle)
|
||||
.setColor(supportbot.EmbedColour)
|
||||
.setFooter(supportbot.EmbedFooter)
|
||||
.addField(
|
||||
"Ticket",
|
||||
`${interaction.channel.name} (${interaction.channel.id})`
|
||||
)
|
||||
.addField("User", `${tUser}#${tUser.discriminator} (${tUser.id})`)
|
||||
.addField("Closed By", interaction.user.tag)
|
||||
.addField("Reason", reason);
|
||||
const logEmbed = new Discord.MessageEmbed()
|
||||
.setTitle(supportbot.TicketLog_Title)
|
||||
.setColor(supportbot.EmbedColour)
|
||||
.setFooter(supportbot.EmbedFooter)
|
||||
.addField(
|
||||
"Ticket",
|
||||
`${interaction.channel.name} (${interaction.channel.id})`
|
||||
)
|
||||
.addField(
|
||||
"User",
|
||||
`${tUser.username}#${tUser.discriminator} (${tUser.id})`
|
||||
)
|
||||
.addField("Closed By", interaction.user.tag)
|
||||
.addField("Reason", reason);
|
||||
let msgs = await interaction.channel.messages.fetch();
|
||||
let html = "";
|
||||
|
||||
msgs = msgs.sort((a, b) => a.createdTimestamp - b.createdTimestamp);
|
||||
html += `<style>* {background-color: #2c2f33;color: #fff;font-family: Arial, Helvetica, sans-serif;}</style>`;
|
||||
html += `<strong>Server Name:</strong> ${interaction.guild.name}<br>`;
|
||||
html += `<strong>Ticket:</strong> ${interaction.channel.name}<br>`;
|
||||
html += `<strong>Message:</strong> ${msgs.size} Messages<br><br><br>`;
|
||||
|
||||
await msgs.forEach((msg) => {
|
||||
if (msg.content) {
|
||||
html += `<strong>User:</strong> ${msg.author.tag}<br>`;
|
||||
html += `<strong>Message:</strong> ${msg.content}<br>`;
|
||||
html += `-----<br><br>`;
|
||||
}
|
||||
});
|
||||
|
||||
await logChannel.send({ embeds: [logEmbed] }).catch((err) => {
|
||||
console.error(err);
|
||||
});
|
||||
|
||||
let file = new Discord.MessageAttachment(
|
||||
Buffer.from(html),
|
||||
`${interaction.channel.name}.html`
|
||||
);
|
||||
await transcriptChannel
|
||||
.send({ embeds: [transcriptEmbed], files: [file] })
|
||||
.catch(async (err) => {
|
||||
console.error(err);
|
||||
});
|
||||
await tUser
|
||||
.send({ embeds: [transcriptEmbed], files: [file] })
|
||||
.catch(async (err) => {
|
||||
console.error(err);
|
||||
});
|
||||
if (ticket.subUsers) {
|
||||
ticket.subUsers.forEach(async (subUser) => {
|
||||
interaction.client.users.cache
|
||||
.get(subUser)
|
||||
.send({ embeds: [transcriptEmbed], files: [file] });
|
||||
});
|
||||
}
|
||||
await interaction.channel.delete().catch(async (error) => {
|
||||
console.error(error);
|
||||
if (error.code !== Discord.Constants.APIErrors.UNKNOWN_CHANNEL) {
|
||||
console.error("Failed to delete the interaction:", error);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -1,67 +0,0 @@
|
||||
// SupportBot | Emerald Services
|
||||
// Embed Command
|
||||
|
||||
const fs = require("fs");
|
||||
|
||||
const Discord = require("discord.js");
|
||||
const yaml = require("js-yaml");
|
||||
const supportbot = yaml.load(
|
||||
fs.readFileSync("./Configs/supportbot.yml", "utf8")
|
||||
);
|
||||
const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8"));
|
||||
|
||||
const Command = require("../Structures/Command.js");
|
||||
|
||||
module.exports = new Command({
|
||||
name: cmdconfig.EmbedCommand,
|
||||
description: cmdconfig.EmbedCommandDesc,
|
||||
options: [
|
||||
{
|
||||
name: "title",
|
||||
description: "Embed Title",
|
||||
type: "STRING",
|
||||
},
|
||||
|
||||
{
|
||||
name: "message",
|
||||
description: "Embed Message",
|
||||
type: "STRING",
|
||||
},
|
||||
],
|
||||
permissions: ["SEND_MESSAGES"],
|
||||
|
||||
async run(interaction) {
|
||||
const { getRole, getChannel, getCategory } = interaction.client;
|
||||
let SupportStaff = await getRole(supportbot.Staff, interaction.guild);
|
||||
let Admin = await getRole(supportbot.Admin, interaction.guild);
|
||||
|
||||
if (!SupportStaff || !Admin)
|
||||
return interaction.reply(
|
||||
"Some roles seem to be missing!\nPlease check for errors when starting the bot."
|
||||
);
|
||||
const NoPerms = new Discord.MessageEmbed()
|
||||
.setTitle("Invalid Permissions!")
|
||||
.setDescription(
|
||||
`${supportbot.IncorrectPerms}\n\nRole Required: \`${supportbot.Staff}\` or \`${supportbot.Admin}\``
|
||||
)
|
||||
.setColor(supportbot.WarningColour);
|
||||
|
||||
if (
|
||||
interaction.member.roles.cache.has(SupportStaff.id) ||
|
||||
interaction.member.roles.cache.has(Admin.id)
|
||||
) {
|
||||
const EmbedTitle = interaction.options.getString("title");
|
||||
const EmbedSubject = interaction.options.getString("message");
|
||||
|
||||
const EmbedMsg = new Discord.MessageEmbed()
|
||||
.setTitle(EmbedTitle)
|
||||
.setDescription(EmbedSubject);
|
||||
|
||||
interaction.reply({
|
||||
embeds: [EmbedMsg],
|
||||
});
|
||||
} else {
|
||||
return interaction.reply({ embeds: [NoPerms] });
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -1,92 +0,0 @@
|
||||
// SupportBot | Emerald Services
|
||||
// Help Command
|
||||
|
||||
const fs = require("fs");
|
||||
|
||||
const Discord = require("discord.js");
|
||||
const yaml = require("js-yaml");
|
||||
const supportbot = yaml.load(
|
||||
fs.readFileSync("./Configs/supportbot.yml", "utf8")
|
||||
);
|
||||
const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8"));
|
||||
|
||||
const Command = require("../Structures/Command.js");
|
||||
|
||||
async function asyncForEach(array, callback) {
|
||||
for (let index = 0; index < array.length; index++) {
|
||||
await callback(array[index], index, array);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new Command({
|
||||
name: cmdconfig.HelpCommand,
|
||||
description: cmdconfig.HelpCommandDesc,
|
||||
options: [],
|
||||
permissions: ["SEND_MESSAGES"],
|
||||
|
||||
async run(interaction) {
|
||||
const { getRole, getChannel, getCategory } = interaction.client;
|
||||
let SupportStaff = await getRole(supportbot.Staff, interaction.guild);
|
||||
let Admin = await getRole(supportbot.Admin, interaction.guild);
|
||||
if (!SupportStaff || !Admin)
|
||||
return interaction.reply(
|
||||
"Some roles seem to be missing!\nPlease check for errors when starting the bot."
|
||||
);
|
||||
|
||||
let botCommands = "";
|
||||
botCommands += `**/${cmdconfig.HelpCommand}** ${cmdconfig.HelpCommandDesc}\n`;
|
||||
botCommands += `**/${cmdconfig.InfoCommand}** ${cmdconfig.InfoCommandDesc}\n`;
|
||||
botCommands += `**/${cmdconfig.PingCommand}** ${cmdconfig.PingCommandDesc}\n`;
|
||||
|
||||
let ticketCommands = "";
|
||||
ticketCommands += `**/${cmdconfig.OpenTicket}** ${cmdconfig.OpenTicketDesc}\n`;
|
||||
ticketCommands += `**/${cmdconfig.CloseTicket}** ${cmdconfig.CloseTicketDesc}\n`;
|
||||
|
||||
let staffCommands = "";
|
||||
staffCommands += `**/${cmdconfig.TicketAdd}** ${cmdconfig.TicketAddDesc}\n`;
|
||||
staffCommands += `**/${cmdconfig.TicketRemove}** ${cmdconfig.TicketRemoveDesc}\n`;
|
||||
|
||||
const HelpEmbed1 = new Discord.MessageEmbed()
|
||||
.setTitle(supportbot.Name + " Commands")
|
||||
.setThumbnail(interaction.user.displayAvatarURL())
|
||||
|
||||
.addFields(
|
||||
{
|
||||
name: "🖥️ General Commands\n",
|
||||
value: `${botCommands}\n`,
|
||||
inline: false,
|
||||
},
|
||||
{
|
||||
name: "🎫 Support Commands\n",
|
||||
value: `${ticketCommands}\n`,
|
||||
inline: false,
|
||||
}
|
||||
)
|
||||
|
||||
.setColor(supportbot.EmbedColour)
|
||||
.setFooter(supportbot.EmbedFooter, interaction.user.displayAvatarURL());
|
||||
|
||||
if (
|
||||
interaction.member.roles.cache.has(SupportStaff.id) ||
|
||||
interaction.member.roles.cache.has(Admin.id)
|
||||
) {
|
||||
HelpEmbed1.addFields({
|
||||
name: "🔐 Staff Commands\n",
|
||||
value: `${staffCommands}\n`,
|
||||
inline: false,
|
||||
});
|
||||
}
|
||||
|
||||
if (supportbot.SendHelpPage === "dm") {
|
||||
interaction.user.reply({
|
||||
embeds: [HelpEmbed1],
|
||||
});
|
||||
}
|
||||
|
||||
if (supportbot.SendHelpPage === "channel") {
|
||||
interaction.reply({
|
||||
embeds: [HelpEmbed1],
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -1,40 +0,0 @@
|
||||
// SupportBot | Emerald Services
|
||||
// Info Command
|
||||
|
||||
const fs = require("fs");
|
||||
|
||||
const Discord = require("discord.js");
|
||||
const yaml = require("js-yaml");
|
||||
const supportbot = yaml.load(
|
||||
fs.readFileSync("./Configs/supportbot.yml", "utf8")
|
||||
);
|
||||
const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8"));
|
||||
|
||||
const Command = require("../Structures/Command.js");
|
||||
|
||||
module.exports = new Command({
|
||||
name: cmdconfig.InfoCommand,
|
||||
description: cmdconfig.InfoCommandDesc,
|
||||
options: [],
|
||||
permissions: ["SEND_MESSAGES"],
|
||||
|
||||
async run(interaction) {
|
||||
const InfoButton = new Discord.MessageButton()
|
||||
.setLabel(supportbot.InfoButtonText)
|
||||
.setURL(supportbot.InfoURL)
|
||||
.setStyle("LINK");
|
||||
|
||||
const inforow = new Discord.MessageActionRow().addComponents(InfoButton);
|
||||
|
||||
const InfoEmbed = new Discord.MessageEmbed()
|
||||
.setURL(supportbot.InfoURL)
|
||||
.setTitle(supportbot.InfoTitle)
|
||||
.setDescription(supportbot.AboutMe)
|
||||
.setColor(supportbot.InfoColour);
|
||||
|
||||
interaction.reply({
|
||||
embeds: [InfoEmbed],
|
||||
components: [inforow],
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -1,32 +0,0 @@
|
||||
// SupportBot | Emerald Services
|
||||
// Ping Command
|
||||
|
||||
const fs = require("fs");
|
||||
|
||||
const Discord = require("discord.js");
|
||||
const yaml = require("js-yaml");
|
||||
const supportbot = yaml.load(
|
||||
fs.readFileSync("./Configs/supportbot.yml", "utf8")
|
||||
);
|
||||
const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8"));
|
||||
|
||||
const Command = require("../Structures/Command.js");
|
||||
|
||||
module.exports = new Command({
|
||||
name: cmdconfig.PingCommand,
|
||||
description: cmdconfig.PingCommandDesc,
|
||||
options: [],
|
||||
permissions: ["SEND_MESSAGES"],
|
||||
|
||||
async run(interaction) {
|
||||
const PingEmbed = new Discord.MessageEmbed()
|
||||
.setDescription(
|
||||
`:ping_pong: **Ping:** \`${interaction.client.ws.ping} ms\``
|
||||
)
|
||||
.setColor(supportbot.EmbedColour);
|
||||
|
||||
interaction.reply({
|
||||
embeds: [PingEmbed],
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -1,100 +0,0 @@
|
||||
// SupportBot | Emerald Services
|
||||
// Ticket Remove Command
|
||||
|
||||
const fs = require("fs");
|
||||
|
||||
const Discord = require("discord.js");
|
||||
const yaml = require("js-yaml");
|
||||
const supportbot = yaml.load(
|
||||
fs.readFileSync("./Configs/supportbot.yml", "utf8")
|
||||
);
|
||||
const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8"));
|
||||
|
||||
const Command = require("../Structures/Command.js");
|
||||
|
||||
async function asyncForEach(array, callback) {
|
||||
for (let index = 0; index < array.length; index++) {
|
||||
await callback(array[index], index, array);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new Command({
|
||||
name: cmdconfig.TicketRemove,
|
||||
description: cmdconfig.TicketRemoveDesc,
|
||||
options: [
|
||||
{
|
||||
name: "user",
|
||||
description: "User to remove",
|
||||
type: "USER",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
permissions: ["SEND_MESSAGES"],
|
||||
|
||||
async run(interaction) {
|
||||
const { getRole, getChannel, getCategory } = interaction.client;
|
||||
let Staff = await getRole(supportbot.Staff, interaction.guild);
|
||||
let Admin = await getRole(supportbot.Admin, interaction.guild);
|
||||
if (!Staff || !Admin)
|
||||
return interaction.reply(
|
||||
"Some roles seem to be missing!\nPlease check for errors when starting the bot."
|
||||
);
|
||||
|
||||
const NoPerms = new Discord.MessageEmbed()
|
||||
.setTitle("Invalid Permissions!")
|
||||
.setDescription(
|
||||
`${supportbot.IncorrectPerms}\n\nRole Required: \`${supportbot.Staff}\` or \`${supportbot.Admin}\``
|
||||
)
|
||||
.setColor(supportbot.WarningColour);
|
||||
|
||||
if (
|
||||
!interaction.member.roles.cache.has(Staff.id) &&
|
||||
!interaction.member.roles.cache.has(Admin.id)
|
||||
)
|
||||
return interaction.reply({ embeds: [NoPerms] });
|
||||
let TicketData = await JSON.parse(
|
||||
fs.readFileSync("./Data/TicketData.json", "utf8")
|
||||
);
|
||||
let ticket = await TicketData.tickets.findIndex(
|
||||
(t) => t.id == interaction.channel.id
|
||||
);
|
||||
if (ticket == -1) {
|
||||
const Exists = new Discord.MessageEmbed()
|
||||
.setTitle("No Ticket Found!")
|
||||
.setDescription(`${supportbot.NoValidTicket}`)
|
||||
.setColor(supportbot.WarningColour);
|
||||
return interaction.reply({ embeds: [Exists] });
|
||||
}
|
||||
|
||||
let uMember = interaction.options.getUser("user");
|
||||
const UserNotExist = new Discord.MessageEmbed()
|
||||
.setTitle("User Not Found!")
|
||||
.setDescription(
|
||||
`${supportbot.UserNotFound}\n\nTry Again:\`/${cmdconfig.TicketRemove} <user#0000>\``
|
||||
)
|
||||
.setColor(supportbot.ErrorColour);
|
||||
|
||||
if (!uMember) return interaction.reply({ embeds: [UserNotExist] });
|
||||
|
||||
interaction.channel.permissionOverwrites.edit(uMember.id, {
|
||||
VIEW_CHANNEL: false,
|
||||
});
|
||||
|
||||
const Complete = new Discord.MessageEmbed()
|
||||
.setTitle("User Removed!")
|
||||
.setDescription(supportbot.RemovedUser.replace(/%user%/g, uMember.id))
|
||||
.setTimestamp()
|
||||
.setColor(supportbot.EmbedColour);
|
||||
interaction.reply({ embeds: [Complete] });
|
||||
TicketData.tickets[ticket].subUsers = TicketData.tickets[
|
||||
ticket
|
||||
].subUsers.filter((u) => u != uMember.id);
|
||||
fs.writeFileSync(
|
||||
"./Data/TicketData.json",
|
||||
JSON.stringify(TicketData, null, 4),
|
||||
(err) => {
|
||||
if (err) console.error(err);
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
@@ -1,66 +0,0 @@
|
||||
// SupportBot | Emerald Services
|
||||
// Ping Command
|
||||
|
||||
const fs = require("fs");
|
||||
|
||||
const Discord = require("discord.js");
|
||||
const yaml = require("js-yaml");
|
||||
const supportbot = yaml.load(
|
||||
fs.readFileSync("./Configs/supportbot.yml", "utf8")
|
||||
);
|
||||
const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8"));
|
||||
|
||||
const Command = require("../Structures/Command.js");
|
||||
|
||||
module.exports = new Command({
|
||||
name: cmdconfig.SuggestCommand,
|
||||
description: cmdconfig.SuggestCommandDesc,
|
||||
options: [
|
||||
{
|
||||
name: "suggestion",
|
||||
description: "Create a Suggestion",
|
||||
type: "STRING",
|
||||
},
|
||||
],
|
||||
permissions: ["SEND_MESSAGES"],
|
||||
|
||||
async run(interaction) {
|
||||
const { getRole, getChannel, getCategory } = interaction.client;
|
||||
const suggestChannel = await getChannel(
|
||||
supportbot.SuggestionChannel,
|
||||
interaction.guild
|
||||
);
|
||||
|
||||
const NoChannel = new Discord.MessageEmbed()
|
||||
.setTitle("Missing Channel!")
|
||||
.setDescription(`${supportbot.InvalidChannel}`)
|
||||
.setColor(supportbot.ErrorColour);
|
||||
|
||||
if (!suggestChannel) return interaction.reply({ embeds: [NoChannel] });
|
||||
|
||||
let suggestion = interaction.options.getString("suggestion");
|
||||
|
||||
const SuggestEmbed = new Discord.MessageEmbed()
|
||||
.addField("Suggestion", suggestion, true)
|
||||
.addField("From", `<@${interaction.user.id}>`)
|
||||
.setThumbnail(interaction.user.displayAvatarURL())
|
||||
.setFooter(supportbot.EmbedFooter, interaction.user.displayAvatarURL())
|
||||
.setColor(supportbot.EmbedColour);
|
||||
|
||||
const suggestionMsg = await suggestChannel.send({ embeds: [SuggestEmbed] });
|
||||
if (supportbot.SuggestionUpvote && supportbot.SuggestionDownvote) {
|
||||
suggestionMsg.react(supportbot.SuggestionUpvote);
|
||||
suggestionMsg.react(supportbot.SuggestionDownvote);
|
||||
}
|
||||
|
||||
const Submitted = new Discord.MessageEmbed()
|
||||
.setTitle("Suggestion Submitted!")
|
||||
.setDescription(
|
||||
`:white_check_mark: You have successfully submitted a suggestion.`
|
||||
)
|
||||
.addField("Sent to:", `<#${suggestChannel.id}>`)
|
||||
.setColor(supportbot.SuccessColour);
|
||||
|
||||
return interaction.reply({ embeds: [Submitted] });
|
||||
},
|
||||
});
|
||||
@@ -1,325 +0,0 @@
|
||||
// SupportBot | Emerald Services
|
||||
// Ticket Command
|
||||
|
||||
const fs = require("fs");
|
||||
|
||||
const {
|
||||
MessageEmbed,
|
||||
Permissions,
|
||||
MessageButton,
|
||||
MessageActionRow,
|
||||
} = require("discord.js");
|
||||
const yaml = require("js-yaml");
|
||||
const supportbot = yaml.load(
|
||||
fs.readFileSync("./Configs/supportbot.yml", "utf8")
|
||||
);
|
||||
const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8"));
|
||||
|
||||
const Command = require("../Structures/Command.js");
|
||||
const TicketNumberID = require("../Structures/TicketID.js");
|
||||
|
||||
module.exports = new Command({
|
||||
name: cmdconfig.OpenTicket,
|
||||
description: cmdconfig.OpenTicketDesc,
|
||||
options: [
|
||||
{
|
||||
name: "reason",
|
||||
description: "Ticket Reason",
|
||||
type: "STRING",
|
||||
},
|
||||
],
|
||||
permissions: ["SEND_MESSAGES"],
|
||||
|
||||
async run(interaction) {
|
||||
let TicketData = await JSON.parse(
|
||||
fs.readFileSync("./Data/TicketData.json", "utf8")
|
||||
);
|
||||
const { getRole, getChannel, getCategory } = interaction.client;
|
||||
let User = interaction.guild.members.cache.get(interaction.user.id);
|
||||
|
||||
if (
|
||||
supportbot.MaxAllowedTickets &&
|
||||
TicketData.tickets.filter((t) => t.user == interaction.user.id && t.open)
|
||||
.length >= supportbot.MaxAllowedTickets
|
||||
) {
|
||||
return interaction.reply({
|
||||
embeds: [
|
||||
{
|
||||
title: "Too Many Tickets!",
|
||||
description: `You can't have more than ${supportbot.MaxAllowedTickets} open tickets!`,
|
||||
color: supportbot.WarningColour,
|
||||
},
|
||||
],
|
||||
ephemeral: true,
|
||||
});
|
||||
}
|
||||
if (
|
||||
User.roles.cache.has(
|
||||
getRole(supportbot.TicketBlackListRole, interaction.guild).id
|
||||
)
|
||||
) {
|
||||
return interaction.reply({
|
||||
content: supportbot.TicketBlackListMessage,
|
||||
ephemeral: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Ticket ID
|
||||
let ticketNumberID = await TicketNumberID.pad();
|
||||
|
||||
// Ticket Subject
|
||||
const TicketSubject =
|
||||
interaction.options?.getString("reason") || supportbot.NoTicketSubject;
|
||||
|
||||
const TicketExists = new MessageEmbed()
|
||||
.setTitle("Ticket Exists!")
|
||||
.setDescription(`${supportbot.TicketExists}`)
|
||||
.setColor(supportbot.WarningColour);
|
||||
|
||||
if (
|
||||
await interaction.guild.channels.cache.find((ticketChannel) => {
|
||||
ticketChannel.name === `${supportbot.TicketPrefix}${ticketNumberID}`;
|
||||
})
|
||||
) {
|
||||
return await interaction.reply({ embeds: [TicketExists] });
|
||||
}
|
||||
const Staff = await getRole(supportbot.Staff, interaction.guild);
|
||||
const Admin = await getRole(supportbot.Admin, interaction.guild);
|
||||
const DeptRole1 = await getRole(
|
||||
supportbot.DepartmentRole_1,
|
||||
interaction.guild
|
||||
);
|
||||
const DeptRole2 = await getRole(
|
||||
supportbot.DepartmentRole_2,
|
||||
interaction.guild
|
||||
);
|
||||
const DeptRole3 = await getRole(
|
||||
supportbot.DepartmentRole_3,
|
||||
interaction.guild
|
||||
);
|
||||
if (!Staff || !Admin || !DeptRole1 || !DeptRole2 || !DeptRole3)
|
||||
return interaction.reply({
|
||||
content:
|
||||
"Some roles seem to be missing!\nPlease check for errors when starting the bot.",
|
||||
ephemeral: true,
|
||||
});
|
||||
const Author = interaction.user;
|
||||
let TicketCategory = await getCategory(
|
||||
supportbot.TicketCategory,
|
||||
interaction.guild
|
||||
);
|
||||
const ticketChannel = await interaction.guild.channels.create(
|
||||
`${supportbot.TicketPrefix}${ticketNumberID}`,
|
||||
{
|
||||
type: "GUILD_TEXT",
|
||||
parent: TicketCategory.id,
|
||||
permissionOverwrites: [
|
||||
{
|
||||
id: Admin.id,
|
||||
allow: [Permissions.FLAGS.VIEW_CHANNEL],
|
||||
},
|
||||
{
|
||||
id: Author.id,
|
||||
allow: [Permissions.FLAGS.VIEW_CHANNEL],
|
||||
deny: [Permissions.FLAGS.SEND_MESSAGES],
|
||||
},
|
||||
{
|
||||
id: interaction.guild.id,
|
||||
deny: [Permissions.FLAGS.VIEW_CHANNEL],
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
if (supportbot.AllowStaff) {
|
||||
ticketChannel.permissionOverwrites.edit(Staff.id, {
|
||||
VIEW_CHANNEL: true,
|
||||
});
|
||||
}
|
||||
await TicketData.tickets.push({
|
||||
id: ticketChannel.id,
|
||||
name: ticketChannel.name,
|
||||
user: Author.id,
|
||||
number: ticketNumberID,
|
||||
reason: TicketSubject,
|
||||
open: true,
|
||||
subUsers: [],
|
||||
});
|
||||
fs.writeFileSync(
|
||||
"./Data/TicketData.json",
|
||||
JSON.stringify(TicketData, null, 4),
|
||||
(err) => {
|
||||
if (err) console.error(err);
|
||||
}
|
||||
);
|
||||
|
||||
const CreatedTicket = new MessageEmbed()
|
||||
.setDescription(
|
||||
supportbot.TicketCreatedAlert.replace(
|
||||
/%ticketauthor%/g,
|
||||
interaction.user.id
|
||||
)
|
||||
.replace(/%ticketid%/g, ticketChannel.id)
|
||||
.replace(/%ticketusername%/g, interaction.user.username)
|
||||
)
|
||||
.setColor(supportbot.EmbedColour);
|
||||
await interaction.reply({ embeds: [CreatedTicket], ephemeral: true });
|
||||
|
||||
if (supportbot.AllowTicketMentions) {
|
||||
await ticketChannel.send(`${interaction.user}`);
|
||||
}
|
||||
|
||||
const TicketMessage = new MessageEmbed()
|
||||
.setTitle(
|
||||
supportbot.Ticket_Title.replace(/%ticketauthor%/g, interaction.user.id)
|
||||
.replace(/%ticketid%/g, ticketChannel.id)
|
||||
.replace(/%ticketusername%/g, interaction.user.username)
|
||||
)
|
||||
.setDescription(
|
||||
supportbot.TicketMessage.replace(/%ticketauthor%/g, interaction.user.id)
|
||||
.replace(/%ticketid%/g, ticketChannel.id)
|
||||
.replace(/%ticketusername%/g, interaction.user.username)
|
||||
)
|
||||
.setColor(supportbot.EmbedColour);
|
||||
|
||||
if (supportbot.TicketSubject === "embed") {
|
||||
if (TicketSubject != "No Reason Provided.") {
|
||||
TicketMessage.addFields({ name: "Reason", value: TicketSubject });
|
||||
}
|
||||
}
|
||||
|
||||
if (supportbot.TicketSubject === "description") {
|
||||
if (TicketSubject != "No Reason Provided.") {
|
||||
await ticketChannel.setTopic(
|
||||
`Reason: ${TicketSubject} - User ID: ${interaction.user.id} - Ticket: ${ticketChannel.name}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (supportbot.TicketDepartments) {
|
||||
TicketMessage.addFields({
|
||||
name: "Departments",
|
||||
value: ` **${supportbot.DepartmentTitle_1}**\n **${supportbot.DepartmentTitle_2}**\n **${supportbot.DepartmentTitle_3}**`,
|
||||
});
|
||||
}
|
||||
const CloseButton = new MessageButton()
|
||||
.setCustomId("ticketclose")
|
||||
.setLabel("Close")
|
||||
.setStyle(supportbot.TicketCloseColour)
|
||||
.setEmoji(supportbot.TicketCloseEmoji);
|
||||
|
||||
const LockButton = new MessageButton()
|
||||
.setCustomId("ticketlock")
|
||||
.setLabel("Lock")
|
||||
.setStyle(supportbot.TicketLockColour)
|
||||
.setEmoji(supportbot.TicketLockEmoji);
|
||||
|
||||
const row2 = new MessageActionRow().addComponents(CloseButton, LockButton);
|
||||
|
||||
if (supportbot.TicketDepartments) {
|
||||
try {
|
||||
const Department1Button = new MessageButton()
|
||||
.setCustomId("Department1")
|
||||
.setLabel(supportbot.DepartmentTitle_1)
|
||||
.setStyle(supportbot.TicketDept1Colour)
|
||||
.setEmoji(supportbot.TicketDept1Emoji);
|
||||
|
||||
const Department2Button = new MessageButton()
|
||||
.setCustomId("Department2")
|
||||
.setLabel(supportbot.DepartmentTitle_2)
|
||||
.setStyle(supportbot.TicketDept2Colour)
|
||||
.setEmoji(supportbot.TicketDept2Emoji);
|
||||
|
||||
const Department3Button = new MessageButton()
|
||||
.setCustomId("Department3")
|
||||
.setLabel(supportbot.DepartmentTitle_3)
|
||||
.setStyle(supportbot.TicketDept3Colour)
|
||||
.setEmoji(supportbot.TicketDept3Emoji);
|
||||
|
||||
const row = new MessageActionRow().addComponents(
|
||||
Department1Button,
|
||||
Department2Button,
|
||||
Department3Button
|
||||
);
|
||||
|
||||
const m = await ticketChannel.send({
|
||||
embeds: [TicketMessage],
|
||||
components: [row],
|
||||
});
|
||||
|
||||
let collector;
|
||||
try {
|
||||
const filter = (i) => i.user.id === interaction.user.id;
|
||||
collector = await m.awaitMessageComponent({
|
||||
filter,
|
||||
max: 1,
|
||||
componentType: "BUTTON",
|
||||
time: supportbot.Timeout * 60000,
|
||||
});
|
||||
} catch (e) {
|
||||
if (e.code == "INTERACTION_COLLECTOR_ERROR") {
|
||||
try {
|
||||
TicketData = await JSON.parse(
|
||||
fs.readFileSync("./Data/TicketData.json", "utf8")
|
||||
);
|
||||
let ticket = await TicketData.tickets.findIndex(
|
||||
(t) => t.id == ticketChannel.id
|
||||
);
|
||||
TicketData.tickets[ticket].open = false;
|
||||
fs.writeFileSync(
|
||||
"./Data/TicketData.json",
|
||||
JSON.stringify(TicketData, null, 4),
|
||||
(err) => {
|
||||
if (err) console.error(err);
|
||||
}
|
||||
);
|
||||
interaction.user.send(
|
||||
"Your ticket has timed out. Please open a new one, and select a department."
|
||||
);
|
||||
} catch (e) {}
|
||||
return await ticketChannel.delete();
|
||||
}
|
||||
}
|
||||
let role;
|
||||
let title;
|
||||
if (collector.customId === "Department1") {
|
||||
role = DeptRole1;
|
||||
title = supportbot.DepartmentTitle_1;
|
||||
}
|
||||
if (collector.customId === "Department2") {
|
||||
role = DeptRole2;
|
||||
title = supportbot.DepartmentTitle_2;
|
||||
}
|
||||
if (collector.customId === "Department3") {
|
||||
role = DeptRole3;
|
||||
title = supportbot.DepartmentTitle_3;
|
||||
}
|
||||
|
||||
ticketChannel.permissionOverwrites.edit(role.id, {
|
||||
VIEW_CHANNEL: true,
|
||||
});
|
||||
ticketChannel.permissionOverwrites.edit(Author.id, {
|
||||
SEND_MESSAGES: true,
|
||||
});
|
||||
|
||||
await collector.reply({
|
||||
embeds: [
|
||||
{
|
||||
description: `> Thank for reaching out to the **${title} Department**. Please provide us information regarding your query.`,
|
||||
color: supportbot.EmbedColour,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
if (supportbot.AllowTicketMentions) {
|
||||
await ticketChannel.send("@here");
|
||||
}
|
||||
await collector.message.edit({
|
||||
embeds: [TicketMessage],
|
||||
components: [row2],
|
||||
});
|
||||
} catch (error) {}
|
||||
} else {
|
||||
await ticketChannel.send({ embeds: [TicketMessage], components: [row2] });
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
# ___ _ ___ _
|
||||
# / __> _ _ ___ ___ ___ _ _ _| |_ | . > ___ _| |_
|
||||
# \__ \| | || . \| . \/ . \| '_> | | | . \/ . \ | |
|
||||
# <___/`___|| _/| _/\___/|_| |_| |___/\___/ |_|
|
||||
# |_| |_|
|
||||
#
|
||||
# SupportBot created by Emerald Services
|
||||
# Installed with MIT License
|
||||
#
|
||||
# Discord Support: https://discord.gg/emeraldsrv
|
||||
# Official Website & Marketplace: https://emeraldsrv.com
|
||||
#
|
||||
# Messages Configuration (api.yml)
|
||||
|
||||
|
||||
# -------------------------------
|
||||
# API Server Configuration
|
||||
# -------------------------------
|
||||
|
||||
API:
|
||||
Enabled: true
|
||||
Port: 3000
|
||||
|
||||
SecretKey: "SECRET_KEY"
|
||||
|
||||
# Set to 1 when the dashboard is behind nginx, Cloudflare, Pterodactyl proxy, etc.
|
||||
# so http/https is detected from X-Forwarded-Proto.
|
||||
|
||||
TrustProxy: false
|
||||
|
||||
EmeraldAPIKey: "EMERALDSRV_API_KEY"
|
||||
# This is required for the one-click addon installer. Get API key from https://emeraldsrv.dev/
|
||||
|
||||
OAuth:
|
||||
Enabled: true
|
||||
ClientId: "CLIENT_ID"
|
||||
ClientSecret: "CLIENT_SECRET"
|
||||
# Get Client ID and Client Secret from https://discord.com/developers/applications
|
||||
|
||||
# Canonical callback URL (host + path). Protocol (http/https) can follow the
|
||||
# browser when UseRequestOrigin is true (default).
|
||||
|
||||
RedirectUri: "http://localhost:3000/api/auth/discord/callback"
|
||||
|
||||
# Optional: register extra callback URLs in Discord and list them here.
|
||||
|
||||
# RedirectUris:
|
||||
# - "https://bot.example.com/api/auth/discord/callback"
|
||||
# UseRequestOrigin: true
|
||||
# CookieSecure: false
|
||||
|
||||
OwnerUserIds:
|
||||
- "OWNER_USER_ID"
|
||||
|
||||
AllowedUserIds: []
|
||||
|
||||
# Dashboard Features
|
||||
|
||||
LiveChat:
|
||||
Mode: "webhook"
|
||||
# "webhook" - Webhook to send messages with staff member's avatar and Discord username
|
||||
# "bot" - Send messages as Bot account with discord username prefix.
|
||||
# "bot_noprefix" - Send messages as Bot account without discord username prefix.
|
||||
+171
-14
@@ -7,29 +7,180 @@
|
||||
# SupportBot created by Emerald Services
|
||||
# Installed with MIT License
|
||||
#
|
||||
# Discord Support: https://emeraldsrv.dev/discord
|
||||
# Community Resources: https://emeraldsrv.dev/resources
|
||||
# Discord Support: https://discord.gg/emeraldsrv
|
||||
# Official Documentation: https://docs.emeraldsrv.com
|
||||
# Addons Marketplace: https://community.emeraldsrv.com
|
||||
#
|
||||
# Commands Configuration (commands.yml)
|
||||
|
||||
# -------------------------------
|
||||
# Command Name Configuration
|
||||
# -------------------------------
|
||||
Help:
|
||||
Command: "help"
|
||||
Description: "Get a list of all the commands."
|
||||
Permission: ["SendMessages"]
|
||||
Enabled: true
|
||||
|
||||
TicketAdd: "add"
|
||||
TicketRemove: "remove"
|
||||
OpenTicket: "ticket"
|
||||
CloseTicket: "close"
|
||||
InfoCommand: "info"
|
||||
PingCommand: "ping"
|
||||
HelpCommand: "help"
|
||||
EmbedCommand: "embed"
|
||||
SuggestCommand: "suggest"
|
||||
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."
|
||||
@@ -37,3 +188,9 @@ 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: "🎫"
|
||||
+320
-127
@@ -7,190 +7,383 @@
|
||||
# SupportBot created by Emerald Services
|
||||
# Installed with MIT License
|
||||
#
|
||||
# Discord Support: https://emeraldsrv.dev/discord
|
||||
# Community Resources: https://emeraldsrv.dev/resources
|
||||
# Discord Support: https://discord.gg/emeraldsrv
|
||||
# Website: https://emeraldsrv.dev
|
||||
#
|
||||
# SupportBot Configuration (supportbot.yml)
|
||||
|
||||
SupportBot_Version: "7.4.0"
|
||||
SupportBot_Version: "v26.2"
|
||||
|
||||
# -------------------------
|
||||
# General Configuration
|
||||
# General
|
||||
# -------------------------
|
||||
|
||||
Name: "SupportBot"
|
||||
Token: "BOT_TOKEN"
|
||||
# Guild ID Ex: 123456789123456789
|
||||
Guild: ""
|
||||
General:
|
||||
Name: "SupportBot"
|
||||
Token: "YOUR_BOT_TOKEN_HERE"
|
||||
Addons:
|
||||
Enabled: true
|
||||
GuildId: "YOUR_GUILD_ID_HERE"
|
||||
|
||||
# -------------------------
|
||||
# Activity Configuration
|
||||
# Database Configuration
|
||||
# -------------------------
|
||||
|
||||
BotActivity: "Need Help? /ticket"
|
||||
Database:
|
||||
# Driver options: "mysql" (works for MariaDB & MySQL) or "sqlite"
|
||||
Driver: "mysql"
|
||||
|
||||
StreamingURL: "https://www.twitch.tv/YOUR_TWITCH_NAME"
|
||||
# Set The ActivityType to "STREAMING" for a streaming status!
|
||||
# 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
|
||||
|
||||
ActivityType: "PLAYING"
|
||||
# Activity status can either be PLAYING, WATCHING, LISTENING or STREAMING.
|
||||
# Connection Settings for SQLite (used when Driver is "sqlite")
|
||||
SQLite:
|
||||
File: "./Data/supportbot.db"
|
||||
|
||||
# -------------------------
|
||||
# Embed Configuration
|
||||
# Activity
|
||||
# -------------------------
|
||||
|
||||
EmbedColour: "#f74343"
|
||||
EmbedFooter: "SupportBot | An Advanced Ticket System"
|
||||
Activity:
|
||||
Status: "SupportBot"
|
||||
Type: "Watching"
|
||||
StreamingURL: "https://www.twitch.tv/YOUR_TWITCH_NAME"
|
||||
# Available Types:
|
||||
# Playing
|
||||
# Watching
|
||||
# Listening
|
||||
# Competing
|
||||
# Streaming (URL required)
|
||||
|
||||
ErrorColour: "#d12519"
|
||||
SuccessColour: "#04b545"
|
||||
WarningColour: "#fca117"
|
||||
# You can also update the activity via Discord using: /settings
|
||||
|
||||
# -------------------------
|
||||
# Information Configuration
|
||||
# -------------------------
|
||||
# -------------------------
|
||||
# Embeds
|
||||
# -------------------------
|
||||
|
||||
InfoTitle: "Information"
|
||||
AboutMe: "Hi, I'm SupportBot an advanced discord ticket bot with 100% customization! [Download](https://emeraldsrv.dev/download)"
|
||||
Embed:
|
||||
Colours:
|
||||
General: "#0cc559"
|
||||
Success: "#04b545"
|
||||
Error: "#d13030"
|
||||
Warn: "#fca117"
|
||||
|
||||
InfoButtonText: "Visit Site"
|
||||
InfoURL: "https://emeraldsrv.dev"
|
||||
InfoColour: "#f74343"
|
||||
Footer: "SupportBot | Developed by Emerald Development"
|
||||
# Use 0x<hexcode> to change the colour of the embeds.
|
||||
|
||||
# -------------------------
|
||||
# Help Configuration
|
||||
# -------------------------
|
||||
# -------------------------
|
||||
# Roles
|
||||
# -------------------------
|
||||
|
||||
SendHelpPage: "channel" # Set to channel or dm
|
||||
Roles:
|
||||
AutoRole:
|
||||
Enabled: true
|
||||
Role: "ROLE_ID"
|
||||
|
||||
# -------------------------
|
||||
# Role Configuration
|
||||
# -------------------------
|
||||
StaffMember:
|
||||
Staff: "ROLE_ID"
|
||||
Admin: "ROLE_ID"
|
||||
Moderator: "ROLE_ID"
|
||||
|
||||
AutoRole: true
|
||||
AutoRole_Role: "Member"
|
||||
Mod:
|
||||
AllowSupportStaff: true
|
||||
# Allow Support Staff to use the /mod command. (More in detail customisation in commands.yml)
|
||||
|
||||
Staff: "Staff"
|
||||
Admin: "Admin"
|
||||
TicketBlackListRole: "Blacklisted"
|
||||
# ----------------------------
|
||||
# System Messages
|
||||
# ----------------------------
|
||||
|
||||
Welcome:
|
||||
Enabled: false
|
||||
Channel: "CHANNEL_ID"
|
||||
|
||||
Leave:
|
||||
Enabled: false
|
||||
Channel: "CHANNEL_ID"
|
||||
|
||||
# ----------------------------
|
||||
# Suggestions Configuration
|
||||
# Suggestions
|
||||
# ----------------------------
|
||||
|
||||
SuggestionChannel: "suggestions"
|
||||
SuggestionUpvote: "✅"
|
||||
SuggestionDownvote: "❌"
|
||||
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 Configuration
|
||||
# Tickets
|
||||
# -------------------------
|
||||
|
||||
TicketCategory: "Tickets"
|
||||
TicketPrefix: "ticket-" # The prefix for ticket channel names
|
||||
VoiceTickets:
|
||||
Name: "%username%'s VC"
|
||||
Category: "CATEGORY_ID"
|
||||
|
||||
TicketLog: "logs"
|
||||
TranscriptLog: "transcripts"
|
||||
Ticket:
|
||||
TicketType: "channels"
|
||||
|
||||
TranscriptTitle: "Ticket Transcript"
|
||||
TicketLog_Title: "Ticket Closed"
|
||||
TicketBlackListMessage: "You are not allowed to open a ticket"
|
||||
# Available TicketTypes:
|
||||
# channels - Tickets will be created as channels in the TicketChannelsCategory
|
||||
# threads - Tickets will be created as threads through the "TicketHome" channel.
|
||||
|
||||
MaxAllowedTickets: 2
|
||||
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
|
||||
|
||||
TicketSubject: "description"
|
||||
# "description" will displays the ticket reason within the channel description
|
||||
# "embed" will displays the ticket reason within the ticket embed
|
||||
TicketHome: "CHANNEL_ID"
|
||||
|
||||
AllowTicketMentions: true
|
||||
# Set to true to allow the ticket to mention the author and the support team.
|
||||
# Set to false to dis-allow mentions within tickets.
|
||||
# What is TicketHome?
|
||||
# - Channel where the Ticket creation panel is sent.
|
||||
# - Channel where ticket threads are made. (only if ticket threads is enabled above.)
|
||||
|
||||
# --- Creation Config ---
|
||||
# --- Start ---
|
||||
# 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"
|
||||
# ---
|
||||
|
||||
# Available Placeholders:
|
||||
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
|
||||
|
||||
# %ticketusername% - User who created the ticket.
|
||||
# <@%ticketauthor%> - Tags the user who created the ticket.
|
||||
# <#%ticketid%> - Mentions the ticket channel.
|
||||
TicketSubject: "description"
|
||||
|
||||
Ticket_Title: "Server Support | %ticketusername%"
|
||||
TicketMessage: "Hello <@%ticketauthor%>, Thank you for creating a support ticket\nPlease wait patiently whilst a member of our team reaches out to you."
|
||||
# Available TicketSubjects:
|
||||
# "description" will display the ticket reason within the channel description
|
||||
# "embed" will display the ticket reason within the ticket embed
|
||||
|
||||
Ticket_Thumbnail: "https://images.emojiterra.com/google/android-pie/512px/1f39f.png"
|
||||
NoTicketSubject: "No Reason Provided!"
|
||||
Timeout: 5
|
||||
# Set to true to allow all staff to access the ticket
|
||||
|
||||
TicketCreatedAlert: ":ticket: Your support ticket has been created: <#%ticketid%>"
|
||||
Close:
|
||||
StaffOnly: true
|
||||
# Set to false is anyone is able to close a ticket.
|
||||
|
||||
AddedUser: "👍 <@%user%> has been added to this ticket"
|
||||
RemovedUser: "👍 <@%user%> has been removed from this ticket"
|
||||
Invites:
|
||||
# Invites is for ticket threads.
|
||||
StaffOnly: true
|
||||
|
||||
# --- Buttons Config ---
|
||||
# --- Start ---
|
||||
ClaimTickets:
|
||||
Enabled: false
|
||||
Channel: "CHANNEL_ID"
|
||||
# Channel where the ticket assignments are sent for staff to claim.
|
||||
|
||||
# Discord Button Colour Selection
|
||||
# PRIMARY - Blue
|
||||
# SECONDARY - Grey
|
||||
# DANGER - Red
|
||||
# SUCCESS - Green
|
||||
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
|
||||
|
||||
LockTicketCategory: "Archive"
|
||||
ButtonTitle: "Claim Ticket"
|
||||
ButtonEmoji: "🎫"
|
||||
Button: "1"
|
||||
# Available Button Styles
|
||||
# 1 - Blurple
|
||||
# 2 - Grey
|
||||
# 3 - Green
|
||||
# 4 - Red
|
||||
|
||||
TicketCloseEmoji: "❌"
|
||||
TicketDeleteEmoji: "⛔"
|
||||
TicketLockEmoji: "🔒"
|
||||
ReviewSystem:
|
||||
Enabled: false
|
||||
Channel: "CHANNEL_ID"
|
||||
UseModal: true
|
||||
|
||||
TicketCloseColour: "DANGER"
|
||||
TicketDeleteColour: "DANGER"
|
||||
TicketLockColour: "PRIMARY"
|
||||
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?"
|
||||
|
||||
TicketDept1Colour: "PRIMARY"
|
||||
TicketDept2Colour: "PRIMARY"
|
||||
TicketDept3Colour: "PRIMARY"
|
||||
Log:
|
||||
TicketDataLog: "CHANNEL_ID"
|
||||
TickeDataTitle: "Ticket Data"
|
||||
TicketBlacklistLog: "CHANNEL_ID"
|
||||
|
||||
TicketDept1Emoji: "1️⃣"
|
||||
TicketDept2Emoji: "2️⃣"
|
||||
TicketDept3Emoji: "3️⃣"
|
||||
AIMode:
|
||||
Enabled: true
|
||||
ThreadName: "Chat with SupportBot AI"
|
||||
WelcomeMessage: "Hi! I’m SupportBot AI. You can chat with me here about your issue."
|
||||
|
||||
TranscriptSavedMessage: "📝 **Transcript Saved!**"
|
||||
TicketLockMessage: "❌ **Ticket Locked!**"
|
||||
TicketDeleteMessage: "⚠ **Ticket Deleting!**" # Tickets deleting after 5 seconds by default.
|
||||
# -------------------------
|
||||
# Ticket Departments
|
||||
# -------------------------
|
||||
|
||||
# --- Departments Config ---
|
||||
# --- Start ---
|
||||
DepartmentSystem:
|
||||
Enabled: false
|
||||
Placeholder: "Choose a ticket department"
|
||||
DefaultDepartment: "general"
|
||||
|
||||
TicketDepartments: true
|
||||
# Delete the ticket if no department selected. TIME IN MINUTES
|
||||
Timeout: 5
|
||||
AllowStaff: true
|
||||
# If set to false users with the "Staff" role will not be able to view tickets
|
||||
# without a valid department role set below.
|
||||
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?"
|
||||
|
||||
# Set to false to allow "Staff" to see tickets whether its a department for them or not.
|
||||
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?"
|
||||
|
||||
DepartmentTitle_1: "General Support"
|
||||
DepartmentTitle_2: "Billing Support"
|
||||
DepartmentTitle_3: "Admin Team"
|
||||
# Ticket department title, This is the title for a ticket department to help users know what is available.
|
||||
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?"
|
||||
|
||||
DepartmentRole_1: "Support"
|
||||
DepartmentRole_2: "Support"
|
||||
DepartmentRole_3: "Admin"
|
||||
# Ticket roles are the roles that will be able to gain access to tickets depending on what department has been chosen.
|
||||
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?"
|
||||
|
||||
# --- Closing Config ---
|
||||
# --- Start ---
|
||||
PrioritySystem:
|
||||
Enabled: false
|
||||
DefaultPriority: "Normal"
|
||||
AllowUsersToChooseOnOpen: true
|
||||
Priorities:
|
||||
low:
|
||||
Name: "Low"
|
||||
Emoji: "🟢"
|
||||
normal:
|
||||
Name: "Normal"
|
||||
Emoji: "🟡"
|
||||
high:
|
||||
Name: "High"
|
||||
Emoji: "🟠"
|
||||
urgent:
|
||||
Name: "Urgent"
|
||||
Emoji: "🔴"
|
||||
|
||||
StaffOnly: true
|
||||
ClosingTicket: ":warning: You have requested to close your support ticket."
|
||||
ClosingConfirmation_Word: "confirm"
|
||||
CloseConfirmation: false
|
||||
# -------------------------
|
||||
# Buttons
|
||||
# -------------------------
|
||||
Buttons:
|
||||
General:
|
||||
Delete: "🗑️"
|
||||
DeleteStyle: "4"
|
||||
|
||||
# ---------------------------
|
||||
# Error / Warning Messages
|
||||
# ---------------------------
|
||||
Voice:
|
||||
TicketDeleteText: "Delete Voice Channel"
|
||||
|
||||
IncorrectPerms: ":x: **Err!** You do not have the correct permissions to use this command."
|
||||
InvalidChannel: ":x: **Err!** Channel not found. Please create it!"
|
||||
UserNotFound: ":x: **Err!** This user doesn't exist, Are they in this server?"
|
||||
NoValidTicket: ":warning: You cannot execute this command here."
|
||||
TicketExists: ":warning: This ticket cannot be opened, It already exists!"
|
||||
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"
|
||||
+48
-22
@@ -7,35 +7,61 @@
|
||||
# SupportBot created by Emerald Services
|
||||
# Installed with MIT License
|
||||
#
|
||||
# Discord Support: https://emeraldsrv.dev/discord
|
||||
# Community Resources: https://emeraldsrv.dev/resources
|
||||
# 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
|
||||
# -------------------------------
|
||||
|
||||
TicketPanel: true
|
||||
Panel: true
|
||||
|
||||
# ----------------------------------------
|
||||
# Ticket Panel Message Configuration
|
||||
# ----------------------------------------
|
||||
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"
|
||||
|
||||
Channel: "create-ticket"
|
||||
ButtonEmoji: "📩"
|
||||
ButtonLabel: "Open Ticket"
|
||||
ButtonColour: "SUCCESS"
|
||||
# Colour Options:
|
||||
# -> PRIMARY - Blurple
|
||||
# -> SECONDARY - Grey
|
||||
# -> SUCCESS - Green
|
||||
# -> DANGER - Red
|
||||
# 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)
|
||||
|
||||
PanelTitle: "Need Help? Create a Support Ticket."
|
||||
Button:
|
||||
Text: "Open"
|
||||
Emoji: "🛟"
|
||||
Color: "2"
|
||||
CustomId: "createticket"
|
||||
|
||||
TicketPanel_Description: true
|
||||
TicketPanel_Thumbnail: false
|
||||
TicketPanel_Image: false
|
||||
# Available Button Styles
|
||||
# 1 - Blurple
|
||||
# 2 - Grey
|
||||
# 3 - Red
|
||||
# 4 - Green
|
||||
|
||||
PanelMessage: "Click the button to open a new support ticket."
|
||||
PanelThumbnail: "https://i.imgur.com/ieeZejl.png"
|
||||
PanelImage: "https://i.imgur.com/5NkcmDq.png"
|
||||
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: ''
|
||||
Vendored
BIN
Binary file not shown.
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"tickets": []
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"id": 0,
|
||||
"TicketPanelID": ""
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
+27
-5
@@ -1,9 +1,31 @@
|
||||
const Event = require("../Structures/Event.js");
|
||||
const {
|
||||
getConfiguredGuildId,
|
||||
syncGuildHealth,
|
||||
logGuildWarning,
|
||||
} = require("../Structures/GuildManager.js");
|
||||
|
||||
module.exports = new Event("guildCreate", async (client, guild) => {
|
||||
console.log(
|
||||
`\u001b[31m`,
|
||||
`${client.user.username} is not in the correct server set in your config. Please leave all other servers and restart the bot.`
|
||||
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.`,
|
||||
);
|
||||
console.log(`\u001b[31m`, `${client.user.username} will now exit.`);
|
||||
return process.exit(1);
|
||||
|
||||
syncGuildHealth(client);
|
||||
});
|
||||
+19
-6
@@ -1,10 +1,23 @@
|
||||
const Event = require("../Structures/Event.js");
|
||||
const {
|
||||
getConfiguredGuildId,
|
||||
syncGuildHealth,
|
||||
logGuildWarning,
|
||||
} = require("../Structures/GuildManager.js");
|
||||
|
||||
module.exports = new Event("guildDelete", async (client, guild) => {
|
||||
console.log(
|
||||
`\u001b[31m`,
|
||||
`${client.user.username} is not in the correct server set in your config. Please join the server and restart the bot.`
|
||||
);
|
||||
console.log(`\u001b[31m`, `${client.user.username} will now exit.`);
|
||||
return process.exit(1);
|
||||
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`);
|
||||
});
|
||||
+991
-84
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]
|
||||
});
|
||||
}
|
||||
});
|
||||
+5
-200
@@ -1,203 +1,8 @@
|
||||
// SupportBot | Emerald Services
|
||||
// Ready Event
|
||||
|
||||
const fs = require("fs");
|
||||
|
||||
const Discord = require("discord.js");
|
||||
const yaml = require("js-yaml");
|
||||
const supportbot = yaml.load(
|
||||
fs.readFileSync("./Configs/supportbot.yml", "utf8")
|
||||
);
|
||||
const panelconfig = yaml.load(
|
||||
fs.readFileSync("./Configs/ticket-panel.yml", "utf8")
|
||||
);
|
||||
|
||||
const Event = require("../Structures/Event.js");
|
||||
const { runBotReady } = require("../Structures/BotStartup.js");
|
||||
|
||||
module.exports = new Event("ready", async (client) => {
|
||||
const { getRole, getChannel, getCategory } = client;
|
||||
client.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.Name} v${supportbot.SupportBot_Version}`,
|
||||
`\u001b[36m`,
|
||||
`Connected to Discord`
|
||||
);
|
||||
console.log(
|
||||
`\u001b[33m`,
|
||||
`Resources:`,
|
||||
`\u001b[36m`,
|
||||
`https://emeraldsrv.dev/resources`
|
||||
);
|
||||
console.log(
|
||||
`\u001b[33m`,
|
||||
`Documentation:`,
|
||||
`\u001b[36m`,
|
||||
`https://supportbot.emeraldsrv.dev`
|
||||
);
|
||||
console.log(
|
||||
`\u001b[33m`,
|
||||
`Discord:`,
|
||||
`\u001b[36m`,
|
||||
`https://emeraldsrv.dev/discord`
|
||||
);
|
||||
console.log(
|
||||
`\u001b[33m`,
|
||||
`Hosting:`,
|
||||
`\u001b[36m`,
|
||||
`https://emeraldsrv.dev/hosting`
|
||||
);
|
||||
console.log(` `);
|
||||
console.log(`\u001b[37m`, `SupportBot proudly created by Emerald Services`);
|
||||
console.log(` `);
|
||||
console.log(`\u001b[31m`, `――――――――――――――――――――――――――――――――――――――――――――`);
|
||||
if (
|
||||
!supportbot.Guild ||
|
||||
(await client.guilds.cache.size) !== 1 ||
|
||||
(await client.guilds.cache.first().id) !== supportbot.Guild
|
||||
) {
|
||||
console.log(
|
||||
`\u001b[31m`,
|
||||
`${client.user.username} is not in the correct server set in your config. Please join the server and restart the bot.`
|
||||
);
|
||||
console.log(`\u001b[31m`, `${client.user.username} will now exit.`);
|
||||
return process.exit(1);
|
||||
}
|
||||
const roles = [
|
||||
supportbot.Admin,
|
||||
supportbot.Staff,
|
||||
supportbot.TicketBlackListRole,
|
||||
supportbot.DepartmentRole_1,
|
||||
supportbot.DepartmentRole_2,
|
||||
supportbot.DepartmentRole_3,
|
||||
];
|
||||
if (supportbot.AutoRole) roles.push(supportbot.AutoRole_Role);
|
||||
const channels = [
|
||||
supportbot.SuggestionChannel,
|
||||
supportbot.TicketLog,
|
||||
supportbot.TranscriptLog,
|
||||
panelconfig.Channel,
|
||||
];
|
||||
const categories = [supportbot.TicketCategory];
|
||||
|
||||
const missingC = [];
|
||||
const missingR = [];
|
||||
const missingCat = [];
|
||||
for (let c of channels) {
|
||||
const find = await getChannel(c, client.guilds.cache.first());
|
||||
if (!find) missingC.push(c);
|
||||
}
|
||||
for (let r of roles) {
|
||||
const find = await getRole(r, client.guilds.cache.first());
|
||||
if (!find) missingR.push(r);
|
||||
}
|
||||
for (let cat of categories) {
|
||||
const find = await getCategory(cat, client.guilds.cache.first());
|
||||
if (!find) missingCat.push(cat);
|
||||
}
|
||||
|
||||
if (missingR.length > 0) {
|
||||
console.log(
|
||||
"\u001b[33m",
|
||||
`[WARN] The following Roles could not be found! Please check your configs!\n${missingR.join(
|
||||
", "
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
if (missingC.length > 0) {
|
||||
console.log(
|
||||
"\u001b[33m",
|
||||
`[WARN] The following Channels could not be found! Please check your configs!\n${missingC.join(
|
||||
", "
|
||||
)}`
|
||||
);
|
||||
}
|
||||
if (missingCat.length > 0) {
|
||||
console.log(
|
||||
"\u001b[33m",
|
||||
`[WARN] The following Categories could not be found! Please check your configs!\n${missingCat.join(
|
||||
", "
|
||||
)}`
|
||||
);
|
||||
}
|
||||
if (panelconfig.TicketPanel) {
|
||||
let chan1 = client.channels.cache.find(
|
||||
(channel) => channel.name === panelconfig.Channel
|
||||
);
|
||||
|
||||
if (!chan1) {
|
||||
console.log(
|
||||
"\u001b[33m",
|
||||
"[WARN] Ticket reaction panel is not setup, You can do so via the configuration file!"
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
const panelid = JSON.parse(
|
||||
fs.readFileSync("./Data/ticket-panel-id.json", "utf8")
|
||||
);
|
||||
|
||||
chan1.messages.fetch(panelid.TicketPanelID).catch(async (r) => {
|
||||
let embed = new Discord.MessageEmbed()
|
||||
.setTitle(`${panelconfig.PanelTitle}`)
|
||||
.setColor(supportbot.SuccessColour)
|
||||
.setFooter(supportbot.EmbedFooter);
|
||||
|
||||
if (panelconfig.TicketPanel_Description) {
|
||||
embed.setDescription(panelconfig.PanelMessage);
|
||||
}
|
||||
|
||||
if (panelconfig.TicketPanel_Thumbnail) {
|
||||
embed.setThumbnail(panelconfig.PanelThumbnail);
|
||||
}
|
||||
|
||||
if (panelconfig.TicketPanel_Image) {
|
||||
embed.setImage(panelconfig.PanelImage);
|
||||
}
|
||||
|
||||
let button = new Discord.MessageButton()
|
||||
.setLabel(panelconfig.ButtonLabel)
|
||||
.setCustomId("openticket")
|
||||
.setStyle(panelconfig.ButtonColour)
|
||||
.setEmoji(panelconfig.ButtonEmoji);
|
||||
|
||||
let row = new Discord.MessageActionRow().addComponents(button);
|
||||
|
||||
await chan1
|
||||
.send({
|
||||
embeds: [embed],
|
||||
components: [row],
|
||||
})
|
||||
.then((r) => {
|
||||
let data = {
|
||||
id: panelid.id,
|
||||
TicketPanelID: `${r.id}`,
|
||||
};
|
||||
fs.writeFileSync(
|
||||
"./Data/ticket-panel-id.json",
|
||||
JSON.stringify(data),
|
||||
"utf8"
|
||||
);
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log("Raw" + e);
|
||||
});
|
||||
});
|
||||
}
|
||||
module.exports = new Event("clientReady", async (client) => {
|
||||
// restartDiscordBot runs startup manually — clientReady often does not re-fire anyway
|
||||
if (client.__explicitStartup) return;
|
||||
await runBotReady(client, { clearConsole: true, logCommands: false });
|
||||
});
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Emerald Services
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,13 +1,85 @@
|
||||

|
||||
<h1 align="center">
|
||||
<br>
|
||||
<a href="https://github.com/Emerald-Services/SupportBot"><img src="https://cdn.1tzemerald.space/Emerald%20Services/SBThreadHeader.png"></a>
|
||||
<br>
|
||||
SupportBot
|
||||
<br>
|
||||
</h1>
|
||||
|
||||
## What is SupportBot?
|
||||
<div align="center">
|
||||
|
||||
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.
|
||||
<a href="https://opensource.org/licenses/MIT">
|
||||
<img src="https://img.shields.io/badge/license-MIT-green?style=for-the-badge" alt="shield.png">
|
||||
</a>
|
||||
|
||||
## Quick Links
|
||||
<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>
|
||||
|
||||
**|** [Discord](https://emeraldsrv.dev/discord) **|**
|
||||
[Hosting](https://emeraldsrv.dev/hosting) **|**
|
||||
[Documentation](https://docs.emeraldsrv.dev) **|**
|
||||
[Download via Polymart](https://polymart.org/resource/supportbot-1-discord-ticket-bot.518) **|**
|
||||
[Download via MC-Market](https://www.mc-market.org/resources/8940/) **|**
|
||||
<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);
|
||||
},
|
||||
};
|
||||
+7
-1
@@ -1,6 +1,6 @@
|
||||
|
||||
// SupportBot | Emerald Services
|
||||
// Addon Structure
|
||||
|
||||
class Command {
|
||||
constructor(options) {
|
||||
this.name = options.name;
|
||||
@@ -10,7 +10,13 @@ class Command {
|
||||
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;
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
+145
-74
@@ -1,120 +1,191 @@
|
||||
// SupportBot | Emerald Services
|
||||
// Client Structure
|
||||
|
||||
const fs = require("fs");
|
||||
|
||||
const path = require("path");
|
||||
const Discord = require("discord.js");
|
||||
const intents = new Discord.Intents(32767);
|
||||
const { GatewayIntentBits, Partials } = require("discord.js");
|
||||
const { Command, Event } = require("./Addon.js");
|
||||
const configStore = require("./ConfigStore.js");
|
||||
const { getPrimaryGuild } = require("./GuildManager.js");
|
||||
|
||||
const yaml = require("js-yaml");
|
||||
const supportbot = yaml.load(
|
||||
fs.readFileSync("./Configs/supportbot.yml", "utf8")
|
||||
);
|
||||
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 });
|
||||
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 === "GUILD_TEXT" || c.type === "GUILD_NEWS") &&
|
||||
(c.id === channel || c.name.toLowerCase() === channel.toLowerCase())
|
||||
(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.toLowerCase() === role.toLowerCase()
|
||||
(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 === "GUILD_CATEGORY" &&
|
||||
(c.id === category || c.name.toLowerCase() === category.toLowerCase())
|
||||
c.type === Discord.ChannelType.GuildCategory &&
|
||||
(c.id === category || (c.name && c.name.toLowerCase() === catStr)),
|
||||
);
|
||||
}
|
||||
|
||||
start(token) {
|
||||
const commandFiles = fs
|
||||
.readdirSync("./Commands")
|
||||
.filter((file) => file.endsWith(".js"));
|
||||
async start(token) {
|
||||
const commandFiles = walk("./Commands");
|
||||
|
||||
const addonFiles = fs
|
||||
.readdirSync("./Addons")
|
||||
.filter((file) => file.endsWith(".js"));
|
||||
const allCommands = commandFiles.map((file) => ({
|
||||
file,
|
||||
command: require(`../${file}`),
|
||||
}));
|
||||
|
||||
const addons = addonFiles.map((file) => {
|
||||
let addon = require(`../Addons/${file}`);
|
||||
addon.name = file.split(".")[0];
|
||||
return addon;
|
||||
});
|
||||
|
||||
const commands = commandFiles.map((file) => require(`../Commands/${file}`));
|
||||
|
||||
// Commands
|
||||
console.log(`\u001b[34;1m`, "▬▬▬▬▬▬▬ Commands ▬▬▬▬▬▬▬");
|
||||
|
||||
commands.forEach((cmd) => {
|
||||
console.log(`\u001b[31;1m`, `${cmd.name}`, `\u001b[32;1m`, "Loaded");
|
||||
this.commands.set(cmd.name, cmd);
|
||||
});
|
||||
|
||||
console.log(`\u001b[34;1m`, "▬▬▬▬▬▬▬ Commands ▬▬▬▬▬▬▬");
|
||||
// Addons
|
||||
console.log(" ");
|
||||
console.log(`\u001b[34;1m`, "▬▬▬▬▬▬▬ Addons ▬▬▬▬▬▬▬");
|
||||
|
||||
addons.forEach((addon) => {
|
||||
console.log(
|
||||
`\u001b[33m`,
|
||||
`[ADDON]`,
|
||||
`\u001b[31;1m`,
|
||||
`${addon.name}`,
|
||||
`\u001b[32;1m`,
|
||||
"Loaded"
|
||||
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(),
|
||||
);
|
||||
addon.commands.forEach((command) => {
|
||||
this.commands.set(command.name, command);
|
||||
});
|
||||
addon.events.forEach((event) => {
|
||||
this.on(event.event, (...args) => event.run(this, ...args));
|
||||
});
|
||||
|
||||
if (!matchingConfig) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return matchingConfig.Enabled !== false;
|
||||
});
|
||||
|
||||
console.log(`\u001b[34;1m`, "▬▬▬▬▬▬▬ Addons ▬▬▬▬▬▬▬");
|
||||
section("Commands");
|
||||
|
||||
// Slash Commands
|
||||
|
||||
this.once("ready", async () => {
|
||||
await this.application?.commands.set(this.commands, supportbot.Guild);
|
||||
enabledCommands.forEach(({ command, file }) => {
|
||||
console.log(
|
||||
`\u001b[32;1m`,
|
||||
`Slash Commands Registered for ${this.guilds.cache.first().name}`
|
||||
`${c.green}✓${c.reset} ${c.white}${command.name}${c.reset} ${c.gray}(${path.basename(file)})${c.reset}`,
|
||||
);
|
||||
this.commands.set(command.name, command);
|
||||
});
|
||||
|
||||
console.log(" ");
|
||||
console.log(`\u001b[34;1m`, "▬▬▬▬▬▬▬ Events ▬▬▬▬▬▬▬");
|
||||
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(`\u001b[31;1m`, `${event.event}`, `\u001b[32;1m`, "Loaded");
|
||||
console.log(
|
||||
`${c.green}✓${c.reset} ${c.white}${event.event}${c.reset} ${c.gray}(${file})${c.reset}`,
|
||||
);
|
||||
this.on(event.event, (...args) => event.run(this, ...args));
|
||||
});
|
||||
|
||||
console.log(`\u001b[34;1m`, "▬▬▬▬▬▬▬ Events ▬▬▬▬▬▬▬");
|
||||
if (process.argv[2] != "test") {
|
||||
this.login(token);
|
||||
console.log("");
|
||||
|
||||
if (process.argv[2] !== "test") {
|
||||
await this.login(token);
|
||||
} else {
|
||||
console.log(
|
||||
`\u001b[31;1m`,
|
||||
"RUNNING IN TEST MODE, IF NOT INTENDED, PLEASE USE npm start"
|
||||
);
|
||||
console.log(`${c.red}RUNNING IN TEST MODE, IF NOT INTENDED, PLEASE USE npm start${c.reset}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
// SupportBot | Emerald Services
|
||||
// Command Structure
|
||||
|
||||
const fs = require("fs");
|
||||
|
||||
const yaml = require("js-yaml");
|
||||
|
||||
class Command {
|
||||
constructor(options) {
|
||||
this.name = options.name;
|
||||
this.description = options.description;
|
||||
this.permissions = options.permissions;
|
||||
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,
|
||||
};
|
||||
@@ -1,5 +1,6 @@
|
||||
// SupportBot | Emerald Services
|
||||
// Event Structure
|
||||
|
||||
class Event {
|
||||
constructor(event, run) {
|
||||
this.event = 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,
|
||||
};
|
||||
+20
-21
@@ -1,7 +1,7 @@
|
||||
// SupportBot | Emerald Services
|
||||
// Ticket ID Structure
|
||||
|
||||
const fs = require("fs");
|
||||
const db = require("./Database");
|
||||
|
||||
class TicketNumberID {
|
||||
static async pad() {
|
||||
@@ -12,30 +12,29 @@ class TicketNumberID {
|
||||
}
|
||||
|
||||
static async get() {
|
||||
let ticketnumberID = await JSON.parse(
|
||||
fs.readFileSync("./Data/ticket-panel-id.json", "utf8")
|
||||
);
|
||||
if (!ticketnumberID.id) {
|
||||
return this.set(0);
|
||||
} else {
|
||||
return ticketnumberID.id;
|
||||
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) {
|
||||
let ticketnumberID = await JSON.parse(
|
||||
fs.readFileSync("./Data/ticket-panel-id.json", "utf8")
|
||||
);
|
||||
|
||||
ticketnumberID.id = value;
|
||||
fs.writeFileSync(
|
||||
"./Data/ticket-panel-id.json",
|
||||
JSON.stringify(ticketnumberID, null, 4),
|
||||
(err) => {
|
||||
if (err) console.error(err);
|
||||
}
|
||||
);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,293 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const Discord = require("discord.js");
|
||||
const yaml = require("js-yaml");
|
||||
const db = require("./Database.js");
|
||||
|
||||
const supportbot = require("./ConfigStore").supportbot;
|
||||
const msgconfig = require("./ConfigStore").messages;
|
||||
const { createTranscriptHTML } = require("./TranscriptTemplate.js");
|
||||
|
||||
function escapeHtml(str = "") {
|
||||
return String(str)
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """);
|
||||
}
|
||||
|
||||
function parseMarkdown(content = "") {
|
||||
let safe = escapeHtml(content);
|
||||
|
||||
safe = safe.replace(/```([\s\S]*?)```/g, "<pre><code>$1</code></pre>");
|
||||
safe = safe.replace(/`(.*?)`/g, "<code>$1</code>");
|
||||
safe = safe.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>");
|
||||
safe = safe.replace(/\*(.*?)\*/g, "<em>$1</em>");
|
||||
safe = safe.replace(/_(.*?)_/g, "<em>$1</em>");
|
||||
safe = safe.replace(
|
||||
/\[(.*?)\]\((.*?)\)/g,
|
||||
'<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>',
|
||||
);
|
||||
|
||||
return safe.replace(/\n/g, "<br>");
|
||||
}
|
||||
|
||||
function formatDate(iso) {
|
||||
try {
|
||||
return new Date(iso).toLocaleString("en-GB", {
|
||||
dateStyle: "medium",
|
||||
timeStyle: "short",
|
||||
});
|
||||
} catch {
|
||||
return iso;
|
||||
}
|
||||
}
|
||||
|
||||
function getFileTypeInfo(att) {
|
||||
const fileName = String(att.name || "attachment").toLowerCase();
|
||||
|
||||
if (/\.(png|jpg|jpeg|gif|webp|bmp|svg)$/i.test(fileName)) {
|
||||
return "image";
|
||||
}
|
||||
|
||||
if (/\.(mp4|webm|mov|m4v)$/i.test(fileName)) {
|
||||
return "video";
|
||||
}
|
||||
|
||||
if (/\.(mp3|wav|ogg|m4a|flac)$/i.test(fileName)) {
|
||||
return "audio";
|
||||
}
|
||||
|
||||
return "file";
|
||||
}
|
||||
|
||||
function renderAttachment(att) {
|
||||
const url = escapeHtml(att.url || "");
|
||||
const name = escapeHtml(att.name || "attachment");
|
||||
const type = getFileTypeInfo(att);
|
||||
|
||||
if (type === "image") {
|
||||
return `
|
||||
<div class="attachment">
|
||||
<a href="${url}" target="_blank" rel="noopener noreferrer" class="attachment-file">📎 ${name}</a>
|
||||
<div class="attachment-preview attachment-image">
|
||||
<img src="${url}" alt="${name}" loading="lazy">
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (type === "video") {
|
||||
return `
|
||||
<div class="attachment">
|
||||
<a href="${url}" target="_blank" rel="noopener noreferrer" class="attachment-file">📎 ${name}</a>
|
||||
<div class="attachment-preview">
|
||||
<video controls preload="metadata">
|
||||
<source src="${url}">
|
||||
</video>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (type === "audio") {
|
||||
return `
|
||||
<div class="attachment">
|
||||
<a href="${url}" target="_blank" rel="noopener noreferrer" class="attachment-file">📎 ${name}</a>
|
||||
<div class="attachment-preview">
|
||||
<audio controls preload="metadata">
|
||||
<source src="${url}">
|
||||
</audio>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="attachment">
|
||||
<a href="${url}" target="_blank" rel="noopener noreferrer" class="attachment-file">📎 ${name}</a>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderEmbeds(embeds = []) {
|
||||
return embeds
|
||||
.map((embed) => {
|
||||
const fields = (embed.fields || [])
|
||||
.map(
|
||||
(field) => `
|
||||
<div class="embed-field">
|
||||
<div class="embed-field-name">${escapeHtml(field.name || "Field")}</div>
|
||||
<div class="embed-field-value">${parseMarkdown(field.value || "")}</div>
|
||||
</div>
|
||||
`,
|
||||
)
|
||||
.join("");
|
||||
|
||||
return `
|
||||
<div class="discord-embed">
|
||||
${embed.title ? `<div class="discord-embed-title">${escapeHtml(embed.title)}</div>` : ""}
|
||||
${embed.description ? `<div class="discord-embed-description">${parseMarkdown(embed.description)}</div>` : ""}
|
||||
${fields}
|
||||
</div>
|
||||
`;
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
|
||||
async function createTranscript(interaction, ticketInput, reasonInput) {
|
||||
let ticketRecord = typeof ticketInput === "object" && ticketInput !== null ? ticketInput : null;
|
||||
let closeReason = typeof ticketInput === "string" ? ticketInput : reasonInput;
|
||||
|
||||
if (!ticketRecord && typeof db.getTicket === "function") {
|
||||
ticketRecord = db.getTicket(interaction.channel.id) || {};
|
||||
}
|
||||
|
||||
const { getChannel } = interaction.client;
|
||||
|
||||
const transcriptChannel = await getChannel(
|
||||
supportbot.Ticket.Log.TicketDataLog,
|
||||
interaction.guild,
|
||||
);
|
||||
|
||||
if (!transcriptChannel) {
|
||||
throw new Error("Transcript log channel missing or inaccessible.");
|
||||
}
|
||||
|
||||
let allMessages = [];
|
||||
let lastId = null;
|
||||
|
||||
while (true) {
|
||||
const options = { limit: 100 };
|
||||
if (lastId) options.before = lastId;
|
||||
|
||||
const messages = await interaction.channel.messages.fetch(options);
|
||||
if (messages.size === 0) break;
|
||||
|
||||
allMessages = [...allMessages, ...messages.values()];
|
||||
lastId = messages.last().id;
|
||||
|
||||
if (messages.size < 100) break;
|
||||
}
|
||||
|
||||
allMessages.sort((a, b) => a.createdTimestamp - b.createdTimestamp);
|
||||
|
||||
const transcriptData = allMessages.map((msg) => ({
|
||||
content: msg.content || "",
|
||||
username: msg.author.username,
|
||||
userId: msg.author.id,
|
||||
avatar: msg.author.displayAvatarURL({ extension: "png", size: 128 }),
|
||||
timestamp: msg.createdAt.toISOString(),
|
||||
attachments: Array.from(msg.attachments.values()).map((att) => ({
|
||||
url: att.url,
|
||||
name: att.name || "attachment",
|
||||
contentType: att.contentType || null,
|
||||
size: att.size || 0,
|
||||
})),
|
||||
embeds: msg.embeds.map((embed) => ({
|
||||
title: embed.title || "",
|
||||
description: embed.description || "",
|
||||
fields: embed.fields || [],
|
||||
})),
|
||||
}));
|
||||
|
||||
if (typeof db.updateTicketStatus === "function") {
|
||||
db.updateTicketStatus(interaction.channel.id, "closed");
|
||||
}
|
||||
|
||||
let ticketUserId = ticketRecord?.user_id || ticketRecord?.user || ticketRecord?.author_id;
|
||||
|
||||
if (!ticketUserId && interaction.channel.permissionOverwrites) {
|
||||
const memberOverwrite = interaction.channel.permissionOverwrites.cache.find(
|
||||
(pow) => pow.type === 1 && pow.id !== interaction.client.user.id
|
||||
);
|
||||
if (memberOverwrite) {
|
||||
ticketUserId = memberOverwrite.id;
|
||||
}
|
||||
}
|
||||
|
||||
if (!ticketUserId && allMessages.length > 0) {
|
||||
const firstNonBotMsg = allMessages.find((m) => !m.author.bot);
|
||||
if (firstNonBotMsg) {
|
||||
ticketUserId = firstNonBotMsg.author.id;
|
||||
}
|
||||
}
|
||||
|
||||
const tUser =
|
||||
(ticketUserId ? interaction.client.users.cache.get(ticketUserId) : null) ||
|
||||
(ticketUserId ? await interaction.client.users.fetch(ticketUserId).catch(() => null) : null);
|
||||
|
||||
const transcriptEmbed = new Discord.EmbedBuilder()
|
||||
.setTitle(msgconfig.TicketLog.Title)
|
||||
.setColor(msgconfig.TicketLog.Colour)
|
||||
.setFooter({
|
||||
text: supportbot.Embed.Footer,
|
||||
iconURL: interaction.user.displayAvatarURL(),
|
||||
})
|
||||
.setDescription(
|
||||
`> **Ticket:** ${interaction.channel.name} (\`${interaction.channel.id}\`)\n` +
|
||||
`> **User:** ${tUser ? `<@${tUser.id}> (\`${tUser.tag}\`)` : "Unknown User"}\n` +
|
||||
`> **Closed by:** <@${interaction.user.id}>\n` +
|
||||
`> **Message Count:** ${transcriptData.length}`,
|
||||
)
|
||||
.addFields({
|
||||
name: "Reason",
|
||||
value: `\`\`\`${closeReason || "No Reason Provided."}\`\`\``,
|
||||
inline: false,
|
||||
});
|
||||
|
||||
try {
|
||||
const configStore = require("./ConfigStore");
|
||||
const transcriptStore = require("./TranscriptStore");
|
||||
const apiConfig = configStore.api?.API;
|
||||
const tSettings = transcriptStore.getSettings();
|
||||
|
||||
if (apiConfig?.Enabled !== false) {
|
||||
const port = apiConfig?.Port || 3000;
|
||||
const isPublic = Boolean(tSettings?.publicAccess?.enabled);
|
||||
const transcriptUrl = `http://localhost:${port}/transcripts/${interaction.channel.id}`;
|
||||
|
||||
transcriptEmbed.addFields({
|
||||
name: isPublic ? "Public Web Transcript" : "Dashboard Transcript",
|
||||
value: `[${isPublic ? "Click here to view public transcript" : "Click here to view in Dashboard"}](${transcriptUrl})`,
|
||||
inline: false,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn("[TicketManager] Could not add web transcript URL:", err.message);
|
||||
}
|
||||
|
||||
const transcriptDir = path.join(process.cwd(), "./Data/Transcripts");
|
||||
if (!fs.existsSync(transcriptDir)) {
|
||||
fs.mkdirSync(transcriptDir, { recursive: true });
|
||||
}
|
||||
|
||||
const htmlFileName = `${interaction.channel.id}-transcript.html`;
|
||||
const htmlPath = path.join(transcriptDir, htmlFileName);
|
||||
|
||||
const html = createTranscriptHTML(
|
||||
{
|
||||
id: interaction.channel.id,
|
||||
name: interaction.channel.name,
|
||||
messages: transcriptData,
|
||||
},
|
||||
closeReason,
|
||||
);
|
||||
|
||||
fs.writeFileSync(htmlPath, html, "utf8");
|
||||
|
||||
await transcriptChannel.send({
|
||||
embeds: [transcriptEmbed],
|
||||
files: [
|
||||
new Discord.AttachmentBuilder(htmlPath, {
|
||||
name: `SPOILER_${htmlFileName}`,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
return { htmlPath };
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createTranscript,
|
||||
};
|
||||
@@ -0,0 +1,223 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const TRANSCRIPT_DIR = path.join(__dirname, '../Data/Transcripts');
|
||||
const SETTINGS_FILE = path.join(__dirname, '../Data/transcript-settings.json');
|
||||
|
||||
const DEFAULT_SETTINGS = {
|
||||
autoDelete: {
|
||||
enabled: false,
|
||||
afterDays: 30,
|
||||
},
|
||||
publicAccess: {
|
||||
enabled: false,
|
||||
},
|
||||
};
|
||||
|
||||
let purgeTimer = null;
|
||||
|
||||
function ensureTranscriptDir() {
|
||||
if (!fs.existsSync(TRANSCRIPT_DIR)) {
|
||||
fs.mkdirSync(TRANSCRIPT_DIR, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
function getSettings() {
|
||||
try {
|
||||
if (!fs.existsSync(SETTINGS_FILE)) {
|
||||
return JSON.parse(JSON.stringify(DEFAULT_SETTINGS));
|
||||
}
|
||||
const raw = JSON.parse(fs.readFileSync(SETTINGS_FILE, 'utf8'));
|
||||
const days = Number(raw?.autoDelete?.afterDays);
|
||||
return {
|
||||
autoDelete: {
|
||||
enabled: Boolean(raw?.autoDelete?.enabled),
|
||||
afterDays:
|
||||
Number.isFinite(days) && days >= 1
|
||||
? Math.min(Math.floor(days), 3650)
|
||||
: DEFAULT_SETTINGS.autoDelete.afterDays,
|
||||
},
|
||||
publicAccess: {
|
||||
enabled: Boolean(raw?.publicAccess?.enabled),
|
||||
},
|
||||
};
|
||||
} catch {
|
||||
return JSON.parse(JSON.stringify(DEFAULT_SETTINGS));
|
||||
}
|
||||
}
|
||||
|
||||
function saveSettings(patch) {
|
||||
const current = getSettings();
|
||||
const next = {
|
||||
autoDelete: {
|
||||
enabled:
|
||||
patch?.autoDelete?.enabled !== undefined
|
||||
? Boolean(patch.autoDelete.enabled)
|
||||
: current.autoDelete.enabled,
|
||||
afterDays:
|
||||
patch?.autoDelete?.afterDays !== undefined
|
||||
? Math.min(
|
||||
Math.max(Math.floor(Number(patch.autoDelete.afterDays) || 1), 1),
|
||||
3650,
|
||||
)
|
||||
: current.autoDelete.afterDays,
|
||||
},
|
||||
publicAccess: {
|
||||
enabled:
|
||||
patch?.publicAccess?.enabled !== undefined
|
||||
? Boolean(patch.publicAccess.enabled)
|
||||
: current.publicAccess.enabled,
|
||||
},
|
||||
};
|
||||
|
||||
const dir = path.dirname(SETTINGS_FILE);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
fs.writeFileSync(SETTINGS_FILE, JSON.stringify(next, null, 2), 'utf8');
|
||||
return next;
|
||||
}
|
||||
|
||||
function normalizeId(rawId) {
|
||||
return String(rawId).replace(/[^0-9]/g, '');
|
||||
}
|
||||
|
||||
function readTranscriptFile(rawId) {
|
||||
const id = normalizeId(rawId);
|
||||
if (!id) return null;
|
||||
ensureTranscriptDir();
|
||||
const filePath = path.join(TRANSCRIPT_DIR, `${id}-transcript.html`);
|
||||
if (!fs.existsSync(filePath)) return null;
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
const titleMatch = content.match(/<title>\s*Transcript\s*-\s*([^<]+)\s*<\/title>/i);
|
||||
return {
|
||||
id,
|
||||
filePath,
|
||||
filename: `${id}-transcript.html`,
|
||||
content,
|
||||
ticketName: titleMatch ? titleMatch[1].trim() : null,
|
||||
};
|
||||
}
|
||||
|
||||
function listTranscripts() {
|
||||
ensureTranscriptDir();
|
||||
if (!fs.existsSync(TRANSCRIPT_DIR)) {
|
||||
return [];
|
||||
}
|
||||
return fs
|
||||
.readdirSync(TRANSCRIPT_DIR)
|
||||
.filter((f) => f.endsWith('-transcript.html'))
|
||||
.map((f) => {
|
||||
const id = f.replace('-transcript.html', '');
|
||||
const stats = fs.statSync(path.join(TRANSCRIPT_DIR, f));
|
||||
const parsed = readTranscriptFile(id);
|
||||
return {
|
||||
id,
|
||||
filename: f,
|
||||
createdAt: stats.mtime.toISOString(),
|
||||
size: stats.size,
|
||||
ticketName: parsed?.ticketName ?? null,
|
||||
};
|
||||
})
|
||||
.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
|
||||
}
|
||||
|
||||
function deleteTranscript(rawId) {
|
||||
const id = normalizeId(rawId);
|
||||
if (!id) return false;
|
||||
const filePath = path.join(TRANSCRIPT_DIR, `${id}-transcript.html`);
|
||||
if (!fs.existsSync(filePath)) return false;
|
||||
fs.unlinkSync(filePath);
|
||||
return true;
|
||||
}
|
||||
|
||||
function purgeExpiredTranscripts() {
|
||||
const settings = getSettings();
|
||||
if (!settings.autoDelete.enabled) {
|
||||
return { deleted: 0, skipped: true };
|
||||
}
|
||||
|
||||
ensureTranscriptDir();
|
||||
const maxAgeMs = settings.autoDelete.afterDays * 24 * 60 * 60 * 1000;
|
||||
const cutoff = Date.now() - maxAgeMs;
|
||||
let deleted = 0;
|
||||
|
||||
for (const f of fs.readdirSync(TRANSCRIPT_DIR)) {
|
||||
if (!f.endsWith('-transcript.html')) continue;
|
||||
const filePath = path.join(TRANSCRIPT_DIR, f);
|
||||
const stats = fs.statSync(filePath);
|
||||
if (stats.mtime.getTime() < cutoff) {
|
||||
fs.unlinkSync(filePath);
|
||||
deleted += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return { deleted, skipped: false };
|
||||
}
|
||||
|
||||
function notFoundHtml(message = 'Transcript not found') {
|
||||
return `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Transcript</title>
|
||||
<style>body{font-family:system-ui,sans-serif;padding:2rem;background:#0b0f14;color:#e6edf3;margin:0}</style></head>
|
||||
<body><h1>${message}</h1></body></html>`;
|
||||
}
|
||||
|
||||
function servePublicTranscript(req, res) {
|
||||
const settings = getSettings();
|
||||
if (!settings.publicAccess.enabled) {
|
||||
return res.status(404).type('html').send(notFoundHtml('Transcript not found'));
|
||||
}
|
||||
|
||||
const transcript = readTranscriptFile(req.params.id);
|
||||
if (!transcript) {
|
||||
return res.status(404).type('html').send(notFoundHtml('Transcript not found'));
|
||||
}
|
||||
|
||||
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
||||
res.setHeader('Cache-Control', 'public, max-age=120');
|
||||
return res.send(transcript.content);
|
||||
}
|
||||
|
||||
function startAutoDeleteScheduler(intervalMs = 60 * 60 * 1000) {
|
||||
if (purgeTimer) return;
|
||||
|
||||
const run = () => {
|
||||
try {
|
||||
const result = purgeExpiredTranscripts();
|
||||
if (result.deleted > 0) {
|
||||
console.log(
|
||||
`[Transcripts] Auto-deleted ${result.deleted} transcript(s) older than ${getSettings().autoDelete.afterDays} day(s)`,
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[Transcripts] Auto-delete failed:', err.message);
|
||||
}
|
||||
};
|
||||
|
||||
run();
|
||||
purgeTimer = setInterval(run, intervalMs);
|
||||
if (typeof purgeTimer.unref === 'function') {
|
||||
purgeTimer.unref();
|
||||
}
|
||||
}
|
||||
|
||||
function getPublicUrl(req, id) {
|
||||
const settings = getSettings();
|
||||
if (!settings.publicAccess.enabled) return null;
|
||||
const host = req.get('host');
|
||||
const proto = req.get('x-forwarded-proto') || req.protocol || 'http';
|
||||
return `${proto}://${host}/transcripts/${id}`;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
TRANSCRIPT_DIR,
|
||||
DEFAULT_SETTINGS,
|
||||
getSettings,
|
||||
saveSettings,
|
||||
readTranscriptFile,
|
||||
listTranscripts,
|
||||
deleteTranscript,
|
||||
purgeExpiredTranscripts,
|
||||
servePublicTranscript,
|
||||
startAutoDeleteScheduler,
|
||||
getPublicUrl,
|
||||
};
|
||||
@@ -0,0 +1,721 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const yaml = require("js-yaml");
|
||||
const {
|
||||
normalizeFontFamily,
|
||||
resolveTypography,
|
||||
} = require("./googleFonts.js");
|
||||
|
||||
const TEMPLATE_PATH = path.join(__dirname, "../Configs/transcript-template.yml");
|
||||
|
||||
let cachedTemplate = null;
|
||||
|
||||
function escapeHtml(str = "") {
|
||||
return String(str)
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """);
|
||||
}
|
||||
|
||||
function parseMarkdown(content = "") {
|
||||
let safe = escapeHtml(content);
|
||||
|
||||
safe = safe.replace(/```([\s\S]*?)```/g, "<pre><code>$1</code></pre>");
|
||||
safe = safe.replace(/`(.*?)`/g, "<code>$1</code>");
|
||||
safe = safe.replace(/\|\|(.*?)\|\|/g, '<span class="spoiler">$1</span>');
|
||||
safe = safe.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>");
|
||||
safe = safe.replace(/\*(.*?)\*/g, "<em>$1</em>");
|
||||
safe = safe.replace(/_(.*?)_/g, "<em>$1</em>");
|
||||
safe = safe.replace(/~~(.*?)~~/g, "<del>$1</del>");
|
||||
safe = safe.replace(/__(.*?)__/g, "<u>$1</u>");
|
||||
safe = safe.replace(/<@!?(\d+)>/g, '<span class="mention">@User</span>');
|
||||
safe = safe.replace(/<@&(\d+)>/g, '<span class="mention">@Role</span>');
|
||||
safe = safe.replace(
|
||||
/\[(.*?)\]\((.*?)\)/g,
|
||||
'<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>',
|
||||
);
|
||||
|
||||
return safe.replace(/\n/g, "<br>");
|
||||
}
|
||||
|
||||
function formatDate(iso) {
|
||||
try {
|
||||
return new Date(iso).toLocaleString("en-GB", {
|
||||
dateStyle: "medium",
|
||||
timeStyle: "short",
|
||||
});
|
||||
} catch {
|
||||
return iso;
|
||||
}
|
||||
}
|
||||
|
||||
function getFileTypeInfo(att) {
|
||||
const fileName = String(att.name || "attachment").toLowerCase();
|
||||
|
||||
if (/\.(png|jpg|jpeg|gif|webp|bmp|svg)$/i.test(fileName)) {
|
||||
return "image";
|
||||
}
|
||||
if (/\.(mp4|webm|mov|m4v)$/i.test(fileName)) {
|
||||
return "video";
|
||||
}
|
||||
if (/\.(mp3|wav|ogg|m4a|flac)$/i.test(fileName)) {
|
||||
return "audio";
|
||||
}
|
||||
return "file";
|
||||
}
|
||||
|
||||
function renderAttachment(att) {
|
||||
const url = escapeHtml(att.url || "");
|
||||
const name = escapeHtml(att.name || "attachment");
|
||||
const type = getFileTypeInfo(att);
|
||||
|
||||
if (type === "image") {
|
||||
return `
|
||||
<div class="attachment">
|
||||
<a href="${url}" target="_blank" rel="noopener noreferrer" class="attachment-file">📎 ${name}</a>
|
||||
<div class="attachment-preview attachment-image">
|
||||
<img src="${url}" alt="${name}" loading="lazy">
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (type === "video") {
|
||||
return `
|
||||
<div class="attachment">
|
||||
<a href="${url}" target="_blank" rel="noopener noreferrer" class="attachment-file">📎 ${name}</a>
|
||||
<div class="attachment-preview">
|
||||
<video controls preload="metadata">
|
||||
<source src="${url}">
|
||||
</video>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (type === "audio") {
|
||||
return `
|
||||
<div class="attachment">
|
||||
<a href="${url}" target="_blank" rel="noopener noreferrer" class="attachment-file">📎 ${name}</a>
|
||||
<div class="attachment-preview">
|
||||
<audio controls preload="metadata">
|
||||
<source src="${url}">
|
||||
</audio>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="attachment">
|
||||
<a href="${url}" target="_blank" rel="noopener noreferrer" class="attachment-file">📎 ${name}</a>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderEmbeds(embeds = []) {
|
||||
return embeds
|
||||
.map((embed) => {
|
||||
const color = embed.color || "#5865F2";
|
||||
const fields = (embed.fields || [])
|
||||
.map(
|
||||
(field) => `
|
||||
<div class="embed-field ${field.inline ? 'inline-field' : ''}">
|
||||
<div class="embed-field-name">${escapeHtml(field.name || "Field")}</div>
|
||||
<div class="embed-field-value">${parseMarkdown(field.value || "")}</div>
|
||||
</div>
|
||||
`,
|
||||
)
|
||||
.join("");
|
||||
|
||||
const author = embed.author ? `
|
||||
<div class="embed-author">
|
||||
${embed.author.iconURL ? `<img src="${escapeHtml(embed.author.iconURL)}" class="embed-author-icon" />` : ''}
|
||||
<span>${escapeHtml(embed.author.name || '')}</span>
|
||||
</div>
|
||||
` : '';
|
||||
|
||||
const thumbnail = embed.thumbnail?.url ? `
|
||||
<img src="${escapeHtml(embed.thumbnail.url)}" class="embed-thumbnail" />
|
||||
` : '';
|
||||
|
||||
const image = embed.image?.url ? `
|
||||
<div class="embed-image"><img src="${escapeHtml(embed.image.url)}" /></div>
|
||||
` : '';
|
||||
|
||||
const footer = (embed.footer || embed.timestamp) ? `
|
||||
<div class="embed-footer">
|
||||
${embed.footer?.iconURL ? `<img src="${escapeHtml(embed.footer.iconURL)}" class="embed-footer-icon" />` : ''}
|
||||
${embed.footer?.text ? `<span>${escapeHtml(embed.footer.text)}</span>` : ''}
|
||||
</div>
|
||||
` : '';
|
||||
|
||||
return `
|
||||
<div class="discord-embed" style="border-left-color: ${escapeHtml(color)}; border-left-width: 4px; border-left-style: solid; position: relative;">
|
||||
${author}
|
||||
${thumbnail}
|
||||
${embed.title ? `<div class="discord-embed-title">${parseMarkdown(embed.title)}</div>` : ""}
|
||||
${embed.description ? `<div class="discord-embed-description">${parseMarkdown(embed.description)}</div>` : ""}
|
||||
${fields ? `<div class="embed-fields-grid">${fields}</div>` : ""}
|
||||
${image}
|
||||
${footer}
|
||||
</div>
|
||||
`;
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
|
||||
function deepMerge(base, patch) {
|
||||
const out = structuredClone(base);
|
||||
for (const [key, val] of Object.entries(patch || {})) {
|
||||
if (
|
||||
val &&
|
||||
typeof val === "object" &&
|
||||
!Array.isArray(val) &&
|
||||
out[key] &&
|
||||
typeof out[key] === "object" &&
|
||||
!Array.isArray(out[key])
|
||||
) {
|
||||
out[key] = deepMerge(out[key], val);
|
||||
} else {
|
||||
out[key] = val;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function getDefaultTemplate() {
|
||||
return {
|
||||
Theme: {
|
||||
Background: "#0b0f14",
|
||||
Panel: "#111821",
|
||||
PanelSecondary: "#182230",
|
||||
PanelTertiary: "#1d2938",
|
||||
Border: "#283548",
|
||||
Text: "#e6edf3",
|
||||
Muted: "#9fb0c3",
|
||||
Accent: "#22c55e",
|
||||
AccentSoft: "rgba(34, 197, 94, 0.12)",
|
||||
AccentBorder: "rgba(34, 197, 94, 0.25)",
|
||||
AccentText: "#8df0ad",
|
||||
Link: "#8fd9ff",
|
||||
AttachmentLink: "#8fd9a8",
|
||||
CodeBackground: "#0f1720",
|
||||
},
|
||||
Typography: {
|
||||
FontFamily: "Inter",
|
||||
},
|
||||
Header: {
|
||||
Title: "Ticket Transcript",
|
||||
Subtitle:
|
||||
"Exported conversation log for support ticket **{{ticketName}}**",
|
||||
Badge: "Closed Transcript",
|
||||
ShowMetaCards: true,
|
||||
},
|
||||
Meta: {
|
||||
ChannelLabel: "Channel",
|
||||
TicketIdLabel: "Ticket ID",
|
||||
MessageCountLabel: "Message Count",
|
||||
CloseReasonLabel: "Close Reason",
|
||||
DefaultCloseReason: "No Reason Provided.",
|
||||
},
|
||||
Messages: {
|
||||
SectionTitle: "Messages",
|
||||
EmptyState: "No messages were found in this ticket.",
|
||||
},
|
||||
Advanced: {
|
||||
CustomCss: "",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeTemplate(raw) {
|
||||
const merged = deepMerge(getDefaultTemplate(), raw || {});
|
||||
if (merged.Typography) {
|
||||
merged.Typography.FontFamily = normalizeFontFamily(
|
||||
merged.Typography.FontFamily,
|
||||
);
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
function loadTemplateFromDisk() {
|
||||
try {
|
||||
if (!fs.existsSync(TEMPLATE_PATH)) {
|
||||
const defaults = getDefaultTemplate();
|
||||
fs.mkdirSync(path.dirname(TEMPLATE_PATH), { recursive: true });
|
||||
fs.writeFileSync(TEMPLATE_PATH, yaml.dump(defaults), "utf8");
|
||||
return defaults;
|
||||
}
|
||||
const parsed = yaml.load(fs.readFileSync(TEMPLATE_PATH, "utf8"));
|
||||
return normalizeTemplate(parsed);
|
||||
} catch (err) {
|
||||
console.error("[TranscriptTemplate] Failed to load template:", err.message);
|
||||
return getDefaultTemplate();
|
||||
}
|
||||
}
|
||||
|
||||
function getTemplate() {
|
||||
if (!cachedTemplate) {
|
||||
cachedTemplate = loadTemplateFromDisk();
|
||||
}
|
||||
return structuredClone(cachedTemplate);
|
||||
}
|
||||
|
||||
function reloadTemplate() {
|
||||
cachedTemplate = loadTemplateFromDisk();
|
||||
return getTemplate();
|
||||
}
|
||||
|
||||
function saveTemplate(data) {
|
||||
const merged = normalizeTemplate(data);
|
||||
fs.mkdirSync(path.dirname(TEMPLATE_PATH), { recursive: true });
|
||||
fs.writeFileSync(TEMPLATE_PATH, yaml.dump(merged), "utf8");
|
||||
cachedTemplate = merged;
|
||||
return merged;
|
||||
}
|
||||
|
||||
function applyPlaceholders(text, ctx) {
|
||||
return String(text || "")
|
||||
.replace(/\{\{ticketName\}\}/g, ctx.name || "")
|
||||
.replace(/\{\{ticketId\}\}/g, ctx.id || "")
|
||||
.replace(/\{\{messageCount\}\}/g, String(ctx.messageCount ?? 0))
|
||||
.replace(/\{\{reason\}\}/g, ctx.reason || "");
|
||||
}
|
||||
|
||||
function formatHeaderText(text, ctx) {
|
||||
const withPlaceholders = applyPlaceholders(text, ctx);
|
||||
const escaped = escapeHtml(withPlaceholders);
|
||||
return escaped.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>");
|
||||
}
|
||||
|
||||
function buildStyles(template) {
|
||||
const t = template.Theme;
|
||||
const { fontFamilyCss } = resolveTypography(template.Typography?.FontFamily);
|
||||
const custom = String(template.Advanced?.CustomCss || "").trim();
|
||||
|
||||
return `
|
||||
:root {
|
||||
--bg: ${t.Background};
|
||||
--panel: ${t.Panel};
|
||||
--panel-2: ${t.PanelSecondary};
|
||||
--panel-3: ${t.PanelTertiary};
|
||||
--border: ${t.Border};
|
||||
--text: ${t.Text};
|
||||
--muted: ${t.Muted};
|
||||
--accent: ${t.Accent};
|
||||
--accent-soft: ${t.AccentSoft};
|
||||
--accent-border: ${t.AccentBorder};
|
||||
--accent-text: ${t.AccentText};
|
||||
--link: ${t.Link};
|
||||
--attachment-link: ${t.AttachmentLink};
|
||||
--code: ${t.CodeBackground};
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-family: ${fontFamilyCss};
|
||||
}
|
||||
|
||||
body { padding: 32px 20px; }
|
||||
|
||||
.page { max-width: 1100px; margin: 0 auto; }
|
||||
|
||||
.header {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 16px;
|
||||
padding: 24px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.header-top {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.title-wrap h1 {
|
||||
margin: 0;
|
||||
font-size: 1.8rem;
|
||||
line-height: 1.2;
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.title-wrap p {
|
||||
margin: 8px 0 0;
|
||||
color: var(--muted);
|
||||
font-size: 0.98rem;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background: var(--accent-soft);
|
||||
color: var(--accent-text);
|
||||
border: 1px solid var(--accent-border);
|
||||
padding: 8px 12px;
|
||||
border-radius: 999px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.meta-grid {
|
||||
margin-top: 18px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.meta-card {
|
||||
background: var(--panel-2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.meta-label {
|
||||
color: var(--muted);
|
||||
font-size: 0.82rem;
|
||||
margin-bottom: 6px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.meta-value {
|
||||
font-size: 0.98rem;
|
||||
font-weight: 600;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.messages-wrap {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.messages-header {
|
||||
padding: 18px 20px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: var(--panel-2);
|
||||
font-weight: 800;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.message {
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
padding: 18px 20px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.message:last-child { border-bottom: none; }
|
||||
|
||||
.avatar {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
min-width: 42px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
background: var(--panel-3);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.message-body { min-width: 0; flex: 1; }
|
||||
|
||||
.message-header {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.username {
|
||||
font-weight: 800;
|
||||
color: var(--text);
|
||||
font-size: 0.98rem;
|
||||
}
|
||||
|
||||
.userid {
|
||||
color: var(--muted);
|
||||
font-size: 0.8rem;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
}
|
||||
|
||||
.timestamp {
|
||||
color: var(--muted);
|
||||
font-size: 0.82rem;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.content {
|
||||
color: var(--text);
|
||||
font-size: 0.96rem;
|
||||
line-height: 1.6;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.content:empty { display: none; }
|
||||
|
||||
.discord-embed {
|
||||
margin-top: 12px;
|
||||
background: var(--panel-2);
|
||||
border: 1px solid var(--border);
|
||||
border-left: 4px solid var(--accent);
|
||||
border-radius: 10px;
|
||||
padding: 12px 14px;
|
||||
}
|
||||
|
||||
.discord-embed-title { font-weight: 800; margin-bottom: 6px; }
|
||||
.discord-embed-description { color: var(--text); line-height: 1.5; }
|
||||
|
||||
.embed-author { display: flex; align-items: center; gap: 8px; font-weight: 700; font-size: 0.9em; margin-bottom: 6px; }
|
||||
.embed-author-icon { width: 18px; height: 18px; border-radius: 50%; }
|
||||
.embed-fields-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 10px; margin-top: 10px; padding-top: 10px; border-top: 1px solid rgba(255,255,255,0.06); }
|
||||
.embed-thumbnail { position: absolute; top: 12px; right: 12px; width: 44px; height: 44px; border-radius: 6px; object-fit: cover; }
|
||||
.embed-image img { width: 100%; max-height: 300px; object-fit: cover; border-radius: 8px; margin-top: 10px; }
|
||||
.embed-footer { display: flex; align-items: center; gap: 6px; font-size: 0.8em; color: var(--muted); margin-top: 8px; padding-top: 6px; border-top: 1px solid rgba(255,255,255,0.04); }
|
||||
.embed-footer-icon { width: 14px; height: 14px; border-radius: 50%; }
|
||||
.mention { background: rgba(88, 101, 242, 0.2); color: #5865F2; padding: 2px 6px; border-radius: 4px; font-weight: 600; font-size: 0.9em; }
|
||||
.spoiler { background: #202225; color: transparent; border-radius: 4px; padding: 0 4px; cursor: pointer; }
|
||||
.spoiler:hover { color: inherit; background: rgba(255,255,255,0.1); }
|
||||
|
||||
.embed-field {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.embed-field-name { font-weight: 700; margin-bottom: 4px; font-size: 0.85em; text-transform: uppercase; color: var(--muted); }
|
||||
.embed-field-value { color: var(--text); line-height: 1.5; }
|
||||
|
||||
.attachment {
|
||||
margin-top: 12px;
|
||||
background: var(--panel-2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.attachment-file {
|
||||
display: inline-block;
|
||||
color: var(--attachment-link);
|
||||
text-decoration: none;
|
||||
font-weight: 700;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.attachment-file:hover { text-decoration: underline; }
|
||||
|
||||
.attachment-preview { margin-top: 10px; }
|
||||
|
||||
.attachment-preview img,
|
||||
.attachment-preview video {
|
||||
width: 100%;
|
||||
max-width: 560px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--border);
|
||||
display: block;
|
||||
background: #0a0f15;
|
||||
}
|
||||
|
||||
.attachment-preview audio {
|
||||
width: 100%;
|
||||
max-width: 560px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
code {
|
||||
background: var(--code);
|
||||
border: 1px solid var(--border);
|
||||
padding: 2px 6px;
|
||||
border-radius: 6px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: var(--code);
|
||||
border: 1px solid var(--border);
|
||||
padding: 12px;
|
||||
border-radius: 10px;
|
||||
overflow-x: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
pre code { background: transparent; border: none; padding: 0; }
|
||||
|
||||
a { color: var(--link); }
|
||||
|
||||
.empty-state { padding: 24px 20px; color: var(--muted); }
|
||||
|
||||
@media (max-width: 700px) {
|
||||
body { padding: 18px 12px; }
|
||||
.header, .messages-wrap { border-radius: 12px; }
|
||||
.message { padding: 14px; }
|
||||
.timestamp { width: 100%; margin-left: 0; }
|
||||
}
|
||||
|
||||
${custom}
|
||||
`;
|
||||
}
|
||||
|
||||
function getSampleTicket() {
|
||||
return {
|
||||
id: "123456789012345678",
|
||||
name: "ticket-support-demo",
|
||||
messages: [
|
||||
{
|
||||
username: "SupportBot",
|
||||
userId: "100000000000000001",
|
||||
avatar: "https://cdn.discordapp.com/embed/avatars/0.png",
|
||||
timestamp: new Date().toISOString(),
|
||||
content: "Hello! Thanks for opening a ticket. How can we help?",
|
||||
embeds: [],
|
||||
attachments: [],
|
||||
},
|
||||
{
|
||||
username: "Member",
|
||||
userId: "100000000000000002",
|
||||
avatar: "https://cdn.discordapp.com/embed/avatars/1.png",
|
||||
timestamp: new Date(Date.now() - 60000).toISOString(),
|
||||
content: "I need help with my purchase — order **#48291**.",
|
||||
embeds: [
|
||||
{
|
||||
title: "Order details",
|
||||
description: "Payment received · awaiting delivery",
|
||||
fields: [{ name: "Status", value: "Pending" }],
|
||||
},
|
||||
],
|
||||
attachments: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function createTranscriptHTML(ticket, reason, templateInput) {
|
||||
const template = templateInput
|
||||
? normalizeTemplate(templateInput)
|
||||
: getTemplate();
|
||||
const header = template.Header;
|
||||
const meta = template.Meta;
|
||||
const messagesCfg = template.Messages;
|
||||
const closeReason = reason || meta.DefaultCloseReason;
|
||||
const ctx = {
|
||||
name: ticket.name,
|
||||
id: ticket.id,
|
||||
messageCount: ticket.messages.length,
|
||||
reason: closeReason,
|
||||
};
|
||||
|
||||
const { headLinks } = resolveTypography(template.Typography?.FontFamily);
|
||||
|
||||
const metaSection = header.ShowMetaCards
|
||||
? `
|
||||
<div class="meta-grid">
|
||||
<div class="meta-card">
|
||||
<div class="meta-label">${escapeHtml(meta.ChannelLabel)}</div>
|
||||
<div class="meta-value">${escapeHtml(ticket.name)}</div>
|
||||
</div>
|
||||
<div class="meta-card">
|
||||
<div class="meta-label">${escapeHtml(meta.TicketIdLabel)}</div>
|
||||
<div class="meta-value">${escapeHtml(ticket.id)}</div>
|
||||
</div>
|
||||
<div class="meta-card">
|
||||
<div class="meta-label">${escapeHtml(meta.MessageCountLabel)}</div>
|
||||
<div class="meta-value">${ticket.messages.length}</div>
|
||||
</div>
|
||||
<div class="meta-card">
|
||||
<div class="meta-label">${escapeHtml(meta.CloseReasonLabel)}</div>
|
||||
<div class="meta-value">${escapeHtml(closeReason)}</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: "";
|
||||
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Transcript - ${escapeHtml(ticket.name)}</title>
|
||||
${headLinks}
|
||||
<style>${buildStyles(template)}</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<section class="header">
|
||||
<div class="header-top">
|
||||
<div class="title-wrap">
|
||||
<h1>${escapeHtml(header.Title)}</h1>
|
||||
<p>${formatHeaderText(header.Subtitle, ctx)}</p>
|
||||
</div>
|
||||
<div class="badge">${escapeHtml(header.Badge)}</div>
|
||||
</div>
|
||||
${metaSection}
|
||||
</section>
|
||||
|
||||
<section class="messages-wrap">
|
||||
<div class="messages-header">${escapeHtml(messagesCfg.SectionTitle)}</div>
|
||||
${
|
||||
ticket.messages.length
|
||||
? ticket.messages
|
||||
.map(
|
||||
(msg) => `
|
||||
<article class="message">
|
||||
<img src="${escapeHtml(msg.avatar || "")}" alt="Avatar" class="avatar">
|
||||
<div class="message-body">
|
||||
<div class="message-header">
|
||||
<span class="username">${escapeHtml(msg.username || "Unknown User")}</span>
|
||||
<span class="userid">${escapeHtml(msg.userId || "Unknown ID")}</span>
|
||||
<span class="timestamp">${formatDate(msg.timestamp)}</span>
|
||||
</div>
|
||||
<div class="content">${parseMarkdown(msg.content || "")}</div>
|
||||
${renderEmbeds(msg.embeds || [])}
|
||||
${(msg.attachments || []).map(renderAttachment).join("")}
|
||||
</div>
|
||||
</article>
|
||||
`,
|
||||
)
|
||||
.join("")
|
||||
: `<div class="empty-state">${escapeHtml(messagesCfg.EmptyState)}</div>`
|
||||
}
|
||||
</section>
|
||||
</div>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
function renderPreview(templateInput) {
|
||||
const sample = getSampleTicket();
|
||||
return createTranscriptHTML(
|
||||
sample,
|
||||
"Resolved — thank you for contacting support.",
|
||||
templateInput,
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
TEMPLATE_PATH,
|
||||
getDefaultTemplate,
|
||||
getTemplate,
|
||||
reloadTemplate,
|
||||
saveTemplate,
|
||||
normalizeTemplate,
|
||||
getSampleTicket,
|
||||
createTranscriptHTML,
|
||||
renderPreview,
|
||||
};
|
||||
@@ -0,0 +1,73 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const REPOSITORIES = [
|
||||
{ id: "bot", name: "SupportBot", repo: "Emerald-Services/SupportBot", branch: "release" },
|
||||
{ id: "dashboard", name: "SupportBot Dashboard", repo: "Emerald-Services/SupportBot-Dashboard", branch: "release" }
|
||||
];
|
||||
|
||||
function compareVersions(a, b) {
|
||||
const pa = String(a)
|
||||
.replace(/^v/i, "")
|
||||
.split(".")
|
||||
.map((n) => parseInt(n, 10) || 0);
|
||||
const pb = String(b)
|
||||
.replace(/^v/i, "")
|
||||
.split(".")
|
||||
.map((n) => parseInt(n, 10) || 0);
|
||||
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
||||
if ((pa[i] || 0) > (pb[i] || 0)) return 1;
|
||||
if ((pa[i] || 0) < (pb[i] || 0)) return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
async function fetchUpdateInfo() {
|
||||
const axios = require("axios");
|
||||
const pkgPath = path.join(__dirname, "../package.json");
|
||||
const currentPkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
||||
const currentVersion = currentPkg.version;
|
||||
|
||||
const checks = [];
|
||||
|
||||
for (const target of REPOSITORIES) {
|
||||
let installedVersion = target.id === "bot" ? currentPkg.version : (currentPkg.dashboardVersion || "1.0.0");
|
||||
let latest = installedVersion;
|
||||
try {
|
||||
const pkgUrl = target.id === 'dashboard'
|
||||
? `https://raw.githubusercontent.com/${target.repo}/${target.branch}/dashboard/package.json`
|
||||
: `https://raw.githubusercontent.com/${target.repo}/${target.branch}/package.json`;
|
||||
|
||||
const remoteRes = await axios.get(pkgUrl, {
|
||||
timeout: 15000,
|
||||
headers: { "User-Agent": "SupportBot-Updater" },
|
||||
});
|
||||
if (remoteRes.data && remoteRes.data.version) {
|
||||
latest = remoteRes.data.version;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(`[Updater] Failed to check ${target.repo} for updates.`);
|
||||
}
|
||||
|
||||
const updateAvailable = compareVersions(latest, installedVersion) > 0;
|
||||
|
||||
checks.push({
|
||||
id: target.id,
|
||||
name: target.name,
|
||||
current: installedVersion,
|
||||
latest,
|
||||
updateAvailable,
|
||||
repository: `https://github.com/${target.repo}`,
|
||||
branch: target.branch,
|
||||
zipUrl: `https://github.com/${target.repo}/archive/refs/heads/${target.branch}.zip`,
|
||||
});
|
||||
}
|
||||
|
||||
return checks;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
compareVersions,
|
||||
fetchUpdateInfo,
|
||||
REPOSITORIES,
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
function withTimeout(promise, ms, message) {
|
||||
let timer;
|
||||
const timeout = new Promise((_, reject) => {
|
||||
timer = setTimeout(() => {
|
||||
reject(new Error(message || `Timed out after ${ms}ms`));
|
||||
}, ms);
|
||||
});
|
||||
|
||||
return Promise.race([promise, timeout]).finally(() => {
|
||||
clearTimeout(timer);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { withTimeout };
|
||||
@@ -0,0 +1,84 @@
|
||||
/** Curated Google Fonts for transcript HTML (keep in sync with dashboard/src/lib/google-fonts.ts). */
|
||||
|
||||
const SYSTEM_FALLBACK =
|
||||
'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif';
|
||||
|
||||
const GOOGLE_FONTS = [
|
||||
{ id: "Inter", label: "Inter" },
|
||||
{ id: "Roboto", label: "Roboto" },
|
||||
{ id: "Open Sans", label: "Open Sans" },
|
||||
{ id: "Lato", label: "Lato" },
|
||||
{ id: "Montserrat", label: "Montserrat" },
|
||||
{ id: "Poppins", label: "Poppins" },
|
||||
{ id: "Nunito", label: "Nunito" },
|
||||
{ id: "Raleway", label: "Raleway" },
|
||||
{ id: "Work Sans", label: "Work Sans" },
|
||||
{ id: "Source Sans 3", label: "Source Sans 3" },
|
||||
{ id: "DM Sans", label: "DM Sans" },
|
||||
{ id: "Manrope", label: "Manrope" },
|
||||
{ id: "Outfit", label: "Outfit" },
|
||||
{ id: "Rubik", label: "Rubik" },
|
||||
{ id: "Ubuntu", label: "Ubuntu" },
|
||||
{ id: "Fira Sans", label: "Fira Sans" },
|
||||
{ id: "PT Sans", label: "PT Sans" },
|
||||
{ id: "Noto Sans", label: "Noto Sans" },
|
||||
{ id: "Oswald", label: "Oswald" },
|
||||
{ id: "Merriweather", label: "Merriweather" },
|
||||
{ id: "Playfair Display", label: "Playfair Display" },
|
||||
{ id: "Libre Baskerville", label: "Libre Baskerville" },
|
||||
{ id: "JetBrains Mono", label: "JetBrains Mono" },
|
||||
{ id: "Fira Code", label: "Fira Code" },
|
||||
];
|
||||
|
||||
const GOOGLE_FONT_IDS = new Set(GOOGLE_FONTS.map((f) => f.id));
|
||||
|
||||
function normalizeFontFamily(fontFamily) {
|
||||
if (!fontFamily || typeof fontFamily !== "string") return "Inter";
|
||||
const trimmed = fontFamily.trim();
|
||||
if (trimmed === "system") return "system";
|
||||
if (!trimmed.includes(",")) {
|
||||
return GOOGLE_FONT_IDS.has(trimmed) ? trimmed : "Inter";
|
||||
}
|
||||
const match = trimmed.match(/^["']?([^"',]+)["']?/);
|
||||
const first = match?.[1]?.trim();
|
||||
if (first && GOOGLE_FONT_IDS.has(first)) return first;
|
||||
return "Inter";
|
||||
}
|
||||
|
||||
function googleFontsCssUrl(familyIds) {
|
||||
const families = familyIds
|
||||
.map((id) => {
|
||||
const encoded = encodeURIComponent(id).replace(/%20/g, "+");
|
||||
return `family=${encoded}:wght@400;600;700;800`;
|
||||
})
|
||||
.join("&");
|
||||
return `https://fonts.googleapis.com/css2?${families}&display=swap`;
|
||||
}
|
||||
|
||||
function resolveTypography(fontFamily) {
|
||||
const id = normalizeFontFamily(fontFamily);
|
||||
|
||||
if (id === "system") {
|
||||
return {
|
||||
fontId: "system",
|
||||
fontFamilyCss: SYSTEM_FALLBACK,
|
||||
headLinks: "",
|
||||
};
|
||||
}
|
||||
|
||||
const url = googleFontsCssUrl([id]);
|
||||
return {
|
||||
fontId: id,
|
||||
fontFamilyCss: `"${id}", ${SYSTEM_FALLBACK}`,
|
||||
headLinks: `<link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link href="${url}" rel="stylesheet">`,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
GOOGLE_FONTS,
|
||||
GOOGLE_FONT_IDS,
|
||||
SYSTEM_FALLBACK,
|
||||
normalizeFontFamily,
|
||||
googleFontsCssUrl,
|
||||
resolveTypography,
|
||||
};
|
||||
@@ -8,17 +8,94 @@
|
||||
// Installed with MIT License
|
||||
//
|
||||
// Discord Support: https://emeraldsrv.dev/discord
|
||||
// Community Resources: https://emeraldsrv.dev/resources
|
||||
// Community Resources: https://community.emeraldsrv.dev
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const Discord = require("discord.js");
|
||||
const yaml = require("js-yaml");
|
||||
const supportbot = yaml.load(
|
||||
fs.readFileSync("./Configs/supportbot.yml", "utf8")
|
||||
);
|
||||
const configStore = require("./Structures/ConfigStore.js");
|
||||
|
||||
const Client = require("./Structures/Client.js");
|
||||
const client = new Client({});
|
||||
const client = new Client({
|
||||
intents: ["Guilds", "GuildMembers", "GuildMessages", "MessageContent"],
|
||||
});
|
||||
|
||||
client.start(supportbot.Token);
|
||||
const { isSetupComplete } = require("./Structures/DashboardSetup.js");
|
||||
const APIServer = require("./API/server.js");
|
||||
|
||||
const api = new APIServer(client);
|
||||
client.apiServer = api;
|
||||
|
||||
const port = api.config?.Port || 25575;
|
||||
if (api.config?.Enabled) {
|
||||
api.start(port);
|
||||
if (!isSetupComplete()) {
|
||||
console.log(`\n\x1b[33m\x1b[1m[Setup Required] Go to http://localhost:${port}/setup to set up your bot and dashboard.\x1b[0m\n`);
|
||||
}
|
||||
} else {
|
||||
console.warn("[Dashboard] API disabled in Configs/api.yml");
|
||||
}
|
||||
|
||||
const token = configStore.supportbot?.General?.Token;
|
||||
const isPlaceholderToken = !token || token.includes("BOT_TOKEN") || token.includes("CHANGE_ME") || token.length < 30;
|
||||
|
||||
if (isPlaceholderToken) {
|
||||
console.warn(`\x1b[33m[SupportBot] Bot token is not configured yet. Complete setup at http://localhost:${port}/setup\x1b[0m`);
|
||||
} else {
|
||||
client.start(token);
|
||||
}
|
||||
|
||||
// SupportBot - New Logging System
|
||||
|
||||
const logTypes = ["Output", "Warn", "Error"];
|
||||
|
||||
if (!fs.existsSync("./Logs")) {
|
||||
fs.mkdirSync("./Logs");
|
||||
}
|
||||
logTypes.forEach((type) => {
|
||||
const dir = `./Logs/${type}`;
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir);
|
||||
}
|
||||
});
|
||||
|
||||
function logToFile(type, data) {
|
||||
const date = new Date().toISOString().split("T")[0];
|
||||
const file = path.join(`./Logs/${type}`, `${type}-${date}.log`);
|
||||
const timestamp = new Date().toISOString();
|
||||
fs.appendFileSync(file, `[${timestamp}] ${data}\n`);
|
||||
|
||||
if (global.apiServer) {
|
||||
global.apiServer.broadcast("log", {
|
||||
type,
|
||||
timestamp,
|
||||
message: String(data)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const origLog = console.log;
|
||||
console.log = (...args) => {
|
||||
origLog(...args);
|
||||
logToFile("Output", args.join(" "));
|
||||
};
|
||||
|
||||
const origWarn = console.warn;
|
||||
console.warn = (...args) => {
|
||||
origWarn(...args);
|
||||
logToFile("Warn", args.join(" "));
|
||||
};
|
||||
|
||||
const origError = console.error;
|
||||
console.error = (...args) => {
|
||||
origError(...args);
|
||||
logToFile("Error", args.join(" "));
|
||||
};
|
||||
|
||||
process.on("unhandledRejection", (reason) => {
|
||||
console.error("Unhandled Rejection:", reason);
|
||||
});
|
||||
|
||||
process.on("uncaughtException", (err) => {
|
||||
console.error("Uncaught Exception:", err);
|
||||
});
|
||||
Generated
+2460
-390
File diff suppressed because it is too large.
Load diff
+25
-7
@@ -1,11 +1,12 @@
|
||||
{
|
||||
"name": "supportbot",
|
||||
"version": "7.4.1",
|
||||
"version": "26.2.0",
|
||||
"description": "SupportBot is an open-sourced support system for discord. It's completely free to use with high amounts of customization to match your own brand.",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "npm i && node index.js",
|
||||
"test": "node index.js test"
|
||||
"setup": "npm i && node index.js",
|
||||
"start": "node index.js",
|
||||
"git:setup": "node scripts/setup-git-security.js"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -16,9 +17,26 @@
|
||||
"bugs": {
|
||||
"url": "https://github.com/Emerald-Services/SupportBot/issues"
|
||||
},
|
||||
"homepage": "https://github.com/Emerald-Services/SupportBot#readme",
|
||||
"homepage": "https://emeraldsrv.dev",
|
||||
"dependencies": {
|
||||
"discord.js": "^13.3.1",
|
||||
"js-yaml": "^4.1.0"
|
||||
"@vitalets/google-translate-api": "^9.2.1",
|
||||
"adm-zip": "^0.6.0",
|
||||
"axios": "^1.7.2",
|
||||
"better-sqlite3": "^12.2.0",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"cors": "^2.8.6",
|
||||
"discord.js": "^14.13.0",
|
||||
"express": "^5.2.1",
|
||||
"figlet": "^1.9.3",
|
||||
"google-translate-api": "^2.3.0",
|
||||
"gradient-string": "^3.0.0",
|
||||
"js": "^0.1.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"moment": "^2.29.3",
|
||||
"ms": "^2.1.3",
|
||||
"mysql2": "^3.23.3",
|
||||
"openai": "^4.50.0",
|
||||
"systeminformation": "^5.31.5",
|
||||
"yaml": "^2.8.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
var w3=Object.defineProperty;var xy=t=>{throw TypeError(t)};var j3=(t,n,a)=>n in t?w3(t,n,{enumerable:!0,configurable:!0,writable:!0,value:a}):t[n]=a;var by=(t,n,a)=>j3(t,typeof n!="symbol"?n+"":n,a),vy=(t,n,a)=>n.has(t)||xy("Cannot "+a);var pn=(t,n,a)=>(vy(t,n,"read from private field"),a?a.call(t):n.get(t)),yy=(t,n,a)=>n.has(t)?xy("Cannot add the same private member more than once"):n instanceof WeakSet?n.add(t):n.set(t,a),dp=(t,n,a,o)=>(vy(t,n,"write to private field"),o?o.call(t,a):n.set(t,a),a);function C3(t,n){for(var a=0;a<n.length;a++){const o=n[a];if(typeof o!="string"&&!Array.isArray(o)){for(const c in o)if(c!=="default"&&!(c in t)){const l=Object.getOwnPropertyDescriptor(o,c);l&&Object.defineProperty(t,c,l.get?l:{enumerable:!0,get:()=>o[c]})}}}return Object.freeze(Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}))}(function(){const n=document.createElement("link").relList;if(n&&n.supports&&n.supports("modulepreload"))return;for(const c of document.querySelectorAll('link[rel="modulepreload"]'))o(c);new MutationObserver(c=>{for(const l of c)if(l.type==="childList")for(const d of l.addedNodes)d.tagName==="LINK"&&d.rel==="modulepreload"&&o(d)}).observe(document,{childList:!0,subtree:!0});function a(c){const l={};return c.integrity&&(l.integrity=c.integrity),c.referrerPolicy&&(l.referrerPolicy=c.referrerPolicy),c.crossOrigin==="use-credentials"?l.credentials="include":c.crossOrigin==="anonymous"?l.credentials="omit":l.credentials="same-origin",l}function o(c){if(c.ep)return;c.ep=!0;const l=a(c);fetch(c.href,l)}})();function s2(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var fp={exports:{}},Sl={};/**
|
||||
* @license React
|
||||
* react-jsx-runtime.production.js
|
||||
*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/var wy;function S3(){if(wy)return Sl;wy=1;var t=Symbol.for("react.transitional.element"),n=Symbol.for("react.fragment");function a(o,c,l){var d=null;if(l!==void 0&&(d=""+l),c.key!==void 0&&(d=""+c.key),"key"in c){l={};for(var m in c)m!=="key"&&(l[m]=c[m])}else l=c;return c=l.ref,{$$typeof:t,type:o,key:d,ref:c!==void 0?c:null,props:l}}return Sl.Fragment=n,Sl.jsx=a,Sl.jsxs=a,Sl}var jy;function N3(){return jy||(jy=1,fp.exports=S3()),fp.exports}var s=N3(),mp={exports:{}},ze={};/**
|
||||
* @license React
|
||||
* react.production.js
|
||||
*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/var Cy;function E3(){if(Cy)return ze;Cy=1;var t=Symbol.for("react.transitional.element"),n=Symbol.for("react.portal"),a=Symbol.for("react.fragment"),o=Symbol.for("react.strict_mode"),c=Symbol.for("react.profiler"),l=Symbol.for("react.consumer"),d=Symbol.for("react.context"),m=Symbol.for("react.forward_ref"),p=Symbol.for("react.suspense"),g=Symbol.for("react.memo"),b=Symbol.for("react.lazy"),v=Symbol.for("react.activity"),w=Symbol.iterator;function y(O){return O===null||typeof O!="object"?null:(O=w&&O[w]||O["@@iterator"],typeof O=="function"?O:null)}var C={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},S=Object.assign,N={};function E(O,D,B){this.props=O,this.context=D,this.refs=N,this.updater=B||C}E.prototype.isReactComponent={},E.prototype.setState=function(O,D){if(typeof O!="object"&&typeof O!="function"&&O!=null)throw Error("takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,O,D,"setState")},E.prototype.forceUpdate=function(O){this.updater.enqueueForceUpdate(this,O,"forceUpdate")};function T(){}T.prototype=E.prototype;function M(O,D,B){this.props=O,this.context=D,this.refs=N,this.updater=B||C}var k=M.prototype=new T;k.constructor=M,S(k,E.prototype),k.isPureReactComponent=!0;var _=Array.isArray;function R(){}var L={H:null,A:null,T:null,S:null},ne=Object.prototype.hasOwnProperty;function X(O,D,B){var V=B.ref;return{$$typeof:t,type:O,key:D,ref:V!==void 0?V:null,props:B}}function I(O,D){return X(O.type,D,O.props)}function H(O){return typeof O=="object"&&O!==null&&O.$$typeof===t}function q(O){var D={"=":"=0",":":"=2"};return"$"+O.replace(/[=:]/g,function(B){return D[B]})}var ce=/\/+/g;function de(O,D){return typeof O=="object"&&O!==null&&O.key!=null?q(""+O.key):D.toString(36)}function se(O){switch(O.status){case"fulfilled":return O.value;case"rejected":throw O.reason;default:switch(typeof O.status=="string"?O.then(R,R):(O.status="pending",O.then(function(D){O.status==="pending"&&(O.status="fulfilled",O.value=D)},function(D){O.status==="pending"&&(O.status="rejected",O.reason=D)})),O.status){case"fulfilled":return O.value;case"rejected":throw O.reason}}throw O}function P(O,D,B,V,Z){var be=typeof O;(be==="undefined"||be==="boolean")&&(O=null);var K=!1;if(O===null)K=!0;else switch(be){case"bigint":case"string":case"number":K=!0;break;case"object":switch(O.$$typeof){case t:case n:K=!0;break;case b:return K=O._init,P(K(O._payload),D,B,V,Z)}}if(K)return Z=Z(O),K=V===""?"."+de(O,0):V,_(Z)?(B="",K!=null&&(B=K.replace(ce,"$&/")+"/"),P(Z,D,B,"",function($){return $})):Z!=null&&(H(Z)&&(Z=I(Z,B+(Z.key==null||O&&O.key===Z.key?"":(""+Z.key).replace(ce,"$&/")+"/")+K)),D.push(Z)),1;K=0;var ge=V===""?".":V+":";if(_(O))for(var me=0;me<O.length;me++)V=O[me],be=ge+de(V,me),K+=P(V,D,B,be,Z);else if(me=y(O),typeof me=="function")for(O=me.call(O),me=0;!(V=O.next()).done;)V=V.value,be=ge+de(V,me++),K+=P(V,D,B,be,Z);else if(be==="object"){if(typeof O.then=="function")return P(se(O),D,B,V,Z);throw D=String(O),Error("Objects are not valid as a React child (found: "+(D==="[object Object]"?"object with keys {"+Object.keys(O).join(", ")+"}":D)+"). If you meant to render a collection of children, use an array instead.")}return K}function W(O,D,B){if(O==null)return O;var V=[],Z=0;return P(O,V,"","",function(be){return D.call(B,be,Z++)}),V}function oe(O){if(O._status===-1){var D=O._result;D=D(),D.then(function(B){(O._status===0||O._status===-1)&&(O._status=1,O._result=B)},function(B){(O._status===0||O._status===-1)&&(O._status=2,O._result=B)}),O._status===-1&&(O._status=0,O._result=D)}if(O._status===1)return O._result.default;throw O._result}var ae=typeof reportError=="function"?reportError:function(O){if(typeof window=="object"&&typeof window.ErrorEvent=="function"){var D=new window.ErrorEvent("error",{bubbles:!0,cancelable:!0,message:typeof O=="object"&&O!==null&&typeof O.message=="string"?String(O.message):String(O),error:O});if(!window.dispatchEvent(D))return}else if(typeof process=="object"&&typeof process.emit=="function"){process.emit("uncaughtException",O);return}console.error(O)},G={map:W,forEach:function(O,D,B){W(O,function(){D.apply(this,arguments)},B)},count:function(O){var D=0;return W(O,function(){D++}),D},toArray:function(O){return W(O,function(D){return D})||[]},only:function(O){if(!H(O))throw Error("React.Children.only expected to receive a single React element child.");return O}};return ze.Activity=v,ze.Children=G,ze.Component=E,ze.Fragment=a,ze.Profiler=c,ze.PureComponent=M,ze.StrictMode=o,ze.Suspense=p,ze.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE=L,ze.__COMPILER_RUNTIME={__proto__:null,c:function(O){return L.H.useMemoCache(O)}},ze.cache=function(O){return function(){return O.apply(null,arguments)}},ze.cacheSignal=function(){return null},ze.cloneElement=function(O,D,B){if(O==null)throw Error("The argument must be a React element, but
Line truncated
|
||||
* @license React
|
||||
* scheduler.production.js
|
||||
*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/var Ny;function k3(){return Ny||(Ny=1,(function(t){function n(P,W){var oe=P.length;P.push(W);e:for(;0<oe;){var ae=oe-1>>>1,G=P[ae];if(0<c(G,W))P[ae]=W,P[oe]=G,oe=ae;else break e}}function a(P){return P.length===0?null:P[0]}function o(P){if(P.length===0)return null;var W=P[0],oe=P.pop();if(oe!==W){P[0]=oe;e:for(var ae=0,G=P.length,O=G>>>1;ae<O;){var D=2*(ae+1)-1,B=P[D],V=D+1,Z=P[V];if(0>c(B,oe))V<G&&0>c(Z,B)?(P[ae]=Z,P[V]=oe,ae=V):(P[ae]=B,P[D]=oe,ae=D);else if(V<G&&0>c(Z,oe))P[ae]=Z,P[V]=oe,ae=V;else break e}}return W}function c(P,W){var oe=P.sortIndex-W.sortIndex;return oe!==0?oe:P.id-W.id}if(t.unstable_now=void 0,typeof performance=="object"&&typeof performance.now=="function"){var l=performance;t.unstable_now=function(){return l.now()}}else{var d=Date,m=d.now();t.unstable_now=function(){return d.now()-m}}var p=[],g=[],b=1,v=null,w=3,y=!1,C=!1,S=!1,N=!1,E=typeof setTimeout=="function"?setTimeout:null,T=typeof clearTimeout=="function"?clearTimeout:null,M=typeof setImmediate<"u"?setImmediate:null;function k(P){for(var W=a(g);W!==null;){if(W.callback===null)o(g);else if(W.startTime<=P)o(g),W.sortIndex=W.expirationTime,n(p,W);else break;W=a(g)}}function _(P){if(S=!1,k(P),!C)if(a(p)!==null)C=!0,R||(R=!0,q());else{var W=a(g);W!==null&&se(_,W.startTime-P)}}var R=!1,L=-1,ne=5,X=-1;function I(){return N?!0:!(t.unstable_now()-X<ne)}function H(){if(N=!1,R){var P=t.unstable_now();X=P;var W=!0;try{e:{C=!1,S&&(S=!1,T(L),L=-1),y=!0;var oe=w;try{t:{for(k(P),v=a(p);v!==null&&!(v.expirationTime>P&&I());){var ae=v.callback;if(typeof ae=="function"){v.callback=null,w=v.priorityLevel;var G=ae(v.expirationTime<=P);if(P=t.unstable_now(),typeof G=="function"){v.callback=G,k(P),W=!0;break t}v===a(p)&&o(p),k(P)}else o(p);v=a(p)}if(v!==null)W=!0;else{var O=a(g);O!==null&&se(_,O.startTime-P),W=!1}}break e}finally{v=null,w=oe,y=!1}W=void 0}}finally{W?q():R=!1}}}var q;if(typeof M=="function")q=function(){M(H)};else if(typeof MessageChannel<"u"){var ce=new MessageChannel,de=ce.port2;ce.port1.onmessage=H,q=function(){de.postMessage(null)}}else q=function(){E(H,0)};function se(P,W){L=E(function(){P(t.unstable_now())},W)}t.unstable_IdlePriority=5,t.unstable_ImmediatePriority=1,t.unstable_LowPriority=4,t.unstable_NormalPriority=3,t.unstable_Profiling=null,t.unstable_UserBlockingPriority=2,t.unstable_cancelCallback=function(P){P.callback=null},t.unstable_forceFrameRate=function(P){0>P||125<P?console.error("forceFrameRate takes a positive int between 0 and 125, forcing frame rates higher than 125 fps is not supported"):ne=0<P?Math.floor(1e3/P):5},t.unstable_getCurrentPriorityLevel=function(){return w},t.unstable_next=function(P){switch(w){case 1:case 2:case 3:var W=3;break;default:W=w}var oe=w;w=W;try{return P()}finally{w=oe}},t.unstable_requestPaint=function(){N=!0},t.unstable_runWithPriority=function(P,W){switch(P){case 1:case 2:case 3:case 4:case 5:break;default:P=3}var oe=w;w=P;try{return W()}finally{w=oe}},t.unstable_scheduleCallback=function(P,W,oe){var ae=t.unstable_now();switch(typeof oe=="object"&&oe!==null?(oe=oe.delay,oe=typeof oe=="number"&&0<oe?ae+oe:ae):oe=ae,P){case 1:var G=-1;break;case 2:G=250;break;case 5:G=1073741823;break;case 4:G=1e4;break;default:G=5e3}return G=oe+G,P={id:b++,callback:W,priorityLevel:P,startTime:oe,expirationTime:G,sortIndex:-1},oe>ae?(P.sortIndex=oe,n(g,P),a(p)===null&&P===a(g)&&(S?(T(L),L=-1):S=!0,se(_,oe-ae))):(P.sortIndex=G,n(p,P),C||y||(C=!0,R||(R=!0,q()))),P},t.unstable_shouldYield=I,t.unstable_wrapCallback=function(P){var W=w;return function(){var oe=w;w=W;try{return P.apply(this,arguments)}finally{w=oe}}}})(gp)),gp}var Ey;function T3(){return Ey||(Ey=1,hp.exports=k3()),hp.exports}var xp={exports:{}},hn={};/**
|
||||
* @license React
|
||||
* react-dom.production.js
|
||||
*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/var ky;function M3(){if(ky)return hn;ky=1;var t=Xh();function n(p){var g="https://react.dev/errors/"+p;if(1<arguments.length){g+="?args[]="+encodeURIComponent(arguments[1]);for(var b=2;b<arguments.length;b++)g+="&args[]="+encodeURIComponent(arguments[b])}return"Minified React error #"+p+"; visit "+g+" for the full message or use the non-minified dev environment for full errors and additional helpful warnings."}function a(){}var o={d:{f:a,r:function(){throw Error(n(522))},D:a,C:a,L:a,m:a,X:a,S:a,M:a},p:0,findDOMNode:null},c=Symbol.for("react.portal");function l(p,g,b){var v=3<arguments.length&&arguments[3]!==void 0?arguments[3]:null;return{$$typeof:c,key:v==null?null:""+v,children:p,containerInfo:g,implementation:b}}var d=t.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;function m(p,g){if(p==="font")return"";if(typeof g=="string")return g==="use-credentials"?g:""}return hn.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE=o,hn.createPortal=function(p,g){var b=2<arguments.length&&arguments[2]!==void 0?arguments[2]:null;if(!g||g.nodeType!==1&&g.nodeType!==9&&g.nodeType!==11)throw Error(n(299));return l(p,g,null,b)},hn.flushSync=function(p){var g=d.T,b=o.p;try{if(d.T=null,o.p=2,p)return p()}finally{d.T=g,o.p=b,o.d.f()}},hn.preconnect=function(p,g){typeof p=="string"&&(g?(g=g.crossOrigin,g=typeof g=="string"?g==="use-credentials"?g:"":void 0):g=null,o.d.C(p,g))},hn.prefetchDNS=function(p){typeof p=="string"&&o.d.D(p)},hn.preinit=function(p,g){if(typeof p=="string"&&g&&typeof g.as=="string"){var b=g.as,v=m(b,g.crossOrigin),w=typeof g.integrity=="string"?g.integrity:void 0,y=typeof g.fetchPriority=="string"?g.fetchPriority:void 0;b==="style"?o.d.S(p,typeof g.precedence=="string"?g.precedence:void 0,{crossOrigin:v,integrity:w,fetchPriority:y}):b==="script"&&o.d.X(p,{crossOrigin:v,integrity:w,fetchPriority:y,nonce:typeof g.nonce=="string"?g.nonce:void 0})}},hn.preinitModule=function(p,g){if(typeof p=="string")if(typeof g=="object"&&g!==null){if(g.as==null||g.as==="script"){var b=m(g.as,g.crossOrigin);o.d.M(p,{crossOrigin:b,integrity:typeof g.integrity=="string"?g.integrity:void 0,nonce:typeof g.nonce=="string"?g.nonce:void 0})}}else g==null&&o.d.M(p)},hn.preload=function(p,g){if(typeof p=="string"&&typeof g=="object"&&g!==null&&typeof g.as=="string"){var b=g.as,v=m(b,g.crossOrigin);o.d.L(p,b,{crossOrigin:v,integrity:typeof g.integrity=="string"?g.integrity:void 0,nonce:typeof g.nonce=="string"?g.nonce:void 0,type:typeof g.type=="string"?g.type:void 0,fetchPriority:typeof g.fetchPriority=="string"?g.fetchPriority:void 0,referrerPolicy:typeof g.referrerPolicy=="string"?g.referrerPolicy:void 0,imageSrcSet:typeof g.imageSrcSet=="string"?g.imageSrcSet:void 0,imageSizes:typeof g.imageSizes=="string"?g.imageSizes:void 0,media:typeof g.media=="string"?g.media:void 0})}},hn.preloadModule=function(p,g){if(typeof p=="string")if(g){var b=m(g.as,g.crossOrigin);o.d.m(p,{as:typeof g.as=="string"&&g.as!=="script"?g.as:void 0,crossOrigin:b,integrity:typeof g.integrity=="string"?g.integrity:void 0})}else o.d.m(p)},hn.requestFormReset=function(p){o.d.r(p)},hn.unstable_batchedUpdates=function(p,g){return p(g)},hn.useFormState=function(p,g,b){return d.H.useFormState(p,g,b)},hn.useFormStatus=function(){return d.H.useHostTransitionStatus()},hn.version="19.2.8",hn}var Ty;function o2(){if(Ty)return xp.exports;Ty=1;function t(){if(!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__>"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(t)}catch(n){console.error(n)}}return t(),xp.exports=M3(),xp.exports}/**
|
||||
* @license React
|
||||
* react-dom-client.production.js
|
||||
*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/var My;function R3(){if(My)return Nl;My=1;var t=T3(),n=Xh(),a=o2();function o(e){var r="https://react.dev/errors/"+e;if(1<arguments.length){r+="?args[]="+encodeURIComponent(arguments[1]);for(var i=2;i<arguments.length;i++)r+="&args[]="+encodeURIComponent(arguments[i])}return"Minified React error #"+e+"; visit "+r+" for the full message or use the non-minified dev environment for full errors and additional helpful warnings."}function c(e){return!(!e||e.nodeType!==1&&e.nodeType!==9&&e.nodeType!==11)}function l(e){var r=e,i=e;if(e.alternate)for(;r.return;)r=r.return;else{e=r;do r=e,(r.flags&4098)!==0&&(i=r.return),e=r.return;while(e)}return r.tag===3?i:null}function d(e){if(e.tag===13){var r=e.memoizedState;if(r===null&&(e=e.alternate,e!==null&&(r=e.memoizedState)),r!==null)return r.dehydrated}return null}function m(e){if(e.tag===31){var r=e.memoizedState;if(r===null&&(e=e.alternate,e!==null&&(r=e.memoizedState)),r!==null)return r.dehydrated}return null}function p(e){if(l(e)!==e)throw Error(o(188))}function g(e){var r=e.alternate;if(!r){if(r=l(e),r===null)throw Error(o(188));return r!==e?null:e}for(var i=e,u=r;;){var h=i.return;if(h===null)break;var x=h.alternate;if(x===null){if(u=h.return,u!==null){i=u;continue}break}if(h.child===x.child){for(x=h.child;x;){if(x===i)return p(h),e;if(x===u)return p(h),r;x=x.sibling}throw Error(o(188))}if(i.return!==u.return)i=h,u=x;else{for(var j=!1,A=h.child;A;){if(A===i){j=!0,i=h,u=x;break}if(A===u){j=!0,u=h,i=x;break}A=A.sibling}if(!j){for(A=x.child;A;){if(A===i){j=!0,i=x,u=h;break}if(A===u){j=!0,u=x,i=h;break}A=A.sibling}if(!j)throw Error(o(189))}}if(i.alternate!==u)throw Error(o(190))}if(i.tag!==3)throw Error(o(188));return i.stateNode.current===i?e:r}function b(e){var r=e.tag;if(r===5||r===26||r===27||r===6)return e;for(e=e.child;e!==null;){if(r=b(e),r!==null)return r;e=e.sibling}return null}var v=Object.assign,w=Symbol.for("react.element"),y=Symbol.for("react.transitional.element"),C=Symbol.for("react.portal"),S=Symbol.for("react.fragment"),N=Symbol.for("react.strict_mode"),E=Symbol.for("react.profiler"),T=Symbol.for("react.consumer"),M=Symbol.for("react.context"),k=Symbol.for("react.forward_ref"),_=Symbol.for("react.suspense"),R=Symbol.for("react.suspense_list"),L=Symbol.for("react.memo"),ne=Symbol.for("react.lazy"),X=Symbol.for("react.activity"),I=Symbol.for("react.memo_cache_sentinel"),H=Symbol.iterator;function q(e){return e===null||typeof e!="object"?null:(e=H&&e[H]||e["@@iterator"],typeof e=="function"?e:null)}var ce=Symbol.for("react.client.reference");function de(e){if(e==null)return null;if(typeof e=="function")return e.$$typeof===ce?null:e.displayName||e.name||null;if(typeof e=="string")return e;switch(e){case S:return"Fragment";case E:return"Profiler";case N:return"StrictMode";case _:return"Suspense";case R:return"SuspenseList";case X:return"Activity"}if(typeof e=="object")switch(e.$$typeof){case C:return"Portal";case M:return e.displayName||"Context";case T:return(e._context.displayName||"Context")+".Consumer";case k:var r=e.render;return e=e.displayName,e||(e=r.displayName||r.name||"",e=e!==""?"ForwardRef("+e+")":"ForwardRef"),e;case L:return r=e.displayName||null,r!==null?r:de(e.type)||"Memo";case ne:r=e._payload,e=e._init;try{return de(e(r))}catch{}}return null}var se=Array.isArray,P=n.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE,W=a.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE,oe={pending:!1,data:null,method:null,action:null},ae=[],G=-1;function O(e){return{current:e}}function D(e){0>G||(e.current=ae[G],ae[G]=null,G--)}function B(e,r){G++,ae[G]=e.current,e.current=r}var V=O(null),Z=O(null),be=O(null),K=O(null);function ge(e,r){switch(B(be,r),B(Z,e),B(V,null),r.nodeType){case 9:case 11:e=(e=r.documentElement)&&(e=e.namespaceURI)?Bv(e):0;break;default:if(e=r.tagName,r=r.namespaceURI)r=Bv(r),e=$v(r,e);else switch(e){case"svg":e=1;break;case"math":e=2;break;default:e=0}}D(V),B(V,e)}function me(){D(V),D(Z),D(be)}function $(e){e.memoizedState!==null&&B(K,e);var r=V.current,i=$v(r,e.type);r!==i&&(B(Z,e),B(V,i))}function ie(e){Z.current===e&&(D(V),D(Z)),K.current===e&&(D(K),yl._currentValue=oe)}var ye,Se;function Be(e){if(ye===void 0)try{throw Error()}catch(i){var r=i.stack.trim().match(/\n( *(at )?)/);ye=r&&r[1]||"",Se=-1<i.stack.indexOf(`
|
||||
at`)?" (<anonymous>)":-1<i.stack.indexOf("@")?"@unknown:0:0":""}return`
|
||||
`+ye+e+Se}var Ke=!1;function We(e,r){if(!e||Ke)return"";Ke=!0;var i=Error.prepareStackTrace;Error.prepareStackTrace=void 0;try{var u={DetermineComponentFrameRoot:function(){try{if(r){var he=function(){throw Error()};if(Object.defineProperty(he.prototype,"props",{set:function(){throw Error()}}),typeof Reflect=="object"&&Reflect.construct){try{Reflect.construct(he,[])}catch(re){var ee=re}Reflect.construct(e,[],he)}else{try{he.call()}catch(re){ee=re}e.call(he.prototype)}}else{try{throw Error()}catch(re){ee=re}(he=e())&&typeof he.catch=="function"&&he.catch(function(){})}}catch(re){if(re&&ee&&typeof re.stack=="string")return[re.stack,ee.stack]}return[null,null]}};u.DetermineComponentFrameRoot.displayName="DetermineComponentFrameRoot";var h=Object.getOwnPropertyDescriptor(u.DetermineComponentFrameRoot,"name");h&&h.configurable&&Object.defineProperty(u.DetermineComponentFrameRoot,"name",{value:"DetermineComponentFrameRoot"});var x=u.DetermineComponentFrameRoot(),j=x[0],A=x[1];if(j&&A){var z=j.split(`
|
||||
`),J=A.split(`
|
||||
`);for(h=u=0;u<z.length&&!z[u].includes("DetermineComponentFrameRoot");)u++;for(;h<J.length&&!J[h].includes("DetermineComponentFrameRoot");)h++;if(u===z.length||h===J.length)for(u=z.length-1,h=J.length-1;1<=u&&0<=h&&z[u]!==J[h];)h--;for(;1<=u&&0<=h;u--,h--)if(z[u]!==J[h]){if(u!==1||h!==1)do if(u--,h--,0>h||z[u]!==J[h]){var le=`
|
||||
`+z[u].replace(" at new "," at ");return e.displayName&&le.includes("<anonymous>")&&(le=le.replace("<anonymous>",e.displayName)),le}while(1<=u&&0<=h);break}}}finally{Ke=!1,Error.prepareStackTrace=i}return(i=e?e.displayName||e.name:"")?Be(i):""}function qt(e,r){switch(e.tag){case 26:case 27:case 5:return Be(e.type);case 16:return Be("Lazy");case 13:return e.child!==r&&r!==null?Be("Suspense Fallback"):Be("Suspense");case 19:return Be("SuspenseList");case 0:case 15:return We(e.type,!1);case 11:return We(e.type.render,!1);case 1:return We(e.type,!0);case 31:return Be("Activity");default:return""}}function vn(e){try{var r="",i=null;do r+=qt(e,i),i=e,e=e.return;while(e);return r}catch(u){return`
|
||||
Error generating stack: `+u.message+`
|
||||
`+u.stack}}var Xn=Object.prototype.hasOwnProperty,br=t.unstable_scheduleCallback,yn=t.unstable_cancelCallback,wn=t.unstable_shouldYield,Zn=t.unstable_requestPaint,Kt=t.unstable_now,po=t.unstable_getCurrentPriorityLevel,Pt=t.unstable_ImmediatePriority,Qn=t.unstable_UserBlockingPriority,Da=t.unstable_NormalPriority,Es=t.unstable_LowPriority,qr=t.unstable_IdlePriority,ho=t.log,Kr=t.unstable_setDisableYieldValue,Dn=null,yt=null;function On(e){if(typeof ho=="function"&&Kr(e),yt&&typeof yt.setStrictMode=="function")try{yt.setStrictMode(Dn,e)}catch{}}var Rt=Math.clz32?Math.clz32:go,Ai=Math.log,_i=Math.LN2;function go(e){return e>>>=0,e===0?32:31-(Ai(e)/_i|0)|0}var Wr=256,Oa=262144,Ar=4194304;function Wt(e){var r=e&42;if(r!==0)return r;switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return e&261888;case 262144:case 524288:case 1048576:case 2097152:return e&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return e&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return e}}function La(e,r,i){var u=e.pendingLanes;if(u===0)return 0;var h=0,x=e.suspendedLanes,j=e.pingedLanes;e=e.warmLanes;var A=u&134217727;return A!==0?(u=A&~x,u!==0?h=Wt(u):(j&=A,j!==0?h=Wt(j):i||(i=A&~e,i!==0&&(h=Wt(i))))):(A=u&~x,A!==0?h=Wt(A):j!==0?h=Wt(j):i||(i=u&~e,i!==0&&(h=Wt(i)))),h===0?0:r!==0&&r!==h&&(r&x)===0&&(x=h&-h,i=r&-r,x>=i||x===32&&(i&4194048)!==0)?r:h}function De(e,r){return(e.pendingLanes&~(e.suspendedLanes&~e.pingedLanes)&r)===0}function rt(e,r){switch(e){case 1:case 2:case 4:case 8:case 64:return r+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return r+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function lt(){var e=Ar;return Ar<<=1,(Ar&62914560)===0&&(Ar=4194304),e}function rn(e){for(var r=[],i=0;31>i;i++)r.push(e);return r}function It(e,r){e.pendingLanes|=r,r!==268435456&&(e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0)}function Jn(e,r,i,u,h,x){var j=e.pendingLanes;e.pendingLanes=i,e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0,e.expiredLanes&=i,e.entangledLanes&=i,e.errorRecoveryDisabledLanes&=i,e.shellSuspendCounter=0;var A=e.entanglements,z=e.expirationTimes,J=e.hiddenUpdates;for(i=j&~i;0<i;){var le=31-Rt(i),he=1<<le;A[le]=0,z[le]=-1;var ee=J[le];if(ee!==null)for(J[le]=null,le=0;le<ee.length;le++){var re=ee[le];re!==null&&(re.lane&=-536870913)}i&=~he}u!==0&&er(e,u,0),x!==0&&h===0&&e.tag!==0&&(e.suspendedLanes|=x&~(j&~r))}function er(e,r,i){e.pendingLanes|=r,e.suspendedLanes&=~r;var u=31-Rt(r);e.entangledLanes|=r,e.entanglements[u]=e.entanglements[u]|1073741824|i&261930}function bt(e,r){var i=e.entangledLanes|=r;for(e=e.entanglements;i;){var u=31-Rt(i),h=1<<u;h&r|e[u]&r&&(e[u]|=r),i&=~h}}function Ln(e,r){var i=r&-r;return i=(i&42)!==0?1:Di(i),(i&(e.suspendedLanes|r))!==0?0:i}function Di(e){switch(e){case 2:e=1;break;case 8:e=4;break;case 32:e=16;break;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:case 4194304:case 8388608:case 16777216:case 33554432:e=128;break;case 268435456:e=134217728;break;default:e=0}return e}function Oi(e){return e&=-e,2<e?8<e?(e&134217727)!==0?32:268435456:8:2}function Li(){var e=W.p;return e!==0?e:(e=window.event,e===void 0?32:uy(e.type))}function zi(e,r){var i=W.p;try{return W.p=e,r()}finally{W.p=i}}var Y=Math.random().toString(36).slice(2),te="__reactFiber$"+Y,je="__reactProps$"+Y,we="__reactContainer$"+Y,Le="__reactEvents$"+Y,Xe="__reactListeners$"+Y,vt="__reactHandles$"+Y,gt="__reactResources$"+Y,jn="__reactMarker$"+Y;function mn(e){delete e[te],delete e[je],delete e[Le],delete e[Xe],delete e[vt]}function an(e){var r=e[te];if(r)return r;for(var i=e.parentNode;i;){if(r=i[we]||i[te]){if(i=r.alternate,r.child!==null||i!==null&&i.child!==null)for(e=Kv(e);e!==null;){if(i=e[te])return i;e=Kv(e)}return r}e=i,i=e.parentNode}return null}function tr(e){if(e=e[te]||e[we]){var r=e.tag;if(r===5||r===6||r===13||r===31||r===26||r===27||r===3)return e}return null}function _r(e){var r=e.tag;if(r===5||r===26||r===27||r===6)return e.stateNode;throw Error(o(33))}function Xr(e){var r=e[gt];return r||(r=e[gt]={hoistableStyles:new Map,hoistableScripts:new Map}),r}function Xt(e){e[jn]=!0}var Tx=new Set,Mx={};function ks(e,r){xo(e,r),xo(e+"Capture",r)}function xo(e,r){for(Mx[e]=r,e=0;e<r.length;e++)Tx.add(r[e])}var f5=RegExp("^[:A-Z_a-z\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u
Line truncated
|
||||
`).replace(G4,"")}function Iv(e,r){return r=Pv(r),Pv(e)===r}function pt(e,r,i,u,h,x){switch(i){case"children":typeof u=="string"?r==="body"||r==="textarea"&&u===""||vo(e,u):(typeof u=="number"||typeof u=="bigint")&&r!=="body"&&vo(e,""+u);break;case"className":gc(e,"class",u);break;case"tabIndex":gc(e,"tabindex",u);break;case"dir":case"role":case"viewBox":case"width":case"height":gc(e,i,u);break;case"style":Ix(e,u,x);break;case"data":if(r!=="object"){gc(e,"data",u);break}case"src":case"href":if(u===""&&(r!=="a"||i!=="href")){e.removeAttribute(i);break}if(u==null||typeof u=="function"||typeof u=="symbol"||typeof u=="boolean"){e.removeAttribute(i);break}u=bc(""+u),e.setAttribute(i,u);break;case"action":case"formAction":if(typeof u=="function"){e.setAttribute(i,"javascript:throw new Error('A React form was unexpectedly submitted. If you called form.submit() manually, consider using form.requestSubmit() instead. If you\\'re trying to use event.stopPropagation() in a submit event handler, consider also calling event.preventDefault().')");break}else typeof x=="function"&&(i==="formAction"?(r!=="input"&&pt(e,r,"name",h.name,h,null),pt(e,r,"formEncType",h.formEncType,h,null),pt(e,r,"formMethod",h.formMethod,h,null),pt(e,r,"formTarget",h.formTarget,h,null)):(pt(e,r,"encType",h.encType,h,null),pt(e,r,"method",h.method,h,null),pt(e,r,"target",h.target,h,null)));if(u==null||typeof u=="symbol"||typeof u=="boolean"){e.removeAttribute(i);break}u=bc(""+u),e.setAttribute(i,u);break;case"onClick":u!=null&&(e.onclick=Qr);break;case"onScroll":u!=null&&Ve("scroll",e);break;case"onScrollEnd":u!=null&&Ve("scrollend",e);break;case"dangerouslySetInnerHTML":if(u!=null){if(typeof u!="object"||!("__html"in u))throw Error(o(61));if(i=u.__html,i!=null){if(h.children!=null)throw Error(o(60));e.innerHTML=i}}break;case"multiple":e.multiple=u&&typeof u!="function"&&typeof u!="symbol";break;case"muted":e.muted=u&&typeof u!="function"&&typeof u!="symbol";break;case"suppressContentEditableWarning":case"suppressHydrationWarning":case"defaultValue":case"defaultChecked":case"innerHTML":case"ref":break;case"autoFocus":break;case"xlinkHref":if(u==null||typeof u=="function"||typeof u=="boolean"||typeof u=="symbol"){e.removeAttribute("xlink:href");break}i=bc(""+u),e.setAttributeNS("http://www.w3.org/1999/xlink","xlink:href",i);break;case"contentEditable":case"spellCheck":case"draggable":case"value":case"autoReverse":case"externalResourcesRequired":case"focusable":case"preserveAlpha":u!=null&&typeof u!="function"&&typeof u!="symbol"?e.setAttribute(i,""+u):e.removeAttribute(i);break;case"inert":case"allowFullScreen":case"async":case"autoPlay":case"controls":case"default":case"defer":case"disabled":case"disablePictureInPicture":case"disableRemotePlayback":case"formNoValidate":case"hidden":case"loop":case"noModule":case"noValidate":case"open":case"playsInline":case"readOnly":case"required":case"reversed":case"scoped":case"seamless":case"itemScope":u&&typeof u!="function"&&typeof u!="symbol"?e.setAttribute(i,""):e.removeAttribute(i);break;case"capture":case"download":u===!0?e.setAttribute(i,""):u!==!1&&u!=null&&typeof u!="function"&&typeof u!="symbol"?e.setAttribute(i,u):e.removeAttribute(i);break;case"cols":case"rows":case"size":case"span":u!=null&&typeof u!="function"&&typeof u!="symbol"&&!isNaN(u)&&1<=u?e.setAttribute(i,u):e.removeAttribute(i);break;case"rowSpan":case"start":u==null||typeof u=="function"||typeof u=="symbol"||isNaN(u)?e.removeAttribute(i):e.setAttribute(i,u);break;case"popover":Ve("beforetoggle",e),Ve("toggle",e),hc(e,"popover",u);break;case"xlinkActuate":Zr(e,"http://www.w3.org/1999/xlink","xlink:actuate",u);break;case"xlinkArcrole":Zr(e,"http://www.w3.org/1999/xlink","xlink:arcrole",u);break;case"xlinkRole":Zr(e,"http://www.w3.org/1999/xlink","xlink:role",u);break;case"xlinkShow":Zr(e,"http://www.w3.org/1999/xlink","xlink:show",u);break;case"xlinkTitle":Zr(e,"http://www.w3.org/1999/xlink","xlink:title",u);break;case"xlinkType":Zr(e,"http://www.w3.org/1999/xlink","xlink:type",u);break;case"xmlBase":Zr(e,"http://www.w3.org/XML/1998/namespace","xml:base",u);break;case"xmlLang":Zr(e,"http://www.w3.org/XML/1998/namespace","xml:lang",u);break;case"xmlSpace":Zr(e,"http://www.w3.org/XML/1998/namespace","xml:space",u);break;case"is":hc(e,"is",u);break;case"innerText":case"textContent":break;default:(!(2<i.length)||i[0]!=="o"&&i[0]!=="O"||i[1]!=="n"&&i[1]!=="N")&&(i=x5.get(i)||i,hc(e,i,u))}}function qm(e,r,i,u,h,x){switch(i){case"style":Ix(e,u,x);break;case"dangerouslySetInnerHTML":if(u!=null){if(typeof u!="object"||!("__html"in u))throw Error(o(61));if(i=u.__html,i!=null){if(h.children!=null)throw Error(o(60));e.innerHTML=i}}break;case"children":typeof u=="string"?vo(e,u):(typeof u=="number"||typeof u=="bigint")&&vo(e,""+u);break;case"onScroll":u!=null&&Ve("scroll",e);break;case"onScrollEnd":u!=null&&Ve("scrollend",e);break;case"onClick":u!=null&&(e.onclick=Qr);break;case"suppressContentEditableWarning":case"suppressHydrationWarning":case"inne
Line truncated
|
||||
* react-router v7.18.2
|
||||
*
|
||||
* Copyright (c) Remix Software Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE.md file in the root directory of this source tree.
|
||||
*
|
||||
* @license MIT
|
||||
*/var Zh=/^(?:[a-z][a-z0-9+.-]*:|[\\/]{2})/i,i2=/^[\\/]{2}/;function D3(t,n){return n+t.replace(/\\/g,"/")}var Ay="popstate";function _y(t){return typeof t=="object"&&t!=null&&"pathname"in t&&"search"in t&&"hash"in t&&"state"in t&&"key"in t}function O3(t={}){function n(o,c){var g;let l=(g=c.state)==null?void 0:g.masked,{pathname:d,search:m,hash:p}=l||o.location;return Qp("",{pathname:d,search:m,hash:p},c.state&&c.state.usr||null,c.state&&c.state.key||"default",l?{pathname:o.location.pathname,search:o.location.search,hash:o.location.hash}:void 0)}function a(o,c){return typeof c=="string"?c:Il(c)}return z3(n,a,null,t)}function kt(t,n){if(t===!1||t===null||typeof t>"u")throw new Error(n)}function pr(t,n){if(!t){typeof console<"u"&&console.warn(n);try{throw new Error(n)}catch{}}}function L3(){return Math.random().toString(36).substring(2,10)}function Dy(t,n){return{usr:t.state,key:t.key,idx:n,masked:t.mask?{pathname:t.pathname,search:t.search,hash:t.hash}:void 0}}function Qp(t,n,a=null,o,c){return{pathname:typeof t=="string"?t:t.pathname,search:"",hash:"",...typeof n=="string"?hi(n):n,state:a,key:n&&n.key||o||L3(),mask:c}}function Il({pathname:t="/",search:n="",hash:a=""}){return n&&n!=="?"&&(t+=n.charAt(0)==="?"?n:"?"+n),a&&a!=="#"&&(t+=a.charAt(0)==="#"?a:"#"+a),t}function hi(t){let n={};if(t){let a=t.indexOf("#");a>=0&&(n.hash=t.substring(a),t=t.substring(0,a));let o=t.indexOf("?");o>=0&&(n.search=t.substring(o),t=t.substring(0,o)),t&&(n.pathname=t)}return n}function z3(t,n,a,o={}){let{window:c=document.defaultView,v5Compat:l=!1}=o,d=c.history,m="POP",p=null,g=b();g==null&&(g=0,d.replaceState({...d.state,idx:g},""));function b(){return(d.state||{idx:null}).idx}function v(){m="POP";let N=b(),E=N==null?null:N-g;g=N,p&&p({action:m,location:S.location,delta:E})}function w(N,E){m="PUSH";let T=_y(N)?N:Qp(S.location,N,E);g=b()+1;let M=Dy(T,g),k=S.createHref(T.mask||T);try{d.pushState(M,"",k)}catch(_){if(_ instanceof DOMException&&_.name==="DataCloneError")throw _;c.location.assign(k)}l&&p&&p({action:m,location:S.location,delta:1})}function y(N,E){m="REPLACE";let T=_y(N)?N:Qp(S.location,N,E);g=b();let M=Dy(T,g),k=S.createHref(T.mask||T);d.replaceState(M,"",k),l&&p&&p({action:m,location:S.location,delta:0})}function C(N){return P3(c,N)}let S={get action(){return m},get location(){return t(c,d)},listen(N){if(p)throw new Error("A history only accepts one active listener");return c.addEventListener(Ay,v),p=N,()=>{c.removeEventListener(Ay,v),p=null}},createHref(N){return n(c,N)},createURL:C,encodeLocation(N){let E=C(N);return{pathname:E.pathname,search:E.search,hash:E.hash}},push:w,replace:y,go(N){return d.go(N)}};return S}function P3(t,n,a=!1){let o="http://localhost";t&&(o=t.location.origin!=="null"?t.location.origin:t.location.href),kt(o,"No window.location.(origin|href) available to create URL");let c=typeof n=="string"?n:Il(n);return c=c.replace(/ $/,"%20"),!a&&i2.test(c)&&(c=o+c),new URL(c,o)}function l2(t,n,a="/"){return I3(t,n,a,!1)}function I3(t,n,a,o,c){let l=typeof n=="string"?hi(n):n,d=Na(l.pathname||"/",a);if(d==null)return null;let m=U3(t),p=null,g=X3(d);for(let b=0;p==null&&b<m.length;++b)p=W3(m[b],g,o);return p}function U3(t){let n=c2(t);return B3(n),n}function c2(t,n=[],a=[],o="",c=!1){let l=(d,m,p=c,g)=>{let b={relativePath:g===void 0?d.path||"":g,caseSensitive:d.caseSensitive===!0,childrenIndex:m,route:d};if(b.relativePath.startsWith("/")){if(!b.relativePath.startsWith(o)&&p)return;kt(b.relativePath.startsWith(o),`Absolute route path "${b.relativePath}" nested under path "${o}" is not valid. An absolute child route path must start with the combined path of all its parent routes.`),b.relativePath=b.relativePath.slice(o.length)}let v=Sr([o,b.relativePath]),w=a.concat(b);d.children&&d.children.length>0&&(kt(d.index!==!0,`Index routes must not have child routes. Please remove all child routes from route path "${v}".`),c2(d.children,n,w,v,p)),!(d.path==null&&!d.index)&&n.push({path:v,score:q3(v,d.index),routesMeta:w.map((y,C)=>{let[S,N]=f2(y.relativePath,y.caseSensitive,C===w.length-1);return{...y,matcher:S,compiledParams:N}})})};return t.forEach((d,m)=>{var p;if(d.path===""||!((p=d.path)!=null&&p.includes("?")))l(d,m);else for(let g of u2(d.path))l(d,m,!0,g)}),n}function u2(t){let n=t.split("/");if(n.length===0)return[];let[a,...o]=n,c=a.endsWith("?"),l=a.replace(/\?$/,"");if(o.length===0)return c?[l,""]:[l];let d=u2(o.join("/")),m=[];return m.push(...d.map(p=>p===""?l:[l,p].join("/"))),c&&m.push(...d),m.map(p=>t.startsWith("/")&&p===""?"/":p)}function B3(t){t.sort((n,a)=>n.score!==a.score?a.score-n.score:K3(n.routesMeta.map(o=>o.childrenIndex),a.routesMeta.map(o=>o.childrenIndex)))}var $3=/^:[\w-]+$/,H3=3,G3=2,V3=1,F3=10,Y3=-2,Oy=t=>t==="*";function q3(t,n){let a=t.split("/"),o=a.length;return a.some(Oy)&&(o+=Y3),n&&(o+=G3),a.filter(c=>!Oy(c)).reduce((c,l)=>c+($3.test(l)?H3:l===""?V3:F3),o)}function K3(t,n){return t.length===n.length&&t.slice(0,-1).every((o,c)=>o===n[c])?t[t.length-1]-n[n.length-1]:0}fuLine truncated
|
||||
|
||||
Please change the parent <Route path="${E}"> to <Route path="${E==="/"?"*":`${E}/*`}">.`)}let b=Kn(),v;if(n){let E=typeof n=="string"?hi(n):n;kt(p==="/"||((N=E.pathname)==null?void 0:N.startsWith(p)),`When overriding the location using \`<Routes location>\` or \`useRoutes(routes, location)\`, the location pathname must begin with the portion of the URL pathname that was matched by all parent routes. The current pathname base is "${p}" but pathname "${E.pathname}" was given in the \`location\` prop.`),v=E}else v=b;let w=v.pathname||"/",y=w;if(p!=="/"){let E=p.replace(/^\//,"").split("/");y="/"+w.replace(/^\//,"").split("/").slice(E.length).join("/")}let C=a&&a.state.matches.length?a.state.matches.map(E=>Object.assign(E,{route:a.manifest[E.route.id]||E.route})):l2(t,{pathname:y});pr(g||C!=null,`No routes matched location "${v.pathname}${v.search}${v.hash}" `),pr(C==null||C[C.length-1].route.element!==void 0||C[C.length-1].route.Component!==void 0||C[C.length-1].route.lazy!==void 0,`Matched leaf route at location "${v.pathname}${v.search}${v.hash}" does not have an element or Component. This means it will render an <Outlet /> with a null value by default resulting in an "empty" page.`);let S=SN(C&&C.map(E=>Object.assign({},E,{params:Object.assign({},d,E.params),pathname:Sr([p,o.encodeLocation?o.encodeLocation(E.pathname.replace(/%/g,"%25").replace(/\?/g,"%3F").replace(/#/g,"%23")).pathname:E.pathname]),pathnameBase:E.pathnameBase==="/"?p:Sr([p,o.encodeLocation?o.encodeLocation(E.pathnameBase.replace(/%/g,"%25").replace(/\?/g,"%3F").replace(/#/g,"%23")).pathname:E.pathnameBase])})),c,a);return n&&S?f.createElement(Jl.Provider,{value:{location:{pathname:"/",search:"",hash:"",state:null,key:"default",mask:void 0,...v},navigationType:"POP"}},S):S}function yN(){let t=MN(),n=rN(t)?`${t.status} ${t.statusText}`:t instanceof Error?t.message:JSON.stringify(t),a=t instanceof Error?t.stack:null,o="rgba(200,200,200, 0.5)",c={padding:"0.5rem",backgroundColor:o},l={padding:"2px 4px",backgroundColor:o},d=null;return console.error("Error handled by React Router default ErrorBoundary:",t),d=f.createElement(f.Fragment,null,f.createElement("p",null,"💿 Hey developer 👋"),f.createElement("p",null,"You can provide a way better UX than this when your app throws errors by providing your own ",f.createElement("code",{style:l},"ErrorBoundary")," or"," ",f.createElement("code",{style:l},"errorElement")," prop on your route.")),f.createElement(f.Fragment,null,f.createElement("h2",null,"Unexpected Application Error!"),f.createElement("h3",{style:{fontStyle:"italic"}},n),a?f.createElement("pre",{style:c},a):null,d)}var wN=f.createElement(yN,null),C2=class extends f.Component{constructor(t){super(t),this.state={location:t.location,revalidation:t.revalidation,error:t.error}}static getDerivedStateFromError(t){return{error:t}}static getDerivedStateFromProps(t,n){return n.location!==t.location||n.revalidation!=="idle"&&t.revalidation==="idle"?{error:t.error,location:t.location,revalidation:t.revalidation}:{error:t.error!==void 0?t.error:n.error,location:n.location,revalidation:t.revalidation||n.revalidation}}componentDidCatch(t,n){this.props.onError?this.props.onError(t,n):console.error("React Router caught the following error during render",t)}render(){let t=this.state.error;if(this.context&&typeof t=="object"&&t&&"digest"in t&&typeof t.digest=="string"){const a=pN(t.digest);a&&(t=a)}let n=t!==void 0?f.createElement(Tr.Provider,{value:this.props.routeContext},f.createElement(Jh.Provider,{value:t,children:this.props.component})):this.props.children;return this.context?f.createElement(jN,{error:t},n):n}};C2.contextType=x2;var vp=new WeakMap;function jN({children:t,error:n}){let{basename:a}=f.useContext(qn);if(typeof n=="object"&&n&&"digest"in n&&typeof n.digest=="string"){let o=mN(n.digest);if(o){let c=vp.get(n);if(c)throw c;let l=h2(o.location,a),d=l.absoluteURL||l.to;if(iN(d))throw new Error("Invalid redirect location");if(p2&&!vp.get(n))if(l.isExternal||o.reloadDocument)window.location.href=d;else{const m=Promise.resolve().then(()=>window.__reactRouterDataRouter.navigate(l.to,{replace:o.replace}));throw vp.set(n,m),m}return f.createElement("meta",{httpEquiv:"refresh",content:`0;url=${d}`})}}return t}function CN({routeContext:t,match:n,children:a}){let o=f.useContext(gi);return o&&o.static&&o.staticContext&&(n.route.errorElement||n.route.ErrorBoundary)&&(o.staticContext._deepestRenderedBoundaryId=n.route.id),f.createElement(Tr.Provider,{value:t},a)}function SN(t,n=[],a){let o=a==null?void 0:a.state;if(t==null){if(!o)return null;if(o.errors)t=o.matches;else if(n.length===0&&!o.initialized&&o.matches.length>0)t=o.matches;else return null}let c=t,l=o==null?void 0:o.errors;if(l!=null){let b=c.findIndex(v=>v.route.id&&(l==null?void 0:l[v.route.id])!==void 0);kt(b>=0,`Could not find a matching route for errors on route IDs: ${Object.keys(l).join(",")}`),c=c.slice(0,Math.min(c.length,b+1))}let d=!1,m=-1;if(a&&o){d=o.renderFallback;for(let b=0;bLine truncated
|
||||
.`.concat(Bk,` {
|
||||
overflow: hidden `).concat(o,`;
|
||||
padding-right: `).concat(m,"px ").concat(o,`;
|
||||
}
|
||||
body[`).concat(li,`] {
|
||||
overflow: hidden `).concat(o,`;
|
||||
overscroll-behavior: contain;
|
||||
`).concat([n&&"position: relative ".concat(o,";"),a==="margin"&&`
|
||||
padding-left: `.concat(c,`px;
|
||||
padding-top: `).concat(l,`px;
|
||||
padding-right: `).concat(d,`px;
|
||||
margin-left:0;
|
||||
margin-top:0;
|
||||
margin-right: `).concat(m,"px ").concat(o,`;
|
||||
`),a==="padding"&&"padding-right: ".concat(m,"px ").concat(o,";")].filter(Boolean).join(""),`
|
||||
}
|
||||
|
||||
.`).concat(Zu,` {
|
||||
right: `).concat(m,"px ").concat(o,`;
|
||||
}
|
||||
|
||||
.`).concat(Qu,` {
|
||||
margin-right: `).concat(m,"px ").concat(o,`;
|
||||
}
|
||||
|
||||
.`).concat(Zu," .").concat(Zu,` {
|
||||
right: 0 `).concat(o,`;
|
||||
}
|
||||
|
||||
.`).concat(Qu," .").concat(Qu,` {
|
||||
margin-right: 0 `).concat(o,`;
|
||||
}
|
||||
|
||||
body[`).concat(li,`] {
|
||||
`).concat($k,": ").concat(m,`px;
|
||||
}
|
||||
`)},f0=function(){var t=parseInt(document.body.getAttribute(li)||"0",10);return isFinite(t)?t:0},o9=function(){f.useEffect(function(){return document.body.setAttribute(li,(f0()+1).toString()),function(){var t=f0()-1;t<=0?document.body.removeAttribute(li):document.body.setAttribute(li,t.toString())}},[])},i9=function(t){var n=t.noRelative,a=t.noImportant,o=t.gapMode,c=o===void 0?"margin":o;o9();var l=f.useMemo(function(){return r9(c)},[c]);return f.createElement(a9,{styles:s9(l,!n,c,a?"":"!important")})},vh=!1;if(typeof window<"u")try{var Ou=Object.defineProperty({},"passive",{get:function(){return vh=!0,!0}});window.addEventListener("test",Ou,Ou),window.removeEventListener("test",Ou,Ou)}catch{vh=!1}var Qo=vh?{passive:!1}:!1,l9=function(t){return t.tagName==="TEXTAREA"},sj=function(t,n){if(!(t instanceof Element))return!1;var a=window.getComputedStyle(t);return a[n]!=="hidden"&&!(a.overflowY===a.overflowX&&!l9(t)&&a[n]==="visible")},c9=function(t){return sj(t,"overflowY")},u9=function(t){return sj(t,"overflowX")},m0=function(t,n){var a=n.ownerDocument,o=n;do{typeof ShadowRoot<"u"&&o instanceof ShadowRoot&&(o=o.host);var c=oj(t,o);if(c){var l=ij(t,o),d=l[1],m=l[2];if(d>m)return!0}o=o.parentNode}while(o&&o!==a.body);return!1},d9=function(t){var n=t.scrollTop,a=t.scrollHeight,o=t.clientHeight;return[n,a,o]},f9=function(t){var n=t.scrollLeft,a=t.scrollWidth,o=t.clientWidth;return[n,a,o]},oj=function(t,n){return t==="v"?c9(n):u9(n)},ij=function(t,n){return t==="v"?d9(n):f9(n)},m9=function(t,n){return t==="h"&&n==="rtl"?-1:1},p9=function(t,n,a,o,c){var l=m9(t,window.getComputedStyle(n).direction),d=l*o,m=a.target,p=n.contains(m),g=!1,b=d>0,v=0,w=0;do{if(!m)break;var y=ij(t,m),C=y[0],S=y[1],N=y[2],E=S-N-l*C;(C||E)&&oj(t,m)&&(v+=E,w+=C);var T=m.parentNode;m=T&&T.nodeType===Node.DOCUMENT_FRAGMENT_NODE?T.host:T}while(!p&&m!==document.body||p&&(n.contains(m)||n===m));return(b&&Math.abs(v)<1||!b&&Math.abs(w)<1)&&(g=!0),g},Lu=function(t){return"changedTouches"in t?[t.changedTouches[0].clientX,t.changedTouches[0].clientY]:[0,0]},p0=function(t){return[t.deltaX,t.deltaY]},h0=function(t){return t&&"current"in t?t.current:t},h9=function(t,n){return t[0]===n[0]&&t[1]===n[1]},g9=function(t){return`
|
||||
.block-interactivity-`.concat(t,` {pointer-events: none;}
|
||||
.allow-interactivity-`).concat(t,` {pointer-events: all;}
|
||||
`)},x9=0,Jo=[];function b9(t){var n=f.useRef([]),a=f.useRef([0,0]),o=f.useRef(),c=f.useState(x9++)[0],l=f.useState(aj)[0],d=f.useRef(t);f.useEffect(function(){d.current=t},[t]),f.useEffect(function(){if(t.inert){document.body.classList.add("block-interactivity-".concat(c));var S=Uk([t.lockRef.current],(t.shards||[]).map(h0),!0).filter(Boolean);return S.forEach(function(N){return N.classList.add("allow-interactivity-".concat(c))}),function(){document.body.classList.remove("block-interactivity-".concat(c)),S.forEach(function(N){return N.classList.remove("allow-interactivity-".concat(c))})}}},[t.inert,t.lockRef.current,t.shards]);var m=f.useCallback(function(S,N){if("touches"in S&&S.touches.length===2||S.type==="wheel"&&S.ctrlKey)return!d.current.allowPinchZoom;var E=Lu(S),T=a.current,M="deltaX"in S?S.deltaX:T[0]-E[0],k="deltaY"in S?S.deltaY:T[1]-E[1],_,R=S.target,L=Math.abs(M)>Math.abs(k)?"h":"v";if("touches"in S&&L==="h"&&R.type==="range")return!1;var ne=window.getSelection(),X=ne&&ne.anchorNode,I=X?X===R||X.contains(R):!1;if(I)return!1;var H=m0(L,R);if(!H)return!0;if(H?_=L:(_=L==="v"?"h":"v",H=m0(L,R)),!H)return!1;if(!o.current&&"changedTouches"in S&&(M||k)&&(o.current=_),!_)return!0;var q=o.current||_;return p9(q,N,S,q==="h"?M:k)},[]),p=f.useCallback(function(S){var N=S;if(!(!Jo.length||Jo[Jo.length-1]!==l)){var E="deltaY"in N?p0(N):Lu(N),T=n.current.filter(function(_){return _.name===N.type&&(_.target===N.target||N.target===_.shadowParent)&&h9(_.delta,E)})[0];if(T&&T.should){N.cancelable&&N.preventDefault();return}if(!T){var M=(d.current.shards||[]).map(h0).filter(Boolean).filter(function(_){return _.contains(N.target)}),k=M.length>0?m(N,M[0]):!d.current.noIsolation;k&&N.cancelable&&N.preventDefault()}}},[]),g=f.useCallback(function(S,N,E,T){var M={name:S,delta:N,target:E,should:T,shadowParent:v9(E)};n.current.push(M),setTimeout(function(){n.current=n.current.filter(function(k){return k!==M})},1)},[]),b=f.useCallback(function(S){a.current=Lu(S),o.current=void 0},[]),v=f.useCallback(function(S){g(S.type,p0(S),S.target,m(S,t.lockRef.current))},[]),w=f.useCallback(function(S){g(S.type,Lu(S),S.target,m(S,t.lockRef.current))},[]);f.useEffect(function(){return Jo.push(l),t.setCallbacks({onScrollCapture:v,onWheelCapture:v,onTouchMoveCapture:w}),document.addEventListener("wheel",p,Qo),document.addEventListener("touchmove",p,Qo),document.addEventListener("touchstart",b,Qo),function(){Jo=Jo.filter(function(S){return S!==l}),document.removeEventListener("wheel",p,Qo),document.removeEventListener("touchmove",p,Qo),document.removeEventListener("touchstart",b,Qo)}},[]);var y=t.removeScrollBar,C=t.inert;return f.createElement(f.Fragment,null,C?f.createElement(l,{styles:g9(c)}):null,y?f.createElement(i9,{noRelative:t.noRelative,gapMode:t.gapMode}):null)}function v9(t){for(var n=null;t!==null;)t instanceof ShadowRoot&&(n=t.host,t=t.host),t=t.parentNode;return n}const y9=Kk(rj,b9);var ac=f.forwardRef(function(t,n){return f.createElement(Hd,Ur({},t,{ref:n,sideCar:y9}))});ac.classNames=Hd.classNames;var w9=Object.defineProperty,ot=(t,n)=>w9(t,"name",{value:n,configurable:!0}),yh=["Enter"," "],j9=["ArrowDown","PageUp","Home"],lj=["ArrowUp","PageDown","End"],C9=[...j9,...lj],S9={ltr:[...yh,"ArrowRight"],rtl:[...yh,"ArrowLeft"]},N9={ltr:["ArrowLeft"],rtl:["ArrowRight"]},Gd="Menu",[Hl,E9,k9]=Td(Gd),[io,cj]=bn(Gd,[k9,Cs,Bd]),Rg=Cs(),uj=Bd(),[T9,lo]=io(Gd),[M9,sc]=io(Gd),R9=ot(t=>{const{__scopeMenu:n,open:a=!1,children:o,dir:c,onOpenChange:l,modal:d=!0}=t,m=Rg(n),[p,g]=f.useState(null),b=f.useRef(!1),v=Vt(l),w=Si(c);return f.useEffect(()=>{const y=ot(()=>{b.current=!0,document.addEventListener("pointerdown",C,{capture:!0,once:!0}),document.addEventListener("pointermove",C,{capture:!0,once:!0})},"handleKeyDown"),C=ot(()=>b.current=!1,"handlePointer");return document.addEventListener("keydown",y,{capture:!0}),()=>{document.removeEventListener("keydown",y,{capture:!0}),document.removeEventListener("pointerdown",C,{capture:!0}),document.removeEventListener("pointermove",C,{capture:!0})}},[]),f.useEffect(()=>{if(!a)return;const y=ot(()=>v(!1),"handleBlur");return window.addEventListener("blur",y),()=>window.removeEventListener("blur",y)},[a,v]),s.jsx(zd,{...m,children:s.jsx(T9,{scope:n,open:a,onOpenChange:v,content:p,onContentChange:g,children:s.jsx(M9,{scope:n,onClose:f.useCallback(()=>v(!1),[v]),isUsingKeyboardRef:b,dir:w,modal:d,children:o})})})},"Menu"),dj=f.forwardRef(ot(function(n,a){const{__scopeMenu:o,...c}=n,l=Rg(o);return s.jsx(Pd,{...l,...c,ref:a})},"MenuAnchor")),fj="MenuPortal",[A9,mj]=io(fj,{forceMount:void 0}),_9=ot(t=>{const{__scopeMenu:n,forceMount:a,children:o,container:c}=t,l=lo(fj,n);return s.jsx(A9,{scope:n,forceMount:a,children:s.jsx(fn,{present:a||l.open,children:s.jsx(rc,{asChild:!0,container:c,children:o})})})},"MenuPortal"),Nr="MenuContent",[D9,Ag]=io(Nr),O9=f.forwardRef(ot(function(n,a){const o=mj(Nr,n.__scopeMenu),{forceMount:c=o.forceMount,...l}=n,d=lo(Nr,n.__scopeMenu),m=sc(Nr,n.__scopeMenu);return s.jsx(
Line truncated
|
||||
* @license lucide-react v0.475.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const MT=t=>t.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase(),Dj=(...t)=>t.filter((n,a,o)=>!!n&&n.trim()!==""&&o.indexOf(n)===a).join(" ").trim();/**
|
||||
* @license lucide-react v0.475.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/var RT={xmlns:"http://www.w3.org/2000/svg",width:24,height:24,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round"};/**
|
||||
* @license lucide-react v0.475.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const AT=f.forwardRef(({color:t="currentColor",size:n=24,strokeWidth:a=2,absoluteStrokeWidth:o,className:c="",children:l,iconNode:d,...m},p)=>f.createElement("svg",{ref:p,...RT,width:n,height:n,stroke:t,strokeWidth:o?Number(a)*24/Number(n):a,className:Dj("lucide",c),...m},[...d.map(([g,b])=>f.createElement(g,b)),...Array.isArray(l)?l:[l]]));/**
|
||||
* @license lucide-react v0.475.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const Ra=(t,n)=>{const a=f.forwardRef(({className:o,...c},l)=>f.createElement(AT,{ref:l,iconNode:n,className:Dj(`lucide-${MT(t)}`,o),...c}));return a.displayName=`${t}`,a};/**
|
||||
* @license lucide-react v0.475.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const _T=[["path",{d:"M20 6 9 17l-5-5",key:"1gmf2c"}]],Oj=Ra("Check",_T);/**
|
||||
* @license lucide-react v0.475.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const DT=[["path",{d:"m6 9 6 6 6-6",key:"qrunsl"}]],Lj=Ra("ChevronDown",DT);/**
|
||||
* @license lucide-react v0.475.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const OT=[["path",{d:"m9 18 6-6-6-6",key:"mthhwq"}]],LT=Ra("ChevronRight",OT);/**
|
||||
* @license lucide-react v0.475.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const zT=[["path",{d:"m18 15-6-6-6 6",key:"153udz"}]],PT=Ra("ChevronUp",zT);/**
|
||||
* @license lucide-react v0.475.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const IT=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"m9 12 2 2 4-4",key:"dzmm74"}]],x0=Ra("CircleCheck",IT);/**
|
||||
* @license lucide-react v0.475.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const UT=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}]],BT=Ra("Circle",UT);/**
|
||||
* @license lucide-react v0.475.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const $T=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",key:"afitv7"}],["path",{d:"M9 3v18",key:"fh3hqa"}]],HT=Ra("PanelLeft",$T);/**
|
||||
* @license lucide-react v0.475.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const GT=[["circle",{cx:"11",cy:"11",r:"8",key:"4ej97u"}],["path",{d:"m21 21-4.3-4.3",key:"1qie3q"}]],VT=Ra("Search",GT);/**
|
||||
* @license lucide-react v0.475.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const FT=[["path",{d:"M18 6 6 18",key:"1bl5f8"}],["path",{d:"m6 6 12 12",key:"d8bk6v"}]],zj=Ra("X",FT),zg=ET,Pg=kT,YT=f.forwardRef(({className:t,inset:n,children:a,...o},c)=>s.jsxs(Aj,{ref:c,className:pe("flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",n&&"pl-8",t),...o,children:[a,s.jsx(LT,{className:"ml-auto"})]}));YT.displayName=Aj.displayName;const qT=f.forwardRef(({className:t,...n},a)=>s.jsx(_j,{ref:a,className:pe("z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]",t),...n}));qT.displayName=_j.displayName;const Fd=f.forwardRef(({className:t,sideOffset:n=4,...a},o)=>s.jsx(TT,{children:s.jsx(Sj,{ref:o,sideOffset:n,className:pe("z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md","data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]",t),...a})}));Fd.displayName=Sj.displayName;const Vl=f.forwardRef(({className:t,inset:n,...a},o)=>s.jsx(Ej,{ref:o,className:pe("relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0",n&&"pl-8",t),...a}));Vl.displayName=Ej.displayName;const KT=f.forwardRef(({className:t,children:n,checked:a,...o},c)=>s.jsxs(kj,{ref:c,className:pe("relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",t),checked:a,...o,children:[s.jsx("span",{className:"absolute left-2 flex h-3.5 w-3.5 items-center justify-center",children:s.jsx(Mj,{children:s.jsx(Oj,{className:"h-4 w-4"})})}),n]}));KT.displayName=kj.displayName;const WT=f.forwardRef(({className:t,children:n,...a},o)=>s.jsxs(Tj,{ref:o,className:pe("relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",t),...a,children:[s.jsx("span",{className:"absolute left-2 flex h-3.5 w-3.5 items-center justify-center",children:s.jsx(Mj,{children:s.jsx(BT,{className:"h-2 w-2 fill-current"})})}),n]}));WT.displayName=Tj.displayName;const Ig=f.forwardRef(({className:t,inset:n,...a},o)=>s.jsx(Nj,{ref:o,className:pe("px-2 py-1.5 text-sm font-semibold",n&&"pl-8",t),...a}));Ig.displayName=Nj.displayName;const Fl=f.forwardRef(({className:t,...n},a)=>s.jsx(Rj,{ref:a,className:pe("-mx-1 my-1 h-px bg-muted",t),...n}));Fl.displayName=Rj.displayName;const zp=768;function XT(){const[t,n]=f.useState(void 0);return f.useEffect(()=>{const a=window.matchMedia(`(max-width: ${zp-1}px)`),o=()=>{n(window.innerWidth<zp)};return a.addEventListener("change",o),n(window.innerWidth<zp),()=>a.removeEventListener("change",o)},[]),!!t}const _e=f.forwardRef(({className:t,type:n,...a},o)=>s.jsx("input",{type:n,className:pe("flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",t),ref:o,...a}));_e.displayName="Input";var ZT=Object.defineProperty,Pj=(t,n)=>ZT(t,"name",{value:n,configurable:!0}),b0="horizontal",QT=["horizontal","vertical"],JT=f.forwardRef(Pj(function(n,a){const{decorative:o,orientation:c=b0,...l}=n,d=Ij(c)?c:b0,p=o?{role:"none"}:{"aria-orientation":d==="vertical"?d:void 0,role:"separator"};return s.jsx(Me.div,{"data-orientation":d,...p,...l,ref:a})},"Separator"));function Ij(t){return QT.includes(t)}Pj(Ij,"isValidOrientation");var Uj=JT;const Bj=f.forwardRef(({className:t,orientation:n="horizontal",decorative:a=!0,...o},c)=>s.jsx(Uj,{ref:c,decorative:a,orientation:n,className:pe(Line truncated
|
||||
--num-groups: ${l.groups.length};
|
||||
--indicator-opacity: ${l.searchMode?0:1};
|
||||
--num-skintones: ${z0};`}),d(()=>{l.customEmoji&&l.database&&L()}),d(()=>{l.customEmoji&&l.customEmoji.length?l.groups!==Fh&&(l.groups=Fh):l.groups!==Hp&&(l.currentGroupIndex&&l.currentGroupIndex--,l.groups=Hp)}),d(()=>{async function $(){l.databaseLoaded&&(l.currentSkinTone=await l.database.getPreferredSkinTone())}$()}),d(()=>{l.skinTones=Array(z0).fill().map(($,ie)=>ID(l.skinToneEmoji,ie))}),d(()=>{l.skinToneButtonText=l.skinTones[l.currentSkinTone]}),d(()=>{l.skinToneButtonLabel=l.i18n.skinToneLabel.replace("{skinTone}",l.i18n.skinTones[l.currentSkinTone])}),d(()=>{async function $(){const{database:ie}=l,ye=(await Promise.all(TD.map(Se=>ie.getEmojiByUnicodeOrName(Se)))).filter(Boolean);l.defaultFavoriteEmojis=ye}l.databaseLoaded&&$()});function L(){const{customEmoji:$,database:ie}=l,ye=$||Kp;ie.customEmoji!==ye&&(ie.customEmoji=ye)}d(()=>{async function $(){L();const{database:ie,defaultFavoriteEmojis:ye,numColumns:Se}=l,Be=await ie.getTopFavoriteEmoji(Se),Ke=await de(XS([...Be,...ye],We=>We.unicode||We.name).slice(0,Se));l.currentFavorites=Ke}l.databaseLoaded&&l.defaultFavoriteEmojis&&$()});function ne($){$D($,c,()=>{{const ie=getComputedStyle(a.rootElement),ye=parseInt(ie.getPropertyValue("--num-columns"),10),Se=ie.getPropertyValue("direction")==="rtl";l.numColumns=ye,l.isRtl=Se}})}function X($){iO($,c,ie=>{for(const{target:ye,isIntersecting:Se}of ie)ye.classList.toggle("onscreen",Se)})}d(()=>{async function $(){const{searchText:ie,currentGroup:ye,databaseLoaded:Se,customEmoji:Be}=l;if(!Se)l.currentEmojis=[],l.searchMode=!1;else if(ie.length>=CD){const Ke=await P(ie);l.searchText===ie&&(y(Ke),C(!0))}else{const{id:Ke}=ye;if(Ke!==-1||Be&&Be.length){const We=await se(Ke);l.currentGroup.id===Ke&&(y(We),C(!1))}}}$()});const I=()=>{ed(()=>FD(a.tabpanelElement))};d(()=>{const{currentEmojis:$,emojiVersion:ie}=l,ye=$.filter(Se=>Se.unicode).filter(Se=>P0(Se)&&!Yh.has(Se.unicode));if(!ie&&ye.length)y($),ed(()=>H(ye));else{const Se=ie?$:$.filter(q);y(Se),I()}});function H($){GD($,a.baselineEmoji,g)?I():l.currentEmojis=[...l.currentEmojis]}function q($){return!$.unicode||!P0($)||Yh.get($.unicode)}async function ce($){const ie=l.emojiVersion||await Vp();return $.filter(({version:ye})=>!ye||ye<=ie)}async function de($){return UD($,l.emojiVersion||await Vp())}async function se($){const ie=$===-1?l.customEmoji:await l.database.getEmojiByGroup($);return de(await ce(ie))}async function P($){return de(await ce(await l.database.getEmojiBySearchQuery($)))}d(()=>{}),d(()=>{function $(){const{searchMode:ye,currentEmojis:Se}=l;if(ye)return[{category:"",emojis:Se}];const Be=new Map;for(const Ke of Se){const We=Ke.category||"";let qt=Be.get(We);qt||(qt=[],Be.set(We,qt)),qt.push(Ke)}return[...Be.entries()].map(([Ke,We])=>({category:Ke,emojis:We})).sort((Ke,We)=>l.customCategorySorting(Ke.category,We.category))}const ie=$();S(ie)}),d(()=>{l.activeSearchItemId=l.activeSearchItem!==-1&&l.currentEmojis[l.activeSearchItem].id}),d(()=>{const{rawSearchText:$}=l;KS(()=>{l.searchText=($||"").trim(),l.activeSearchItem=-1})});function W($){if(!l.searchMode||!l.currentEmojis.length)return;const ie=ye=>{Ir($),l.activeSearchItem=Fp(ye,l.activeSearchItem,l.currentEmojis)};switch($.key){case"ArrowDown":return ie(!1);case"ArrowUp":return ie(!0);case"Enter":if(l.activeSearchItem===-1)l.activeSearchItem=0;else return Ir($),O(l.currentEmojis[l.activeSearchItem].id)}}function oe($){const{target:ie}=$,ye=ie.closest(".nav-button");if(!ye)return;const Se=parseInt(ye.dataset.groupId,10);a.searchElement.value="",l.rawSearchText="",l.searchText="",l.activeSearchItem=-1,l.currentGroupIndex=l.groups.findIndex(Be=>Be.id===Se)}function ae($){const{target:ie,key:ye}=$,Se=Be=>{Be&&(Ir($),Be.focus())};switch(ye){case"ArrowLeft":return Se(ie.previousElementSibling);case"ArrowRight":return Se(ie.nextElementSibling);case"Home":return Se(ie.parentElement.firstElementChild);case"End":return Se(ie.parentElement.lastElementChild)}}async function G($){const ie=await l.database.getEmojiByUnicodeOrName($),ye=[...l.currentEmojis,...l.currentFavorites].find(Be=>Be.id===$),Se=ye.unicode&&N(ye,l.currentSkinTone);return await l.database.incrementFavoriteEmojiCount($),{emoji:ie,skinTone:l.currentSkinTone,...Se&&{unicode:Se},...ye.name&&{name:ye.name}}}async function O($){const ie=G($);b("emoji-click-sync",ie),b("emoji-click",await ie)}function D($){const{target:ie}=$;if(!ie.classList.contains("emoji"))return;Ir($);const ye=ie.id.substring(4);O(ye)}function B($){l.currentSkinTone=$,l.skinTonePickerExpanded=!1,p("skintone-button"),b("skin-tone-change",{skinTone:$}),l.database.setPreferredSkinTone($)}function V($){const{target:{id:ie}}=$,ye=ie&&ie.match(/^skintone-(\d)/);if(!ye)return;Ir($);const Se=parseInt(ye[1],10);B(Se)}function Z($){l.skinTonePickerExpanded=!l.skinTonePickerExpanded,l.activeSkinTone=l.currentSkinTone,l.skinTonePickerExpanded&&(Ir($),ed(()=>p("skintone-list")))}d(()=>{l.skinTonePickerExpanded?a.skinToneDropdown.addEventListener("transitionend",()=>{l.skin
Line truncated
|
||||
`);function Hu(t){return t.label!==void 0}var IO=3,UO="32px",BO="16px",q0=4e3,$O=356,HO=14,GO=20,VO=200;function jr(...t){return t.filter(Boolean).join(" ")}function FO(t){let[n,a]=t.split("-"),o=[];return n&&o.push(n),a&&o.push(a),o}var YO=t=>{var n,a,o,c,l,d,m,p,g,b,v;let{invert:w,toast:y,unstyled:C,interacting:S,setHeights:N,visibleToasts:E,heights:T,index:M,toasts:k,expanded:_,removeToast:R,defaultRichColors:L,closeButton:ne,style:X,cancelButtonStyle:I,actionButtonStyle:H,className:q="",descriptionClassName:ce="",duration:de,position:se,gap:P,loadingIcon:W,expandByDefault:oe,classNames:ae,icons:G,closeButtonAriaLabel:O="Close toast",pauseWhenPageIsHidden:D}=t,[B,V]=ve.useState(null),[Z,be]=ve.useState(null),[K,ge]=ve.useState(!1),[me,$]=ve.useState(!1),[ie,ye]=ve.useState(!1),[Se,Be]=ve.useState(!1),[Ke,We]=ve.useState(!1),[qt,vn]=ve.useState(0),[Xn,br]=ve.useState(0),yn=ve.useRef(y.duration||de||q0),wn=ve.useRef(null),Zn=ve.useRef(null),Kt=M===0,po=M+1<=E,Pt=y.type,Qn=y.dismissible!==!1,Da=y.className||"",Es=y.descriptionClassName||"",qr=ve.useMemo(()=>T.findIndex(De=>De.toastId===y.id)||0,[T,y.id]),ho=ve.useMemo(()=>{var De;return(De=y.closeButton)!=null?De:ne},[y.closeButton,ne]),Kr=ve.useMemo(()=>y.duration||de||q0,[y.duration,de]),Dn=ve.useRef(0),yt=ve.useRef(0),On=ve.useRef(0),Rt=ve.useRef(null),[Ai,_i]=se.split("-"),go=ve.useMemo(()=>T.reduce((De,rt,lt)=>lt>=qr?De:De+rt.height,0),[T,qr]),Wr=RO(),Oa=y.invert||w,Ar=Pt==="loading";yt.current=ve.useMemo(()=>qr*P+go,[qr,go]),ve.useEffect(()=>{yn.current=Kr},[Kr]),ve.useEffect(()=>{ge(!0)},[]),ve.useEffect(()=>{let De=Zn.current;if(De){let rt=De.getBoundingClientRect().height;return br(rt),N(lt=>[{toastId:y.id,height:rt,position:y.position},...lt]),()=>N(lt=>lt.filter(rn=>rn.toastId!==y.id))}},[N,y.id]),ve.useLayoutEffect(()=>{if(!K)return;let De=Zn.current,rt=De.style.height;De.style.height="auto";let lt=De.getBoundingClientRect().height;De.style.height=rt,br(lt),N(rn=>rn.find(It=>It.toastId===y.id)?rn.map(It=>It.toastId===y.id?{...It,height:lt}:It):[{toastId:y.id,height:lt,position:y.position},...rn])},[K,y.title,y.description,N,y.id]);let Wt=ve.useCallback(()=>{$(!0),vn(yt.current),N(De=>De.filter(rt=>rt.toastId!==y.id)),setTimeout(()=>{R(y)},VO)},[y,R,N,yt]);ve.useEffect(()=>{if(y.promise&&Pt==="loading"||y.duration===1/0||y.type==="loading")return;let De;return _||S||D&&Wr?(()=>{if(On.current<Dn.current){let rt=new Date().getTime()-Dn.current;yn.current=yn.current-rt}On.current=new Date().getTime()})():yn.current!==1/0&&(Dn.current=new Date().getTime(),De=setTimeout(()=>{var rt;(rt=y.onAutoClose)==null||rt.call(y,y),Wt()},yn.current)),()=>clearTimeout(De)},[_,S,y,Pt,D,Wr,Wt]),ve.useEffect(()=>{y.delete&&Wt()},[Wt,y.delete]);function La(){var De,rt,lt;return G!=null&&G.loading?ve.createElement("div",{className:jr(ae==null?void 0:ae.loader,(De=y==null?void 0:y.classNames)==null?void 0:De.loader,"sonner-loader"),"data-visible":Pt==="loading"},G.loading):W?ve.createElement("div",{className:jr(ae==null?void 0:ae.loader,(rt=y==null?void 0:y.classNames)==null?void 0:rt.loader,"sonner-loader"),"data-visible":Pt==="loading"},W):ve.createElement(SO,{className:jr(ae==null?void 0:ae.loader,(lt=y==null?void 0:y.classNames)==null?void 0:lt.loader),visible:Pt==="loading"})}return ve.createElement("li",{tabIndex:0,ref:Zn,className:jr(q,Da,ae==null?void 0:ae.toast,(n=y==null?void 0:y.classNames)==null?void 0:n.toast,ae==null?void 0:ae.default,ae==null?void 0:ae[Pt],(a=y==null?void 0:y.classNames)==null?void 0:a[Pt]),"data-sonner-toast":"","data-rich-colors":(o=y.richColors)!=null?o:L,"data-styled":!(y.jsx||y.unstyled||C),"data-mounted":K,"data-promise":!!y.promise,"data-swiped":Ke,"data-removed":me,"data-visible":po,"data-y-position":Ai,"data-x-position":_i,"data-index":M,"data-front":Kt,"data-swiping":ie,"data-dismissible":Qn,"data-type":Pt,"data-invert":Oa,"data-swipe-out":Se,"data-swipe-direction":Z,"data-expanded":!!(_||oe&&K),style:{"--index":M,"--toasts-before":M,"--z-index":k.length-M,"--offset":`${me?qt:yt.current}px`,"--initial-height":oe?"auto":`${Xn}px`,...X,...y.style},onDragEnd:()=>{ye(!1),V(null),Rt.current=null},onPointerDown:De=>{Ar||!Qn||(wn.current=new Date,vn(yt.current),De.target.setPointerCapture(De.pointerId),De.target.tagName!=="BUTTON"&&(ye(!0),Rt.current={x:De.clientX,y:De.clientY}))},onPointerUp:()=>{var De,rt,lt,rn;if(Se||!Qn)return;Rt.current=null;let It=Number(((De=Zn.current)==null?void 0:De.style.getPropertyValue("--swipe-amount-x").replace("px",""))||0),Jn=Number(((rt=Zn.current)==null?void 0:rt.style.getPropertyValue("--swipe-amount-y").replace("px",""))||0),er=new Date().getTime()-((lt=wn.current)==null?void 0:lt.getTime()),bt=B==="x"?It:Jn,Ln=Math.abs(bt)/er;if(Math.abs(bt)>=GO||Ln>.11){vn(yt.current),(rn=y.onDismiss)==null||rn.call(y,y),be(B==="x"?It>0?"right":"left":Jn>0?"down":"up"),Wt(),Be(!0),We(!1);return}ye(!1),V(null)},onPointerMove:De=>{var rt,lt,rn,It;if(!Rt.current||!Qn||((rt=window.getSelection())==null?void 0:rt.toS
Line truncated
|
||||
In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}function X0(t,n){var a=Object.keys(t);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(t);n&&(o=o.filter(function(c){return Object.getOwnPropertyDescriptor(t,c).enumerable})),a.push.apply(a,o)}return a}function Z0(t){for(var n=1;n<arguments.length;n++){var a=arguments[n]!=null?arguments[n]:{};n%2?X0(Object(a),!0).forEach(function(o){JO(t,o,a[o])}):Object.getOwnPropertyDescriptors?Object.defineProperties(t,Object.getOwnPropertyDescriptors(a)):X0(Object(a)).forEach(function(o){Object.defineProperty(t,o,Object.getOwnPropertyDescriptor(a,o))})}return t}function nL(t,n){if(t==null)return{};var a,o,c=rL(t,n);if(Object.getOwnPropertySymbols){var l=Object.getOwnPropertySymbols(t);for(o=0;o<l.length;o++)a=l[o],n.indexOf(a)===-1&&{}.propertyIsEnumerable.call(t,a)&&(c[a]=t[a])}return c}function rL(t,n){if(t==null)return{};var a={};for(var o in t)if({}.hasOwnProperty.call(t,o)){if(n.indexOf(o)!==-1)continue;a[o]=t[o]}return a}function aL(t,n){return QO(t)||eL(t,n)||iL(t,n)||tL()}function sL(t,n){if(typeof t!="object"||!t)return t;var a=t[Symbol.toPrimitive];if(a!==void 0){var o=a.call(t,n);if(typeof o!="object")return o;throw new TypeError("@@toPrimitive must return a primitive value.")}return(n==="string"?String:Number)(t)}function oL(t){var n=sL(t,"string");return typeof n=="symbol"?n:n+""}function iL(t,n){if(t){if(typeof t=="string")return W0(t,n);var a={}.toString.call(t).slice(8,-1);return a==="Object"&&t.constructor&&(a=t.constructor.name),a==="Map"||a==="Set"?Array.from(t):a==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(a)?W0(t,n):void 0}}function lL(t,n,a){return n in t?Object.defineProperty(t,n,{value:a,enumerable:!0,configurable:!0,writable:!0}):t[n]=a,t}function Q0(t,n){var a=Object.keys(t);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(t);n&&(o=o.filter(function(c){return Object.getOwnPropertyDescriptor(t,c).enumerable})),a.push.apply(a,o)}return a}function J0(t){for(var n=1;n<arguments.length;n++){var a=arguments[n]!=null?arguments[n]:{};n%2?Q0(Object(a),!0).forEach(function(o){lL(t,o,a[o])}):Object.getOwnPropertyDescriptors?Object.defineProperties(t,Object.getOwnPropertyDescriptors(a)):Q0(Object(a)).forEach(function(o){Object.defineProperty(t,o,Object.getOwnPropertyDescriptor(a,o))})}return t}function cL(){for(var t=arguments.length,n=new Array(t),a=0;a<t;a++)n[a]=arguments[a];return function(o){return n.reduceRight(function(c,l){return l(c)},o)}}function Al(t){return function n(){for(var a=this,o=arguments.length,c=new Array(o),l=0;l<o;l++)c[l]=arguments[l];return c.length>=t.length?t.apply(this,c):function(){for(var d=arguments.length,m=new Array(d),p=0;p<d;p++)m[p]=arguments[p];return n.apply(a,[].concat(c,m))}}}function hd(t){return{}.toString.call(t).includes("Object")}function uL(t){return!Object.keys(t).length}function Ql(t){return typeof t=="function"}function dL(t,n){return Object.prototype.hasOwnProperty.call(t,n)}function fL(t,n){return hd(n)||us("changeType"),Object.keys(n).some(function(a){return!dL(t,a)})&&us("changeField"),n}function mL(t){Ql(t)||us("selectorType")}function pL(t){Ql(t)||hd(t)||us("handlerType"),hd(t)&&Object.values(t).some(function(n){return!Ql(n)})&&us("handlersType")}function hL(t){t||us("initialIsRequired"),hd(t)||us("initialType"),uL(t)&&us("initialContent")}function gL(t,n){throw new Error(t[n]||t.default)}var xL={initialIsRequired:"initial state is required",initialType:"initial state should be an object",initialContent:"initial state shouldn't be an empty object",handlerType:"handler should be an object or a function",handlersType:"all handlers should be a functions",selectorType:"selector should be a function",changeType:"provided value of changes should be an object",changeField:'it seams you want to change a field in the state which is not specified in the "initial" state',default:"an unknown error accured in `state-local` package"},us=Al(gL)(xL),Gu={changes:fL,selector:mL,handler:pL,initial:hL};function bL(t){var n=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{};Gu.initial(t),Gu.handler(n);var a={current:t},o=Al(wL)(a,n),c=Al(yL)(a),l=Al(Gu.changes)(t),d=Al(vL)(a);function m(){var g=arguments.length>0&&arguments[0]!==void 0?arguments[0]:function(b){return b};return Gu.selector(g),g(a.current)}function p(g){cL(o,c,l,d)(g)}return[m,p]}function vL(t,n){return Ql(n)?n(t.current):n}function yL(t,n){return t.current=J0(J0({},t.current),n),n}function wL(t,n,a){return Ql(n)?n(t.current):Object.keys(a).forEach(function(o){var c;return(c=n[o])===null||c===void 0?void 0:c.call(n,t.current[o])}),a}var jL={create:bL},CL={paths:{vs:"https://cdn.jsdelivr.net/npm/monaco-editor@0.55.1/min/vs"}};function SL(t){return function n(){for(var a=this,o=arguments.length,c=new Array(o),l=0;l<o;l++)c[l]=arguments[l];return c.length>=t.length?t.apply(this,c):function(){for(var d=arguments.length,m=new Array(d),p=0;p<d;p++)m[p]=arguments[p];
Line truncated
|
||||
You are using deprecated way of configuration.
|
||||
|
||||
Instead of using
|
||||
monaco.config({ urls: { monacoBase: '...' } })
|
||||
use
|
||||
monaco.config({ paths: { vs: '...' } })
|
||||
|
||||
For more please check the link https://github.com/suren-atoyan/monaco-loader#config
|
||||
`},e2=SL(TL)(t5),ML={config:EL},RL=function(){for(var n=arguments.length,a=new Array(n),o=0;o<n;o++)a[o]=arguments[o];return function(c){return a.reduceRight(function(l,d){return d(l)},c)}};function n5(t,n){return Object.keys(n).forEach(function(a){n[a]instanceof Object&&t[a]&&Object.assign(n[a],n5(t[a],n[a]))}),Z0(Z0({},t),n)}var AL={type:"cancelation",msg:"operation is manually canceled"};function Wp(t){var n=!1,a=new Promise(function(o,c){t.then(function(l){return n?c(AL):o(l)}),t.catch(c)});return a.cancel=function(){return n=!0},a}var _L=["monaco"],DL=jL.create({config:CL,isInitialized:!1,resolve:null,reject:null,monaco:null}),r5=aL(DL,2),pc=r5[0],nf=r5[1];function OL(t){var n=ML.config(t),a=n.monaco,o=nL(n,_L);nf(function(c){return{config:n5(c.config,o),monaco:a}})}function LL(){var t=pc(function(n){var a=n.monaco,o=n.isInitialized,c=n.resolve;return{monaco:a,isInitialized:o,resolve:c}});if(!t.isInitialized){if(nf({isInitialized:!0}),t.monaco)return t.resolve(t.monaco),Wp(Xp);if(window.monaco&&window.monaco.editor)return a5(window.monaco),t.resolve(window.monaco),Wp(Xp);RL(zL,IL)(UL)}return Wp(Xp)}function zL(t){return document.body.appendChild(t)}function PL(t){var n=document.createElement("script");return t&&(n.src=t),n}function IL(t){var n=pc(function(o){var c=o.config,l=o.reject;return{config:c,reject:l}}),a=PL("".concat(n.config.paths.vs,"/loader.js"));return a.onload=function(){return t()},a.onerror=n.reject,a}function UL(){var t=pc(function(a){var o=a.config,c=a.resolve,l=a.reject;return{config:o,resolve:c,reject:l}}),n=window.require;n.config(t.config),n(["vs/editor/editor.main"],function(a){var o=a.m||a;a5(o),t.resolve(o)},function(a){t.reject(a)})}function a5(t){pc().monaco||nf({monaco:t})}function BL(){return pc(function(t){var n=t.monaco;return n})}var Xp=new Promise(function(t,n){return nf({resolve:t,reject:n})}),s5={config:OL,init:LL,__getMonacoInstance:BL},$L={wrapper:{display:"flex",position:"relative",textAlign:"initial"},fullWidth:{width:"100%"},hide:{display:"none"}},Zp=$L,HL={container:{display:"flex",height:"100%",width:"100%",justifyContent:"center",alignItems:"center"}},GL=HL;function VL({children:t}){return ve.createElement("div",{style:GL.container},t)}var FL=VL,YL=FL;function qL({width:t,height:n,isEditorReady:a,loading:o,_ref:c,className:l,wrapperProps:d}){return ve.createElement("section",{style:{...Zp.wrapper,width:t,height:n},...d},!a&&ve.createElement(YL,null,o),ve.createElement("div",{ref:c,style:{...Zp.fullWidth,...!a&&Zp.hide},className:l}))}var KL=qL,o5=f.memo(KL);function WL(t){f.useEffect(t,[])}var i5=WL;function XL(t,n,a=!0){let o=f.useRef(!0);f.useEffect(o.current||!a?()=>{o.current=!1}:t,n)}var Yn=XL;function zl(){}function si(t,n,a,o){return ZL(t,o)||QL(t,n,a,o)}function ZL(t,n){return t.editor.getModel(l5(t,n))}function QL(t,n,a,o){return t.editor.createModel(n,a,o?l5(t,o):void 0)}function l5(t,n){return t.Uri.parse(n)}function JL({original:t,modified:n,language:a,originalLanguage:o,modifiedLanguage:c,originalModelPath:l,modifiedModelPath:d,keepCurrentOriginalModel:m=!1,keepCurrentModifiedModel:p=!1,theme:g="light",loading:b="Loading...",options:v={},height:w="100%",width:y="100%",className:C,wrapperProps:S={},beforeMount:N=zl,onMount:E=zl}){let[T,M]=f.useState(!1),[k,_]=f.useState(!0),R=f.useRef(null),L=f.useRef(null),ne=f.useRef(null),X=f.useRef(E),I=f.useRef(N),H=f.useRef(!1);i5(()=>{let se=s5.init();return se.then(P=>(L.current=P)&&_(!1)).catch(P=>(P==null?void 0:P.type)!=="cancelation"&&console.error("Monaco initialization: error:",P)),()=>R.current?de():se.cancel()}),Yn(()=>{if(R.current&&L.current){let se=R.current.getOriginalEditor(),P=si(L.current,t||"",o||a||"text",l||"");P!==se.getModel()&&se.setModel(P)}},[l],T),Yn(()=>{if(R.current&&L.current){let se=R.current.getModifiedEditor(),P=si(L.current,n||"",c||a||"text",d||"");P!==se.getModel()&&se.setModel(P)}},[d],T),Yn(()=>{let se=R.current.getModifiedEditor();se.getOption(L.current.editor.EditorOption.readOnly)?se.setValue(n||""):n!==se.getValue()&&(se.executeEdits("",[{range:se.getModel().getFullModelRange(),text:n||"",forceMoveMarkers:!0}]),se.pushUndoStop())},[n],T),Yn(()=>{var se,P;(P=(se=R.current)==null?void 0:se.getModel())==null||P.original.setValue(t||"")},[t],T),Yn(()=>{let{original:se,modified:P}=R.current.getModel();L.current.editor.setModelLanguage(se,o||a||"text"),L.current.editor.setModelLanguage(P,c||a||"text")},[a,o,c],T),Yn(()=>{var se;(se=L.current)==null||se.editor.setTheme(g)},[g],T),Yn(()=>{var se;(se=R.current)==null||se.updateOptions(v)},[v],T);let q=f.useCallback(()=>{var W;if(!L.current)return;I.current(L.current);let se=si(L.current,t||"",o||a||"text",l||""),P=si(L.current,n||"",c||a||"text",d||"");(W=R.current)==null||W.setModel({original:se,modified:P})},[a,n,c,t,o,l,d]),ce=f.useCallback(()=>{var se;!H.current&&ne.current&&(R.current=L.current.editor.createDiffEditor(ne.current,{automaticLayout:!0,...v}),q(),(se=L.current)==null||se.editor.setTheme(g),M(!0),H.current=!
Line truncated
|
||||
987654321098765432`,className:"min-h-[120px] font-mono text-sm"})]}),s.jsx(mo,{result:a.owners}),s.jsx("div",{className:"mt-auto",children:s.jsx(ue,{type:"button",variant:"secondary",disabled:o||!t.ownerUserIds.trim(),onClick:c,children:o?"Checking with Discord…":"Verify owner IDs"})})]})}function Mz({onContinue:t}){return s.jsx("div",{className:"mx-auto flex w-full max-w-lg flex-1 flex-col justify-center py-8",children:s.jsxs("div",{className:"rounded-2xl border border-border bg-card px-6 py-10 text-center md:px-10",children:[s.jsx("div",{className:"mx-auto mb-6 flex size-16 items-center justify-center rounded-2xl bg-primary/10 text-primary",children:s.jsx(xe,{icon:jd,size:40})}),s.jsx("h2",{className:"text-2xl font-semibold tracking-tight",children:"Setup complete"}),s.jsxs("p",{className:"mx-auto mt-3 max-w-sm text-sm leading-relaxed text-muted-foreground",children:["Your configuration has been saved. Restart the server for changes to take effect — stop the running process, then start it again (for example"," ",s.jsx("code",{className:"rounded-md bg-muted px-1.5 py-0.5 font-mono text-xs",children:"npm start"}),")."]}),s.jsx(Ye,{className:"mt-6 text-left",children:s.jsx(qe,{children:"After restarting, sign in with Discord to open the dashboard."})}),s.jsx(ue,{type:"button",size:"lg",className:"mt-8 min-w-[220px]",onClick:t,children:"Continue to sign in"})]})})}function Rz({currentIndex:t,validating:n,saving:a,onBack:o,onNext:c,onFinish:l}){const d=t===ui.length-1;return s.jsxs("div",{className:"flex flex-wrap items-center justify-between gap-3",children:[s.jsxs(ue,{type:"button",variant:"ghost",disabled:t===0||a,onClick:o,children:[s.jsx(xe,{icon:T2,size:18,className:"mr-1"}),"Back"]}),s.jsx("div",{className:"flex flex-1 justify-end gap-2",children:d?s.jsx(ue,{type:"button",size:"lg",disabled:a||n,onClick:l,className:"min-w-[200px]",children:a?"Saving…":"Finish setup"}):s.jsxs(ue,{type:"button",size:"lg",disabled:n||a,onClick:c,children:["Continue",s.jsx(xe,{icon:lg,size:18,className:"ml-1"})]})})]})}function Az({values:t,setValues:n,validation:a,validating:o,onValidate:c}){return s.jsxs("div",{className:"flex flex-1 flex-col gap-5",children:[s.jsxs("p",{className:"text-sm leading-relaxed text-muted-foreground",children:["An Emerald Services API key is optional, but required to use the one-click addon installer in the dashboard. You can get yours from the"," ",s.jsx("a",{href:"https://emeraldsrv.dev",target:"_blank",rel:"noreferrer",className:"font-medium text-primary hover:underline",children:"Emerald Services Marketplace"}),"."]}),s.jsxs("div",{className:"space-y-2",children:[s.jsx(Ae,{htmlFor:"emeraldApiKey",children:"Emerald API key (Optional)"}),s.jsx(_e,{id:"emeraldApiKey",type:"password",autoComplete:"off",value:t.emeraldApiKey,onChange:l=>n({...t,emeraldApiKey:l.target.value}),placeholder:"em_...",className:"h-11 font-mono text-sm"})]}),s.jsx(mo,{result:a.emeraldApi}),s.jsx("div",{className:"mt-auto",children:s.jsx(ue,{type:"button",variant:"secondary",disabled:o,onClick:c,children:o?"Checking…":t.emeraldApiKey?"Verify key":"Skip step"})})]})}function _z({values:t,setValues:n,validation:a,validating:o,onValidate:c}){const l=a.database,d=t.dbDriver==="mysql";return s.jsxs("div",{className:"flex flex-1 flex-col gap-6",children:[s.jsxs("p",{className:"text-sm leading-relaxed text-muted-foreground",children:["Choose your database storage engine. Select ",s.jsx("strong",{children:"SQLite"})," for instant zero-config local storage, or ",s.jsx("strong",{children:"MySQL / MariaDB"})," for external host databases."]}),s.jsxs("div",{className:"grid gap-4 sm:grid-cols-2",children:[s.jsxs("div",{onClick:()=>n(m=>({...m,dbDriver:"sqlite"})),className:pe("cursor-pointer rounded-xl border p-4 transition-all hover:border-primary",t.dbDriver==="sqlite"?"border-primary bg-primary/5 ring-2 ring-primary/20":"border-border bg-card"),children:[s.jsxs("div",{className:"flex items-center justify-between mb-2",children:[s.jsx("span",{className:"font-semibold text-foreground",children:"SQLite"}),s.jsx("span",{className:"rounded-full bg-emerald-500/10 px-2.5 py-0.5 text-xs font-medium text-emerald-500",children:"Zero Config"})]}),s.jsxs("p",{className:"text-xs text-muted-foreground",children:["Single-file SQL database stored locally at ",s.jsx("code",{className:"font-mono",children:"./Data/supportbot.db"}),". Zero extra setup required."]})]}),s.jsxs("div",{onClick:()=>n(m=>({...m,dbDriver:"mysql"})),className:pe("cursor-pointer rounded-xl border p-4 transition-all hover:border-primary",t.dbDriver==="mysql"?"border-primary bg-primary/5 ring-2 ring-primary/20":"border-border bg-card"),children:[s.jsxs("div",{className:"flex items-center justify-between mb-2",children:[s.jsx("span",{className:"font-semibold text-foreground",children:"MySQL / MariaDB"}),s.jsx("span",{className:"rounded-full bg-blue-500/10 px-2.5 py-0.5 text-xs font-medium text-blue-500",children:"External Database"})]}),s.jsx("p",{className:"text-xs te
Line truncated
|
||||
@@ -0,0 +1 @@
|
||||
*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:Poppins,system-ui,sans-serif;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}:root{--background: 240 10% 4%;--foreground: 0 0% 98%;--card: 240 7% 11%;--card-foreground: 0 0% 98%;--popover: 240 8% 10%;--popover-foreground: 0 0% 98%;--primary: 141 45% 49%;--primary-foreground: 0 0% 100%;--secondary: 240 6% 14%;--secondary-foreground: 0 0% 90%;--muted: 240 6% 14%;--muted-foreground: 240 5%Line truncated
|
||||
@@ -0,0 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>SupportBot Dashboard</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,400;0,500;0,600;0,700;1,400&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<script type="module" crossorigin src="/assets/index-CzmlDpil.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-Dx-VdUAO.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>␍
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 81 KiB |
Reference in new issue
Block a user