Compare commits

...
96 Commits
Author SHA1 Message Date
1tzEmerald d5fa42b55d Merge pull request #206 from Emerald-Services/26.0
26.0
2026-03-27 16:45:39 +00:00
1tzEmerald 4bb35b1e50 Update account setup instructions for SupportBot AI 2026-03-27 16:31:40 +00:00
1tzEmerald d451857d7b Update SupportBot version in configuration file 2026-03-27 16:31:07 +00:00
1tzEmerald 78b804176a 26.0 2026-03-27 16:25:59 +00:00
1tzEmerald 15b780ab62 26.0-build-2 2026-03-11 18:58:15 +00:00
1tzEmerald 9448c4e3d6 26.0-build-1 2026-03-10 20:02:43 +00:00
1tzEmerald 4af9759700 Update SupportBot version to v8.5 2026-02-14 13:32:59 +00:00
1tzEmerald 835cbbe937 Delete package-lock.json 2026-02-14 13:32:10 +00:00
1tzEmerald 491bb9ba65 . 2026-02-14 13:14:05 +00:00
1tzEmerald ced29535f9 Merge pull request #201 from Emerald-Services/8.5
8.5 Update
2026-02-14 13:11:56 +00:00
1tzEmerald d99e9a105b 8.5 Update
Added TicketRename command
2026-02-14 13:05:07 +00:00
1tzEmerald c72c7c5c34 Merge pull request #200 from Emerald-Services/1tzemerald-patch-3
Delete .idea directory
2026-02-03 16:37:22 +00:00
1tzEmerald 23960bc30d Delete .idea directory 2026-02-03 16:37:08 +00:00
1tzEmerald abb48c281b Merge branch 'release' of https://github.com/Emerald-Services/SupportBot into release 2026-02-03 16:33:08 +00:00
1tzEmerald 7acb825713 Added rename command 2026-02-03 16:33:02 +00:00
1tzEmerald dd2f329485 Merge pull request #199 from Emerald-Services/1tzemerald-patch-2
Update Node.JS version for SupportBot 8.x
2025-11-22 12:52:33 +00:00
1tzEmerald 75577ec0bd Update Node.JS version for SupportBot 8.x 2025-11-22 12:52:23 +00:00
1tzEmerald 7366bcda71 Merge pull request #198 from Emerald-Services/1tzemerald-patch-1
Change support status for SupportBot 7.7.3
2025-11-22 12:51:59 +00:00
1tzEmerald 23e43cfad3 Change support status for SupportBot 7.7.3
Updated support status for SupportBot versions.
2025-11-22 12:51:47 +00:00
Nyo ad9a8085aa Merge pull request #197 from Emerald-Services/ItsNyoty-patch-1
Fix link format for BuiltByBit in README.md
2025-09-30 18:50:12 +02:00
Nyo 2f23e8f23d Fix link format for BuiltByBit in README.md 2025-09-30 18:49:52 +02:00
1tzEmerald 4c07b8a236 Merge pull request #196 from Emerald-Services/dev
8.4.1 guilldMemberAdd update.
2025-09-25 13:16:42 +01:00
1tzEmerald 571bd586fa 8.4.1 guilldMemberAdd update.
Added if statement to .setAccentColor allowing you to remove the color from the config.
2025-09-25 13:16:20 +01:00
1tzEmerald eac2d88873 Merge pull request #195 from Emerald-Services/dev
8.4.1 small fix
2025-09-25 13:10:23 +01:00
1tzEmerald 93c793f393 Merge branch 'dev' of https://github.com/Emerald-Services/SupportBot into dev 2025-09-25 13:09:09 +01:00
1tzEmerald 1aa97b63f9 8.4.1 small fix 2025-09-25 13:09:00 +01:00
1tzEmerald c1b76a2b84 Merge pull request #194 from Emerald-Services/dev
8.4 from Dev to Release
2025-09-23 23:26:30 +01:00
1tzEmerald a1429aa723 Merge branch 'release' into dev 2025-09-23 23:26:13 +01:00
1tzEmerald 2de7c7b19c 8.4 2025-09-23 23:23:19 +01:00
1tzEmerald 7383bd2c48 8.4-DEV-2
Added Buttons to WelcomeMessage
Added Components v2 Support for Welcome Message
Added improved logging system
2025-09-22 18:36:04 +01:00
1tzEmerald a05d65099b 8.4 DEV 1
Changed JSON to SQLlite
Added Suggestion Forums
Added /suggestadmin
2025-09-20 23:09:22 +01:00
1tzEmerald c6db929d12 8.4-Dev 1
Added Suggestions mode switch
2025-08-07 13:54:35 +01:00
1tzEmerald dc88f6c51a Merge pull request #186 from chorimethod/release
Added "InvalidChannel" message (required by suggest.js), fixed a double quote that was preventing the bot to start correctly.
2025-07-14 16:37:45 +01:00
Nyo d69fb0e26d Update supportbot.yml 2025-05-22 20:30:06 +02:00
chorisancio3d 4b2d53d3f4 Added "InvalidChannel" message (required by suggest.js)
Fixed a double quote that was preventing the bot to start correctly.
2025-02-03 21:43:03 -03:00
1tzEmerald e3fa8b6d72 Merge pull request #185 from Emerald-Services/dev
Update close.js
2024-11-10 13:31:17 +00:00
1tzEmerald 5c6a9f7306 Update close.js 2024-11-10 13:30:42 +00:00
1tzEmerald 71e33dbf66 Update close.js 2024-11-10 13:30:27 +00:00
1tzEmerald d468ab1575 Delete Data/Profiles directory 2024-11-10 13:18:50 +00:00
1tzEmerald 74482af471 Merge pull request #184 from Emerald-Services/dev
Dev
2024-11-09 13:04:16 +00:00
1tzEmerald 0b8b092cb9 Update profile.js 2024-11-09 13:03:55 +00:00
1tzEmerald 3cdfaef942 Update profile.js 2024-11-09 13:03:41 +00:00
1tzEmerald fcf441a190 Update close.js 2024-11-09 02:46:35 +00:00
1tzEmerald 5a1c4ad99c Update close.js 2024-11-09 02:46:25 +00:00
1tzEmerald d9cb72f22e Update close.js 2024-11-09 02:44:48 +00:00
1tzEmerald 58eb37124e Update close.js 2024-11-09 02:44:39 +00:00
1tzEmerald afeeda3a8d Merge pull request #183 from Emerald-Services/dev
Dev
2024-11-09 02:38:20 +00:00
1tzEmerald fece90d2bd Update close.js 2024-11-09 02:37:26 +00:00
1tzEmerald 357a85fa3d Update close.js 2024-11-09 02:37:14 +00:00
1tzEmerald f34158caa6 Update close.js 2024-11-09 02:32:52 +00:00
1tzEmerald 1bb343e49a Update ticket.js 2024-11-09 02:32:28 +00:00
1tzEmerald dffa5d4161 Update close.js 2024-11-09 02:32:06 +00:00
1tzEmerald 13590b08a8 Update ticket.js 2024-11-09 01:07:16 +00:00
1tzEmerald 6e14ce209d Merge pull request #182 from Emerald-Services/dev
8.3 Dev -> Release
2024-11-09 00:45:40 +00:00
1tzEmerald 72b8c2247e Merge branch 'release' into dev 2024-11-09 00:45:28 +00:00
1tzEmerald 925504a928 Merge branch 'dev' of https://github.com/Emerald-Services/SupportBot into dev 2024-11-09 00:37:53 +00:00
1tzEmerald 236d67707f 8.3 Update 2024-11-09 00:37:51 +00:00
1tzEmerald 753f432c95 Merge pull request #179 from Emerald-Services/1tzemerald-patch-4
Update package.json
2024-10-11 15:04:37 +01:00
1tzEmerald c824bc51b2 Update package.json 2024-10-11 15:03:47 +01:00
1tzEmerald 39ee540b55 Merge pull request #177 from Emerald-Services/dev
8.2
2024-10-11 15:00:05 +01:00
1tzEmerald 903b214051 Merge branch 'release' into dev 2024-10-11 14:59:50 +01:00
1tzEmerald ac0daf5fe7 8.2 2024-10-10 19:14:45 +01:00
ItsNyoty 80e38a07e9 Merge pull request #175 from Emerald-Services/ItsNyoty-patch-1
Update README.md
2024-09-19 13:06:05 +02:00
ItsNyoty d2598e09f7 Update README.md 2024-09-19 13:05:26 +02:00
ItsNyoty 28aab47150 Update README.md 2024-09-19 13:04:44 +02:00
1tzEmerald 81bda67ae4 Merge pull request #172 from Emerald-Services/1tzemerald-patch-3
Update supportbot.yml
2024-07-30 17:58:41 +01:00
1tzEmerald 25bbd1ff85 Update supportbot.yml 2024-07-30 17:58:01 +01:00
1tzEmerald 164d695a5f Merge pull request #171 from Emerald-Services/1tzemerald-patch-2
Create 0000000000000000.json
2024-07-30 17:57:33 +01:00
1tzEmerald 57e4a3e77c Create 0000000000000000.json 2024-07-30 17:57:12 +01:00
1tzEmerald a6ac532b2b Merge pull request #169 from Emerald-Services/dev
8.0.1 Dev -> Release Branch
2024-07-30 17:47:13 +01:00
1tzEmerald 4f14722e14 8.0.1 Update 2024-07-30 17:45:35 +01:00
1tzEmerald 9a6eece7a1 v8.1 2024-07-09 22:49:49 +01:00
1tzEmerald fd65d0a9a7 8.1-BETA 2024-07-03 13:46:57 +01:00
ItsNyoty 48500fe8a4 Update README.md 2024-07-03 13:42:46 +02:00
1tzEmerald 8d8d9e4bee 8.0.1 Bug Fix 2024-06-13 18:01:41 +01:00
1tzEmerald dcbddf9f23 8.0.1 - Bug Fix 2024-06-13 17:36:44 +01:00
1tzEmerald 7449c0a61a License files 2024-06-12 12:55:45 +01:00
1tzEmerald c95b8ccb32 SupportBot 8.0 2024-06-12 12:54:31 +01:00
1tzEmerald 2c5bfe8e33 Merge pull request #135 from ItsNyoty/patch-1
Update messages.yml
2022-09-05 13:42:52 +01:00
ItsNyoty a3bbd97333 Update messages.yml
TicketExists wasn't defined before
2022-08-29 14:58:01 +02:00
1tzEmerald 4ea488951d Delete oldconfig.yml 2022-08-12 01:38:07 +01:00
1tzEmerald 481424d5a6 Update LICENSE.md 2022-08-12 01:37:49 +01:00
1tzEmerald fe82e96cc1 Update LICENSE.md 2022-08-12 01:37:31 +01:00
1tzEmerald 976258c959 Delete .gitignore 2022-08-12 01:37:14 +01:00
1tzEmerald 83cf0689e2 Delete .DS_Store 2022-08-12 01:37:00 +01:00
ItzEmerald c911cb4423 v8.0 New configuration files 2022-08-12 01:35:36 +01:00
ItzEmerald 4eadef8b65 8.0 Dev 1.2
Reworked configuration files & completed upgrade to V14
2022-08-11 19:19:29 +01:00
ItzEmerald 4e081358ff v8.0-Dev-1.1 2022-07-23 07:50:12 +01:00
ItzEmerald 209ec0b435 v8.0 - Dev-1 2022-07-22 13:19:05 +01:00
Xiled Emerald 6d363abb54 Merge pull request #129 from SypherRed/dev
Better looking readme
2022-07-01 22:19:03 +01:00
SypherRed 6f8b84aeda Update README.md 2022-07-01 23:16:16 +02:00
SypherRed 5b8c5da33a Update README.md 2022-07-01 23:14:01 +02:00
Xiled Emerald b6ef0f95cb Merge pull request #126 from SypherRed/release
Muted Role check and issue fix
2022-06-30 15:49:14 +01:00
SypherRed cce1480a42 remove empty declaration
AboutMe was removed in a previous version.
2022-06-29 13:12:56 +02:00
SypherRed 26e9b56961 Issue fix with Muted Role check
Fixed the issue to recognize a Muted Role and made changes to ready.js accordingly
2022-06-27 19:03:34 +02:00
SypherRed 979271cfb5 beta push of muted check
push of a check if author has a muted role on ticket opening, if so, decline the interaction

Also either check for missing role or not
2022-06-23 20:58:03 +02:00
63 changed files with 9613 additions and 3175 deletions

No files matched your search

Vendored
BIN
View File
Binary file not shown.
-2
View File
@@ -1,2 +0,0 @@
node_modules/
.DS_Store
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+26
View File
@@ -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 = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8"));
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],
});
},
});
-45
View File
@@ -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!");
}),
*/
];
+62
View File
@@ -0,0 +1,62 @@
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.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(),
});
interaction.reply({
flags: Discord.MessageFlags.Ephemeral,
embeds: [HelpEmbed1],
});
},
});
+51
View File
@@ -0,0 +1,51 @@
// 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 msgconfig = yaml.load(
fs.readFileSync("./Configs/messages.yml", "utf8")
);
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],
});
},
});
@@ -10,20 +10,22 @@ const supportbot = yaml.load(
);
const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8"));
const Command = require("../Structures/Command.js");
const Command = require("../../Structures/Command.js");
module.exports = new Command({
name: cmdconfig.PingCommand,
description: cmdconfig.PingCommandDesc,
name: cmdconfig.Ping.Command,
description: cmdconfig.Ping.Description,
options: [],
permissions: ["SEND_MESSAGES"],
permissions: cmdconfig.Ping.Permission,
async run(interaction) {
const PingEmbed = new Discord.MessageEmbed()
let disableCommand = true;
const PingEmbed = new Discord.EmbedBuilder()
.setDescription(
`:ping_pong: **Ping:** \`${interaction.client.ws.ping} ms\``
)
.setColor(supportbot.EmbedColour);
.setColor(supportbot.Embed.Colours.General);
interaction.reply({
embeds: [PingEmbed],
+527
View File
@@ -0,0 +1,527 @@
const {
ModalBuilder,
TextInputBuilder,
TextInputStyle,
ApplicationCommandType,
ActionRowBuilder,
ButtonBuilder,
ButtonStyle,
ApplicationCommandOptionType,
MessageFlags,
ContainerBuilder,
TextDisplayBuilder,
SeparatorBuilder,
ThumbnailBuilder,
MediaGalleryBuilder,
MediaGalleryItemBuilder,
} = require("discord.js");
const fs = require("fs");
const yaml = require("js-yaml");
const db = require("../../Structures/Database.js");
const Command = require("../../Structures/Command.js");
const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8"));
const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8"));
const msgconfig = yaml.load(fs.readFileSync("./Configs/messages.yml", "utf8"));
const clockedInUsers = new Set();
function safeGetProfile(userId) {
try {
if (typeof db.getProfile !== "function") {
console.error("db.getProfile is not a function");
return {
bio: "",
timezone: "",
clockedIn: false,
};
}
const profile = db.getProfile(userId);
return {
bio: profile?.bio || "",
timezone: profile?.timezone || "",
clockedIn: Boolean(profile?.clockedIn),
};
} catch (error) {
console.error("Error getting profile:", error);
return {
bio: "",
timezone: "",
clockedIn: false,
};
}
}
function safeSetClockedIn(userId, value) {
try {
if (typeof db.setClockedIn !== "function") {
console.error("db.setClockedIn is not a function");
return false;
}
db.setClockedIn(userId, value);
return true;
} catch (error) {
console.error("Error setting clocked in status:", error);
return false;
}
}
function buildProfileContainer({
viewingUser,
viewingUserId,
bio,
timezone,
clockedIn,
isStaff,
}) {
const container = new ContainerBuilder();
if (supportbot.Embed?.Colours?.General) {
container.setAccentColor(
parseInt(String(supportbot.Embed.Colours.General).replace("#", ""), 16),
);
}
const avatarUrl = viewingUser.displayAvatarURL({ size: 128, extension: "png" });
const gallery = new MediaGalleryBuilder().addItems(
new MediaGalleryItemBuilder().setURL(avatarUrl)
);
container.addMediaGalleryComponents(gallery);
container.addTextDisplayComponents(
new TextDisplayBuilder().setContent(`## ${viewingUser.username}'s Profile`)
);
container.addSeparatorComponents(new SeparatorBuilder().setDivider(true));
container.addTextDisplayComponents(
new TextDisplayBuilder().setContent(
[
`**Bio:** ${bio || "No bio set."}`,
`**Timezone:** ${timezone || "No timezone set."}`,
...(isStaff
? [`**Clocked In:** ${clockedIn ? "✅ Clocked In" : "❌ Clocked Out"}`]
: []),
].join("\n")
)
);
return container;
}
function buildProfileButtons({ isOwnProfile, isStaff, clockedIn }) {
const row = new ActionRowBuilder();
if (isOwnProfile) {
row.addComponents(
new ButtonBuilder()
.setCustomId("editProfile")
.setLabel("Edit Profile")
.setStyle(ButtonStyle.Secondary),
);
if (isStaff) {
row.addComponents(
new ButtonBuilder()
.setCustomId("clockInOut")
.setLabel(clockedIn ? "Clock Out" : "Clock In")
.setStyle(ButtonStyle.Secondary),
);
}
}
if (isStaff && supportbot.Ticket?.ClaimTickets?.Enabled) {
row.addComponents(
new ButtonBuilder()
.setCustomId("viewTicketStats")
.setLabel("View Ticket Stats")
.setStyle(ButtonStyle.Secondary),
);
}
return row.components.length ? [row] : [];
}
function buildTicketStatsContainer(viewingUserId, ticketsClaimed, ticketsOpen, avgMinutes) {
const container = new ContainerBuilder();
if (supportbot.Embed?.Colours?.General) {
container.setAccentColor(
parseInt(String(supportbot.Embed.Colours.General).replace("#", ""), 16),
);
}
container.addTextDisplayComponents(
new TextDisplayBuilder().setContent(`# ${msgconfig.TicketStats.Title || "Ticket Stats"}`),
);
container.addSeparatorComponents(new SeparatorBuilder().setDivider(true));
container.addTextDisplayComponents(
new TextDisplayBuilder().setContent(
[
`> Ticket stats for <@${viewingUserId}>`,
``,
`**${msgconfig.TicketStats.OpenTickets || "Open Tickets"}:** ${ticketsOpen.length}`,
`**${msgconfig.TicketStats.TotalTickets || "Total Tickets"}:** ${ticketsClaimed.length}`,
`**${msgconfig.TicketStats.ResponseTime || "Average Response Time"}:** ${avgMinutes} minutes`,
].join("\n"),
),
);
return container;
}
function buildOpenTicketsContainer(ticketsOpen) {
const container = new ContainerBuilder();
if (supportbot.Embed?.Colours?.General) {
container.setAccentColor(
parseInt(String(supportbot.Embed.Colours.General).replace("#", ""), 16),
);
}
container.addTextDisplayComponents(new TextDisplayBuilder().setContent(`# Active Tickets`));
container.addSeparatorComponents(new SeparatorBuilder().setDivider(true));
container.addTextDisplayComponents(
new TextDisplayBuilder().setContent(
ticketsOpen.length ? ticketsOpen.map((t) => `<#${t.id}>`).join("\n") : "No active tickets.",
),
);
return container;
}
module.exports = new Command({
name: cmdconfig.Profile.Command,
description: cmdconfig.Profile.Description,
type: ApplicationCommandType.ChatInput,
permissions: cmdconfig.Profile.Permission,
options: [
{
name: "user",
description: "The user to view the profile of",
type: ApplicationCommandOptionType.User,
required: false,
},
],
async run(interaction) {
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
try {
const viewingUser = interaction.options.getUser("user") || interaction.user;
const viewingUserId = viewingUser.id;
const interactionUserId = interaction.user.id;
const member = await interaction.guild.members.fetch(viewingUserId);
const { getRole } = interaction.client;
const Staff = await getRole(supportbot.Roles.StaffMember.Staff, interaction.guild);
const Admin = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild);
if (!Staff || !Admin) {
return interaction.editReply({
content: "Some roles seem to be missing!\nPlease check for errors when starting the bot.",
});
}
const isStaff = member.roles.cache.has(Staff.id) || member.roles.cache.has(Admin.id);
const isOwnProfile = interactionUserId === viewingUserId;
if (typeof db.ensureProfile === "function") {
db.ensureProfile(viewingUserId);
}
let { bio, timezone, clockedIn } = safeGetProfile(viewingUserId);
const profileContainer = buildProfileContainer({
viewingUser,
viewingUserId,
bio,
timezone,
clockedIn,
isStaff,
});
const buttonRows = buildProfileButtons({
isOwnProfile,
isStaff,
clockedIn,
});
await interaction.editReply({
flags: MessageFlags.IsComponentsV2,
components: [profileContainer, ...buttonRows],
});
const filter = (i) =>
[
"editProfile",
"clockInOut",
"viewTicketStats",
"showOpenTickets",
"backToStats",
"backToProfile",
].includes(i.customId) && i.user.id === interactionUserId;
const collector = interaction.channel.createMessageComponentCollector({
filter,
time: 60000,
});
collector.on("collect", async (i) => {
if (i.customId === "editProfile") {
const modal = new ModalBuilder()
.setCustomId("editProfileModal")
.setTitle("Edit Profile")
.addComponents(
new ActionRowBuilder().addComponents(
new TextInputBuilder()
.setCustomId("bio")
.setLabel("Bio")
.setStyle(TextInputStyle.Paragraph)
.setPlaceholder("Enter your bio")
.setValue(String(bio || ""))
.setRequired(false),
),
new ActionRowBuilder().addComponents(
new TextInputBuilder()
.setCustomId("timezone")
.setLabel("Timezone")
.setStyle(TextInputStyle.Short)
.setPlaceholder("Enter your timezone")
.setValue(String(timezone || ""))
.setRequired(false),
),
);
return i.showModal(modal);
}
if (i.customId === "clockInOut") {
const newStatus = !clockedIn;
const saved = safeSetClockedIn(viewingUserId, newStatus);
if (!saved) {
return i.reply({
content:
"Could not update clock in status because the profile database is not available.",
flags: MessageFlags.Ephemeral,
});
}
clockedIn = newStatus;
if (newStatus) clockedInUsers.add(viewingUserId);
else clockedInUsers.delete(viewingUserId);
const updatedProfileContainer = buildProfileContainer({
viewingUser,
viewingUserId,
bio,
timezone,
clockedIn,
isStaff,
});
const updatedButtonRows = buildProfileButtons({
isOwnProfile,
isStaff,
clockedIn,
});
await i.update({
flags: MessageFlags.IsComponentsV2,
components: [updatedProfileContainer, ...updatedButtonRows],
});
return interaction.followUp({
content: newStatus
? "You have successfully clocked in."
: "You have successfully clocked out.",
flags: MessageFlags.Ephemeral,
});
}
if (i.customId === "viewTicketStats") {
let TicketData = { tickets: [] };
try {
TicketData = JSON.parse(fs.readFileSync("./Data/TicketData.json", "utf8"));
} catch (error) {
console.error("Error reading TicketData.json:", error);
}
const ticketsClaimed = TicketData.tickets.filter((t) => t.claimedBy === viewingUserId);
const ticketsOpen = ticketsClaimed.filter((t) => t.open);
const totalTickets = ticketsClaimed.length;
let totalResponseTime = 0;
let responseCount = 0;
ticketsClaimed.forEach((t) => {
if (t.claimedAt && t.createdAt) {
totalResponseTime += new Date(t.claimedAt) - new Date(t.createdAt);
responseCount++;
}
});
const avgMinutes = responseCount
? Math.round(totalResponseTime / responseCount / 60000)
: 0;
const statsContainer = buildTicketStatsContainer(
viewingUserId,
ticketsClaimed,
ticketsOpen,
avgMinutes,
);
const statsRow = new ActionRowBuilder().addComponents(
new ButtonBuilder()
.setCustomId("showOpenTickets")
.setLabel("Show Open Tickets")
.setStyle(ButtonStyle.Secondary),
new ButtonBuilder()
.setCustomId("backToProfile")
.setLabel("Back to Profile")
.setStyle(ButtonStyle.Secondary),
);
return i.update({
flags: MessageFlags.IsComponentsV2,
components: [statsContainer, statsRow],
});
}
if (i.customId === "showOpenTickets") {
let TicketData = { tickets: [] };
try {
TicketData = JSON.parse(fs.readFileSync("./Data/TicketData.json", "utf8"));
} catch (error) {
console.error("Error reading TicketData.json:", error);
}
const ticketsOpen = TicketData.tickets.filter(
(t) => t.claimedBy === viewingUserId && t.open,
);
const openContainer = buildOpenTicketsContainer(ticketsOpen);
const backRow = new ActionRowBuilder().addComponents(
new ButtonBuilder()
.setCustomId("backToStats")
.setLabel("Back to Stats")
.setStyle(ButtonStyle.Secondary),
);
return i.update({
flags: MessageFlags.IsComponentsV2,
components: [openContainer, backRow],
});
}
if (i.customId === "backToStats") {
let TicketData = { tickets: [] };
try {
TicketData = JSON.parse(fs.readFileSync("./Data/TicketData.json", "utf8"));
} catch (error) {
console.error("Error reading TicketData.json:", error);
}
const ticketsClaimed = TicketData.tickets.filter((t) => t.claimedBy === viewingUserId);
const ticketsOpen = ticketsClaimed.filter((t) => t.open);
let totalResponseTime = 0;
let responseCount = 0;
ticketsClaimed.forEach((t) => {
if (t.claimedAt && t.createdAt) {
totalResponseTime += new Date(t.claimedAt) - new Date(t.createdAt);
responseCount++;
}
});
const avgMinutes = responseCount
? Math.round(totalResponseTime / responseCount / 60000)
: 0;
const statsContainer = buildTicketStatsContainer(
viewingUserId,
ticketsClaimed,
ticketsOpen,
avgMinutes,
);
const statsRow = new ActionRowBuilder().addComponents(
new ButtonBuilder()
.setCustomId("showOpenTickets")
.setLabel("Show Open Tickets")
.setStyle(ButtonStyle.Secondary),
new ButtonBuilder()
.setCustomId("backToProfile")
.setLabel("Back to Profile")
.setStyle(ButtonStyle.Secondary),
);
return i.update({
flags: MessageFlags.IsComponentsV2,
components: [statsContainer, statsRow],
});
}
if (i.customId === "backToProfile") {
const updatedProfileContainer = buildProfileContainer({
viewingUser,
viewingUserId,
bio,
timezone,
clockedIn,
isStaff,
});
const updatedButtonRows = buildProfileButtons({
isOwnProfile,
isStaff,
clockedIn,
});
return i.update({
flags: MessageFlags.IsComponentsV2,
components: [updatedProfileContainer, ...updatedButtonRows],
});
}
});
} catch (error) {
console.error("Error executing profile command:", error);
if (interaction.deferred || interaction.replied) {
return interaction
.editReply({
content: "An error occurred while loading the profile.",
components: [],
})
.catch(() => {});
}
return interaction
.reply({
content: "An error occurred while loading the profile.",
flags: MessageFlags.Ephemeral,
})
.catch(() => {});
}
},
});
module.exports.clockedInUsers = clockedInUsers;
+206
View File
@@ -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 = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8"));
const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8"));
const msgconfig = yaml.load(fs.readFileSync("./Configs/messages.yml", "utf8"));
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(() => {});
}
},
});
+147
View File
@@ -0,0 +1,147 @@
const fs = require("fs");
const Discord = require("discord.js");
const yaml = require("js-yaml");
const path = require("path");
const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8"));
const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8"));
const msgconfig = yaml.load(fs.readFileSync("./Configs/messages.yml", "utf8"));
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,
});
}
});
+183
View File
@@ -0,0 +1,183 @@
const Discord = require("discord.js");
const fs = require("fs");
const yaml = require("js-yaml");
const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8"));
const msgconfig = yaml.load(fs.readFileSync("./Configs/messages.yml", "utf8"));
const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8"));
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,
});
}
});
+493
View File
@@ -0,0 +1,493 @@
const fs = require("fs");
const Discord = require("discord.js");
const yaml = require("js-yaml");
const db = require("../../Structures/Database.js");
const TicketManager = require("../../Structures/TicketManager.js");
const Command = require("../../Structures/Command.js");
const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8"));
const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8"));
const msgconfig = yaml.load(fs.readFileSync("./Configs/messages.yml", "utf8"));
function getDepartmentConfig(departmentKey) {
return supportbot.Ticket?.DepartmentSystem?.Departments?.[departmentKey] || null;
}
function getTicketUserId(ticket) {
return ticket?.user || ticket?.user_id || null;
}
function buildContainer(title, lines = []) {
const container = new Discord.ContainerBuilder();
if (supportbot.Embed?.Colours?.General) {
container.setAccentColor(
parseInt(String(supportbot.Embed.Colours.General).replace("#", ""), 16),
);
}
container.addTextDisplayComponents(new Discord.TextDisplayBuilder().setContent(`## ${title}`));
container.addSeparatorComponents(new Discord.SeparatorBuilder().setDivider(true));
if (lines.length > 0) {
container.addTextDisplayComponents(
new Discord.TextDisplayBuilder().setContent(lines.join("\n")),
);
}
return container;
}
function buildReviewSummaryContainer(ticket, rating, comment) {
const departmentConfig = ticket.department ? getDepartmentConfig(ticket.department) : null;
const departmentName = departmentConfig?.Name || ticket.department || null;
const departmentEmoji = departmentConfig?.Emoji || "🎫";
const claimedByEnabled = supportbot.Ticket?.ClaimTickets?.Enabled === true;
const claimedBy = claimedByEnabled && ticket.claimedBy ? `<@${ticket.claimedBy}>` : null;
const ticketUserId = getTicketUserId(ticket);
const lines = [
`**${msgconfig.ReviewSystem.ReviewEmbed.ReviewedByTitle}** ${ticketUserId ? `<@${ticketUserId}>` : "Unknown User"}`,
];
if (claimedBy) {
lines.unshift(`**${msgconfig.ReviewSystem.ReviewEmbed.ReviewedStaffTitle}** ${claimedBy}`);
}
if (supportbot.Ticket?.DepartmentSystem?.Enabled === true && departmentName) {
lines.push(`**Department** ${departmentEmoji} ${departmentName}`);
}
lines.push("");
lines.push(`**${msgconfig.ReviewSystem.ReviewEmbed.RatingTitle}**`);
lines.push(`${msgconfig.ReviewSystem.ReviewEmbed.ReviewEmoji.repeat(Number(rating))}`);
lines.push("");
lines.push(`**${msgconfig.ReviewSystem.ReviewEmbed.CommentTitle}**`);
lines.push(comment || "No Comment Provided");
const container = new Discord.ContainerBuilder();
if (msgconfig.ReviewSystem?.ReviewEmbed?.Color) {
container.setAccentColor(
parseInt(String(msgconfig.ReviewSystem.ReviewEmbed.Color).replace("#", ""), 16),
);
} else if (supportbot.Embed?.Colours?.General) {
container.setAccentColor(
parseInt(String(supportbot.Embed.Colours.General).replace("#", ""), 16),
);
}
container.addTextDisplayComponents(
new Discord.TextDisplayBuilder().setContent(`## ${msgconfig.ReviewSystem.ReviewEmbed.Title}`),
);
container.addSeparatorComponents(new Discord.SeparatorBuilder().setDivider(true));
container.addTextDisplayComponents(new Discord.TextDisplayBuilder().setContent(lines.join("\n")));
return container;
}
async function collectReview(interaction, ticket) {
const ticketUserId = getTicketUserId(ticket);
if (!ticketUserId) return null;
const ratingContainer = buildContainer(msgconfig.ReviewSystem.Rate.Title, [
msgconfig.ReviewSystem.Rate.Description,
"",
]);
const ratingRow = new Discord.ActionRowBuilder().addComponents(
new Discord.StringSelectMenuBuilder()
.setCustomId(`closeReviewRating:${interaction.id}`)
.setPlaceholder("Select a rating")
.addOptions([
{ label: msgconfig.ReviewSystem.Stars.One, value: "1" },
{ label: msgconfig.ReviewSystem.Stars.Two, value: "2" },
{ label: msgconfig.ReviewSystem.Stars.Three, value: "3" },
{ label: msgconfig.ReviewSystem.Stars.Four, value: "4" },
{ label: msgconfig.ReviewSystem.Stars.Five, value: "5" },
]),
);
ratingContainer.addActionRowComponents(ratingRow);
const ratingPrompt = await interaction.followUp({
flags: Discord.MessageFlags.IsComponentsV2,
components: [ratingContainer],
});
let rating = null;
try {
const ratingInteraction = await ratingPrompt.awaitMessageComponent({
componentType: Discord.ComponentType.StringSelect,
time: 120000,
filter: (i) =>
i.customId === `closeReviewRating:${interaction.id}` && i.user.id === ticketUserId,
});
rating = ratingInteraction.values[0];
await ratingInteraction.deferUpdate().catch(() => {});
} catch {
return null;
}
const commentContainer = buildContainer(msgconfig.ReviewSystem.Comment.Title, [
msgconfig.ReviewSystem.Comment.Description,
"",
]);
const commentRow = new Discord.ActionRowBuilder().addComponents(
new Discord.ButtonBuilder()
.setCustomId(`closeReviewWrite:${interaction.id}`)
.setLabel("Write Comment")
.setStyle(Discord.ButtonStyle.Secondary),
new Discord.ButtonBuilder()
.setCustomId(`closeReviewSkip:${interaction.id}`)
.setLabel("Skip Comment")
.setStyle(Discord.ButtonStyle.Danger),
);
commentContainer.addActionRowComponents(commentRow);
const commentPrompt = await interaction.followUp({
flags: Discord.MessageFlags.IsComponentsV2,
components: [commentContainer],
});
let comment = "No Comment Provided";
try {
const buttonInteraction = await commentPrompt.awaitMessageComponent({
componentType: Discord.ComponentType.Button,
time: 120000,
filter: (i) =>
(i.customId === `closeReviewWrite:${interaction.id}` ||
i.customId === `closeReviewSkip:${interaction.id}`) &&
i.user.id === ticketUserId,
});
if (buttonInteraction.customId === `closeReviewSkip:${interaction.id}`) {
await buttonInteraction.deferUpdate().catch(() => {});
return { rating, comment };
}
await buttonInteraction
.update({
flags: Discord.MessageFlags.IsComponentsV2,
components: [
buildContainer(msgconfig.ReviewSystem.Comment.Title, [
`Requested from: <@${ticketUserId}>`,
"",
"Please send your review comment in the channel now.",
"Type `no` to skip.",
]),
],
})
.catch(() => {});
const collected = await interaction.channel.awaitMessages({
max: 1,
time: 120000,
filter: (m) => m.author.id === ticketUserId,
});
const message = collected.first();
if (message) {
const rawComment = message.content?.trim();
if (rawComment && rawComment.toLowerCase() !== "no") {
comment = rawComment;
}
await message.delete().catch(() => {});
}
} catch {
return { rating, comment };
}
return { rating, comment };
}
function getTranscriptAttachmentFromResult(result) {
if (!result) return null;
if (typeof result === "string") {
return result;
}
if (result.filePath) return result.filePath;
if (result.attachment) return result.attachment;
if (result.path) return result.path;
return null;
}
async function maybeDMTranscript(interaction, ticket, transcriptResult) {
if (supportbot.Ticket?.UserManagement?.DMTranscript !== true) {
return;
}
const ticketUserId = getTicketUserId(ticket);
if (!ticketUserId) return;
const ticketUser = await interaction.client.users.fetch(ticketUserId).catch(() => null);
if (!ticketUser) return;
const attachmentPath = getTranscriptAttachmentFromResult(transcriptResult);
if (!attachmentPath) return;
try {
await ticketUser.send({
content: `Here is a copy of your ticket transcript from \`${interaction.guild.name}\`.`,
files: [attachmentPath],
});
} catch (error) {
console.error("Failed to DM transcript to user:", error);
}
}
async function collectCloseConfirmation(interaction, ticket, reason) {
const ticketUserId = getTicketUserId(ticket);
const lines = [
"Are you sure you want to close this ticket?",
"",
`**Ticket:** ${interaction.channel.name}`,
`**Reason:** ${reason || "No Reason Provided."}`,
];
if (ticketUserId) {
lines.push(`**Ticket User:** <@${ticketUserId}>`);
}
if (supportbot.Ticket?.ReviewSystem?.Enabled === true) {
lines.push("");
lines.push("A review will be requested before the ticket is closed.");
}
const confirmContainer = buildContainer("Close Confirmation", lines);
const confirmRow = new Discord.ActionRowBuilder().addComponents(
new Discord.ButtonBuilder()
.setCustomId(`confirmClose:${interaction.id}`)
.setLabel("Confirm Close")
.setStyle(Discord.ButtonStyle.Danger),
new Discord.ButtonBuilder()
.setCustomId(`cancelClose:${interaction.id}`)
.setLabel("Cancel")
.setStyle(Discord.ButtonStyle.Secondary),
);
confirmContainer.addActionRowComponents(confirmRow);
const confirmMessage = await interaction.editReply({
flags: Discord.MessageFlags.IsComponentsV2,
components: [confirmContainer],
});
try {
const buttonInteraction = await confirmMessage.awaitMessageComponent({
componentType: Discord.ComponentType.Button,
time: 120000,
filter: (i) =>
(i.customId === `confirmClose:${interaction.id}` ||
i.customId === `cancelClose:${interaction.id}`) &&
i.user.id === interaction.user.id,
});
if (buttonInteraction.customId === `cancelClose:${interaction.id}`) {
await buttonInteraction
.update({
flags: Discord.MessageFlags.IsComponentsV2,
components: [
buildContainer("Close Cancelled", ["The ticket close process has been cancelled."]),
],
})
.catch(() => {});
return false;
}
await buttonInteraction.deferUpdate().catch(() => {});
return true;
} catch {
await interaction
.editReply({
flags: Discord.MessageFlags.IsComponentsV2,
components: [buildContainer("Close Cancelled", ["The close confirmation timed out."])],
})
.catch(() => {});
return false;
}
}
async function postReview(interaction, ticket, reviewData) {
const { getChannel } = interaction.client;
const reviewChannel = await getChannel(supportbot.Ticket.ReviewSystem.Channel, interaction.guild);
if (!reviewChannel || !reviewData) return;
const reviewContainer = buildReviewSummaryContainer(
ticket,
reviewData.rating,
reviewData.comment,
);
await reviewChannel
.send({
flags: Discord.MessageFlags.IsComponentsV2,
components: [reviewContainer],
})
.catch(() => {});
}
async function handleCloseTicket(interaction, reason, ticket) {
try {
if (typeof db.updateTicketStatus === "function") {
db.updateTicketStatus(interaction.channel.id, "closed");
}
const transcriptResult = await TicketManager.createTranscript(interaction, ticket, reason);
await maybeDMTranscript(interaction, ticket, transcriptResult);
await interaction
.editReply({
flags: Discord.MessageFlags.IsComponentsV2,
components: [
buildContainer("Ticket Closed", [
`**Ticket:** ${interaction.channel.name}`,
`**Closed By:** <@${interaction.user.id}>`,
`**Reason:** ${reason || "No Reason Provided."}`,
]),
],
})
.catch(() => {});
await interaction.channel.delete().catch(() => {});
} catch (err) {
console.error("Error handling ticket close:", err);
if (interaction.deferred || interaction.replied) {
await interaction
.editReply({
content: "An error occurred while closing the ticket.",
components: [],
})
.catch(() => {});
return;
}
await interaction
.reply({
content: "An error occurred while closing the ticket.",
flags: Discord.MessageFlags.Ephemeral,
})
.catch(() => {});
}
}
module.exports = new Command({
name: cmdconfig.CloseTicket.Command,
description: cmdconfig.CloseTicket.Description,
type: Discord.ApplicationCommandType.ChatInput,
options: [
{
name: "reason",
description: "Ticket Close Reason",
type: Discord.ApplicationCommandOptionType.String,
},
],
permissions: cmdconfig.CloseTicket.Permission,
async run(interaction) {
const { getRole } = interaction.client;
if (supportbot.Ticket.Close.StaffOnly) {
const SupportStaff = await getRole(supportbot.Roles.StaffMember.Staff, interaction.guild);
const Admin = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild);
if (!SupportStaff || !Admin) {
return interaction.reply({
content: "Some roles seem to be missing! Please check for errors when starting the bot.",
flags: Discord.MessageFlags.Ephemeral,
});
}
if (
!interaction.member.roles.cache.has(SupportStaff.id) &&
!interaction.member.roles.cache.has(Admin.id)
) {
return interaction.reply({
flags: Discord.MessageFlags.IsComponentsV2 | Discord.MessageFlags.Ephemeral,
components: [
buildContainer("Invalid Permissions!", [
msgconfig.Error.IncorrectPerms,
"",
`Role Required: \`${supportbot.Roles.StaffMember.Staff}\` or \`${supportbot.Roles.StaffMember.Admin}\``,
]),
],
});
}
}
const isThread = interaction.channel.type === Discord.ChannelType.PrivateThread;
if (
(supportbot.Ticket.TicketType === "threads" && !isThread) ||
(supportbot.Ticket.TicketType === "channels" &&
interaction.channel.type !== Discord.ChannelType.GuildText)
) {
return interaction.reply({
flags: Discord.MessageFlags.IsComponentsV2 | Discord.MessageFlags.Ephemeral,
components: [
buildContainer("Invalid Channel!", [
`This command can only be used in a ${
supportbot.Ticket.TicketType === "threads" ? "ticket thread" : "ticket channel"
}.`,
]),
],
});
}
await interaction.deferReply({ flags: Discord.MessageFlags.Ephemeral });
const ticket = db.getTicket(interaction.channel.id);
if (!ticket) {
return interaction.editReply({
flags: Discord.MessageFlags.IsComponentsV2,
components: [buildContainer("No Ticket Found!", [msgconfig.Error.NoValidTicket])],
});
}
const reason = interaction.options?.getString("reason") || "No Reason Provided.";
const confirmed = await collectCloseConfirmation(interaction, ticket, reason);
if (!confirmed) {
return;
}
if (supportbot.Ticket?.ReviewSystem?.Enabled === true) {
const reviewData = await collectReview(interaction, ticket);
if (reviewData) {
await postReview(interaction, ticket, reviewData);
}
await handleCloseTicket(interaction, reason, ticket);
return;
}
await handleCloseTicket(interaction, reason, ticket);
},
});
+664
View File
@@ -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 = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8"));
const msgconfig = yaml.load(fs.readFileSync("./Configs/messages.yml", "utf8"));
const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8"));
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.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
+295
View File
@@ -0,0 +1,295 @@
// SupportBot | Emerald Services
// Ticket Panel Command
const fs = require("fs");
const {
ApplicationCommandType,
ActionRowBuilder,
ButtonBuilder,
ButtonStyle,
MessageFlags,
ContainerBuilder,
TextDisplayBuilder,
MediaGalleryBuilder,
SeparatorBuilder,
SectionBuilder,
StringSelectMenuBuilder,
} = require("discord.js");
const yaml = require("js-yaml");
const panelconfig = yaml.load(fs.readFileSync("./Configs/ticket-panel.yml", "utf8"));
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 db = require("../../Structures/Database.js");
function parseButtonStyle(style) {
switch (String(style || "1")) {
case "1":
return ButtonStyle.Primary;
case "2":
return ButtonStyle.Secondary;
case "3":
return ButtonStyle.Danger;
case "4":
return ButtonStyle.Success;
default:
return ButtonStyle.Primary;
}
}
function getPanelMessage(key, fallback) {
return panelconfig.Messages?.[key] || fallback;
}
module.exports = new Command({
name: cmdconfig.TicketPanel.Command,
description: cmdconfig.TicketPanel.Description,
type: ApplicationCommandType.ChatInput,
options: [],
permissions: cmdconfig.TicketPanel.Permission,
async run(interaction) {
if (!panelconfig.Panel) {
console.log(
"\u001b[31m",
`[TICKET PANEL]`,
"\u001b[33m",
"Ticket Panel is not setup, You can set this in",
`\u001b[31m`,
"/Configs/ticket-panel.yml",
`\n `,
);
return interaction.reply({
content: getPanelMessage(
"NotConfigured",
"Ticket Panel is not set up. Please configure it in `/Configs/ticket-panel.yml`.",
),
flags: MessageFlags.Ephemeral,
});
}
const { getChannel } = interaction.client;
const channel = await getChannel(supportbot.Ticket.TicketHome, interaction.guild);
if (!channel) {
console.log(`[TICKET PANEL] TicketHome channel not found. Please check your config file.`);
return interaction.reply({
content: getPanelMessage(
"InvalidChannel",
"TicketHome channel not found. Please check your config file.",
),
flags: MessageFlags.Ephemeral,
});
}
const panelRow = db.getTicketPanel();
if (panelRow) {
try {
const oldMessage = await channel.messages.fetch(panelRow.message_id);
if (oldMessage) {
await oldMessage.delete().catch(() => {});
}
} catch (err) {
console.log(`[TICKET PANEL] Previous panel message not found, creating a new one.`);
}
}
const departmentSystem = supportbot.Ticket?.DepartmentSystem || {};
const prioritySystem = supportbot.Ticket?.PrioritySystem || {};
const usingThreads = supportbot.Ticket?.TicketType === "threads";
const departmentsEnabled = departmentSystem.Enabled === true && !usingThreads;
const prioritiesEnabled = prioritySystem.Enabled === true && !usingThreads;
const departments = departmentSystem.Departments || {};
if (usingThreads && (departmentSystem.Enabled || prioritySystem.Enabled)) {
console.log(
"[SupportBot] Departments and Priority are disabled because TicketType is set to 'threads'.",
);
}
const departmentOptions = Object.entries(departments)
.slice(0, 25)
.map(([key, dept]) => ({
label: dept.Name || key,
description:
dept.PanelDescription ||
dept.Description ||
panelconfig.DepartmentMenu?.DefaultDescription ||
`Open a ${String(dept.Name || key).toLowerCase()} ticket`,
value: key,
emoji:
dept.Emoji ||
panelconfig.DepartmentMenu?.DefaultEmoji ||
panelconfig.Button?.Emoji ||
undefined,
}));
const shouldUseDepartments = departmentsEnabled && departmentOptions.length > 0;
let openerComponent;
if (shouldUseDepartments) {
openerComponent = new StringSelectMenuBuilder()
.setCustomId(panelconfig.DepartmentMenu?.CustomId || "ticketdepartmentselect")
.setPlaceholder(
panelconfig.DepartmentMenu?.Placeholder ||
departmentSystem.Placeholder ||
panelconfig.Button?.Text ||
"Choose a ticket department",
)
.addOptions(departmentOptions);
} else {
openerComponent = new ButtonBuilder()
.setCustomId(panelconfig.Button?.CustomId || "createticket")
.setLabel(panelconfig.Button?.Text || "Create Ticket")
.setStyle(parseButtonStyle(panelconfig.Button?.Color));
if (panelconfig.Button?.Emoji) {
openerComponent.setEmoji(panelconfig.Button.Emoji);
}
}
const panelContainer = new ContainerBuilder();
if (panelconfig.Style?.Color) {
panelContainer.setAccentColor(parseInt(String(panelconfig.Style.Color).replace("#", ""), 16));
}
const layout = String(panelconfig.Style?.Layout || "3");
const showImage = panelconfig.Settings?.Image === true && !!panelconfig.Style?.Image;
const showTopTitle = panelconfig.Settings?.TopTitle === true;
const showTopDivider = panelconfig.Settings?.TopDivider === true;
const showBottomTitle = panelconfig.Settings?.BottomTitle === true;
const showBottomDivider = panelconfig.Settings?.BottomDivider === true;
const panelTitle = new TextDisplayBuilder().setContent(
panelconfig.Style?.Title || "## SupportBot Ticket Creation",
);
const 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 (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,
});
}
},
});
+468
View File
@@ -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 = yaml.load(fs.readFileSync('./Configs/supportbot.yml', 'utf8'));
const cmdconfig = yaml.load(fs.readFileSync('./Configs/commands.yml', 'utf8'));
const msgconfig = yaml.load(fs.readFileSync('./Configs/messages.yml', 'utf8'));
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(() => {});
});
}
});
+261
View File
@@ -0,0 +1,261 @@
const fs = require("fs");
const { ApplicationCommandOptionType, ApplicationCommandType, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, ComponentType, MessageFlags } = require("discord.js");
const yaml = require("js-yaml");
const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8"));
const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8"));
const msgconfig = yaml.load(fs.readFileSync("./Configs/messages.yml", "utf8"));
const Command = require("../../Structures/Command.js");
// Function to chunk array into smaller arrays
function chunkArray(arr, size) {
const chunks = [];
for (let i = 0; i < arr.length; i += size) {
chunks.push(arr.slice(i, i + size));
}
return chunks;
}
module.exports = new Command({
name: cmdconfig.Mod.Command,
description: cmdconfig.Mod.Command,
type: ApplicationCommandType.ChatInput,
options: [
{
name: cmdconfig.Mod.TicketBlacklist.Command,
description: cmdconfig.Mod.TicketBlacklist.Description,
type: ApplicationCommandOptionType.SubcommandGroup,
options: [
{
name: cmdconfig.Mod.TicketBlacklist.Add.Command,
description: msgconfig.Mod.TicketBlacklist.Add.Description || "Add a user to the ticket blacklist",
type: ApplicationCommandOptionType.Subcommand,
options: [
{
name: 'user',
description: msgconfig.Mod.TicketBlacklist.Add.UserDescription || "User to blacklist",
type: ApplicationCommandOptionType.User,
required: true
},
{
name: 'reason',
description: msgconfig.Mod.TicketBlacklist.Add.ReasonDescription || "Reason for blacklisting",
type: ApplicationCommandOptionType.String,
required: false
}
]
},
{
name: cmdconfig.Mod.TicketBlacklist.Remove.Command,
description: msgconfig.Mod.TicketBlacklist.Remove.Description || "Remove a user from the ticket blacklist",
type: ApplicationCommandOptionType.Subcommand,
options: [
{
name: 'user',
description: msgconfig.Mod.TicketBlacklist.Remove.UserDescription || "User to remove from blacklist",
type: ApplicationCommandOptionType.User,
required: true
},
{
name: 'reason',
description: msgconfig.Mod.TicketBlacklist.Remove.ReasonDescription || "Reason for removing from blacklist",
type: ApplicationCommandOptionType.String,
required: false
}
]
},
{
name: "view",
description: msgconfig.Mod.TicketBlacklist.View.Description || "View all blacklisted users",
type: ApplicationCommandOptionType.Subcommand,
}
]
}
],
permissions: cmdconfig.Mod.Permission,
async run(interaction) {
const { getRole } = interaction.client;
let SupportStaff = await getRole(supportbot.Roles.StaffMember.Staff, interaction.guild);
let Admin = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild);
let Moderator = await getRole(supportbot.Roles.StaffMember.Moderator, interaction.guild);
if (!SupportStaff || !Admin || !Moderator) {
const missingRolesEmbed = new EmbedBuilder()
.setDescription(msgconfig.Error.InvalidChannel || "Required roles are missing!")
.setColor(supportbot.Embed.Colours.Warn);
return interaction.reply({ embeds: [missingRolesEmbed] });
}
const NoPerms = new EmbedBuilder()
.setDescription(msgconfig.Error.IncorrectPerms || "You do not have the correct permissions!")
.setColor(supportbot.Embed.Colours.Warn);
if (!interaction.member.roles.cache.has(Admin.id) && !interaction.member.roles.cache.has(Moderator.id) && (!supportbot.Mod.AllowSupportStaff || !interaction.member.roles.cache.has(SupportStaff.id))) {
return interaction.reply({ embeds: [NoPerms] });
}
const subcommand = interaction.options.getSubcommand(false);
const user = interaction.options.getUser('user');
const reason = interaction.options.getString('reason') || "No reason provided";
try {
let blacklistedUsers;
try {
blacklistedUsers = JSON.parse(fs.readFileSync("./Data/BlacklistedUsers.json", "utf8")).blacklistedUsers;
if (!Array.isArray(blacklistedUsers)) {
blacklistedUsers = [];
}
} catch (error) {
blacklistedUsers = [];
}
if (subcommand === cmdconfig.Mod.TicketBlacklist.Add.Command) {
if (blacklistedUsers.includes(user.id)) {
const alreadyBlacklistedEmbed = new EmbedBuilder()
.setDescription(msgconfig.Mod.TicketBlacklist.Add.AlreadyBlacklisted.replace("{userTag}", user.tag) || ":x: User is already blacklisted.")
.setColor(supportbot.Embed.Colours.Warn);
return interaction.reply({ embeds: [alreadyBlacklistedEmbed], flags: MessageFlags.Ephemeral });
}
blacklistedUsers.push(user.id);
fs.writeFileSync("./Data/BlacklistedUsers.json", JSON.stringify({ "blacklistedUsers": blacklistedUsers }, null, 4));
const successEmbed = new EmbedBuilder()
.setDescription(msgconfig.Mod.TicketBlacklist.Add.Success.replace("{userTag}", user.tag) || ":white_check_mark: User has been blacklisted.")
.setColor(supportbot.Embed.Colours.Success);
const blacklistChannel = interaction.guild.channels.cache.get(supportbot.Ticket.Log.TicketBlacklistLog);
if (blacklistChannel) {
const blacklistLogEmbed = new EmbedBuilder()
.setTitle(msgconfig.TicketBlacklistLog.Title || "Ticket Blacklist Log")
.setColor(msgconfig.TicketBlacklistLog.Colour || supportbot.Embed.Colours.Success)
.setFooter({ text: supportbot.Embed.Footer, iconURL: interaction.user.displayAvatarURL() })
.setDescription(`> **User:** ${user.tag} (\`${user.id}\`)\n> **Actioned by:** <@${interaction.user.id}>`)
.addFields({ name: "Action", value: "\`\`\`Removed from blacklist\`\`\`", inline: false })
.addFields({ name: "Reason", value: `\`\`\`${reason}\`\`\``, inline: false });
blacklistChannel.send({ embeds: [blacklistLogEmbed] });
}
return interaction.reply({
embeds: [successEmbed],
flags: MessageFlags.Ephemeral
});
} else if (subcommand === cmdconfig.Mod.TicketBlacklist.Remove.Command) {
if (!blacklistedUsers.includes(user.id)) {
const notBlacklistedEmbed = new EmbedBuilder()
.setDescription(msgconfig.Mod.TicketBlacklist.Remove.NotBlacklisted.replace("{userTag}", user.tag) || ":x: User is not blacklisted.")
.setColor(supportbot.Embed.Colours.Warn);
return interaction.reply({ embeds: [notBlacklistedEmbed], flags: MessageFlags.Ephemeral });
}
blacklistedUsers = blacklistedUsers.filter(id => id !== user.id);
fs.writeFileSync("./Data/BlacklistedUsers.json", JSON.stringify({ "blacklistedUsers": blacklistedUsers }, null, 4));
const removedEmbed = new EmbedBuilder()
.setDescription(msgconfig.Mod.TicketBlacklist.Remove.Success.replace("{userTag}", user.tag) || ":white_check_mark: User has been removed from the blacklist.")
.setColor(supportbot.Embed.Colours.Success);
const blacklistChannel = interaction.guild.channels.cache.get(supportbot.Ticket.Log.TicketBlacklistLog);
if (blacklistChannel) {
const blacklistLogEmbed = new EmbedBuilder()
.setTitle(msgconfig.TicketBlacklistLog.Title || "Ticket Blacklist Log")
.setColor(msgconfig.TicketBlacklistLog.Colour || supportbot.Embed.Colours.Success)
.setFooter({ text: supportbot.Embed.Footer, iconURL: interaction.user.displayAvatarURL() })
.setDescription(`> **User:** ${user.tag} (\`${user.id}\`)\n> **Actioned by:** <@${interaction.user.id}>`)
.addFields({ name: "Action", value: "\`\`\`Removed from blacklist\`\`\`", inline: false })
.addFields({ name: "Reason", value: `\`\`\`${reason}\`\`\``, inline: false });
blacklistChannel.send({ embeds: [blacklistLogEmbed] });
}
return interaction.reply({ embeds: [removedEmbed], flags: MessageFlags.Ephemeral });
} else if (subcommand === "view") {
if (blacklistedUsers.length === 0) {
const noBlacklistedUsersEmbed = new EmbedBuilder()
.setDescription(msgconfig.Mod.TicketBlacklist.View.NoBlacklistedUsers || "No users are blacklisted.")
.setColor(supportbot.Embed.Colours.Success);
return interaction.reply({ embeds: [noBlacklistedUsersEmbed], flags: MessageFlags.Ephemeral });
}
const chunkedUsers = chunkArray(blacklistedUsers, 5);
let currentPage = 0;
const createEmbed = (page) => {
return new EmbedBuilder()
.setTitle(msgconfig.Mod.TicketBlacklist.View.EmbedTitle || "Blacklisted Users")
.setDescription(chunkedUsers[page].map(id => `<@${id}>`).join("\n"))
.setColor(supportbot.Embed.Colours.Success)
.setFooter({ text: `Page ${page + 1} of ${chunkedUsers.length}` });
};
const buttons = new ActionRowBuilder()
.addComponents(
new ButtonBuilder()
.setCustomId('previous')
.setLabel('Previous')
.setStyle(ButtonStyle.Secondary)
.setDisabled(currentPage === 0),
new ButtonBuilder()
.setCustomId('next')
.setLabel('Next')
.setStyle(ButtonStyle.Secondary)
.setDisabled(currentPage === chunkedUsers.length - 1)
);
const response = await interaction.reply({
embeds: [createEmbed(currentPage)],
components: chunkedUsers.length > 1 ? [buttons] : [],
flags: MessageFlags.Ephemeral
});
if (chunkedUsers.length <= 1) return;
const collector = response.createMessageComponentCollector({
componentType: ComponentType.Button,
time: 60000
});
collector.on('collect', async (i) => {
if (i.user.id !== interaction.user.id) {
return i.reply({
content: 'You cannot use these buttons.',
flags: MessageFlags.Ephemeral
});
}
if (i.customId === 'previous' && currentPage > 0) {
currentPage--;
} else if (i.customId === 'next' && currentPage < chunkedUsers.length - 1) {
currentPage++;
}
buttons.components[0].setDisabled(currentPage === 0);
buttons.components[1].setDisabled(currentPage === chunkedUsers.length - 1);
await i.update({
embeds: [createEmbed(currentPage)],
components: [buttons]
});
});
collector.on('end', () => {
buttons.components.forEach(button => button.setDisabled(true));
interaction.editReply({
components: [buttons]
}).catch(() => {});
});
}
} catch (error) {
console.error("Error in mod command:", error);
const errorEmbed = new EmbedBuilder()
.setDescription(msgconfig.Error.ActionFailed || "An error occurred while processing your request.")
.setColor(supportbot.Embed.Colours.Warn);
interaction.reply({ embeds: [errorEmbed], flags: MessageFlags.Ephemeral });
}
}
});
+237
View File
@@ -0,0 +1,237 @@
const { Command } = require('../../Structures/Addon.js');
const Discord = require("discord.js");
const fs = require("fs");
const yaml = require("js-yaml");
const db = require("../../Structures/Database.js");
const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8"));
const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8"));
async function loadSettings() {
return new Promise((resolve, reject) => {
db.get("SELECT data FROM settings WHERE id = 1", (err, row) => {
if (err) return reject(err);
if (!row) return resolve({});
try {
resolve(JSON.parse(row.data));
} catch (e) {
resolve({});
}
});
});
}
async function saveSettings(settings) {
const data = JSON.stringify(settings);
return new Promise((resolve, reject) => {
db.run(
"INSERT INTO settings (id, data) VALUES (1, ?) ON CONFLICT(id) DO UPDATE SET data = excluded.data",
[data],
(err) => (err ? reject(err) : resolve())
);
});
}
module.exports = new Command({
name: cmdconfig.Settings.Command,
description: cmdconfig.Settings.Description,
permissions: cmdconfig.Settings.Permission,
options: [
{
name: 'setstatus',
type: Discord.ApplicationCommandOptionType.Subcommand,
description: 'Set the bot status',
options: [
{
name: 'status',
type: Discord.ApplicationCommandOptionType.String,
description: 'Choose the bot status',
required: true,
choices: [
{ name: 'Online 🟢', value: 'online' },
{ name: 'Idle 🌙', value: 'idle' },
{ name: 'Do Not Disturb ⛔', value: 'dnd' },
{ name: 'Invisible 👻', value: 'invisible' }
],
},
],
},
{
name: 'setactivity',
type: Discord.ApplicationCommandOptionType.Subcommand,
description: 'Set the bot activity',
options: [
{
name: 'type',
type: Discord.ApplicationCommandOptionType.String,
description: 'Choose the activity type',
required: true,
choices: [
{ name: 'Playing 🎮', value: 'PLAYING' },
{ name: 'Watching 👀', value: 'WATCHING' },
{ name: 'Listening 🎧', value: 'LISTENING' },
{ name: 'Competing 🏆', value: 'COMPETING' }
],
},
{
name: 'message',
type: Discord.ApplicationCommandOptionType.String,
description: 'The activity message',
required: true,
},
],
},
{
name: 'streammode',
type: Discord.ApplicationCommandOptionType.Subcommand,
description: 'Set the bot streaming status',
options: [
{
name: 'toggle',
type: Discord.ApplicationCommandOptionType.String,
description: 'Turn streaming mode on or off',
required: true,
choices: [
{ name: 'On', value: 'on' },
{ name: 'Off', value: 'off' }
],
},
{
name: 'twitchurl',
type: Discord.ApplicationCommandOptionType.String,
description: 'Twitch stream URL (Required if turning on)',
required: false,
},
],
},
{
name: 'setnickname',
type: Discord.ApplicationCommandOptionType.Subcommand,
description: 'Set the bot nickname',
options: [
{
name: 'nickname',
type: Discord.ApplicationCommandOptionType.String,
description: 'The new nickname for the bot',
required: true,
},
],
},
],
async run(interaction) {
const subcommand = interaction.options.getSubcommand();
const settings = await loadSettings();
if (subcommand === 'setstatus') {
const status = interaction.options.getString('status');
try {
interaction.client.user.setPresence({
status: status,
activities: interaction.client.user.presence.activities || [],
});
settings.status = status;
await saveSettings(settings);
const embed = new Discord.EmbedBuilder()
.setDescription(`Status set to **${status.charAt(0).toUpperCase() + status.slice(1)}**`)
.setColor(supportbot.Embed.Colours.General);
return interaction.reply({ embeds: [embed], flags: Discord.MessageFlags.Ephemeral });
} catch (error) {
return interaction.reply({ content: `Failed to set status: ${error.message}`, flags: Discord.MessageFlags.Ephemeral });
}
}
if (subcommand === 'setactivity') {
const activityType = interaction.options.getString('type');
const activityMessage = interaction.options.getString('message');
try {
interaction.client.user.setPresence({
activities: [{ name: activityMessage, type: Discord.ActivityType[activityType] }],
status: interaction.client.user.presence.status || 'online',
});
settings.activity = { type: activityType, message: activityMessage };
await saveSettings(settings);
const embed = new Discord.EmbedBuilder()
.setDescription(`Activity set to **${activityType.toLowerCase()} ${activityMessage}**`)
.setColor(supportbot.Embed.Colours.General);
return interaction.reply({ embeds: [embed], flags: Discord.MessageFlags.Ephemeral });
} catch (error) {
return interaction.reply({ content: `Failed to set activity: ${error.message}`, flags: Discord.MessageFlags.Ephemeral });
}
} else if (subcommand === 'streammode') {
const toggle = interaction.options.getString('toggle');
const twitchUrl = interaction.options.getString('twitchurl');
if (toggle === 'on') {
if (!twitchUrl) {
return interaction.reply({ content: 'Please provide a Twitch URL to stream.', flags: Discord.MessageFlags.Ephemeral });
}
try {
interaction.client.user.setPresence({
activities: [{ name: 'Streaming', type: Discord.ActivityType.Streaming, url: twitchUrl }],
status: 'online',
});
settings.streaming = { active: true, url: twitchUrl };
await saveSettings(settings);
const embed = new Discord.EmbedBuilder()
.setDescription(`**Streaming mode enabled!**\nStreaming: [Twitch](${twitchUrl})`)
.setColor(supportbot.Embed.Colours.General);
return interaction.reply({ embeds: [embed], flags: Discord.MessageFlags.Ephemeral });
} catch (error) {
return interaction.reply({ content: `Failed to enable streaming mode: ${error.message}`, flags: Discord.MessageFlags.Ephemeral });
}
} else if (toggle === 'off') {
try {
interaction.client.user.setPresence({
activities: interaction.client.user.presence.activities.filter(a => a.type !== Discord.ActivityType.Streaming),
status: settings.status || 'online',
});
settings.streaming = { active: false };
await saveSettings(settings);
const embed = new Discord.EmbedBuilder()
.setDescription(`**Streaming mode disabled!**`)
.setColor(supportbot.Embed.Colours.General);
return interaction.reply({ embeds: [embed], flags: Discord.MessageFlags.Ephemeral });
} catch (error) {
return interaction.reply({ content: `Failed to disable streaming mode: ${error.message}`, flags: Discord.MessageFlags.Ephemeral });
}
}
} else if (subcommand === 'setnickname') {
const nickname = interaction.options.getString('nickname');
try {
const guild = interaction.guild;
const botMember = guild.members.cache.get(interaction.client.user.id);
await botMember.setNickname(nickname);
settings.nickname = nickname;
await saveSettings(settings);
const embed = new Discord.EmbedBuilder()
.setDescription(`Bot nickname set to **${nickname}**`)
.setColor(supportbot.Embed.Colours.General);
return interaction.reply({ embeds: [embed], flags: Discord.MessageFlags.Ephemeral });
} catch (error) {
return interaction.reply({ content: `Failed to set nickname: ${error.message}`, flags: Discord.MessageFlags.Ephemeral });
}
} else {
return interaction.reply({ content: 'Invalid command usage.', flags: Discord.MessageFlags.Ephemeral });
}
},
});
-92
View File
@@ -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);
}
);
},
});
-199
View File
@@ -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);
}
},
});
-107
View File
@@ -1,107 +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)
.setColor(EmbedColour)
.setThumbnail(EmbedThumbnail)
.setImage(EmbedImage)
if (EmbedFieldTitle && EmbedFieldContent) { EmbedMsg.addField(EmbedFieldTitle, EmbedFieldContent) };
interaction.reply({
embeds: [EmbedMsg],
});
} else {
return interaction.reply({ embeds: [NoPerms] });
}
},
});
-89
View File
@@ -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],
});
},
});
-40
View File
@@ -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],
});
},
});
-94
View File
@@ -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);
}
);
},
});
-84
View File
@@ -1,84 +0,0 @@
// SupportBot | Emerald Services
// Suggest 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]
})
},
});
-329
View File
@@ -1,329 +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,
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 (!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 (!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 (!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,
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(supportbot.TicketRoleMention);
}
}
},
});
-108
View File
@@ -1,108 +0,0 @@
// SupportBot | Emerald Services
// Translate 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] });
},
})
-127
View File
@@ -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/
**/
+144 -17
View File
@@ -7,25 +7,147 @@
# SupportBot created by Emerald Services
# Installed with MIT License
#
# Discord Support: https://emeraldsrv.dev/discord
# Community Resources: https://emeraldsrv.dev/resources
# Discord Support: https://discord.gg/emeraldsrv
# Official Documentation: https://docs.emeraldsrv.com
# Addons Marketplace: https://community.emeraldsrv.com
#
# Commands Configuration (commands.yml)
# -------------------------------
# Command Name Configuration
# -------------------------------
Help:
Command: "help"
Description: "Get a list of all the commands."
Permission: ["SendMessages"]
Enabled: true
Blacklist: "blacklist"
TicketAdd: "add"
TicketRemove: "remove"
OpenTicket: "ticket"
CloseTicket: "close"
InfoCommand: "info"
PingCommand: "ping"
HelpCommand: "help"
EmbedCommand: "embed"
SuggestCommand: "suggest"
TranslateCommand: "translate"
UserInfoCommand: "userinfo"
Info:
Command: "info"
Description: "Get information about the **ServerName**."
Permission: ["SendMessages"]
Enabled: true
TicketPanel:
Command: "ticketpanel"
Description: "Create the ticket creation panel"
Permission: ["Administrator"]
Enabled: true
Profile:
Command: "profile"
Description: "Shows the profile for a discord member."
Permission: ["SendMessages"]
# Command is automatically disabled when the TicketClaims is disabled.
OpenTicket:
Command: "ticket"
Description: "Create a support ticket."
Permission: ["SendMessages"]
Enabled: true
AddUser:
Command: "add"
Description: "Add a user to a support ticket."
Permission: ["SendMessages"]
Enabled: true
CloseTicket:
Command: "close"
Description: "Close a support ticket"
Permission: ["SendMessages"]
Enabled: true
TicketManage:
Command: "ticketmanage"
Description: "Manage an existing support ticket."
Permission: ["SendMessages"]
Enabled: true
Close:
Command: "close"
Description: "Close a support ticket."
ForceAdd:
Command: "forceadd"
Description: "Add a user to a support ticket by bypassing the invite system."
ForceRemove:
Command: "forceremove"
Description: "Remove a user from a support ticket by bypassing the normal restrictions."
Rename:
Command: "rename"
Description: "Rename a support ticket by bypassing the normal restrictions."
Suggestion:
Command: "suggest"
Description: "Create a suggestion."
Permission: ["SendMessages"]
Enabled: true
SuggestionAdmin:
Command: "suggestadmin"
Description: "Update a suggestion’s status."
Permission: ['SendMessages']
# Disable "Suggestion" to disable this command.
Embed:
Command: "embed"
Description: "Build a discord embed."
Permission: ["SendMessages"]
Enabled: true
Translate:
Command: "translate"
Description: "Translate something to english"
Permission: ["SendMessages"]
Enabled: true
UserInfo:
Command: "userinfo"
Description: "Get info about a user."
Permission: ["SendMessages"]
Enabled: true
Ping:
Command: "ping"
Description: "Pong."
Permission: ["SendMessages"]
Enabled: true
# ---------------------------------------
# Bot Moderator Commands
# ---------------------------------------
Mod:
Command: "mod"
Description: "Execute a moderation action."
Permission: ["BanMembers", "KickMembers"]
Enabled: true
TicketBlacklist:
Command: "ticket-blacklist"
Description: "Blacklist a user from creating Tickets."
Add:
Command: "add"
Description: "Add a user to the blacklist."
Remove:
Command: "remove"
Description: "Remove a user from the blacklist."
View:
Command: "view"
Description: "View all blacklisted users."
# ---------------------------------------
# Bot Administrator Commands
# ---------------------------------------
Settings:
Command: "settings"
Description: "Update bot settings."
Permission: ["Administrator"]
Enabled: true
# ---------------------------------------
# Command Description Configuration
@@ -43,3 +165,8 @@ HelpCommandDesc: "Get a list of all the commands."
EmbedCommandDesc: "Send a message as an embed."
SuggestCommandDesc: "Suggest a feature for the server."
UserInfoCommandDesc: "Get info about a user."
TicketManageDesc: "Manage an existing support ticket."
TicketManageCloseDesc: "Close a support ticket."
TicketManageForceAddDesc: "Add a user to a support ticket by bypassing the invite system."
TicketManageForceRemoveDesc: "Remove a user from a support ticket by bypassing the normal restrictions."
TicketManageForceRenameDesc: "Rename a support ticket by bypassing the normal restrictions."
+333
View File
@@ -0,0 +1,333 @@
# ___ _ ___ _
# / __> _ _ ___ ___ ___ _ _ _| |_ | . > ___ _| |_
# \__ \| | || . \| . \/ . \| '_> | | | . \/ . \ | |
# <___/`___|| _/| _/\___/|_| |_| |___/\___/ |_|
# |_| |_|
#
# 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://docs.emeraldsrv.com"
Colour: "#45b463"
# ----------------------------
# Welcome/Leave Messages
# ----------------------------
Welcome:
Enabled: true
LayoutStyle: "modern"
# Available Layout Styles
# "modern" - For the new Discord v2 components
# "classic" - Use the classic Discord look.
Embed:
Colour: "#45b463"
Title: "Welcome to ServerName"
Message: ":smile: %joined_user% has just landed!\n\n🔨 **Server Rules:** <#1202667199853232129>"
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: "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: "🛒"
Modern:
Colour: "#45b463"
Title: "**Welcome to ServerName**"
Body: ":smile: %joined_user% has just landed!\n\n🔨 **Server Rules:** <#1202667199853232129>"
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."
+75
View File
@@ -0,0 +1,75 @@
# -------------------------
# 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: "gpt-5.4"
Model_API_Key: "AI_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"
Tokens: 1200
Temperature: 0.4
Name: "SupportBot AI"
PastebinAPI_Key: "PASTEBIN_API_KEY"
PastebinAPI_URL: "https://pastebin.com/api/api_post.php"
Style:
MessageType: "text"
# Available Styles for AI:
# "embed" - AI responses will be sent as Discord embeds.
# "text" - AI responses will be sent as plain messages.
# "components_v2" - AI responses will be sent with the new components style.
Features:
SuggestTickets: true # Should SupportBot AI suggest creating a ticket if the user needs more help?
Channels:
AIChannel: "CHANNEL_ID"
Embed:
Color: "#45b463"
Footer: "SupportBot AI"
Memory:
Enabled: true
StoreFacts: true
# StoreFacts - Enables saving important facts the AI learns.
StoreSummaries: true
# StoreSummaries - Enables saving conversation summaries.
MaxRecentMessages: 15
# MaxRecentMessages - Number of recent messages used for context.
Scope: "guild" # channel | guild
# Available Scopes for Memory:
# "channel" - Memory is per channel only.
# "guild" - Memory is shared across the server.
CustomInstructions:
- "You are SupportBot AI made by Emerald Services"
- "You are built with DiscordJS/NodeJS and currently running on dev build 26.0 build 2"
- "You're current build can be found here https://github.com/Emerald-Services/SupportBot/tree/26.0"
- "You're main build can be found here https://builtbybit.com/resources/supportbot-free-advanced-ticket-bot.8940/ and Polymart at https://polymart.org/product/518/supportbot-1-discord-ticket-bot which is version 8.4"
- "dev build 26.0 build 2 includes all-new AI features for SupportBot such as memory system that allows you to remember facts and summaries of conversations, which can be used to provide better responses."
- "SupportBot AI also have a new tickets AI Mode which is enabled in supportbot.ymll, which allows you to assist users in support tickets and provide better responses based on the context of the ticket."
- "Your main purpose is to assist users in the support server and provide helpful responses to their questions and issues. Always be accurate and if you do not have answer then simply ask the person to create a ticket if they need more help."
#- "Add custom instructions here"
Messages:
ErrorResponse: "I couldn't generate a response."
AIError: "There was an error contacting the AI provider."
AIDisabled: "AI is currently disabled."
MessageTooLong: "My response was too long, so I uploaded it here: {pastebin}"
SuggestTicketText: "-# Need more help? React with 🎫 to open a ticket."
SuggestTicketReaction : "🎫"
+305 -196
View File
@@ -7,253 +7,362 @@
# SupportBot created by Emerald Services
# Installed with MIT License
#
# Discord Support: https://emeraldsrv.dev/discord
# Community Resources: https://emeraldsrv.dev/resources
# Discord Support: https://discord.gg/emeraldsrv
# Website: https://emeraldsrv.dev
#
# SupportBot Configuration (supportbot.yml)
SupportBot_Version: "v7.7"
SupportBot_Version: "v26.0.0"
# -------------------------
# General Configuration
# General
# -------------------------
Name: "SupportBot"
Token: "BOT_TOKEN HTTPS://DISCORD.COM/DEVELOPERS"
General:
Name: "SupportBot"
Token: "BOT_TOKEN"
Addons:
Enabled: true
# -------------------------
# Activity Configuration
# Activity
# -------------------------
BotActivity: "Here to help!"
Activity:
Status: "SupportBot v8"
Type: "Competing"
StreamingURL: "https://www.twitch.tv/YOUR_TWITCH_NAME"
StreamingURL: "https://www.twitch.tv/YOUR_TWITCH_NAME"
# Set The ActivityType to "STREAMING" for a streaming status!
# Available Types:
# Playing
# Watching
# Listening
# Competing
# Streaming (URL required)
ActivityType: "PLAYING"
# Activity status can either be PLAYING, WATCHING, LISTENING or STREAMING.
# You can also update the activity via Discord using: /settings
# -------------------------
# Embed Configuration
# Embeds
# -------------------------
EmbedColour: "#f74343"
EmbedFooter: "SupportBot | Developed by Emerald Services"
Embed:
Colours:
General: "#0cc559"
Success: "#04b545"
Error: "#d13030"
Warn: "#fca117"
ErrorColour: "#d12519"
SuccessColour: "#04b545"
WarningColour: "#fca117"
Footer: "SupportBot | Developed by Emerald Development"
# Use 0x<hexcode> to change the colour of the embeds.
# -------------------------
# Role Configuration
# Roles
# -------------------------
AutoRole: true
JoinRole: "Member"
Roles:
AutoRole:
Enabled: true
Role: "ROLE_ID"
Staff: "Staff"
Admin: "Admin"
TicketBlackListRole: "Blacklisted"
StaffMember:
Staff: "ROLE_ID"
Admin: "ROLE_ID"
Moderator: "ROLE_ID"
Mod:
AllowSupportStaff: false
# Allow Support Staff to use the /mod command. (More in detail customisation in commands.yml)
# ----------------------------
# Join/Leave Configuration
# System Messages
# ----------------------------
Welcome: true
# set to false to disable joining logs
Welcome:
Enabled: true
Channel: "CHANNEL_ID"
WelcomeChannel: "join-leave-logs"
WelcomeColour: "#f74343"
WelcomeTitle: "User Joined!"
WelcomeMessage: "%joined_user% has joined the server!"
# Available Placeholders:
# %joined_user% - Mentions the user who joined
EmbedWelcomeImage: true
EmbedWelcomeImageURL: "https://media.discordapp.net/attachments/958247876344025108/986777740738986094/unknown.png"
EmbedWelcomeThumbnail: "BOT"
# "USER" will display the user's profile picture.
# "BOT" will display the bot's profile picture.
# false to disable
Leave: true
# set to false to disable leaving logs
LeaveChannel: "join-leave-logs"
LeaveColour: "#f74343"
LeaveTitle: "User Left!"
LeaveMessage: "%joined_user% has left the server!"
# Available Placeholders:
# %joined_user% - Mentions the user who joined
EmbedLeaveImage: true
EmbedLeaveImageURL: "https://cdn.discordapp.com/attachments/958247876344025108/986778170223120384/unknown.png"
EmbedLeaveThumbnail: false
# "USER" will display the user's profile picture.
# "BOT" will display the bot's profile picture.
# false to disable
# -------------------------
# Information Configuration
# -------------------------
InfoTitle: "Information"
InfoButtonText: "Visit Site"
InfoURL: "https://emeraldsrv.dev"
InfoColour: "#f74343"
Leave:
Enabled: true
Channel: "CHANNEL_ID"
# ----------------------------
# Suggestions Configuration
# Suggestions
# ----------------------------
SuggestionChannel: "suggestions"
SuggestionUpvote: "✅"
SuggestionDownvote: "❌"
Suggestions:
Mode: "Channel" # Channel or Forum
ForumChannel: "FORUM_CHANNEL_ID" # A forum channel, disabled if mode is set to Channel.
Channel: "CHANNEL_ID" # A text channel, disabled if mode is set to Forum.
SuggestionThreads: true
# set to false to disable threads being created from suggestions.
UpvoteEmoji: "✅"
DownvoteEmoji: "❌"
DisableSuggestions: false
# Set to true to disable the suggestions feature.
UpvoteTitle: "Yes"
DownvoteTitle: "No"
OwnSuggestion: true
# If false this will not allow the user to vote for a suggestion they created
# If true this will allow the user to vote for a suggestion they created
ShowUsers: true
Threads:
Enabled: true
Title: "Suggestion-releated Thread"
Reason: "A thread releted to this suggestion"
Buttons:
Upvote: "2"
Downvote: "2"
RemoveVote: "4"
RemoveVote_Title: "Remove Vote"
# Available Button Styles
# 1 - Blurple
# 2 - Grey
# 3 - Green
# 4 - Red
Tags:
Created: "TAG_ID"
Considering: "TAG_ID"
Solved: "TAG_ID"
Denied: "TAG_ID"
# -------------------------
# 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."
# -------------------------
# Tickets Configuration
# Ticket Departments
# -------------------------
TicketPrefix: "ticket-" # The prefix for ticket channel names
DepartmentSystem:
Enabled: false
Placeholder: "Choose a ticket department"
DefaultDepartment: "general"
TicketLog: "ticket-data"
TranscriptLog: "ticket-data"
Departments:
general:
Name: "General Support"
Emoji: "🎫"
Category: "CATEGORY_ID"
Role: "ROLE_ID"
ChannelPrefix: "support-"
DefaultPriority: "normal"
Questions:
- "What do you need help with?"
- "When did the issue start?"
DMTranscripts: true
TranscriptTitle: "Ticket Transcript"
TicketLog_Title: "Ticket Closed"
TicketBlackListMessage: "You are not allowed to open a ticket"
TicketRoleMention: "@here"
# To ping a role: <@ROLE_ID>
purchase:
Name: "Purchase Support"
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?"
MaxAllowedTickets: 2
reports:
Name: "Player Report"
Emoji: "🚨"
Category: "CATEGORY_ID"
Role: "ROLE_ID"
ChannelPrefix: "report-"
DefaultPriority: "normal"
Questions:
- "Who are you reporting?"
- "What happened?"
- "Do you have evidence?"
TicketSubject: "description"
# "description" will display the ticket reason within the channel description
# "embed" will display the ticket reason within the ticket embed
appeals:
Name: "Ban Appeals"
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?"
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"
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 Categories
# 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
# -------------------------
Timeout: 5
SelectMenus:
Tickets:
PanelEmoji: "⚙️"
CloseEmoji: "🗑️"
ArchiveEmoji: "♻️"
LockEmoji: "🔐"
RenameEmoji: "🏷️"
VCEmoji: "🔊"
# Set to true to allow all staff to access the ticket
AllowAllStaff: false
EnableInvitesEmoji: "📤"
DisableInvitesEmoji: "📥"
# Ticket roles are the roles that will be able to gain access to tickets depending on what department has been chosen.
# -------------------------
# Logging
# -------------------------
Departments:
- title: "Support"
role: "Staff"
color: "SECONDARY"
emoji: "1️⃣"
category: "Tickets"
MessageDelete:
Channel: "CHANNEL_ID"
Colour: "#d92525"
- title: "Billing Support"
role: "Staff"
color: "SECONDARY"
emoji: "2️⃣"
category: "Tickets"
MessageUpdate:
Channel: "CHANNEL_ID"
Colour: "#f2a024"
- title: "Admin Team"
role: "Admin"
color: "SECONDARY"
emoji: "3️⃣"
category: "Tickets"
# - title: "Other"
# role: "Staff"
# color: "SECONDARY"
# emoji: "3️⃣"
# category: "Tickets"
# Discord Button Colour Selection
# PRIMARY - Blurple
# SECONDARY - Grey
# DANGER - Red
# SUCCESS - Green
# --- Buttons Config ---
# --- Start ---
LockTicketCategory: "Archive"
TicketCloseEmoji: "❌"
TicketDeleteEmoji: "⛔"
TicketLockEmoji: "🔒"
TicketCloseColour: "DANGER"
TicketDeleteColour: "DANGER"
TicketLockColour: "PRIMARY"
# Discord Button Colour Selection
# PRIMARY - Blue
# SECONDARY - Grey
# DANGER - Red
# SUCCESS - Green
TranscriptSavedMessage: "📝 **Transcript Saved!**"
TicketLockMessage: "❌ **Ticket Locked!**"
TicketDeleteMessage: "⚠ **Ticket Deleting!**" # Tickets deleting after 5 seconds by default.
# --- 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!"
Translate:
TranslateLog: "CHANNEL_ID"
+45 -15
View File
@@ -7,28 +7,58 @@
# SupportBot created by Emerald Services
# Installed with MIT License
#
# Discord Support: https://emeraldsrv.dev/discord
# Community Resources: https://emeraldsrv.dev/resources
# Discord Support: https://discord.gg/emeraldsrv
# Official Documentation: https://docs.emeraldsrv.com
# Addons Marketplace: https://community.emeraldsrv.com
#
# Ticket Creation Panel Configuration (ticket-panel.yml)
# -------------------------------
# Ticket Panel Configuration
# -------------------------------
TicketPanel: true
Panel: true
# ----------------------------------------
# Ticket Panel Message Configuration
# ----------------------------------------
Style:
Title: "## SupportBot Ticket Creation"
Description: "Click on the button to file a `Ticket` and our team will try their best to assist you in the best way we can!"
Image: "https://i.imgur.com/dejQvkg.png"
Color: "#45b463"
Layout: "3"
Channel: "ticket-creation"
# Available Layout Styles
# "1" - Style 1 - Title & Text, Button at the top -- Image at the bottom
# "2" - Style 2 - Title & Text, Button at the bottom -- Image at the top
# "3" - Style 3 - The classic look - Title & Text at the top -- Image in the middle -- Button at the bottom
PanelTitle: "Need Help? Create a Support Ticket."
PanelColour: "#f74343"
Button:
Text: "Create Ticket"
Emoji: "🎫"
Color: "2"
CustomId: "createticket"
TicketPanel_Description: true
TicketPanel_Thumbnail: true
TicketPanel_Image: true
# Available Button Styles
# 1 - Blurple
# 2 - Grey
# 3 - Red
# 4 - Green
PanelMessage: "Click the button to open a new support ticket."
PanelThumbnail: "https://i.imgur.com/ieeZejl.png"
PanelImage: "https://i.imgur.com/5NkcmDq.png"
DepartmentMenu:
CustomId: "ticketdepartmentselect"
Placeholder: "Choose a ticket department"
DefaultDescription: "Open a support ticket"
DefaultEmoji: "🎫"
Settings:
Image: true
TopTitle: true
TopDivider: true
BottomTitle: false
BottomDivider: false
Messages:
NotConfigured: "Ticket Panel is not set up. Please configure it in `/Configs/ticket-panel.yml`."
InvalidChannel: "TicketHome channel not found. Please check your config file."
PanelSent: "Ticket panel message has been sent!"
PanelSentWithDepartments: "Ticket panel message with department selection has been sent!"
SendError: "An error occurred while sending the ticket panel message."
BIN
View File
Binary file not shown.
+3
View File
@@ -0,0 +1,3 @@
{
"blacklistedUsers": []
}
+1
View File
@@ -0,0 +1 @@
-1
View File
@@ -1 +0,0 @@
{"tickets": [{}]}
-4
View File
@@ -1,4 +0,0 @@
{
"id": 0,
"TicketPanelID": ""
}
+72
View File
@@ -0,0 +1,72 @@
// Emerald Services | SupportBot
// Groq integration for SupportBot AI
const fs = require("fs");
const yaml = require("js-yaml");
const OpenAI = require("openai");
const supportbotai = yaml.load(
fs.readFileSync("./Configs/supportbot-ai.yml", "utf8"),
);
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,
};
+68
View File
@@ -0,0 +1,68 @@
// Emerald Services | SupportBot
// Open AI integration for SupportBot AI
const fs = require("fs");
const yaml = require("js-yaml");
const OpenAI = require("openai");
const supportbotai = yaml.load(
fs.readFileSync("./Configs/supportbot-ai.yml", "utf8"),
);
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,
};
+144 -36
View File
@@ -1,46 +1,154 @@
const Discord = require('discord.js');
const {
EmbedBuilder,
ActionRowBuilder,
ButtonBuilder,
ButtonStyle,
MessageFlags,
ContainerBuilder,
TextDisplayBuilder,
SectionBuilder,
ThumbnailBuilder,
AttachmentBuilder
} = require("discord.js");
const fs = require("fs");
const Event = require("../Structures/Event.js");
const yaml = require("js-yaml");
const Event = require("../Structures/Event.js");
const supportbot = yaml.load(
fs.readFileSync("./Configs/supportbot.yml", "utf8")
fs.readFileSync("./Configs/supportbot.yml", "utf8")
);
const msgconfig = yaml.load(
fs.readFileSync("./Configs/messages.yml", "utf8")
);
module.exports = new Event("guildMemberAdd", async (client, member, interaction, guild) => {
function getWelcomeThumbnail(client, member, iconCfg) {
switch ((iconCfg?.Mode || "").toUpperCase()) {
case "USER":
return member.user.displayAvatarURL();
case "BOT":
return client.user.displayAvatarURL();
case "CUSTOM":
return iconCfg.CustomURL;
default:
return null;
}
}
if (supportbot.Welcome) {
const WelcomeChannel = member.guild.channels.cache.find(channel => channel.name === supportbot.WelcomeChannel)
function buildModernComponents(client, member) {
const container = new ContainerBuilder()
const WelcomeEmbed = new Discord.MessageEmbed()
.setColor(supportbot.WelcomeColour)
.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);
if (msgconfig.Welcome.Modern.Image?.Enabled && msgconfig.Welcome.Modern.Image.URL) {
container.addTextDisplayComponents(
new TextDisplayBuilder().setContent(
`![image](${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!`);
});
+32 -33
View File
@@ -1,41 +1,40 @@
const Discord = require('discord.js');
const { EmbedBuilder } = require("discord.js");
const fs = require("fs");
const Event = require("../Structures/Event.js");
const yaml = require("js-yaml");
const supportbot = yaml.load(
fs.readFileSync("./Configs/supportbot.yml", "utf8")
);
const Event = require("../Structures/Event.js");
module.exports = new Event("guildMemberRemove", async (client, member, interaction, guild) => {
const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8"));
const msgconfig = yaml.load(fs.readFileSync("./Configs/messages.yml", "utf8"));
if (supportbot.Leave) {
const LeaveChannel = member.guild.channels.cache.find(channel => channel.name === supportbot.LeaveChannel)
module.exports = new Event("guildMemberRemove", async (client, member) => {
const LeaveEmbed = new Discord.MessageEmbed()
.setColor(supportbot.LeaveColour)
.setTitle(supportbot.LeaveTitle)
.setDescription(supportbot.LeaveMessage.replace(/%joined_user%/g, member.user))
.setTimestamp();
if (supportbot.EmbedLeaveThumbnail === "BOT") {
LeaveEmbed.setThumbnail(client.user.displayAvatarURL())
}
if (supportbot.EmbedLeaveThumbnail === "USER") {
LeaveEmbed.setThumbnail(member.user.displayAvatarURL())
}
if (supportbot.EmbedLeaveImage) {
LeaveEmbed.setImage(supportbot.EmbedLeaveImageURL)
}
LeaveChannel.send({
embeds: [LeaveEmbed]
})
console.log(`\u001b[31m`, `[-]`, `\u001b[33m`, `${member.user.username} joined the server!`)
if (!supportbot.Leave || !supportbot.Leave.Enabled) return;
const leaveChannel =
member.guild.channels.cache.get(supportbot.Leave.Channel) ||
member.guild.channels.cache.find(c => c.name === supportbot.Leave.Channel);
if (!leaveChannel) return;
const leaveEmbed = new EmbedBuilder()
.setColor(msgconfig.Leave.Embed.Colour)
.setTitle(msgconfig.Leave.Embed.Title)
.setDescription(
msgconfig.Leave.Embed.Message.replace(/%left_user%/g, member.user.toString())
)
.setTimestamp();
if (msgconfig.Leave.Embed.Thumbnail === "BOT") {
leaveEmbed.setThumbnail(client.user.displayAvatarURL());
} else if (msgconfig.Leave.Embed.Thumbnail === "USER") {
leaveEmbed.setThumbnail(member.user.displayAvatarURL());
}
});
if (msgconfig.Leave.Embed.ImageEnabled) {
leaveEmbed.setImage(msgconfig.Leave.Embed.ImageURL);
}
await leaveChannel.send({ embeds: [leaveEmbed] });
console.log(`[-] ${member.user.tag} left the server`);
});
File diff suppressed because it is too large. Load diff
+562
View File
@@ -0,0 +1,562 @@
const fs = require("fs");
const Discord = require("discord.js");
const yaml = require("js-yaml");
const axios = require("axios");
const supportbotai = yaml.load(
fs.readFileSync("./Configs/supportbot-ai.yml", "utf8"),
);
const supportbot = yaml.load(
fs.readFileSync("./Configs/supportbot.yml", "utf8"),
);
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 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;
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(
`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,
)
: [];
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}`,
});
}
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.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(() => {});
}
});
+36
View File
@@ -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 = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8"));
const msgconfig = yaml.load(fs.readFileSync("./Configs/messages.yml", "utf8"));
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] });
}
});
+75
View File
@@ -0,0 +1,75 @@
const fs = require("fs");
const yaml = require("js-yaml");
const supportbot = yaml.load(
fs.readFileSync("./Configs/supportbot.yml", "utf8"),
);
const supportbotai = yaml.load(
fs.readFileSync("./Configs/supportbot-ai.yml", "utf8"),
);
const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8"));
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);
}
},
);
+57
View File
@@ -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 = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8"));
const msgconfig = yaml.load(fs.readFileSync("./Configs/messages.yml", "utf8"));
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]
});
}
});
+105 -196
View File
@@ -1,217 +1,126 @@
// 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");
module.exports = new Event("ready", async (client, interaction) => {
const { getRole, getChannel, getCategory } = client;
client.user.setActivity(supportbot.BotActivity, {
type: supportbot.ActivityType,
url: supportbot.StreamingURL,
// Load configs
const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8"));
const ACTIVITY_TYPES = {
PLAYING: Discord.ActivityType.Playing,
WATCHING: Discord.ActivityType.Watching,
LISTENING: Discord.ActivityType.Listening,
COMPETING: Discord.ActivityType.Competing
};
const setActivity = (client, type, status) => {
const activityType = ACTIVITY_TYPES[type?.toUpperCase()];
if (!activityType) return;
client.user.setPresence({
activities: [{ name: status || "Online", type: activityType }],
status: "online"
});
};
console.log(`\u001b[107m`, `――――――――――――――――――――――――――――――――――――――――――――`);
console.log(`\u001b[91m`, `┏━━━┓╋╋╋╋╋╋╋╋╋╋╋╋╋┏┓┏━━┓╋╋╋┏┓`);
console.log(`\u001b[91m`, `┃┏━┓┃╋╋╋╋╋╋╋╋╋╋╋╋┏┛┗┫┏┓┃╋╋┏┛┗┓`);
console.log(`\u001b[91m`, `┃┗━━┳┓┏┳━━┳━━┳━━┳┻┓┏┫┗┛┗┳━┻┓┏┛`);
console.log(`\u001b[91m`, `┗━━┓┃┃┃┃┏┓┃┏┓┃┏┓┃┏┫┃┃┏━┓┃┏┓┃┃`);
console.log(`\u001b[91m`, `┃┗━┛┃┗┛┃┗┛┃┗┛┃┗┛┃┃┃┗┫┗━┛┃┗┛┃┗┓`);
console.log(`\u001b[91m`, `┗━━━┻━━┫┏━┫┏━┻━━┻┛┗━┻━━━┻━━┻━┛`);
console.log(`\u001b[91m`, `┗╋╋╋╋╋╋╋┃┃╋┃┃`);
console.log(`\u001b[91m`, `╋╋╋╋╋╋╋┗┛╋┗┛`);
console.log(` `);
console.log(`\u001b[32m`, `――――――――――――――――――――――――――――――――――――――――――――`);
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`, `――――――――――――――――――――――――――――――――――――――――――――`);
// simple colors
const c = {
reset: "\x1b[0m",
gray: "\x1b[90m",
green: "\x1b[32m",
yellow: "\x1b[33m",
cyan: "\x1b[36m",
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 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})` : ""}`);
}
const logStartup = async (client) => {
console.clear();
const botName = supportbot?.General?.Name || "SupportBot";
const botVersion = supportbot?.SupportBot_Version || "Unknown";
const inviteUrl = `https://discord.com/api/oauth2/authorize?client_id=${client.user.id}&permissions=8&scope=bot%20applications.commands`;
line();
console.log(`${c.bold}${c.green}SupportBot${c.reset}`);
console.log(`${c.gray}Emerald Services${c.reset}`);
line();
section("System");
info("Bot Name", botName);
info("Version", botVersion);
info("Node.js", process.version);
info("Discord.js", `v${Discord.version}`);
info("Logged in as", client.user.tag);
info("Guilds", `${client.guilds.cache.size}`);
section("Links");
info("Docs", "https://emerald-services.gitbook.io/");
info("Discord", "https://dsc.gg/emerald-dev");
info("Invite", inviteUrl);
console.log("");
};
const checkConfigs = async (client) => {
const guild = client.guilds.cache.first();
if (!guild) return;
section("Config Check");
const roles = [
supportbot.Admin,
supportbot.Staff,
supportbot.TicketBlackListRole,
];
supportbot?.Roles?.StaffMember?.Admin,
supportbot?.Roles?.StaffMember?.Staff,
supportbot?.Roles?.AutoRole?.Role
].filter(Boolean);
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];
supportbot?.Ticket?.TicketHome
].filter(Boolean);
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);
// }
const [roleResults, channelResults] = await Promise.all([
Promise.all(roles.map((role) => client.getRole(role, guild))),
Promise.all(channels.map((channel) => client.getChannel(channel, guild)))
]);
if (missingR.length > 0) {
console.log(
"\u001b[33m",
`[WARN] The following Roles could not be found! Please check your configs!\n${missingR.join(
", "
)}`
);
}
const missingRoles = roles.filter((_, i) => !roleResults[i]);
const missingChannels = channels.filter((_, i) => !channelResults[i]);
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;
if (!missingRoles.length && !missingChannels.length) {
status("All configurations validated successfully", true);
} else {
if (missingRoles.length) {
status("Missing roles", false, missingRoles.join(", "));
}
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(panelconfig.PanelColour)
.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;
button = supportbot.Departments.map((x) =>
new Discord.MessageButton()
.setCustomId("department-" + supportbot.Departments.indexOf(x))
.setLabel(x.title)
.setStyle(x.color)
.setEmoji(x.emoji)
);
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);
});
});
if (missingChannels.length) {
status("Missing channels", false, missingChannels.join(", "));
}
}
console.log("");
};
module.exports = new Event("clientReady", async (client) => {
setActivity(client, supportbot?.Activity?.Type, supportbot?.Activity?.Status);
await logStartup(client);
await checkConfigs(client);
});
+1 -1
View File
@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2022 Emerald Services
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
+92 -8
View File
@@ -1,12 +1,96 @@
![imgur](https://i.imgur.com/TQj40Mt.png)
<h1 align="center">
<br>
<a href="https://github.com/Emerald-Services/SupportBot"><img src="https://i.imgur.com/cuadAYv.png"></a>
<br>
SupportBot
<br>
</h1>
## What is SupportBot?
<div align="center">
<a href="https://opensource.org/licenses/MIT">
<img src="https://img.shields.io/badge/license-MIT-green?style=for-the-badge" alt="shield.png">
</a>
<a href="https://github.com/discordjs">
<img src="https://img.shields.io/badge/DISCORD.JS%20-%20v14-blue?style=for-the-badge" alt="shield.png">
</a>
<a href="https://github.com/nodejs/node">
<img src="https://img.shields.io/badge/NODE.JS%20-%20v20.x-green?style=for-the-badge" alt="shield.png">
</a>
<a href="https://github.com/Emerald-Services/SupportBot/releases">
<img src="https://img.shields.io/github/downloads/Emerald-Services/SupportBot/total?style=for-the-badge" alt="shield.png">
</a>
## Supported Versions
| SupportBot | Node.JS | Discord.JS | Supported |
|------------|---------|------------|------------|
| 8.x | 22.x | 14.x. | ✅ Yes
| 7.7.3 | 16.x | 13.x. | ❌ No
| 7.x | 16.x | 13.x. | ❌ No
| 6.x | 14.x | 13.x. | ❌ No
| 5.x | 12.x | 13.x. | ❌ No
| 4.x | 11.x | 13.x. | ❌ No
| Older | 10.x | 13.x. | ❌ No
</div>
<p align="center">
<a href="https://emeraldsrv.com/">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.com/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
- [Official addons repository](https://github.com/Emerald-Services/Addons/)
- [Syphers Addons](https://github.com/SypherRed/SB_Addons_Unofficial)
- [BuiltByBit]([https://builtbybit.com/supportbot](https://builtbybit.com/resources/supportbot-advanced-ticket-bot.8940/))
- [Polymart](https://polymart.org/resource/supportbot-1-discord-ticket-bot.518)
## Contributors
<a href="https://github.com/Emerald-Services/SupportBot/graphs/contributors">
<img src="https://contrib.rocks/image?repo=Emerald-Services/SupportBot">
</a>
## Hosting
You can use our [Pterodactyl egg](https://github.com/Emerald-Services/Pterodactyl-Egg) to host it on your own panel. We currently don't offer hosting for you, altough we might add this in the future.
If you don't know how to install pterodactyl, you could always host it on replit:
[![Run on Repl.it](https://repl.it/badge/github/ItsNyoty/IKI-bot)]([https://repl.it/github/ItsNyoty/iki](https://repl.it/github/Emerald-Services/SupportBot
)
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.
## Quick Links
**|** [Discord](https://emeraldsrv.dev/discord) **|**
[Hosting](https://emeraldsrv.dev/hosting) **|**
[Documentation](https://emeraldsrv.dev/wiki/supportbot) **|**
[Download via MC-Market](https://www.mc-market.org/resources/8940/) **|**
+20
View File
@@ -0,0 +1,20 @@
# Security Policy
## Supported Versions
Use this section to tell people about which versions of your project are
currently being supported with security updates.
| Version | Supported |
| ------- | ------------------ |
| 7.7.2 | :white_check_mark: |
| 7.7.1 | :white_check_mark: |
| < 4.0 | :x: |
## Reporting a Vulnerability
Use this section to tell people how to report a vulnerability.
Tell them where to go, how often they can expect to get an update on a
reported vulnerability, what to expect if the vulnerability is accepted or
declined, etc.
+429
View File
@@ -0,0 +1,429 @@
const Database = require("better-sqlite3");
const path = require("path");
const db = new Database(path.join(__dirname, "../Data/ai-memory.db"));
db.pragma("journal_mode = WAL");
db.prepare(
`
CREATE TABLE IF NOT EXISTS ai_conversations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
guild_id TEXT,
channel_id TEXT,
user_id TEXT,
role TEXT NOT NULL,
content TEXT NOT NULL,
created_at INTEGER NOT NULL
)
`,
).run();
db.prepare(
`
CREATE TABLE IF NOT EXISTS ai_summaries (
id INTEGER PRIMARY KEY AUTOINCREMENT,
guild_id TEXT,
channel_id TEXT,
summary TEXT NOT NULL,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
)
`,
).run();
db.prepare(
`
CREATE TABLE IF NOT EXISTS ai_facts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
guild_id TEXT,
channel_id TEXT,
user_id TEXT,
fact_type TEXT NOT NULL,
fact_key TEXT NOT NULL,
fact_value TEXT NOT NULL,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
)
`,
).run();
db.prepare(
`
CREATE TABLE IF NOT EXISTS ai_knowledge (
id INTEGER PRIMARY KEY AUTOINCREMENT,
source_type TEXT NOT NULL,
source_name TEXT NOT NULL,
title TEXT NOT NULL,
content TEXT NOT NULL,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
)
`,
).run();
db.prepare(
`
CREATE TABLE IF NOT EXISTS ai_sessions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
guild_id TEXT,
channel_id TEXT,
user_id TEXT,
model TEXT,
mode TEXT,
last_message_at INTEGER,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
)
`,
).run();
try {
db.prepare(`ALTER TABLE ai_facts ADD COLUMN user_id TEXT`).run();
} catch (e) {
if (!String(e).includes("duplicate column name")) throw e;
}
module.exports = {
saveUserFact({ guildId, userId, factType, factKey, factValue }) {
const existing = db
.prepare(
`
SELECT id FROM ai_facts
WHERE guild_id IS ? AND user_id IS ? AND fact_type = ? AND fact_key = ?
LIMIT 1
`,
)
.get(guildId || null, userId || null, factType, factKey);
if (existing) {
db.prepare(
`
UPDATE ai_facts
SET fact_value = ?, updated_at = ?
WHERE id = ?
`,
).run(factValue, Date.now(), existing.id);
return;
}
db.prepare(
`
INSERT INTO ai_facts (
guild_id, channel_id, user_id, fact_type, fact_key, fact_value, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`,
).run(
guildId || null,
null,
userId || null,
factType,
factKey,
factValue,
Date.now(),
Date.now(),
);
},
getUserFacts(guildId, userId) {
return db
.prepare(
`
SELECT * FROM ai_facts
WHERE guild_id IS ? AND user_id IS ?
ORDER BY updated_at DESC
`,
)
.all(guildId || null, userId || null);
},
getUserFactsByScope({ guildId = null, userId = null, scope = "guild" }) {
if (!userId) return [];
if (scope === "global") {
return db
.prepare(
`
SELECT * FROM ai_facts
WHERE user_id IS ?
ORDER BY updated_at DESC
`,
)
.all(userId || null);
}
return db
.prepare(
`
SELECT * FROM ai_facts
WHERE guild_id IS ? AND user_id IS ?
ORDER BY updated_at DESC
`,
)
.all(guildId || null, userId || null);
},
addMessage({ guildId, channelId, userId, role, content }) {
db.prepare(
`
INSERT INTO ai_conversations (
guild_id, channel_id, user_id, role, content, created_at
) VALUES (?, ?, ?, ?, ?, ?)
`,
).run(
guildId || null,
channelId || null,
userId || null,
role,
content,
Date.now(),
);
},
getRecentMessages(channelId, limit = 15) {
return db
.prepare(
`
SELECT * FROM ai_conversations
WHERE channel_id = ?
ORDER BY created_at DESC
LIMIT ?
`,
)
.all(channelId, limit)
.reverse();
},
getRecentMessagesByScope({ guildId = null, channelId = null, limit = 15, scope = "guild" }) {
if (scope === "channel") {
return db
.prepare(
`
SELECT * FROM ai_conversations
WHERE channel_id = ?
ORDER BY created_at DESC
LIMIT ?
`,
)
.all(channelId || null, limit)
.reverse();
}
if (scope === "global") {
return db
.prepare(
`
SELECT * FROM ai_conversations
ORDER BY created_at DESC
LIMIT ?
`,
)
.all(limit)
.reverse();
}
return db
.prepare(
`
SELECT * FROM ai_conversations
WHERE guild_id IS ?
ORDER BY created_at DESC
LIMIT ?
`,
)
.all(guildId || null, limit)
.reverse();
},
saveSummary({ guildId, channelId, summary }) {
const existing = db
.prepare(
`
SELECT id FROM ai_summaries
WHERE guild_id IS ? AND channel_id IS ?
ORDER BY updated_at DESC
LIMIT 1
`,
)
.get(guildId || null, channelId || null);
if (existing) {
db.prepare(
`
UPDATE ai_summaries
SET summary = ?, updated_at = ?
WHERE id = ?
`,
).run(summary, Date.now(), existing.id);
return;
}
db.prepare(
`
INSERT INTO ai_summaries (
guild_id, channel_id, summary, created_at, updated_at
) VALUES (?, ?, ?, ?, ?)
`,
).run(guildId || null, channelId || null, summary, Date.now(), Date.now());
},
getLatestSummary(channelId) {
return db
.prepare(
`
SELECT * FROM ai_summaries
WHERE channel_id = ?
ORDER BY updated_at DESC
LIMIT 1
`,
)
.get(channelId);
},
getLatestSummaryByScope({ guildId = null, channelId = null, scope = "guild" }) {
if (scope === "channel") {
return db
.prepare(
`
SELECT * FROM ai_summaries
WHERE channel_id = ?
ORDER BY updated_at DESC
LIMIT 1
`,
)
.get(channelId || null);
}
if (scope === "global") {
return db
.prepare(
`
SELECT * FROM ai_summaries
ORDER BY updated_at DESC
LIMIT 1
`,
)
.get();
}
return db
.prepare(
`
SELECT * FROM ai_summaries
WHERE guild_id IS ?
ORDER BY updated_at DESC
LIMIT 1
`,
)
.get(guildId || null);
},
saveFact({ guildId, channelId, factType, factKey, factValue }) {
const existing = db
.prepare(
`
SELECT id FROM ai_facts
WHERE guild_id IS ? AND channel_id IS ? AND fact_type = ? AND fact_key = ?
LIMIT 1
`,
)
.get(guildId || null, channelId || null, factType, factKey);
if (existing) {
db.prepare(
`
UPDATE ai_facts
SET fact_value = ?, updated_at = ?
WHERE id = ?
`,
).run(factValue, Date.now(), existing.id);
return;
}
db.prepare(
`
INSERT INTO ai_facts (
guild_id, channel_id, fact_type, fact_key, fact_value, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?)
`,
).run(
guildId || null,
channelId || null,
factType,
factKey,
factValue,
Date.now(),
Date.now(),
);
},
getFacts(channelId) {
return db
.prepare(
`
SELECT * FROM ai_facts
WHERE channel_id = ?
ORDER BY updated_at DESC
`,
)
.all(channelId);
},
getFactsByScope({ guildId = null, channelId = null, scope = "guild" }) {
if (scope === "channel") {
return db
.prepare(
`
SELECT * FROM ai_facts
WHERE channel_id = ? AND user_id IS NULL
ORDER BY updated_at DESC
`,
)
.all(channelId || null);
}
if (scope === "global") {
return db
.prepare(
`
SELECT * FROM ai_facts
WHERE user_id IS NULL
ORDER BY updated_at DESC
`,
)
.all();
}
return db
.prepare(
`
SELECT * FROM ai_facts
WHERE guild_id IS ? AND user_id IS NULL
ORDER BY updated_at DESC
`,
)
.all(guildId || null);
},
addKnowledge({ sourceType, sourceName, title, content }) {
db.prepare(
`
INSERT INTO ai_knowledge (
source_type, source_name, title, content, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?)
`,
).run(sourceType, sourceName, title, content, Date.now(), Date.now());
},
getKnowledge(limit = 25) {
return db
.prepare(
`
SELECT * FROM ai_knowledge
ORDER BY updated_at DESC
LIMIT ?
`,
)
.all(limit);
},
};
+7 -1
View File
@@ -1,6 +1,6 @@
// SupportBot | Emerald Services
// Addon Structure
class Command {
constructor(options) {
this.name = options.name;
@@ -10,7 +10,13 @@ class Command {
this.run = options.run;
this.defaultPermission = true;
}
// Method to dynamically update options
setOptions(options) {
this.options = options;
}
}
class Event {
constructor(event, run) {
this.event = event;
+128 -77
View File
@@ -1,124 +1,175 @@
// SupportBot | Emerald Services
// Client Structure
const fs = require("fs");
const path = require("path");
const Discord = require("discord.js");
const intents = new Discord.Intents(32767);
const { GatewayIntentBits, Partials } = require("discord.js");
const yaml = require("js-yaml");
const supportbot = yaml.load(
fs.readFileSync("./Configs/supportbot.yml", "utf8")
);
const { Command, Event } = require("./Addon.js");
const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8"));
const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.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 {
constructor() {
super({ intents });
super({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMembers,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.GuildMessageReactions,
GatewayIntentBits.MessageContent,
],
partials: [Partials.Message, Partials.Channel, Partials.Reaction],
});
this.commands = new Discord.Collection();
}
async getChannel(channel, guild) {
if (!channel) return null;
return guild.channels.cache.find(
(c) =>
(c.type === "GUILD_TEXT" || c.type === "GUILD_NEWS") &&
(c.id === channel || c.name.toLowerCase() === channel.toLowerCase())
(c.type === Discord.ChannelType.GuildText || c.type === Discord.ChannelType.GuildNews) &&
(c.id === channel || (c.name && c.name.toLowerCase() === channel.toLowerCase())),
);
}
async getRole(role, guild) {
if (!role) return null;
return guild.roles.cache.find(
(r) => r.id === role || r.name.toLowerCase() === role.toLowerCase()
(r) => r.id === role || (r.name && r.name.toLowerCase() === role.toLowerCase()),
);
}
async getCategory(category, guild) {
if (!category) return null;
return guild.channels.cache.find(
(c) =>
c.type === "GUILD_CATEGORY" &&
(c.id === category || c.name.toLowerCase() === category.toLowerCase())
c.type === Discord.ChannelType.GuildCategory &&
(c.id === category || (c.name && c.name.toLowerCase() === category.toLowerCase())),
);
}
async start(token) {
var tempCommandFiles = fs.readdirSync("./Commands").filter((file) => file.endsWith(".js"));
const commandFiles = walk("./Commands");
if (supportbot.DisableSuggestions) {
tempCommandFiles = tempCommandFiles.filter(item => item !== "suggest.js");
const allCommands = commandFiles.map((file) => ({
file,
command: require(`../${file}`),
}));
const enabledCommands = allCommands.filter(({ command }) => {
const matchingConfig = Object.values(cmdconfig).find(
(entry) =>
entry &&
typeof entry === "object" &&
entry.Command &&
String(entry.Command).toLowerCase() === String(command.name).toLowerCase(),
);
if (!matchingConfig) {
return true;
}
return matchingConfig.Enabled !== false;
});
section("Commands");
enabledCommands.forEach(({ command, file }) => {
console.log(
`${c.green}✓${c.reset} ${c.white}${command.name}${c.reset} ${c.gray}(${path.basename(file)})${c.reset}`,
);
this.commands.set(command.name, command);
});
if (supportbot.General.Addons.Enabled) {
const addonFiles = fs.readdirSync("./Addons").filter((file) => file.endsWith(".js"));
const addons = addonFiles.map((file) => {
const addon = require(`../Addons/${file}`);
addon.name = file.split(".")[0];
return addon;
});
section("Addons");
addons.forEach((addon) => {
console.log(`${c.green}✓${c.reset} ${c.white}${addon.name}${c.reset}`);
if (addon instanceof Command) {
this.commands.set(addon.name, addon);
}
if (addon.events && Array.isArray(addon.events)) {
addon.events.forEach((event) => {
if (event instanceof Event) {
this.on(event.event, (...args) => event.run(this, ...args));
}
});
}
});
}
const commandFiles = tempCommandFiles;
const addonFiles = fs
.readdirSync("./Addons")
.filter((file) => file.endsWith(".js"));
const addons = addonFiles.map((file) => {
let addon = require(`../Addons/${file}`);
addon.name = file.split(".")[0];
return addon;
});
const commands = commandFiles.map((file) => require(`../Commands/${file}`));
// Commands
console.log(`\u001b[34;1m`, "▬▬▬▬▬▬▬ Commands ▬▬▬▬▬▬▬");
commands.forEach((cmd) => {
console.log(`\u001b[31;1m`, `${cmd.name}`, `\u001b[32;1m`, "Loaded");
this.commands.set(cmd.name, cmd);
});
console.log(`\u001b[34;1m`, "▬▬▬▬▬▬▬ Commands ▬▬▬▬▬▬▬");
// Addons
console.log(" ");
console.log(`\u001b[34;1m`, "▬▬▬▬▬▬▬ Addons ▬▬▬▬▬▬▬");
addons.forEach((addon) => {
console.log(
`\u001b[33m`,
`[ADDON]`,
`\u001b[31;1m`,
`${addon.name}`,
`\u001b[32;1m`,
"Loaded"
);
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 () => {
this.once("clientReady", async () => {
await this.guilds.cache.first()?.commands.set(this.commands);
console.log(
`\u001b[32;1m`,
`Slash Commands Registered for ${this.guilds.cache.first().name}`
`\n${c.green}✓${c.reset} ${c.white}Slash Commands Registered${c.reset} ${c.gray}for ${this.guilds.cache.first().name}${c.reset}`,
);
});
console.log(" ");
console.log(`\u001b[34;1m`, "▬▬▬▬▬▬▬ Events ▬▬▬▬▬▬▬");
section("Events");
fs.readdirSync("./Events")
.filter((file) => file.endsWith(".js"))
.forEach((file) => {
const event = require(`../Events/${file}`);
console.log(`\u001b[31;1m`, `${event.event}`, `\u001b[32;1m`, "Loaded");
console.log(
`${c.green}✓${c.reset} ${c.white}${event.event}${c.reset} ${c.gray}(${file})${c.reset}`,
);
this.on(event.event, (...args) => event.run(this, ...args));
});
console.log(`\u001b[34;1m`, "▬▬▬▬▬▬▬ Events ▬▬▬▬▬▬▬");
console.log("");
if (process.argv[2] !== "test") {
await this.login(token);
} else {
console.log(
`\u001b[31;1m`,
"RUNNING IN TEST MODE, IF NOT INTENDED, PLEASE USE npm start"
);
console.log(`${c.red}RUNNING IN TEST MODE, IF NOT INTENDED, PLEASE USE npm start${c.reset}`);
}
}
}
+474
View File
@@ -0,0 +1,474 @@
// SupportBot | Emerald Services
const Database = require("better-sqlite3");
const path = require("path");
const db = new Database(path.join(__dirname, "../Data/supportbot.db"));
db.prepare(
`
CREATE TABLE IF NOT EXISTS suggestions (
thread_id TEXT PRIMARY KEY,
author_id TEXT,
text TEXT,
status TEXT DEFAULT 'Created'
)
`,
).run();
db.prepare(
`
CREATE TABLE IF NOT EXISTS profiles (
user_id TEXT PRIMARY KEY,
bio TEXT,
timezone TEXT,
clockedIn INTEGER DEFAULT 0
)
`,
).run();
db.prepare(
`
CREATE TABLE IF NOT EXISTS tickets (
ticket_id TEXT PRIMARY KEY,
user_id TEXT,
status TEXT DEFAULT 'open',
created_at INTEGER,
updated_at INTEGER,
subject TEXT,
description TEXT
)
`,
).run();
// Safe column upgrades
const safeAlter = (sql) => {
try {
db.prepare(sql).run();
} catch (e) {
if (!String(e).includes("duplicate column name")) throw e;
}
};
safeAlter(`ALTER TABLE tickets ADD COLUMN voiceChannelId TEXT`);
safeAlter(`ALTER TABLE tickets ADD COLUMN department TEXT DEFAULT 'general'`);
safeAlter(`ALTER TABLE tickets ADD COLUMN priority TEXT DEFAULT 'medium'`);
safeAlter(`ALTER TABLE tickets ADD COLUMN aiModeEnabled INTEGER DEFAULT 0`);
safeAlter(`ALTER TABLE tickets ADD COLUMN aiChannelId TEXT`);
safeAlter(`ALTER TABLE tickets ADD COLUMN aiType TEXT`);
safeAlter(`ALTER TABLE tickets ADD COLUMN aiEnabledBy TEXT`);
safeAlter(`ALTER TABLE tickets ADD COLUMN aiEnabledAt INTEGER`);
safeAlter(`ALTER TABLE tickets ADD COLUMN questionAnswers TEXT`);
safeAlter(`ALTER TABLE tickets ADD COLUMN ticketName TEXT`);
safeAlter(`ALTER TABLE tickets ADD COLUMN ticketNumber TEXT`);
db.prepare(
`
CREATE TABLE IF NOT EXISTS ticket_panel (
id INTEGER PRIMARY KEY AUTOINCREMENT,
message_id TEXT,
channel_id TEXT,
created_at INTEGER
)
`,
).run();
db.prepare(
`
CREATE TABLE IF NOT EXISTS ticket_users (
ticket_id TEXT,
user_id TEXT,
PRIMARY KEY(ticket_id, user_id)
)
`,
).run();
db.prepare(
`
CREATE TABLE IF NOT EXISTS settings (
id INTEGER PRIMARY KEY,
data TEXT
)
`,
).run();
function ensureProfile(userId) {
const existing = db
.prepare(
`
SELECT user_id FROM profiles WHERE user_id = ?
`,
)
.get(userId);
if (!existing) {
db.prepare(
`
INSERT INTO profiles (user_id, bio, timezone, clockedIn)
VALUES (?, '', '', 0)
`,
).run(userId);
}
}
function normaliseTicketRow(row) {
if (!row) return null;
let questionAnswers = [];
if (row.questionAnswers) {
try {
questionAnswers = JSON.parse(row.questionAnswers);
} catch {
questionAnswers = [];
}
}
return {
...row,
aiModeEnabled: Boolean(row.aiModeEnabled),
clockedIn:
row.clockedIn !== undefined ? Boolean(row.clockedIn) : row.clockedIn,
questionAnswers,
};
}
module.exports = {
addTicket(ticket) {
const now = Date.now();
db.prepare(
`
INSERT INTO tickets (
ticket_id,
user_id,
subject,
description,
created_at,
updated_at,
department,
priority,
aiModeEnabled,
aiChannelId,
aiType,
aiEnabledBy,
aiEnabledAt,
questionAnswers,
ticketName,
ticketNumber
)
VALUES (
@ticket_id,
@user_id,
@subject,
@description,
@created_at,
@updated_at,
@department,
@priority,
@aiModeEnabled,
@aiChannelId,
@aiType,
@aiEnabledBy,
@aiEnabledAt,
@questionAnswers,
@ticketName,
@ticketNumber
)
ON CONFLICT(ticket_id) DO NOTHING
`,
).run({
ticket_id: ticket.id,
user_id: ticket.user,
subject: ticket.reason || "",
description: ticket.description || "",
created_at: now,
updated_at: now,
department: ticket.department || "general",
priority: ticket.priority || "medium",
aiModeEnabled: ticket.aiModeEnabled ? 1 : 0,
aiChannelId: ticket.aiChannelId || null,
aiType: ticket.aiType || null,
aiEnabledBy: ticket.aiEnabledBy || null,
aiEnabledAt: ticket.aiEnabledAt || null,
questionAnswers: JSON.stringify(ticket.questionAnswers || []),
ticketName: ticket.name || null,
ticketNumber: ticket.number || null,
});
},
updateTicketQuestionAnswers(ticketId, questionAnswers = []) {
db.prepare(
`
UPDATE tickets
SET questionAnswers=@questionAnswers, updated_at=@updatedAt
WHERE ticket_id=@ticketId
`,
).run({
ticketId,
questionAnswers: JSON.stringify(questionAnswers),
updatedAt: Date.now(),
});
},
updateTicketVoice(ticketId, voiceChannelId) {
db.prepare(
`
UPDATE tickets
SET voiceChannelId=@voiceChannelId, updated_at=@updatedAt
WHERE ticket_id=@ticketId
`,
).run({
ticketId,
voiceChannelId,
updatedAt: Date.now(),
});
},
updateTicketStatus(ticketId, status) {
db.prepare(
`
UPDATE tickets
SET status=@status, updated_at=@updatedAt
WHERE ticket_id=@ticketId
`,
).run({
status,
updatedAt: Date.now(),
ticketId,
});
},
updateTicketDepartment(ticketId, department) {
db.prepare(
`
UPDATE tickets
SET department=@department, updated_at=@updatedAt
WHERE ticket_id=@ticketId
`,
).run({
ticketId,
department,
updatedAt: Date.now(),
});
},
updateTicketPriority(ticketId, priority) {
db.prepare(
`
UPDATE tickets
SET priority=@priority, updated_at=@updatedAt
WHERE ticket_id=@ticketId
`,
).run({
ticketId,
priority,
updatedAt: Date.now(),
});
},
setTicketAIState(ticketId, aiState = {}) {
db.prepare(
`
UPDATE tickets
SET
aiModeEnabled=@aiModeEnabled,
aiChannelId=@aiChannelId,
aiType=@aiType,
aiEnabledBy=@aiEnabledBy,
aiEnabledAt=@aiEnabledAt,
updated_at=@updatedAt
WHERE ticket_id=@ticketId
`,
).run({
ticketId,
aiModeEnabled: aiState.enabled ? 1 : 0,
aiChannelId: aiState.aiChannelId || null,
aiType: aiState.aiType || null,
aiEnabledBy: aiState.enabledBy || null,
aiEnabledAt: aiState.enabledAt
? new Date(aiState.enabledAt).getTime()
: aiState.enabled
? Date.now()
: null,
updatedAt: Date.now(),
});
},
enableTicketAI(
ticketId,
aiChannelId = null,
aiType = null,
enabledBy = null,
) {
db.prepare(
`
UPDATE tickets
SET
aiModeEnabled=1,
aiChannelId=@aiChannelId,
aiType=@aiType,
aiEnabledBy=@aiEnabledBy,
aiEnabledAt=@aiEnabledAt,
updated_at=@updatedAt
WHERE ticket_id=@ticketId
`,
).run({
ticketId,
aiChannelId,
aiType,
aiEnabledBy,
aiEnabledAt: Date.now(),
updatedAt: Date.now(),
});
},
disableTicketAI(ticketId) {
db.prepare(
`
UPDATE tickets
SET
aiModeEnabled=0,
updated_at=@updatedAt
WHERE ticket_id=@ticketId
`,
).run({
ticketId,
updatedAt: Date.now(),
});
},
getTicket(ticketId) {
const row = db
.prepare(
`
SELECT * FROM tickets WHERE ticket_id=@ticketId
`,
)
.get({ ticketId });
return normaliseTicketRow(row);
},
getTicketByAIChannel(aiChannelId) {
const row = db
.prepare(
`
SELECT * FROM tickets WHERE aiChannelId=@aiChannelId LIMIT 1
`,
)
.get({ aiChannelId });
return normaliseTicketRow(row);
},
getAllTickets() {
return db.prepare(`SELECT * FROM tickets`).all().map(normaliseTicketRow);
},
saveTicketPanel(messageId, channelId) {
db.prepare(
`
INSERT INTO ticket_panel (message_id, channel_id, created_at)
VALUES (@messageId, @channelId, @createdAt)
`,
).run({ messageId, channelId, createdAt: Date.now() });
},
getTicketPanel() {
return db
.prepare(
`
SELECT * FROM ticket_panel ORDER BY id DESC LIMIT 1
`,
)
.get();
},
addUserToTicket(ticketId, userId) {
db.prepare(
`
INSERT INTO ticket_users (ticket_id, user_id)
VALUES (?, ?)
ON CONFLICT(ticket_id, user_id) DO NOTHING
`,
).run(ticketId, userId);
},
removeUserFromTicket(ticketId, userId) {
db.prepare(
`
DELETE FROM ticket_users WHERE ticket_id=? AND user_id=?
`,
).run(ticketId, userId);
},
getTicketUsers(ticketId) {
return db
.prepare(
`
SELECT user_id FROM ticket_users WHERE ticket_id=?
`,
)
.all(ticketId)
.map((r) => r.user_id);
},
getSettings() {
const row = db.prepare(`SELECT data FROM settings WHERE id=1`).get();
if (!row) return {};
try {
return JSON.parse(row.data);
} catch {
return {};
}
},
getProfile(userId) {
ensureProfile(userId);
const row = db
.prepare(
`
SELECT * FROM profiles WHERE user_id = ?
`,
)
.get(userId);
if (!row) {
return {
user_id: userId,
bio: "",
timezone: "",
clockedIn: false,
};
}
return {
...row,
clockedIn: Boolean(row.clockedIn),
};
},
setClockedIn(userId, clockedIn) {
ensureProfile(userId);
db.prepare(
`
UPDATE profiles
SET clockedIn = ?
WHERE user_id = ?
`,
).run(clockedIn ? 1 : 0, userId);
},
saveSettings(obj) {
const data = JSON.stringify(obj);
db.prepare(
`
INSERT INTO settings (id, data)
VALUES (1, @data)
ON CONFLICT(id) DO UPDATE SET data = excluded.data
`,
).run({ data });
},
};
+1
View File
@@ -1,5 +1,6 @@
// SupportBot | Emerald Services
// Event Structure
class Event {
constructor(event, run) {
this.event = event;
+12 -29
View File
@@ -1,47 +1,30 @@
// SupportBot | Emerald Services
// Ticket ID Structure
const fs = require("fs");
const db = require("./Database")
class TicketNumberID {
static async pad() {
let num = await this.get();
let n = num + "";
await this.set(num + 1);
return n.length >= 4 ? n : new Array(4 - n.length + 1).join("0") + n;
let num = await this.get()
let n = num + ""
await this.set(num + 1)
return n.length >= 4 ? n : new Array(4 - n.length + 1).join("0") + n
}
static async get() {
let ticketnumberID = await JSON.parse(
fs.readFileSync("./Data/ticket-panel-id.json", "utf8")
);
if (!ticketnumberID.id) {
return this.set(0);
} else {
return ticketnumberID.id;
}
const row = db.getTicketPanel()
if (!row) return await this.set(0)
return row.id
}
static async set(value) {
let ticketnumberID = await JSON.parse(
fs.readFileSync("./Data/ticket-panel-id.json", "utf8")
);
ticketnumberID.id = value;
fs.writeFileSync(
"./Data/ticket-panel-id.json",
JSON.stringify(ticketnumberID, null, 4),
(err) => {
if (err) console.error(err);
}
);
return value;
db.saveTicketPanel(`panel-${value}`, "none")
return value
}
static async reset() {
return await this.set(0);
return await this.set(0)
}
}
module.exports = TicketNumberID;
module.exports = TicketNumberID
+636
View File
@@ -0,0 +1,636 @@
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 = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8"));
const msgconfig = yaml.load(fs.readFileSync("./Configs/messages.yml", "utf8"));
function escapeHtml(str = "") {
return String(str)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
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("");
}
function createTranscriptHTML(ticket, reason) {
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>
<style>
:root {
--bg: #0b0f14;
--panel: #111821;
--panel-2: #182230;
--panel-3: #1d2938;
--border: #283548;
--text: #e6edf3;
--muted: #9fb0c3;
--accent: #22c55e;
--accent-soft: rgba(34, 197, 94, 0.12);
--code: #0f1720;
}
* {
box-sizing: border-box;
}
html, body {
margin: 0;
padding: 0;
background: var(--bg);
color: var(--text);
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
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: #8df0ad;
border: 1px solid rgba(34, 197, 94, 0.25);
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-field {
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid rgba(255,255,255,0.06);
}
.embed-field-name {
font-weight: 700;
margin-bottom: 4px;
}
.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: #8fd9a8;
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: #8fd9ff;
}
.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;
}
}
</style>
</head>
<body>
<div class="page">
<section class="header">
<div class="header-top">
<div class="title-wrap">
<h1>Ticket Transcript</h1>
<p>Exported conversation log for support ticket <strong>${escapeHtml(ticket.name)}</strong></p>
</div>
<div class="badge">Closed Transcript</div>
</div>
<div class="meta-grid">
<div class="meta-card">
<div class="meta-label">Channel</div>
<div class="meta-value">${escapeHtml(ticket.name)}</div>
</div>
<div class="meta-card">
<div class="meta-label">Ticket ID</div>
<div class="meta-value">${escapeHtml(ticket.id)}</div>
</div>
<div class="meta-card">
<div class="meta-label">Message Count</div>
<div class="meta-value">${ticket.messages.length}</div>
</div>
<div class="meta-card">
<div class="meta-label">Close Reason</div>
<div class="meta-value">${escapeHtml(reason || "No Reason Provided.")}</div>
</div>
</div>
</section>
<section class="messages-wrap">
<div class="messages-header">Messages</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">No messages were found in this ticket.</div>`
}
</section>
</div>
</body>
</html>`;
}
async function createTranscript(interaction, ticket, reason) {
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");
}
const ticketUserId = ticket.user_id || ticket.user;
const tUser =
interaction.client.users.cache.get(ticketUserId) ||
(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?.tag || "Unknown User"} (\`${tUser?.id || ticketUserId || "Unknown"}\`)\n` +
`> **Closed by:** <@${interaction.user.id}>\n` +
`> **Message Count:** ${transcriptData.length}`,
)
.addFields({
name: "Reason",
value: `\`\`\`${reason || "No Reason Provided."}\`\`\``,
inline: false,
});
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,
},
reason,
);
fs.writeFileSync(htmlPath, html, "utf8");
await transcriptChannel.send({
embeds: [transcriptEmbed],
files: [
new Discord.AttachmentBuilder(htmlPath, {
name: `SPOILER_${htmlFileName}`,
}),
],
});
return { htmlPath };
}
module.exports = {
createTranscript,
};
+63 -3
View File
@@ -8,9 +8,10 @@
// Installed with MIT License
//
// Discord Support: https://emeraldsrv.dev/discord
// Community Resources: https://emeraldsrv.dev/resources
// Community Resources: https://community.emeraldsrv.dev
const fs = require("fs");
const path = require("path");
const yaml = require("js-yaml");
const supportbot = yaml.load(
@@ -18,6 +19,65 @@ const supportbot = yaml.load(
);
const Client = require("./Structures/Client.js");
const client = new Client({});
const client = new Client({
intents: ['Guilds', 'GuildMembers', 'GuildMessages', 'MessageContent']
});
client.start(supportbot.General.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`);
fs.appendFileSync(file, `[${new Date().toISOString()}] ${data}\n`);
}
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);
});
client.start(supportbot.Token);
-1146
View File
File diff suppressed because it is too large. Load diff
+15 -7
View File
@@ -1,11 +1,11 @@
{
"name": "supportbot",
"version": "7.7b",
"version": "26.0.0-prerelease-1",
"description": "SupportBot is an open-sourced support system for discord. It's completely free to use with high amounts of customization to match your own brand.",
"main": "index.js",
"scripts": {
"start": "npm i && node index.js",
"test": "node index.js test"
"setup": "npm i && node index.js",
"start": "node index.js"
},
"repository": {
"type": "git",
@@ -16,11 +16,19 @@
"bugs": {
"url": "https://github.com/Emerald-Services/SupportBot/issues"
},
"homepage": "https://github.com/Emerald-Services/SupportBot#readme",
"homepage": "https://community.emeraldsrv.dev",
"dependencies": {
"@vitalets/google-translate-api": "^8.0.0",
"discord.js": "^13.6.0",
"@vitalets/google-translate-api": "^9.2.1",
"axios": "^1.7.2",
"better-sqlite3": "^12.2.0",
"discord.js": "^14.13.0",
"figlet": "^1.9.3",
"google-translate-api": "^2.3.0",
"gradient-string": "^3.0.0",
"js": "^0.1.0",
"js-yaml": "^4.1.0",
"moment": "^2.29.3"
"moment": "^2.29.3",
"ms": "^2.1.3",
"openai": "^4.50.0"
}
}