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 |
No files matched your search
@@ -0,0 +1,2 @@
|
|||||||
|
Configs/supportbot.yml filter=supportbot-token
|
||||||
|
Configs/api.yml filter=api-secrets
|
||||||
+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
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
@@ -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 } = 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({ ephemeral: true, 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({ ephemeral: true, 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] });
|
|
||||||
|
|
||||||
await 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,199 +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 } = 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({
|
|
||||||
text: supportbot.EmbedFooter,
|
|
||||||
iconURL: interaction.user.displayAvatarURL(),
|
|
||||||
})
|
|
||||||
.addField(
|
|
||||||
"Ticket",
|
|
||||||
`${interaction.channel.name} (${interaction.channel.id})`
|
|
||||||
)
|
|
||||||
.addField(
|
|
||||||
"User",
|
|
||||||
`${tUser?.username || "N/A"}#${tUser?.discriminator || "N/A"} (${
|
|
||||||
tUser?.id || ticket.user
|
|
||||||
})`
|
|
||||||
)
|
|
||||||
.addField("Closed By", interaction.user.tag)
|
|
||||||
.addField("Reason", reason);
|
|
||||||
const logEmbed = new Discord.MessageEmbed()
|
|
||||||
.setTitle(supportbot.TicketLog_Title)
|
|
||||||
.setColor(supportbot.EmbedColour)
|
|
||||||
.setFooter({
|
|
||||||
text: supportbot.EmbedFooter,
|
|
||||||
iconURL: interaction.user.displayAvatarURL(),
|
|
||||||
})
|
|
||||||
.addField(
|
|
||||||
"Ticket",
|
|
||||||
`${interaction.channel.name} (${interaction.channel.id})`
|
|
||||||
)
|
|
||||||
.addField(
|
|
||||||
"User",
|
|
||||||
`${tUser?.username || "N/A"}#${tUser?.discriminator || "N/A"} (${
|
|
||||||
tUser?.id || ticket.user
|
|
||||||
})`
|
|
||||||
)
|
|
||||||
.addField("Closed By", interaction.user.tag)
|
|
||||||
.addField("Reason", reason);
|
|
||||||
let msgs = await interaction.channel.messages.fetch({ limit: 100 });
|
|
||||||
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>`;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (!(supportbot.DisableTicketLogChannel)) {
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (supportbot.DMTranscripts) {
|
|
||||||
|
|
||||||
await tUser
|
|
||||||
?.send({ embeds: [transcriptEmbed], files: [file] })
|
|
||||||
.catch(async (err) => {
|
|
||||||
console.error(err);
|
|
||||||
});
|
|
||||||
if (ticket.subUsers) {
|
|
||||||
ticket.subUsers.forEach(async (subUser) => {
|
|
||||||
await 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,113 +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",
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
name: "message",
|
|
||||||
description: "Embed Message",
|
|
||||||
type: "STRING",
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
name: "color",
|
|
||||||
description: "Embed HEX Color",
|
|
||||||
type: "STRING",
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
name: "fieldtitle",
|
|
||||||
description: "Add an additional Embed Field",
|
|
||||||
type: "STRING",
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
name: "fieldcontent",
|
|
||||||
description: "Add an additional Embed Field",
|
|
||||||
type: "STRING",
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
name: "thumbnail",
|
|
||||||
description: "Embed Thumbnail URL",
|
|
||||||
type: "STRING",
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
name: "image",
|
|
||||||
description: "Embed Image URL",
|
|
||||||
type: "STRING",
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
permissions: ["SEND_MESSAGES"],
|
|
||||||
|
|
||||||
async run(interaction) {
|
|
||||||
const { getRole } = 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 EmbedColour = interaction.options.getString("color");
|
|
||||||
const EmbedThumbnail = interaction.options.getString("thumbnail");
|
|
||||||
const EmbedImage = interaction.options.getString("image");
|
|
||||||
const EmbedFieldContent = interaction.options.getString("fieldcontent");
|
|
||||||
const EmbedFieldTitle = interaction.options.getString("fieldtitle");
|
|
||||||
|
|
||||||
const EmbedMsg = new Discord.MessageEmbed()
|
|
||||||
.setTitle(EmbedTitle)
|
|
||||||
.setDescription(EmbedSubject)
|
|
||||||
.addField(EmbedFieldTitle, EmbedFieldContent)
|
|
||||||
.setColor(EmbedColour)
|
|
||||||
.setThumbnail(EmbedThumbnail)
|
|
||||||
.setImage(EmbedImage);
|
|
||||||
|
|
||||||
interaction.reply({
|
|
||||||
embeds: [EmbedMsg],
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
return interaction.reply({ embeds: [NoPerms] });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,89 +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");
|
|
||||||
|
|
||||||
module.exports = new Command({
|
|
||||||
name: cmdconfig.HelpCommand,
|
|
||||||
description: cmdconfig.HelpCommandDesc,
|
|
||||||
options: [],
|
|
||||||
permissions: ["SEND_MESSAGES"],
|
|
||||||
|
|
||||||
async run(interaction) {
|
|
||||||
const { getRole } = 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`;
|
|
||||||
botCommands += `**/${cmdconfig.UserInfoCommand}** ${cmdconfig.UserInfoCommandDesc}\n`;
|
|
||||||
|
|
||||||
if (supportbot.DisableSuggestions === false) {
|
|
||||||
botCommands += `**/${cmdconfig.SuggestCommand}** ${cmdconfig.SuggestCommandDesc}\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`;
|
|
||||||
staffCommands += `**/${cmdconfig.UserInfoCommand}** ${cmdconfig.UserInfoCommandDesc}\n`;
|
|
||||||
staffCommands += `**/${cmdconfig.TranslateCommand}** ${cmdconfig.TranslateCommandDesc}\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({
|
|
||||||
text: supportbot.EmbedFooter,
|
|
||||||
iconURL: 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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
interaction.reply({
|
|
||||||
ephemeral: true,
|
|
||||||
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,94 +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");
|
|
||||||
|
|
||||||
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 } = 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({ ephemeral: true, 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({ ephemeral: true, embeds: [UserNotExist] });
|
|
||||||
|
|
||||||
await 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,84 +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",
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
permissions: ["SEND_MESSAGES"],
|
|
||||||
|
|
||||||
async run(interaction) {
|
|
||||||
const { getChannel } = 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({
|
|
||||||
text: supportbot.EmbedFooter,
|
|
||||||
iconURL: interaction.user.displayAvatarURL(),
|
|
||||||
})
|
|
||||||
.setColor(supportbot.EmbedColour);
|
|
||||||
|
|
||||||
const suggestionMsg = await suggestChannel.send({ embeds: [SuggestEmbed] });
|
|
||||||
if (supportbot.SuggestionUpvote && supportbot.SuggestionDownvote) {
|
|
||||||
await suggestionMsg.react(supportbot.SuggestionUpvote);
|
|
||||||
await suggestionMsg.react(supportbot.SuggestionDownvote);
|
|
||||||
|
|
||||||
if (supportbot.SuggestionThreads) {
|
|
||||||
await suggestionMsg.startThread({
|
|
||||||
name: `Suggestion-releated Thread`,
|
|
||||||
autoArchiveDuration: 60,
|
|
||||||
type: 'GUILD_PUBLIC_THREAD',
|
|
||||||
reason: 'Suggestion-releated thread',
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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({
|
|
||||||
ephemeral: true,
|
|
||||||
embeds: [Submitted]
|
|
||||||
})
|
|
||||||
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,330 +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 disableCommand = true;
|
|
||||||
if (interaction.isCommand() && disableCommand)
|
|
||||||
return interaction.reply({
|
|
||||||
content: "This command is disabled",
|
|
||||||
ephemeral: true,
|
|
||||||
});
|
|
||||||
let department = interaction.customId?.split("-")[1] || null;
|
|
||||||
let TicketData = await JSON.parse(
|
|
||||||
fs.readFileSync("./Data/TicketData.json", "utf8")
|
|
||||||
);
|
|
||||||
const { getRole, 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);
|
|
||||||
|
|
||||||
if (
|
|
||||||
await interaction.guild.channels.cache.find(
|
|
||||||
(ticketChannel) =>
|
|
||||||
ticketChannel.name === `${supportbot.TicketPrefix}${ticketNumberID}`
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return await interaction.reply({
|
|
||||||
embeds: [TicketExists],
|
|
||||||
ephemeral: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const Staff = await getRole(supportbot.Staff, interaction.guild);
|
|
||||||
const Admin = await getRole(supportbot.Admin, interaction.guild);
|
|
||||||
if (!Staff || !Admin)
|
|
||||||
return interaction.reply({
|
|
||||||
content:
|
|
||||||
"Some roles seem to be missing!\nPlease check for errors when starting the bot.",
|
|
||||||
ephemeral: true,
|
|
||||||
});
|
|
||||||
let TicketCategory = await getCategory(
|
|
||||||
supportbot.Departments[department].category || 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: interaction.user.id,
|
|
||||||
allow: [Permissions.FLAGS.VIEW_CHANNEL],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: interaction.guild.id,
|
|
||||||
deny: [Permissions.FLAGS.VIEW_CHANNEL],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
);
|
|
||||||
if (supportbot.AllowAllStaff) {
|
|
||||||
await ticketChannel.permissionOverwrites.edit(Staff.id, {
|
|
||||||
VIEW_CHANNEL: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (supportbot.TicketDepartments && !department) {
|
|
||||||
await ticketChannel.permissionOverwrites.edit(interaction.user.id, {
|
|
||||||
SEND_MESSAGES: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
await TicketData.tickets.push({
|
|
||||||
id: ticketChannel.id,
|
|
||||||
name: ticketChannel.name,
|
|
||||||
user: interaction.user.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 && !department) {
|
|
||||||
TicketMessage.addFields({
|
|
||||||
name: "Departments",
|
|
||||||
value: ` **${supportbot.Departments.map((X) => X.title).join("\n")}**`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
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 && !department) {
|
|
||||||
try {
|
|
||||||
let buttons = await supportbot.Departments.map((x) =>
|
|
||||||
new MessageButton()
|
|
||||||
.setCustomId("Department" + supportbot.Departments.indexOf(x))
|
|
||||||
.setLabel(x.title)
|
|
||||||
.setStyle(x.color)
|
|
||||||
.setEmoji(x.emoji)
|
|
||||||
);
|
|
||||||
const row = new MessageActionRow().addComponents(buttons);
|
|
||||||
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 num = collector.customId.split("Department")[1];
|
|
||||||
let role = getRole(supportbot.Departments[num].role, interaction.guild);
|
|
||||||
let title = supportbot.Departments[num].title;
|
|
||||||
|
|
||||||
await ticketChannel.permissionOverwrites.edit(role.id, {
|
|
||||||
VIEW_CHANNEL: true,
|
|
||||||
});
|
|
||||||
await ticketChannel.permissionOverwrites.edit(interaction.user.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],
|
|
||||||
});
|
|
||||||
if (department) {
|
|
||||||
let role = await getRole(
|
|
||||||
supportbot.Departments[department].role,
|
|
||||||
interaction.guild
|
|
||||||
);
|
|
||||||
let title = supportbot.Departments[department].title;
|
|
||||||
await ticketChannel.permissionOverwrites.edit(role.id, {
|
|
||||||
VIEW_CHANNEL: true,
|
|
||||||
});
|
|
||||||
let TicketCat = await getCategory(
|
|
||||||
supportbot.Departments[department].category ||
|
|
||||||
supportbot.TicketCategory,
|
|
||||||
interaction.guild
|
|
||||||
);
|
|
||||||
await ticketChannel.setParent(TicketCat.id, { lockPermissions: false });
|
|
||||||
if (supportbot.AllowThanksForReachingOutMessage) {
|
|
||||||
await ticketChannel.send({
|
|
||||||
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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,108 +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");
|
|
||||||
const translate = require("@vitalets/google-translate-api");
|
|
||||||
|
|
||||||
module.exports = new Command({
|
|
||||||
name: cmdconfig.TranslateCommand, // Name of command
|
|
||||||
description: cmdconfig.TranslateCommandDesc, // Description of command
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
type: "STRING",
|
|
||||||
name: "language",
|
|
||||||
description: "A 2 letter language code",
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "STRING",
|
|
||||||
name: "text",
|
|
||||||
description: "The text to translate",
|
|
||||||
required: true,
|
|
||||||
}, // The input text for the translation
|
|
||||||
],
|
|
||||||
permissions: ["SEND_MESSAGES"], // The permission the user/role at least requires
|
|
||||||
|
|
||||||
async run(interaction) {
|
|
||||||
|
|
||||||
const { getRole } = 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 the error logs."
|
|
||||||
);
|
|
||||||
|
|
||||||
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] });
|
|
||||||
|
|
||||||
const { getChannel } = interaction.client;
|
|
||||||
let lang = interaction.options.getString("language"); // Grab choice of language code by user
|
|
||||||
let url = "https://www.science.co.il/language/Codes.php"; // URL to all available language codes
|
|
||||||
let text = await interaction.options.getString("text"); // Grab the text to translate by the user
|
|
||||||
let translatelog = await getChannel(
|
|
||||||
supportbot.TranslateLogChannel,
|
|
||||||
interaction.guild
|
|
||||||
); // Grab the set logging channel for translations
|
|
||||||
|
|
||||||
|
|
||||||
// Return message if user has provided an invalid language code
|
|
||||||
const result = await translate(text, { to: lang }).catch(async (err) => {
|
|
||||||
if (err && err.code == 400) {
|
|
||||||
await interaction.reply({
|
|
||||||
embeds: [
|
|
||||||
new Discord.MessageEmbed()
|
|
||||||
.setColor(supportbot.WarningColour)
|
|
||||||
.setTitle("Valid Language Codes")
|
|
||||||
.setURL(url)
|
|
||||||
.setDescription(
|
|
||||||
"Click the title to see which 2 letter language codes are valid."
|
|
||||||
),
|
|
||||||
],
|
|
||||||
ephemeral: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (!result) return;
|
|
||||||
|
|
||||||
// Embed containing language code, original text and translated text
|
|
||||||
let transembed = new Discord.MessageEmbed()
|
|
||||||
.setAuthor({ name: interaction.user.tag, iconURL: interaction.user.displayAvatarURL({ dynamic: true }) })
|
|
||||||
.setColor(supportbot.SuccessColour)
|
|
||||||
.setDescription(`**Translation to ${lang}**`)
|
|
||||||
.addFields(
|
|
||||||
{ name: "Original text", value: text },
|
|
||||||
{ name: "Translated text", value: result.text },
|
|
||||||
)
|
|
||||||
.setFooter({ text: `Author: ${interaction.user.id}` })
|
|
||||||
.setTimestamp();
|
|
||||||
|
|
||||||
// Send embed [transembed] with translated message to channel
|
|
||||||
await interaction.reply({ embeds: [transembed] });
|
|
||||||
|
|
||||||
// Send embed [transembed] to logging channel
|
|
||||||
await translatelog.send({ embeds: [transembed] });
|
|
||||||
},
|
|
||||||
|
|
||||||
})
|
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
// SupportBot | Emerald Services
|
|
||||||
// User Info Command
|
|
||||||
|
|
||||||
const fs = require("fs");
|
|
||||||
const moment = require("moment");
|
|
||||||
|
|
||||||
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.UserInfoCommand, // Name of command
|
|
||||||
description: cmdconfig.UserInfoCommandDesc, // Description of command
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
type: "USER",
|
|
||||||
name: "user",
|
|
||||||
description: "The user you want to get info about.",
|
|
||||||
required: true,
|
|
||||||
}, // The user input
|
|
||||||
],
|
|
||||||
permissions: ["SEND_MESSAGES"], // The permission the user/role at least requires
|
|
||||||
|
|
||||||
async run(interaction) {
|
|
||||||
const { getRole } = 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 the error logs."
|
|
||||||
);
|
|
||||||
|
|
||||||
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 user = await interaction.options.getUser("user") || interaction.author; // Grab the mentioned user
|
|
||||||
let member = await interaction.guild.members.cache.get(user.id) // Check for user and his ID
|
|
||||||
|
|
||||||
const UserNotExist = new Discord.MessageEmbed()
|
|
||||||
.setTitle("User Not Found!")
|
|
||||||
.setDescription(
|
|
||||||
`${supportbot.UserNotFound}\n\nTry Again:\`/${cmdconfig.UserInfoCommand} <@!User_ID> or @User\``
|
|
||||||
)
|
|
||||||
.setColor(supportbot.ErrorColour);
|
|
||||||
|
|
||||||
if (!user) return interaction.reply({ embeds: [UserNotExist] });
|
|
||||||
|
|
||||||
// Flags a user/bot can have
|
|
||||||
const flags = {
|
|
||||||
DISCORD_EMPLOYEE: 'Discord Employee',
|
|
||||||
DISCORD_PARTNER: 'Discord Partner',
|
|
||||||
BUGHUNTER_LEVEL_1: 'Bug Hunter (Level 1)',
|
|
||||||
BUGHUNTER_LEVEL_2: 'Bug Hunter (Level 2)',
|
|
||||||
HYPESQUAD_EVENTS: 'HypeSquad Events',
|
|
||||||
HOUSE_BRAVERY: 'House of Bravery',
|
|
||||||
HOUSE_BRILLIANCE: 'House of Brilliance',
|
|
||||||
HOUSE_BALANCE: 'House of Balance',
|
|
||||||
EARLY_SUPPORTER: 'Early Supporter',
|
|
||||||
TEAM_USER: 'Team User',
|
|
||||||
SYSTEM: 'System',
|
|
||||||
VERIFIED_BOT: 'Verified Bot',
|
|
||||||
VERIFIED_DEVELOPER: 'Verified Bot Developer'
|
|
||||||
};
|
|
||||||
|
|
||||||
// Status message with icon
|
|
||||||
let status = {
|
|
||||||
online: "🟢Online",
|
|
||||||
idle: "🟡Idle",
|
|
||||||
dnd: "🔴Do Not Disturb",
|
|
||||||
offline: "⚫Offline"
|
|
||||||
};
|
|
||||||
|
|
||||||
const userFlags = user.flags.toArray();
|
|
||||||
|
|
||||||
// Embed containing the userinfo
|
|
||||||
let userembed = new Discord.MessageEmbed()
|
|
||||||
.setTitle(`User Info for User: ${user.tag}`)
|
|
||||||
.setThumbnail(member.displayAvatarURL({ dynamic: true, size: 512 }))
|
|
||||||
.setColor(member.roles.highest.hexColor)
|
|
||||||
.addFields(
|
|
||||||
{ name: "Name:", value: `${user.tag}\n<@${member.id}>`, inline: true },
|
|
||||||
{ name: "ID:", value: `${member.id}`, inline: true },
|
|
||||||
{ name: "Is Bot:", value: user.bot ? "Yes" : "No", inline: true },
|
|
||||||
{ name: "House/Badge:", value: `${userFlags.length ? userFlags.map(flag => flags[flag]).join("\n") : "None"}`, inline: true },
|
|
||||||
{ name: "Current Status:", value: `${status[member.presence ? member.presence.status : "offline"]}`, inline: true },
|
|
||||||
{ name: "Avatar URL:", value: `[Click Here](${member.displayAvatarURL()})`, inline: true },
|
|
||||||
{ name: "\u200b", value: "\u200b", inline: false}
|
|
||||||
)
|
|
||||||
.addFields(
|
|
||||||
{ name: "Creation Date:", value: `${moment(user.createdAt).format("MMMM Do YYYY, h:mm:ss a")}\n> ${moment(user.createdAt).startOf("day").fromNow()}`, inline: true },
|
|
||||||
{ name: "Joined Date:", value: `${moment(member.joinedAt).format("MMMM Do YYYY, h:mm:ss a")}\n> ${moment(member.joinedAt).startOf("day").fromNow()}`, inline: true },
|
|
||||||
{ name: "Roles Total:", value: `${member.roles.cache.size -1}`, inline: false },
|
|
||||||
{ name: "Highest Role:", value: `${member.roles.highest.id === interaction.guild.id ? "None" : member.roles.highest.toString()}`, inline: true },
|
|
||||||
{ name: "Roles:", value: `${member.roles.cache.map(r => r).join(" ").replace("@everyone", " ") || "None"}`, inline: false },
|
|
||||||
)
|
|
||||||
.setFooter({ text: supportbot.EmbedFooter })
|
|
||||||
.setTimestamp();
|
|
||||||
|
|
||||||
// Send embed [userembed]
|
|
||||||
await interaction.reply({ embeds: [userembed] }).catch((err) => console.error("An error has occured: ", err));
|
|
||||||
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @INFO
|
|
||||||
* UserInfo command by Sypher#3415 | 321332200668790786
|
|
||||||
* @INFO
|
|
||||||
* Solely created for SupportBot | https://github.com/Emerald-Services/SupportBot/
|
|
||||||
**/
|
|
||||||
@@ -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.
|
||||||
+168
-17
@@ -7,25 +7,171 @@
|
|||||||
# SupportBot created by Emerald Services
|
# SupportBot created by Emerald Services
|
||||||
# Installed with MIT License
|
# Installed with MIT License
|
||||||
#
|
#
|
||||||
# Discord Support: https://emeraldsrv.dev/discord
|
# Discord Support: https://discord.gg/emeraldsrv
|
||||||
# Community Resources: https://emeraldsrv.dev/resources
|
# Official Documentation: https://docs.emeraldsrv.com
|
||||||
|
# Addons Marketplace: https://community.emeraldsrv.com
|
||||||
|
#
|
||||||
|
# Commands Configuration (commands.yml)
|
||||||
|
|
||||||
# -------------------------------
|
Help:
|
||||||
# Command Name Configuration
|
Command: "help"
|
||||||
# -------------------------------
|
Description: "Get a list of all the commands."
|
||||||
|
Permission: ["SendMessages"]
|
||||||
|
Enabled: true
|
||||||
|
|
||||||
Blacklist: "blacklist"
|
Info:
|
||||||
TicketAdd: "add"
|
Command: "info"
|
||||||
TicketRemove: "remove"
|
Description: "Get information about the **ServerName**."
|
||||||
OpenTicket: "ticket"
|
Permission: ["SendMessages"]
|
||||||
CloseTicket: "close"
|
Enabled: true
|
||||||
InfoCommand: "info"
|
|
||||||
PingCommand: "ping"
|
TicketPanel:
|
||||||
HelpCommand: "help"
|
Command: "ticketpanel"
|
||||||
EmbedCommand: "embed"
|
Description: "Create the ticket creation panel"
|
||||||
SuggestCommand: "suggestion"
|
Permission: ["Administrator"]
|
||||||
TranslateCommand: "translate"
|
Enabled: true
|
||||||
UserInfoCommand: "userinfo"
|
|
||||||
|
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
|
# Command Description Configuration
|
||||||
@@ -43,3 +189,8 @@ HelpCommandDesc: "Get a list of all the commands."
|
|||||||
EmbedCommandDesc: "Send a message as an embed."
|
EmbedCommandDesc: "Send a message as an embed."
|
||||||
SuggestCommandDesc: "Suggest a feature for the server."
|
SuggestCommandDesc: "Suggest a feature for the server."
|
||||||
UserInfoCommandDesc: "Get info about a user."
|
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: "🎫"
|
||||||
+337
-207
@@ -7,253 +7,383 @@
|
|||||||
# SupportBot created by Emerald Services
|
# SupportBot created by Emerald Services
|
||||||
# Installed with MIT License
|
# Installed with MIT License
|
||||||
#
|
#
|
||||||
# Discord Support: https://emeraldsrv.dev/discord
|
# Discord Support: https://discord.gg/emeraldsrv
|
||||||
# Community Resources: https://emeraldsrv.dev/resources
|
# Website: https://emeraldsrv.dev
|
||||||
|
#
|
||||||
|
# SupportBot Configuration (supportbot.yml)
|
||||||
|
|
||||||
SupportBot_Version: "7.7 BETA-v2"
|
SupportBot_Version: "v26.2"
|
||||||
|
|
||||||
# -------------------------
|
# -------------------------
|
||||||
# General Configuration
|
# General
|
||||||
# -------------------------
|
# -------------------------
|
||||||
|
|
||||||
Name: "SupportBot"
|
General:
|
||||||
Token: "BOT_TOKEN_HERE"
|
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"
|
# Connection Settings for MariaDB / MySQL (used when Driver is "mysql" or "mariadb")
|
||||||
# Set The ActivityType to "STREAMING" for a streaming status!
|
MySQL:
|
||||||
|
Host: "localhost"
|
||||||
|
Port: 3306
|
||||||
|
User: "root"
|
||||||
|
Password: "password"
|
||||||
|
Database: "supportbot"
|
||||||
|
ConnectionLimit: 10
|
||||||
|
|
||||||
ActivityType: "PLAYING"
|
# Connection Settings for SQLite (used when Driver is "sqlite")
|
||||||
# Activity status can either be PLAYING, WATCHING, LISTENING or STREAMING.
|
SQLite:
|
||||||
|
File: "./Data/supportbot.db"
|
||||||
|
|
||||||
# -------------------------
|
# Activity
|
||||||
# Embed Configuration
|
|
||||||
# -------------------------
|
# -------------------------
|
||||||
|
|
||||||
EmbedColour: "#f74343"
|
Activity:
|
||||||
EmbedFooter: "SupportBot | An Advanced Ticket System"
|
Status: "SupportBot"
|
||||||
|
Type: "Watching"
|
||||||
|
StreamingURL: "https://www.twitch.tv/YOUR_TWITCH_NAME"
|
||||||
|
# Available Types:
|
||||||
|
# Playing
|
||||||
|
# Watching
|
||||||
|
# Listening
|
||||||
|
# Competing
|
||||||
|
# Streaming (URL required)
|
||||||
|
|
||||||
ErrorColour: "#d12519"
|
# You can also update the activity via Discord using: /settings
|
||||||
SuccessColour: "#04b545"
|
|
||||||
WarningColour: "#fca117"
|
|
||||||
|
|
||||||
# -------------------------
|
# -------------------------
|
||||||
# Information Configuration
|
# Embeds
|
||||||
# -------------------------
|
# -------------------------
|
||||||
|
|
||||||
InfoTitle: "Information"
|
Embed:
|
||||||
AboutMe: "Hi, I'm SupportBot an advanced discord ticket bot with 100% customization! [Download](https://emeraldsrv.dev/download)"
|
Colours:
|
||||||
|
General: "#0cc559"
|
||||||
|
Success: "#04b545"
|
||||||
|
Error: "#d13030"
|
||||||
|
Warn: "#fca117"
|
||||||
|
|
||||||
InfoButtonText: "Visit Site"
|
Footer: "SupportBot | Developed by Emerald Development"
|
||||||
InfoURL: "https://emeraldsrv.dev"
|
# Use 0x<hexcode> to change the colour of the embeds.
|
||||||
InfoColour: "#f74343"
|
|
||||||
|
|
||||||
# -------------------------
|
# -------------------------
|
||||||
# Role Configuration
|
# Roles
|
||||||
# -------------------------
|
# -------------------------
|
||||||
|
|
||||||
AutoRole: true
|
Roles:
|
||||||
JoinRole: "Member"
|
AutoRole:
|
||||||
|
Enabled: true
|
||||||
|
Role: "ROLE_ID"
|
||||||
|
|
||||||
Staff: "Staff"
|
StaffMember:
|
||||||
Admin: "Admin"
|
Staff: "ROLE_ID"
|
||||||
TicketBlackListRole: "Blacklisted"
|
Admin: "ROLE_ID"
|
||||||
|
Moderator: "ROLE_ID"
|
||||||
|
|
||||||
|
Mod:
|
||||||
|
AllowSupportStaff: true
|
||||||
|
# Allow Support Staff to use the /mod command. (More in detail customisation in commands.yml)
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
# System Messages
|
||||||
|
# ----------------------------
|
||||||
|
|
||||||
|
Welcome:
|
||||||
|
Enabled: false
|
||||||
|
Channel: "CHANNEL_ID"
|
||||||
|
|
||||||
|
Leave:
|
||||||
|
Enabled: false
|
||||||
|
Channel: "CHANNEL_ID"
|
||||||
|
|
||||||
# ----------------------------
|
# ----------------------------
|
||||||
# Join/Leave Configuration
|
# Suggestions
|
||||||
# ----------------------------
|
# ----------------------------
|
||||||
|
|
||||||
Welcome: true
|
Suggestions:
|
||||||
# set to false to disable joining logs
|
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.
|
||||||
|
|
||||||
WelcomeChannel: "join-leave-logs"
|
UpvoteEmoji: "✅"
|
||||||
WelcomeColour: "#f74343"
|
DownvoteEmoji: "❌"
|
||||||
WelcomeTitle: "User Joined!"
|
|
||||||
WelcomeMessage: "%joined_user% has joined the server!"
|
|
||||||
# Available Placeholders:
|
|
||||||
# %joined_user% - Mentions the user who joined
|
|
||||||
|
|
||||||
EmbedWelcomeImage: true
|
UpvoteTitle: "Yes"
|
||||||
EmbedWelcomeImageURL: "https://media.discordapp.net/attachments/958247876344025108/986777740738986094/unknown.png"
|
DownvoteTitle: "No"
|
||||||
|
|
||||||
EmbedWelcomeThumbnail: "BOT"
|
OwnSuggestion: true
|
||||||
# "USER" will display the user's profile picture.
|
|
||||||
# "BOT" will display the bot's profile picture.
|
|
||||||
# false to disable
|
|
||||||
|
|
||||||
Leave: true
|
# If false this will not allow the user to vote for a suggestion they created
|
||||||
# set to false to disable leaving logs
|
# If true this will allow the user to vote for a suggestion they created
|
||||||
|
|
||||||
LeaveChannel: "join-leave-logs"
|
ShowUsers: true
|
||||||
LeaveColour: "#f74343"
|
|
||||||
LeaveTitle: "User Left!"
|
|
||||||
LeaveMessage: "%joined_user% has left the server!"
|
|
||||||
# Available Placeholders:
|
|
||||||
# %joined_user% - Mentions the user who joined
|
|
||||||
|
|
||||||
EmbedLeaveImage: true
|
Threads:
|
||||||
EmbedLeaveImageURL: "https://cdn.discordapp.com/attachments/958247876344025108/986778170223120384/unknown.png"
|
Enabled: true
|
||||||
|
Title: "Suggestion-releated Thread"
|
||||||
|
Reason: "A thread releted to this suggestion"
|
||||||
|
|
||||||
EmbedLeaveThumbnail: false
|
Buttons:
|
||||||
# "USER" will display the user's profile picture.
|
Upvote: "2"
|
||||||
# "BOT" will display the bot's profile picture.
|
Downvote: "2"
|
||||||
# false to disable
|
RemoveVote: "4"
|
||||||
|
RemoveVote_Title: "Remove Vote"
|
||||||
|
# Available Button Styles
|
||||||
|
# 1 - Blurple
|
||||||
|
# 2 - Grey
|
||||||
|
# 3 - Green
|
||||||
|
# 4 - Red
|
||||||
|
|
||||||
|
Tags:
|
||||||
# ----------------------------
|
Created: "TAG_ID"
|
||||||
# Suggestions Configuration
|
Considering: "TAG_ID"
|
||||||
# ----------------------------
|
Solved: "TAG_ID"
|
||||||
|
Denied: "TAG_ID"
|
||||||
SuggestionChannel: "suggestions"
|
|
||||||
SuggestionUpvote: "✅"
|
|
||||||
SuggestionDownvote: "❌"
|
|
||||||
|
|
||||||
SuggestionThreads: true
|
|
||||||
# set to false to disable threads being created from suggestions.
|
|
||||||
|
|
||||||
DisableSuggestions: false
|
|
||||||
# Set to true to disable the suggestiosn feature.
|
|
||||||
|
|
||||||
# -------------------------
|
# -------------------------
|
||||||
# Translate Configuration
|
# Tickets
|
||||||
# -------------------------
|
# -------------------------
|
||||||
|
|
||||||
TranslateLogChannel: "translations-log"
|
VoiceTickets:
|
||||||
|
Name: "%username%'s VC"
|
||||||
|
Category: "CATEGORY_ID"
|
||||||
|
|
||||||
|
Ticket:
|
||||||
|
TicketType: "channels"
|
||||||
|
|
||||||
|
# Available TicketTypes:
|
||||||
|
# channels - Tickets will be created as channels in the TicketChannelsCategory
|
||||||
|
# threads - Tickets will be created as threads through the "TicketHome" channel.
|
||||||
|
|
||||||
|
UserManagement:
|
||||||
|
DMOnAdd: true # DM the user when they are added to the ticket
|
||||||
|
DMOnRemove: true # DM the user when they are removed from the ticket
|
||||||
|
DMTranscript: true # DM the user a transcript of the ticket when it is closed
|
||||||
|
|
||||||
|
TicketHome: "CHANNEL_ID"
|
||||||
|
|
||||||
|
# What is TicketHome?
|
||||||
|
# - Channel where the Ticket creation panel is sent.
|
||||||
|
# - Channel where ticket threads are made. (only if ticket threads is enabled above.)
|
||||||
|
|
||||||
|
# For TicketType: channels
|
||||||
|
TicketChannelsCategory: "CATEGORY_ID"
|
||||||
|
TicketChannelsCategory2: "CATEGORY_ID" # This will be used if the first category has reached the max channel limit.
|
||||||
|
TicketArchiveCategory: "CATEGORY_ID"
|
||||||
|
# ---
|
||||||
|
|
||||||
|
Channel: "ticket-" # The prefix for ticket channel names
|
||||||
|
TicketsPerUser: 100
|
||||||
|
TicketReason: true
|
||||||
|
# If ticket reason is set to false, It will not prompt to create a reason
|
||||||
|
|
||||||
|
TicketSubject: "description"
|
||||||
|
|
||||||
|
# Available TicketSubjects:
|
||||||
|
# "description" will display the ticket reason within the channel description
|
||||||
|
# "embed" will display the ticket reason within the ticket embed
|
||||||
|
|
||||||
|
Timeout: 5
|
||||||
|
# Set to true to allow all staff to access the ticket
|
||||||
|
|
||||||
|
Close:
|
||||||
|
StaffOnly: true
|
||||||
|
# Set to false is anyone is able to close a ticket.
|
||||||
|
|
||||||
|
Invites:
|
||||||
|
# Invites is for ticket threads.
|
||||||
|
StaffOnly: true
|
||||||
|
|
||||||
|
ClaimTickets:
|
||||||
|
Enabled: false
|
||||||
|
Channel: "CHANNEL_ID"
|
||||||
|
# Channel where the ticket assignments are sent for staff to claim.
|
||||||
|
|
||||||
|
ClaimAcceptTime: 120000
|
||||||
|
# How long the assigned user has to accept the ticket before it gets re-assigned to someone else.
|
||||||
|
# 120000 = 2 Minutes
|
||||||
|
# 180000 = 3 Minutes
|
||||||
|
# 240000 = 4 Minutes
|
||||||
|
# 300000 = 5 Minutes
|
||||||
|
# Time in Mliseconds
|
||||||
|
|
||||||
|
ButtonTitle: "Claim Ticket"
|
||||||
|
ButtonEmoji: "🎫"
|
||||||
|
Button: "1"
|
||||||
|
# Available Button Styles
|
||||||
|
# 1 - Blurple
|
||||||
|
# 2 - Grey
|
||||||
|
# 3 - Green
|
||||||
|
# 4 - Red
|
||||||
|
|
||||||
|
ReviewSystem:
|
||||||
|
Enabled: false
|
||||||
|
Channel: "CHANNEL_ID"
|
||||||
|
UseModal: true
|
||||||
|
|
||||||
|
Questions:
|
||||||
|
# Default questions asked when a ticket is created, these are used if departments are disabled.
|
||||||
|
Enabled: false
|
||||||
|
List:
|
||||||
|
- "Please provide your account email?"
|
||||||
|
- "What is the nature of your issue?"
|
||||||
|
- "Have you tried resolving the issue yourself?"
|
||||||
|
|
||||||
|
Log:
|
||||||
|
TicketDataLog: "CHANNEL_ID"
|
||||||
|
TickeDataTitle: "Ticket Data"
|
||||||
|
TicketBlacklistLog: "CHANNEL_ID"
|
||||||
|
|
||||||
|
AIMode:
|
||||||
|
Enabled: true
|
||||||
|
ThreadName: "Chat with SupportBot AI"
|
||||||
|
WelcomeMessage: "Hi! I’m SupportBot AI. You can chat with me here about your issue."
|
||||||
|
|
||||||
|
# -------------------------
|
||||||
|
# Ticket Departments
|
||||||
|
# -------------------------
|
||||||
|
|
||||||
|
DepartmentSystem:
|
||||||
|
Enabled: false
|
||||||
|
Placeholder: "Choose a ticket department"
|
||||||
|
DefaultDepartment: "general"
|
||||||
|
|
||||||
|
Departments:
|
||||||
|
general:
|
||||||
|
Name: "General Support"
|
||||||
|
Description: "Open a general support ticket"
|
||||||
|
Emoji: "🎫"
|
||||||
|
Category: "CATEGORY_ID"
|
||||||
|
Role: "ROLE_ID"
|
||||||
|
ChannelPrefix: "support-"
|
||||||
|
DefaultPriority: "normal"
|
||||||
|
Questions:
|
||||||
|
- "What do you need help with?"
|
||||||
|
- "When did the issue start?"
|
||||||
|
|
||||||
|
purchase:
|
||||||
|
Name: "Purchase Support"
|
||||||
|
Description: "Get help with store purchases"
|
||||||
|
Emoji: "💳"
|
||||||
|
Category: "CATEGORY_ID"
|
||||||
|
Role: "ROLE_ID"
|
||||||
|
ChannelPrefix: "purchase-"
|
||||||
|
DefaultPriority: "high"
|
||||||
|
Questions:
|
||||||
|
- "What is your order ID?"
|
||||||
|
- "What product did you purchase?"
|
||||||
|
- "What payment issue are you having?"
|
||||||
|
|
||||||
|
reports:
|
||||||
|
Name: "Player Report"
|
||||||
|
Description: "Report a player for breaking the rules"
|
||||||
|
Emoji: "🚨"
|
||||||
|
Category: "CATEGORY_ID"
|
||||||
|
Role: "ROLE_ID"
|
||||||
|
ChannelPrefix: "report-"
|
||||||
|
DefaultPriority: "normal"
|
||||||
|
Questions:
|
||||||
|
- "Who are you reporting?"
|
||||||
|
- "What happened?"
|
||||||
|
- "Do you have evidence?"
|
||||||
|
|
||||||
|
appeals:
|
||||||
|
Name: "Ban Appeals"
|
||||||
|
Description: "Appeal a server punishment"
|
||||||
|
Emoji: "⚖️"
|
||||||
|
Category: "CATEGORY_ID"
|
||||||
|
Role: "ROLE_ID"
|
||||||
|
ChannelPrefix: "appeals-"
|
||||||
|
DefaultPriority: "low"
|
||||||
|
Questions:
|
||||||
|
- "What punishment are you appealing?"
|
||||||
|
- "Why should it be reviewed?"
|
||||||
|
- "Anything else staff should know?"
|
||||||
|
|
||||||
|
PrioritySystem:
|
||||||
|
Enabled: false
|
||||||
|
DefaultPriority: "Normal"
|
||||||
|
AllowUsersToChooseOnOpen: true
|
||||||
|
Priorities:
|
||||||
|
low:
|
||||||
|
Name: "Low"
|
||||||
|
Emoji: "🟢"
|
||||||
|
normal:
|
||||||
|
Name: "Normal"
|
||||||
|
Emoji: "🟡"
|
||||||
|
high:
|
||||||
|
Name: "High"
|
||||||
|
Emoji: "🟠"
|
||||||
|
urgent:
|
||||||
|
Name: "Urgent"
|
||||||
|
Emoji: "🔴"
|
||||||
|
|
||||||
# -------------------------
|
# -------------------------
|
||||||
# Tickets Configuration
|
# Buttons
|
||||||
|
# -------------------------
|
||||||
|
Buttons:
|
||||||
|
General:
|
||||||
|
Delete: "🗑️"
|
||||||
|
DeleteStyle: "4"
|
||||||
|
|
||||||
|
Voice:
|
||||||
|
TicketDeleteText: "Delete Voice Channel"
|
||||||
|
|
||||||
|
Tickets:
|
||||||
|
ClaimEmoji: "🧑💻"
|
||||||
|
ClaimStyle: "1"
|
||||||
|
|
||||||
|
Unarchive: "Unarchive Ticket"
|
||||||
|
Unarchive_Emoji: "♻️"
|
||||||
|
Unarchive_Style: "2"
|
||||||
|
|
||||||
|
Unlock: "Unlock Ticket"
|
||||||
|
Unlock_Emoji: "🔓"
|
||||||
|
Unlock_Style: "2"
|
||||||
|
# Available Button Styles
|
||||||
|
# 1 - Blurple
|
||||||
|
# 2 - Grey
|
||||||
|
# 3 - Green
|
||||||
|
# 4 - Red
|
||||||
|
|
||||||
|
# -------------------------
|
||||||
|
# Select Menus
|
||||||
|
# -------------------------
|
||||||
|
|
||||||
|
SelectMenus:
|
||||||
|
Tickets:
|
||||||
|
PanelEmoji: "⚙️"
|
||||||
|
|
||||||
|
CloseEmoji: "🗑️"
|
||||||
|
ArchiveEmoji: "♻️"
|
||||||
|
LockEmoji: "🔐"
|
||||||
|
RenameEmoji: "🏷️"
|
||||||
|
VCEmoji: "🔊"
|
||||||
|
|
||||||
|
EnableInvitesEmoji: "📤"
|
||||||
|
DisableInvitesEmoji: "📥"
|
||||||
|
|
||||||
|
# -------------------------
|
||||||
|
# Logging
|
||||||
# -------------------------
|
# -------------------------
|
||||||
|
|
||||||
TicketCategory: "Tickets" # Only for tickets without a category
|
MessageDelete:
|
||||||
TicketPrefix: "ticket-" # The prefix for ticket channel names
|
Channel: "CHANNEL_ID"
|
||||||
|
Colour: "#d92525"
|
||||||
|
|
||||||
TicketLog: "ticket-data"
|
MessageUpdate:
|
||||||
TranscriptLog: "ticket-data"
|
Channel: "CHANNEL_ID"
|
||||||
|
Colour: "#f2a024"
|
||||||
|
|
||||||
DMTranscripts: true
|
Translate:
|
||||||
TranscriptTitle: "Ticket Transcript"
|
TranslateLog: "CHANNEL_ID"
|
||||||
TicketLog_Title: "Ticket Closed"
|
|
||||||
TicketBlackListMessage: "You are not allowed to open a ticket"
|
|
||||||
|
|
||||||
MaxAllowedTickets: 2
|
|
||||||
|
|
||||||
TicketSubject: "description"
|
|
||||||
# "description" will display the ticket reason within the channel description
|
|
||||||
# "embed" will display the ticket reason within the ticket embed
|
|
||||||
|
|
||||||
AllowTicketMentions: true
|
|
||||||
# Set to true to allow the ticket to mention the author and the support team.
|
|
||||||
# Set false to dis-allow mentions within tickets.
|
|
||||||
|
|
||||||
AllowThanksForReachingOutMessage: true
|
|
||||||
# Set to true to allow the "Thanks for reaching out Message" to be sent when the ticket is created
|
|
||||||
DisableTicketLogChannel: false
|
|
||||||
# Set to true to disable the requirement of the "TicketLog"
|
|
||||||
# Set to true to also disable the ticket logs to be sent in the "TicketLogs" channel
|
|
||||||
|
|
||||||
# --- Creation Config ---
|
|
||||||
# --- Start ---
|
|
||||||
|
|
||||||
# Available Placeholders:
|
|
||||||
|
|
||||||
# %ticketusername% - User who created the ticket.
|
|
||||||
# <@%ticketauthor%> - Tags the user who created the ticket.
|
|
||||||
# <#%ticketid%> - Mentions the ticket channel.
|
|
||||||
|
|
||||||
Ticket_Title: "Server Support | %ticketusername%"
|
|
||||||
TicketMessage: "Hello <@%ticketauthor%>, Thank you for creating a support ticket\nPlease wait patiently whilst a member of our team reaches out to you."
|
|
||||||
|
|
||||||
Ticket_Thumbnail: "https://images.emojiterra.com/google/android-pie/512px/1f39f.png"
|
|
||||||
NoTicketSubject: "No Reason Provided!"
|
|
||||||
|
|
||||||
TicketCreatedAlert: ":ticket: Your support ticket has been created: <#%ticketid%>"
|
|
||||||
|
|
||||||
AddedUser: "👍 <@%user%> has been added to this ticket"
|
|
||||||
RemovedUser: "👍 <@%user%> has been removed from this ticket"
|
|
||||||
|
|
||||||
# --- Buttons Config ---
|
|
||||||
# --- Start ---
|
|
||||||
|
|
||||||
# Discord Button Colour Selection
|
|
||||||
# PRIMARY - Blue
|
|
||||||
# SECONDARY - Grey
|
|
||||||
# DANGER - Red
|
|
||||||
# SUCCESS - Green
|
|
||||||
|
|
||||||
LockTicketCategory: "Archive"
|
|
||||||
|
|
||||||
TicketCloseEmoji: "❌"
|
|
||||||
TicketDeleteEmoji: "⛔"
|
|
||||||
TicketLockEmoji: "🔒"
|
|
||||||
|
|
||||||
TicketCloseColour: "DANGER"
|
|
||||||
TicketDeleteColour: "DANGER"
|
|
||||||
TicketLockColour: "PRIMARY"
|
|
||||||
|
|
||||||
TranscriptSavedMessage: "📝 **Transcript Saved!**"
|
|
||||||
TicketLockMessage: "❌ **Ticket Locked!**"
|
|
||||||
TicketDeleteMessage: "⚠ **Ticket Deleting!**" # Tickets deleting after 5 seconds by default.
|
|
||||||
|
|
||||||
# --- Departments Config ---
|
|
||||||
# --- Start ---
|
|
||||||
|
|
||||||
TicketDepartments: true
|
|
||||||
# Delete the ticket if no department selected. TIME IN MINUTES
|
|
||||||
Timeout: 5
|
|
||||||
# Set to true to allow all staff to access the ticket
|
|
||||||
AllowAllStaff: false
|
|
||||||
|
|
||||||
# Ticket roles are the roles that will be able to gain access to tickets depending on what department has been chosen.
|
|
||||||
|
|
||||||
Departments:
|
|
||||||
- title: "Support"
|
|
||||||
role: "Staff"
|
|
||||||
color: "SECONDARY"
|
|
||||||
emoji: "1️⃣"
|
|
||||||
category: "Billing Support"
|
|
||||||
|
|
||||||
- title: "Billing Support"
|
|
||||||
role: "Staff"
|
|
||||||
color: "SECONDARY"
|
|
||||||
emoji: "2️⃣"
|
|
||||||
category: "Admin Team"
|
|
||||||
|
|
||||||
- title: "Admin Team"
|
|
||||||
role: "Admin"
|
|
||||||
color: "SECONDARY"
|
|
||||||
emoji: "3️⃣"
|
|
||||||
category: "Admin Support"
|
|
||||||
# - title: "Other"
|
|
||||||
# role: "Staff"
|
|
||||||
# color: "SECONDARY"
|
|
||||||
# emoji: "3️⃣"
|
|
||||||
# category: "Tickets"
|
|
||||||
|
|
||||||
# Discord Button Colour Selection
|
|
||||||
# PRIMARY - Blurple
|
|
||||||
# SECONDARY - Grey
|
|
||||||
# DANGER - Red
|
|
||||||
# SUCCESS - Green
|
|
||||||
|
|
||||||
# --- Closing Config ---
|
|
||||||
# --- Start ---
|
|
||||||
|
|
||||||
StaffOnly: true
|
|
||||||
ClosingTicket: ":warning: You have requested to close your support ticket."
|
|
||||||
ClosingConfirmation_Word: "confirm"
|
|
||||||
CloseConfirmation: true
|
|
||||||
|
|
||||||
# ---------------------------
|
|
||||||
# Error / Warning Messages
|
|
||||||
# ---------------------------
|
|
||||||
|
|
||||||
IncorrectPerms: ":x: **Err!** You do not have the correct permissions to use this command."
|
|
||||||
InvalidChannel: ":x: **Err!** Channel not found. Please create it!"
|
|
||||||
UserNotFound: ":x: **Err!** This user doesn't exist, Are they in this server?"
|
|
||||||
NoValidTicket: ":warning: You cannot execute this command here."
|
|
||||||
TicketExists: ":warning: This ticket cannot be opened, It already exists!"
|
|
||||||
+48
-22
@@ -7,35 +7,61 @@
|
|||||||
# SupportBot created by Emerald Services
|
# SupportBot created by Emerald Services
|
||||||
# Installed with MIT License
|
# Installed with MIT License
|
||||||
#
|
#
|
||||||
# Discord Support: https://emeraldsrv.dev/discord
|
# Discord Support: https://discord.gg/emeraldsrv
|
||||||
# Community Resources: https://emeraldsrv.dev/resources
|
# Official Documentation: https://docs.emeraldsrv.com
|
||||||
|
# Addons Marketplace: https://community.emeraldsrv.com
|
||||||
|
#
|
||||||
|
# Ticket Creation Panel Configuration (ticket-panel.yml)
|
||||||
|
|
||||||
# -------------------------------
|
# -------------------------------
|
||||||
# Ticket Panel Configuration
|
# Ticket Panel Configuration
|
||||||
# -------------------------------
|
# -------------------------------
|
||||||
|
|
||||||
TicketPanel: true
|
Panel: true
|
||||||
|
|
||||||
# ----------------------------------------
|
Style:
|
||||||
# Ticket Panel Message Configuration
|
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: "▣┃ticket-creation"
|
# Available Layout Styles
|
||||||
ButtonEmoji: "📩"
|
# "1" - Style 1 - Title & Text, Button at the top -- Image at the bottom
|
||||||
ButtonLabel: "Create Ticket"
|
# "2" - Style 2 - Title & Text, Button at the bottom -- Image at the top
|
||||||
ButtonColour: "SUCCESS"
|
# "3" - Style 3 - The classic look - Title & Text at the top -- Image in the middle -- Button at the bottom
|
||||||
# Colour Options:
|
# "4" - Style 4 - Classic Embed (Title & Text, Image on right, Button at bottom)
|
||||||
# -> PRIMARY - Purple
|
|
||||||
# -> SECONDARY - Grey
|
|
||||||
# -> SUCCESS - Green
|
|
||||||
# -> DANGER - Red
|
|
||||||
|
|
||||||
PanelTitle: "Need Help? Create a Support Ticket."
|
Button:
|
||||||
|
Text: "Open"
|
||||||
|
Emoji: "🛟"
|
||||||
|
Color: "2"
|
||||||
|
CustomId: "createticket"
|
||||||
|
|
||||||
TicketPanel_Description: true
|
# Available Button Styles
|
||||||
TicketPanel_Thumbnail: true
|
# 1 - Blurple
|
||||||
TicketPanel_Image: true
|
# 2 - Grey
|
||||||
|
# 3 - Red
|
||||||
|
# 4 - Green
|
||||||
|
|
||||||
PanelMessage: "Click the button to open a new support ticket."
|
DepartmentMenu:
|
||||||
PanelThumbnail: "https://i.imgur.com/ieeZejl.png"
|
CustomId: "ticketdepartmentselect"
|
||||||
PanelImage: "https://i.imgur.com/5NkcmDq.png"
|
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 +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,
|
||||||
|
};
|
||||||
+28
-6
@@ -1,9 +1,31 @@
|
|||||||
const Event = require("../Structures/Event.js");
|
const Event = require("../Structures/Event.js");
|
||||||
module.exports = new Event("guildCreate", async (client) => {
|
const {
|
||||||
console.log(
|
getConfiguredGuildId,
|
||||||
`\u001b[31m`,
|
syncGuildHealth,
|
||||||
`${client.user.username} is not in the correct server set in your config. Please leave all other servers and restart the bot.`
|
logGuildWarning,
|
||||||
|
} = require("../Structures/GuildManager.js");
|
||||||
|
|
||||||
|
module.exports = new Event("guildCreate", async (client, guild) => {
|
||||||
|
const configuredId = getConfiguredGuildId();
|
||||||
|
|
||||||
|
if (!configuredId) {
|
||||||
|
logGuildWarning(
|
||||||
|
client,
|
||||||
|
`Joined "${guild.name}" (${guild.id}). Set General.GuildId in config to lock the bot to one server.`,
|
||||||
|
);
|
||||||
|
syncGuildHealth(client);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (guild.id === configuredId) {
|
||||||
|
syncGuildHealth(client);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logGuildWarning(
|
||||||
|
client,
|
||||||
|
`Joined unauthorized server "${guild.name}" (${guild.id}). Leave it from the dashboard or remove the invite.`,
|
||||||
);
|
);
|
||||||
console.log(`\u001b[31m`, `${client.user.username} will now exit.`);
|
|
||||||
return process.exit(1);
|
syncGuildHealth(client);
|
||||||
});
|
});
|
||||||
+20
-7
@@ -1,10 +1,23 @@
|
|||||||
const Event = require("../Structures/Event.js");
|
const Event = require("../Structures/Event.js");
|
||||||
|
const {
|
||||||
|
getConfiguredGuildId,
|
||||||
|
syncGuildHealth,
|
||||||
|
logGuildWarning,
|
||||||
|
} = require("../Structures/GuildManager.js");
|
||||||
|
|
||||||
module.exports = new Event("guildDelete", async (client) => {
|
module.exports = new Event("guildDelete", async (client, guild) => {
|
||||||
console.log(
|
const configuredId = getConfiguredGuildId();
|
||||||
`\u001b[31m`,
|
|
||||||
`${client.user.username} is not in the correct server set in your config. Please join the server and restart the bot.`
|
if (configuredId && guild.id === configuredId) {
|
||||||
);
|
logGuildWarning(
|
||||||
console.log(`\u001b[31m`, `${client.user.username} will now exit.`);
|
client,
|
||||||
return process.exit(1);
|
`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);
|
||||||
});
|
});
|
||||||
+143
-38
@@ -1,46 +1,151 @@
|
|||||||
|
const {
|
||||||
const Discord = require('discord.js');
|
EmbedBuilder,
|
||||||
const fs = require("fs");
|
ActionRowBuilder,
|
||||||
|
ButtonBuilder,
|
||||||
|
ButtonStyle,
|
||||||
|
MessageFlags,
|
||||||
|
ContainerBuilder,
|
||||||
|
TextDisplayBuilder,
|
||||||
|
SectionBuilder,
|
||||||
|
ThumbnailBuilder,
|
||||||
|
MediaGalleryBuilder
|
||||||
|
} = require("discord.js");
|
||||||
const Event = require("../Structures/Event.js");
|
const Event = require("../Structures/Event.js");
|
||||||
const yaml = require("js-yaml");
|
|
||||||
const supportbot = yaml.load(
|
|
||||||
fs.readFileSync("./Configs/supportbot.yml", "utf8")
|
|
||||||
);
|
|
||||||
|
|
||||||
module.exports = new Event("guildMemberAdd", async (client, member, interaction, guild) => {
|
const supportbot = require("../Structures/ConfigStore").supportbot;
|
||||||
|
const msgconfig = require("../Structures/ConfigStore").messages;
|
||||||
|
|
||||||
if (supportbot.Welcome) {
|
function getWelcomeThumbnail(client, member, iconCfg) {
|
||||||
const WelcomeChannel = member.guild.channels.cache.find(channel => channel.name === supportbot.WelcomeChannel)
|
switch ((iconCfg?.Mode || "").toUpperCase()) {
|
||||||
|
case "USER":
|
||||||
|
return member.user.displayAvatarURL();
|
||||||
|
case "BOT":
|
||||||
|
return client.user.displayAvatarURL();
|
||||||
|
case "CUSTOM":
|
||||||
|
return iconCfg.CustomURL;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const WelcomeEmbed = new Discord.MessageEmbed()
|
function buildModernComponents(client, member) {
|
||||||
.setColor(supportbot.WelcomeColour)
|
const container = new ContainerBuilder();
|
||||||
.setTitle(supportbot.WelcomeTitle)
|
|
||||||
.setDescription(supportbot.WelcomeMessage.replace(/%joined_user%/g, member.user))
|
|
||||||
.setTimestamp();
|
|
||||||
|
|
||||||
if (supportbot.EmbedWelcomeThumbnail === "BOT") {
|
|
||||||
WelcomeEmbed.setThumbnail(client.user.displayAvatarURL())
|
|
||||||
}
|
|
||||||
|
|
||||||
if (supportbot.EmbedWelcomeThumbnail === "USER") {
|
|
||||||
WelcomeEmbed.setThumbnail(member.user.displayAvatarURL())
|
|
||||||
}
|
|
||||||
|
|
||||||
if (supportbot.EmbedWelcomeImage) {
|
|
||||||
WelcomeEmbed.setImage(supportbot.EmbedWelcomeImageURL)
|
|
||||||
}
|
|
||||||
|
|
||||||
WelcomeChannel.send({
|
|
||||||
embeds: [WelcomeEmbed]
|
|
||||||
})
|
|
||||||
|
|
||||||
if (supportbot.AutoRole) {
|
|
||||||
var role = member.guild.roles.cache.find(role => role.name === supportbot.JoinRole);
|
|
||||||
member.roles.add(role);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`\u001b[32m`, `[+]`, `\u001b[33m`, `${member.user.username} joined the server!`)
|
|
||||||
|
|
||||||
|
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!`);
|
||||||
});
|
});
|
||||||
+28
-29
@@ -1,41 +1,40 @@
|
|||||||
|
const { EmbedBuilder } = require("discord.js");
|
||||||
const Discord = require('discord.js');
|
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const Event = require("../Structures/Event.js");
|
|
||||||
const yaml = require("js-yaml");
|
const yaml = require("js-yaml");
|
||||||
const supportbot = yaml.load(
|
const Event = require("../Structures/Event.js");
|
||||||
fs.readFileSync("./Configs/supportbot.yml", "utf8")
|
|
||||||
);
|
|
||||||
|
|
||||||
module.exports = new Event("guildMemberRemove", async (client, member, interaction, guild) => {
|
const supportbot = require("../Structures/ConfigStore").supportbot;
|
||||||
|
const msgconfig = require("../Structures/ConfigStore").messages;
|
||||||
|
|
||||||
if (supportbot.Leave) {
|
module.exports = new Event("guildMemberRemove", async (client, member) => {
|
||||||
const LeaveChannel = member.guild.channels.cache.find(channel => channel.name === supportbot.LeaveChannel)
|
|
||||||
|
|
||||||
const LeaveEmbed = new Discord.MessageEmbed()
|
if (!supportbot.Leave || !supportbot.Leave.Enabled) return;
|
||||||
.setColor(supportbot.LeaveColour)
|
|
||||||
.setTitle(supportbot.LeaveTitle)
|
|
||||||
.setDescription(supportbot.LeaveMessage.replace(/%joined_user%/g, member.user))
|
|
||||||
.setTimestamp();
|
|
||||||
|
|
||||||
if (supportbot.EmbedLeaveThumbnail === "BOT") {
|
const leaveChannel =
|
||||||
LeaveEmbed.setThumbnail(client.user.displayAvatarURL())
|
member.guild.channels.cache.get(supportbot.Leave.Channel) ||
|
||||||
}
|
member.guild.channels.cache.find(c => c.name === supportbot.Leave.Channel);
|
||||||
|
|
||||||
if (supportbot.EmbedLeaveThumbnail === "USER") {
|
if (!leaveChannel) return;
|
||||||
LeaveEmbed.setThumbnail(member.user.displayAvatarURL())
|
|
||||||
}
|
|
||||||
|
|
||||||
if (supportbot.EmbedLeaveImage) {
|
const leaveEmbed = new EmbedBuilder()
|
||||||
LeaveEmbed.setImage(supportbot.EmbedLeaveImageURL)
|
.setColor(msgconfig.Leave.Embed.Colour)
|
||||||
}
|
.setTitle(msgconfig.Leave.Embed.Title)
|
||||||
|
.setDescription(
|
||||||
|
msgconfig.Leave.Embed.Message.replace(/%left_user%/g, member.user.toString())
|
||||||
|
)
|
||||||
|
.setTimestamp();
|
||||||
|
|
||||||
LeaveChannel.send({
|
if (msgconfig.Leave.Embed.Thumbnail === "BOT") {
|
||||||
embeds: [LeaveEmbed]
|
leaveEmbed.setThumbnail(client.user.displayAvatarURL());
|
||||||
})
|
} else if (msgconfig.Leave.Embed.Thumbnail === "USER") {
|
||||||
|
leaveEmbed.setThumbnail(member.user.displayAvatarURL());
|
||||||
console.log(`\u001b[31m`, `[-]`, `\u001b[33m`, `${member.user.username} joined the server!`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
if (msgconfig.Leave.Embed.ImageEnabled) {
|
||||||
|
leaveEmbed.setImage(msgconfig.Leave.Embed.ImageURL);
|
||||||
|
}
|
||||||
|
|
||||||
|
await leaveChannel.send({ embeds: [leaveEmbed] });
|
||||||
|
|
||||||
|
console.log(`[-] ${member.user.tag} left the server`);
|
||||||
|
});
|
||||||
+990
-88
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
-219
@@ -1,222 +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 Event = require("../Structures/Event.js");
|
||||||
|
const { runBotReady } = require("../Structures/BotStartup.js");
|
||||||
|
|
||||||
module.exports = new Event("ready", async (client, interaction) => {
|
module.exports = new Event("clientReady", async (client) => {
|
||||||
const { getRole, getChannel, getCategory } = client;
|
// restartDiscordBot runs startup manually — clientReady often does not re-fire anyway
|
||||||
client.user.setActivity(supportbot.BotActivity, {
|
if (client.__explicitStartup) return;
|
||||||
type: supportbot.ActivityType,
|
await runBotReady(client, { clearConsole: true, logCommands: false });
|
||||||
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`,
|
|
||||||
`Invite to your server:`,
|
|
||||||
`\u001b[36m`,
|
|
||||||
`https://discord.com/api/oauth2/authorize?client_id=` +
|
|
||||||
client.user.id +
|
|
||||||
`&permissions=8&scope=bot%20applications.commands`
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
`\u001b[33m`,
|
|
||||||
`Resources:`,
|
|
||||||
`\u001b[36m`,
|
|
||||||
`https://emeraldsrv.dev/resources`
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
`\u001b[33m`,
|
|
||||||
`Documentation:`,
|
|
||||||
`\u001b[36m`,
|
|
||||||
`https://emeraldsrv.dev/wiki/supportbot`
|
|
||||||
);
|
|
||||||
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`, `――――――――――――――――――――――――――――――――――――――――――――`);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const roles = [
|
|
||||||
supportbot.Admin,
|
|
||||||
supportbot.Staff,
|
|
||||||
supportbot.TicketBlackListRole,
|
|
||||||
];
|
|
||||||
if (supportbot.TicketDepartments) {
|
|
||||||
supportbot.Departments.forEach((department) => roles.push(department.role));
|
|
||||||
}
|
|
||||||
if (supportbot.AutoRole) roles.push(supportbot.JoinRole);
|
|
||||||
const channels = [
|
|
||||||
supportbot.SuggestionChannel,
|
|
||||||
supportbot.TicketLog,
|
|
||||||
supportbot.TranscriptLog,
|
|
||||||
panelconfig.Channel,
|
|
||||||
supportbot.WelcomeChannel,
|
|
||||||
supportbot.LeaveChannel,
|
|
||||||
supportbot.TranslateLogChannel
|
|
||||||
];
|
|
||||||
const categories = [supportbot.TicketCategory];
|
|
||||||
|
|
||||||
const missingC = [];
|
|
||||||
const missingR = [];
|
|
||||||
const missingCat = [];
|
|
||||||
for (let c of channels) {
|
|
||||||
if ((c === supportbot.SuggestionChannel && supportbot.DisableSuggestionsChannel) || c === supportbot.TicketLog && supportbot.DisableTicketLogChannel) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
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 = await client.getChannel(
|
|
||||||
panelconfig.Channel,
|
|
||||||
client.guilds.cache.first()
|
|
||||||
);
|
|
||||||
|
|
||||||
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 () => {
|
|
||||||
let embed = new Discord.MessageEmbed()
|
|
||||||
.setTitle(panelconfig.PanelTitle)
|
|
||||||
.setColor(supportbot.SuccessColour)
|
|
||||||
.setFooter({
|
|
||||||
text: supportbot.EmbedFooter,
|
|
||||||
iconURL: interaction.user.displayAvatarURL(),
|
|
||||||
});
|
|
||||||
|
|
||||||
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;
|
|
||||||
if (supportbot.TicketDepartments) {
|
|
||||||
button = supportbot.Departments.map((x) =>
|
|
||||||
new Discord.MessageButton()
|
|
||||||
.setCustomId("department-" + supportbot.Departments.indexOf(x))
|
|
||||||
.setLabel(x.title)
|
|
||||||
.setStyle(x.color)
|
|
||||||
.setEmoji(x.emoji)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
+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,12 +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) **|**
|
<a href="https://github.com/nodejs/node">
|
||||||
[Hosting](https://emeraldsrv.dev/hosting) **|**
|
<img src="https://img.shields.io/badge/NODE.JS%20-%20v20.x-green?style=for-the-badge" alt="shield.png">
|
||||||
[Documentation](https://emeraldsrv.dev/wiki/supportbot) **|**
|
</a>
|
||||||
[Download via MC-Market](https://www.mc-market.org/resources/8940/) **|**
|
|
||||||
|
<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
|
// SupportBot | Emerald Services
|
||||||
// Addon Structure
|
// Addon Structure
|
||||||
|
|
||||||
class Command {
|
class Command {
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
this.name = options.name;
|
this.name = options.name;
|
||||||
@@ -10,7 +10,13 @@ class Command {
|
|||||||
this.run = options.run;
|
this.run = options.run;
|
||||||
this.defaultPermission = true;
|
this.defaultPermission = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Method to dynamically update options
|
||||||
|
setOptions(options) {
|
||||||
|
this.options = options;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Event {
|
class Event {
|
||||||
constructor(event, run) {
|
constructor(event, run) {
|
||||||
this.event = event;
|
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,
|
||||||
|
};
|
||||||
+147
-80
@@ -1,124 +1,191 @@
|
|||||||
// SupportBot | Emerald Services
|
|
||||||
// Client Structure
|
|
||||||
|
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
const Discord = require("discord.js");
|
const Discord = require("discord.js");
|
||||||
const 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 = configStore.supportbot;
|
||||||
const supportbot = yaml.load(
|
const cmdconfig = configStore.commands;
|
||||||
fs.readFileSync("./Configs/supportbot.yml", "utf8")
|
|
||||||
);
|
const c = {
|
||||||
|
reset: "\x1b[0m",
|
||||||
|
gray: "\x1b[90m",
|
||||||
|
green: "\x1b[32m",
|
||||||
|
yellow: "\x1b[33m",
|
||||||
|
cyan: "\x1b[36m",
|
||||||
|
red: "\x1b[31m",
|
||||||
|
white: "\x1b[37m",
|
||||||
|
bold: "\x1b[1m",
|
||||||
|
};
|
||||||
|
|
||||||
|
function line() {
|
||||||
|
console.log(`${c.gray}────────────────────────────────────────────────────────${c.reset}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function section(title) {
|
||||||
|
console.log(`\n${c.green}${title}${c.reset}`);
|
||||||
|
console.log(`${c.gray}${"-".repeat(title.length)}${c.reset}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function walk(dir, ext = ".js", fileList = []) {
|
||||||
|
const files = fs.readdirSync(dir);
|
||||||
|
for (const file of files) {
|
||||||
|
const full = `${dir}/${file}`;
|
||||||
|
const stat = fs.statSync(full);
|
||||||
|
if (stat.isDirectory()) {
|
||||||
|
walk(full, ext, fileList);
|
||||||
|
} else if (file.endsWith(ext)) {
|
||||||
|
fileList.push(full);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fileList;
|
||||||
|
}
|
||||||
|
|
||||||
class Client extends Discord.Client {
|
class Client extends Discord.Client {
|
||||||
constructor() {
|
constructor() {
|
||||||
super({ intents });
|
super({
|
||||||
|
intents: [
|
||||||
|
GatewayIntentBits.Guilds,
|
||||||
|
GatewayIntentBits.GuildMembers,
|
||||||
|
GatewayIntentBits.GuildMessages,
|
||||||
|
GatewayIntentBits.GuildMessageReactions,
|
||||||
|
GatewayIntentBits.GuildEmojisAndStickers,
|
||||||
|
GatewayIntentBits.MessageContent,
|
||||||
|
],
|
||||||
|
partials: [Partials.Message, Partials.Channel, Partials.Reaction],
|
||||||
|
});
|
||||||
|
|
||||||
this.commands = new Discord.Collection();
|
this.commands = new Discord.Collection();
|
||||||
}
|
}
|
||||||
|
|
||||||
async getChannel(channel, guild) {
|
async getChannel(channel, guild) {
|
||||||
|
if (!channel) return null;
|
||||||
|
const channelStr = String(channel).toLowerCase();
|
||||||
return guild.channels.cache.find(
|
return guild.channels.cache.find(
|
||||||
(c) =>
|
(c) =>
|
||||||
(c.type === "GUILD_TEXT" || c.type === "GUILD_NEWS") &&
|
(c.type === Discord.ChannelType.GuildText || c.type === Discord.ChannelType.GuildNews) &&
|
||||||
(c.id === channel || c.name.toLowerCase() === channel.toLowerCase())
|
(c.id === channel || (c.name && c.name.toLowerCase() === channelStr)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getRole(role, guild) {
|
async getRole(role, guild) {
|
||||||
|
if (!role) return null;
|
||||||
|
const roleStr = String(role).toLowerCase();
|
||||||
return guild.roles.cache.find(
|
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) {
|
async getCategory(category, guild) {
|
||||||
|
if (!category) return null;
|
||||||
|
const catStr = String(category).toLowerCase();
|
||||||
return guild.channels.cache.find(
|
return guild.channels.cache.find(
|
||||||
(c) =>
|
(c) =>
|
||||||
c.type === "GUILD_CATEGORY" &&
|
c.type === Discord.ChannelType.GuildCategory &&
|
||||||
(c.id === category || c.name.toLowerCase() === category.toLowerCase())
|
(c.id === category || (c.name && c.name.toLowerCase() === catStr)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async start(token) {
|
async start(token) {
|
||||||
var tempCommandFiles = fs.readdirSync("./Commands").filter((file) => file.endsWith(".js"));
|
const commandFiles = walk("./Commands");
|
||||||
|
|
||||||
if (supportbot.DisableSuggestions) {
|
const allCommands = commandFiles.map((file) => ({
|
||||||
tempCommandFiles = tempCommandFiles.filter(item => item !== "suggest.js");
|
file,
|
||||||
|
command: require(`../${file}`),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const enabledCommands = allCommands.filter(({ command }) => {
|
||||||
|
const matchingConfig = Object.values(cmdconfig).find(
|
||||||
|
(entry) =>
|
||||||
|
entry &&
|
||||||
|
typeof entry === "object" &&
|
||||||
|
entry.Command &&
|
||||||
|
String(entry.Command).toLowerCase() === String(command.name).toLowerCase(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!matchingConfig) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return matchingConfig.Enabled !== false;
|
||||||
|
});
|
||||||
|
|
||||||
|
section("Commands");
|
||||||
|
|
||||||
|
enabledCommands.forEach(({ command, file }) => {
|
||||||
|
console.log(
|
||||||
|
`${c.green}✓${c.reset} ${c.white}${command.name}${c.reset} ${c.gray}(${path.basename(file)})${c.reset}`,
|
||||||
|
);
|
||||||
|
this.commands.set(command.name, command);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (supportbot.General.Addons.Enabled) {
|
||||||
|
const addonFiles = fs.readdirSync("./Addons").filter((file) => file.endsWith(".js"));
|
||||||
|
|
||||||
|
section("Addons");
|
||||||
|
|
||||||
|
addonFiles.forEach((file) => {
|
||||||
|
let addon = require(`../Addons/${file}`);
|
||||||
|
|
||||||
|
// Handle single or multiple addons per file
|
||||||
|
const addonArray = Array.isArray(addon) ? addon : [addon];
|
||||||
|
|
||||||
|
addonArray.forEach((item, index) => {
|
||||||
|
// If name is missing, use filename (handling index for arrays)
|
||||||
|
if (!item.name) {
|
||||||
|
item.name = index === 0 ? file.split(".")[0] : `${file.split(".")[0]}_${index}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`${c.green}✓${c.reset} ${c.white}${item.name}${c.reset}`);
|
||||||
|
|
||||||
|
if (item instanceof Command) {
|
||||||
|
this.commands.set(item.name, item);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.events && Array.isArray(item.events)) {
|
||||||
|
item.events.forEach((event) => {
|
||||||
|
if (event instanceof Event) {
|
||||||
|
this.on(event.event, (...args) => event.run(this, ...args));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's a direct Event instance
|
||||||
|
if (item instanceof Event) {
|
||||||
|
this.on(item.event, (...args) => item.run(this, ...args));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const commandFiles = tempCommandFiles;
|
this.on("clientReady", async () => {
|
||||||
|
const guild = getPrimaryGuild(this);
|
||||||
const addonFiles = fs
|
if (!guild) return;
|
||||||
.readdirSync("./Addons")
|
await guild.commands.set(this.commands);
|
||||||
.filter((file) => file.endsWith(".js"));
|
const { logSlashCommandsRegistered } = require("./BotStartup.js");
|
||||||
|
logSlashCommandsRegistered(this);
|
||||||
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}`));
|
section("Events");
|
||||||
|
|
||||||
// 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"
|
|
||||||
);
|
|
||||||
addon.commands?.forEach((command) => {
|
|
||||||
this.commands.set(command.name, command);
|
|
||||||
});
|
|
||||||
addon.events?.forEach((event) => {
|
|
||||||
this.on(event.event, (...args) => event.run(this, ...args));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`\u001b[34;1m`, "▬▬▬▬▬▬▬ Addons ▬▬▬▬▬▬▬");
|
|
||||||
|
|
||||||
// Slash Commands
|
|
||||||
|
|
||||||
this.once("ready", async () => {
|
|
||||||
await this.guilds.cache.first()?.commands.set(this.commands);
|
|
||||||
console.log(
|
|
||||||
`\u001b[32;1m`,
|
|
||||||
`Slash Commands Registered for ${this.guilds.cache.first().name}`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(" ");
|
|
||||||
console.log(`\u001b[34;1m`, "▬▬▬▬▬▬▬ Events ▬▬▬▬▬▬▬");
|
|
||||||
|
|
||||||
fs.readdirSync("./Events")
|
fs.readdirSync("./Events")
|
||||||
.filter((file) => file.endsWith(".js"))
|
.filter((file) => file.endsWith(".js"))
|
||||||
.forEach((file) => {
|
.forEach((file) => {
|
||||||
const event = require(`../Events/${file}`);
|
const event = require(`../Events/${file}`);
|
||||||
console.log(`\u001b[31;1m`, `${event.event}`, `\u001b[32;1m`, "Loaded");
|
console.log(
|
||||||
|
`${c.green}✓${c.reset} ${c.white}${event.event}${c.reset} ${c.gray}(${file})${c.reset}`,
|
||||||
|
);
|
||||||
this.on(event.event, (...args) => event.run(this, ...args));
|
this.on(event.event, (...args) => event.run(this, ...args));
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`\u001b[34;1m`, "▬▬▬▬▬▬▬ Events ▬▬▬▬▬▬▬");
|
console.log("");
|
||||||
|
|
||||||
if (process.argv[2] !== "test") {
|
if (process.argv[2] !== "test") {
|
||||||
await this.login(token);
|
await this.login(token);
|
||||||
} else {
|
} else {
|
||||||
console.log(
|
console.log(`${c.red}RUNNING IN TEST MODE, IF NOT INTENDED, PLEASE USE npm start${c.reset}`);
|
||||||
`\u001b[31;1m`,
|
|
||||||
"RUNNING IN TEST MODE, IF NOT INTENDED, PLEASE USE npm start"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
// SupportBot | Emerald Services
|
||||||
// Event Structure
|
// Event Structure
|
||||||
|
|
||||||
class Event {
|
class Event {
|
||||||
constructor(event, run) {
|
constructor(event, run) {
|
||||||
this.event = event;
|
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
|
// SupportBot | Emerald Services
|
||||||
// Ticket ID Structure
|
// Ticket ID Structure
|
||||||
|
|
||||||
const fs = require("fs");
|
const db = require("./Database");
|
||||||
|
|
||||||
class TicketNumberID {
|
class TicketNumberID {
|
||||||
static async pad() {
|
static async pad() {
|
||||||
@@ -12,30 +12,29 @@ class TicketNumberID {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static async get() {
|
static async get() {
|
||||||
let ticketnumberID = await JSON.parse(
|
const settings = db.getSettings ? db.getSettings() : {};
|
||||||
fs.readFileSync("./Data/ticket-panel-id.json", "utf8")
|
if (typeof settings.ticket_counter === "number") {
|
||||||
);
|
return settings.ticket_counter;
|
||||||
if (!ticketnumberID.id) {
|
|
||||||
return this.set(0);
|
|
||||||
} else {
|
|
||||||
return ticketnumberID.id;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
static async set(value) {
|
||||||
let ticketnumberID = await JSON.parse(
|
const settings = db.getSettings ? db.getSettings() : {};
|
||||||
fs.readFileSync("./Data/ticket-panel-id.json", "utf8")
|
settings.ticket_counter = value;
|
||||||
);
|
if (db.saveSettings) db.saveSettings(settings);
|
||||||
|
if (db.saveTicketPanel) db.saveTicketPanel(`panel-${value}`, "none");
|
||||||
ticketnumberID.id = value;
|
|
||||||
fs.writeFileSync(
|
|
||||||
"./Data/ticket-panel-id.json",
|
|
||||||
JSON.stringify(ticketnumberID, null, 4),
|
|
||||||
(err) => {
|
|
||||||
if (err) console.error(err);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return value;
|
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,16 +8,94 @@
|
|||||||
// Installed with MIT License
|
// Installed with MIT License
|
||||||
//
|
//
|
||||||
// Discord Support: https://emeraldsrv.dev/discord
|
// Discord Support: https://emeraldsrv.dev/discord
|
||||||
// Community Resources: https://emeraldsrv.dev/resources
|
// Community Resources: https://community.emeraldsrv.dev
|
||||||
|
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
const yaml = require("js-yaml");
|
const configStore = require("./Structures/ConfigStore.js");
|
||||||
const supportbot = yaml.load(
|
|
||||||
fs.readFileSync("./Configs/supportbot.yml", "utf8")
|
|
||||||
);
|
|
||||||
|
|
||||||
const Client = require("./Structures/Client.js");
|
const Client = require("./Structures/Client.js");
|
||||||
const client = new Client({});
|
const client = new Client({
|
||||||
|
intents: ["Guilds", "GuildMembers", "GuildMessages", "MessageContent"],
|
||||||
|
});
|
||||||
|
|
||||||
client.start(supportbot.Token);
|
const { 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
+2151
-4872
File diff suppressed because it is too large.
Load diff
+22
-8
@@ -1,11 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "supportbot",
|
"name": "supportbot",
|
||||||
"version": "7.4.2",
|
"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.",
|
"description": "SupportBot is an open-sourced support system for discord. It's completely free to use with high amounts of customization to match your own brand.",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "npm i && node index.js",
|
"setup": "npm i && node index.js",
|
||||||
"test": "node index.js test"
|
"start": "node index.js",
|
||||||
|
"git:setup": "node scripts/setup-git-security.js"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -16,13 +17,26 @@
|
|||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/Emerald-Services/SupportBot/issues"
|
"url": "https://github.com/Emerald-Services/SupportBot/issues"
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/Emerald-Services/SupportBot#readme",
|
"homepage": "https://emeraldsrv.dev",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitalets/google-translate-api": "^8.0.0",
|
"@vitalets/google-translate-api": "^9.2.1",
|
||||||
"discord.js": "^13.6.0",
|
"adm-zip": "^0.6.0",
|
||||||
|
"axios": "^1.7.2",
|
||||||
|
"better-sqlite3": "^12.2.0",
|
||||||
|
"cookie-parser": "^1.4.7",
|
||||||
|
"cors": "^2.8.6",
|
||||||
|
"discord.js": "^14.13.0",
|
||||||
|
"express": "^5.2.1",
|
||||||
|
"figlet": "^1.9.3",
|
||||||
|
"google-translate-api": "^2.3.0",
|
||||||
|
"gradient-string": "^3.0.0",
|
||||||
|
"js": "^0.1.0",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"moment": "^2.29.3",
|
"moment": "^2.29.3",
|
||||||
"translate": "^1.4.1",
|
"ms": "^2.1.3",
|
||||||
"twitter": "^1.7.1"
|
"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