Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4bb35b1e50 | ||
|
|
d451857d7b | ||
|
|
78b804176a | ||
|
|
15b780ab62 | ||
|
|
9448c4e3d6 | ||
|
|
4af9759700 | ||
|
|
835cbbe937 | ||
|
|
491bb9ba65 | ||
|
|
ced29535f9 | ||
|
|
d99e9a105b | ||
|
|
c72c7c5c34 | ||
|
|
23960bc30d | ||
|
|
abb48c281b | ||
|
|
7acb825713 | ||
|
|
dd2f329485 | ||
|
|
75577ec0bd | ||
|
|
7366bcda71 | ||
|
|
23e43cfad3 | ||
|
|
ad9a8085aa | ||
|
|
2f23e8f23d | ||
|
|
4c07b8a236 | ||
|
|
571bd586fa | ||
|
|
eac2d88873 | ||
|
|
93c793f393 | ||
|
|
1aa97b63f9 |
No files matched your search
@@ -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],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
// SupportBot | Emerald Services
|
|
||||||
// Info Command
|
|
||||||
|
|
||||||
const fs = require("fs");
|
|
||||||
|
|
||||||
const { ContainerBuilder, UserSelectMenuBuilder, ButtonStyle, 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 db = require("../../Structures/Database.js");
|
|
||||||
const Command = require("../../Structures/Command.js");
|
|
||||||
|
|
||||||
module.exports = new Command({
|
|
||||||
name: "test",
|
|
||||||
description: cmdconfig.Info.Description,
|
|
||||||
options: [],
|
|
||||||
permissions: cmdconfig.Info.Permission,
|
|
||||||
|
|
||||||
async run(interaction, client) {
|
|
||||||
|
|
||||||
const ticketPanelembed = new ContainerBuilder()
|
|
||||||
.addTextDisplayComponents(
|
|
||||||
textDisplay => textDisplay
|
|
||||||
.setContent("SupportBot"),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
await interaction.reply({
|
|
||||||
components: [ticketPanelembed],
|
|
||||||
flags: MessageFlags.IsComponentsV2,
|
|
||||||
});
|
|
||||||
|
|
||||||
},
|
|
||||||
});
|
|
||||||
+372
-141
@@ -1,5 +1,4 @@
|
|||||||
const {
|
const {
|
||||||
EmbedBuilder,
|
|
||||||
ModalBuilder,
|
ModalBuilder,
|
||||||
TextInputBuilder,
|
TextInputBuilder,
|
||||||
TextInputStyle,
|
TextInputStyle,
|
||||||
@@ -7,7 +6,14 @@ const {
|
|||||||
ActionRowBuilder,
|
ActionRowBuilder,
|
||||||
ButtonBuilder,
|
ButtonBuilder,
|
||||||
ButtonStyle,
|
ButtonStyle,
|
||||||
ApplicationCommandOptionType
|
ApplicationCommandOptionType,
|
||||||
|
MessageFlags,
|
||||||
|
ContainerBuilder,
|
||||||
|
TextDisplayBuilder,
|
||||||
|
SeparatorBuilder,
|
||||||
|
ThumbnailBuilder,
|
||||||
|
MediaGalleryBuilder,
|
||||||
|
MediaGalleryItemBuilder,
|
||||||
} = require("discord.js");
|
} = require("discord.js");
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const yaml = require("js-yaml");
|
const yaml = require("js-yaml");
|
||||||
@@ -20,13 +26,177 @@ const msgconfig = yaml.load(fs.readFileSync("./Configs/messages.yml", "utf8"));
|
|||||||
|
|
||||||
const clockedInUsers = new Set();
|
const clockedInUsers = new Set();
|
||||||
|
|
||||||
function updateClockedInStatus(embed, clockedIn) {
|
function safeGetProfile(userId) {
|
||||||
embed.data.fields = embed.data.fields.map(f =>
|
try {
|
||||||
f.name === "Clocked In Status"
|
if (typeof db.getProfile !== "function") {
|
||||||
? { ...f, value: clockedIn ? "✅ Clocked In" : "❌ Clocked Out" }
|
console.error("db.getProfile is not a function");
|
||||||
: f
|
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),
|
||||||
);
|
);
|
||||||
return embed;
|
}
|
||||||
|
|
||||||
|
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({
|
module.exports = new Command({
|
||||||
@@ -39,94 +209,75 @@ module.exports = new Command({
|
|||||||
name: "user",
|
name: "user",
|
||||||
description: "The user to view the profile of",
|
description: "The user to view the profile of",
|
||||||
type: ApplicationCommandOptionType.User,
|
type: ApplicationCommandOptionType.User,
|
||||||
required: false
|
required: false,
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
async run(interaction) {
|
async run(interaction) {
|
||||||
await interaction.deferReply({ ephemeral: true });
|
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
||||||
|
|
||||||
|
try {
|
||||||
const viewingUser = interaction.options.getUser("user") || interaction.user;
|
const viewingUser = interaction.options.getUser("user") || interaction.user;
|
||||||
const viewingUserId = viewingUser.id;
|
const viewingUserId = viewingUser.id;
|
||||||
const interactionUserId = interaction.user.id;
|
const interactionUserId = interaction.user.id;
|
||||||
|
|
||||||
const member = await interaction.guild.members.fetch(viewingUserId);
|
const member = await interaction.guild.members.fetch(viewingUserId);
|
||||||
const { getRole } = interaction.client;
|
const { getRole } = interaction.client;
|
||||||
|
|
||||||
const Staff = await getRole(supportbot.Roles.StaffMember.Staff, interaction.guild);
|
const Staff = await getRole(supportbot.Roles.StaffMember.Staff, interaction.guild);
|
||||||
const Admin = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild);
|
const Admin = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild);
|
||||||
|
|
||||||
if (!Staff || !Admin) {
|
if (!Staff || !Admin) {
|
||||||
return interaction.editReply({
|
return interaction.editReply({
|
||||||
content: "Some roles seem to be missing!\nPlease check for errors when starting the bot.",
|
content: "Some roles seem to be missing!\nPlease check for errors when starting the bot.",
|
||||||
ephemeral: true
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const isStaff = member.roles.cache.has(Staff.id) || member.roles.cache.has(Admin.id);
|
const isStaff = member.roles.cache.has(Staff.id) || member.roles.cache.has(Admin.id);
|
||||||
const isOwnProfile = interactionUserId === viewingUserId;
|
const isOwnProfile = interactionUserId === viewingUserId;
|
||||||
|
|
||||||
const profileData = db.getProfile(viewingUserId);
|
if (typeof db.ensureProfile === "function") {
|
||||||
let { bio, timezone, clockedIn } = profileData;
|
db.ensureProfile(viewingUserId);
|
||||||
clockedIn = Boolean(clockedIn);
|
}
|
||||||
|
|
||||||
let profileEmbed = new EmbedBuilder()
|
let { bio, timezone, clockedIn } = safeGetProfile(viewingUserId);
|
||||||
.setTitle(`${viewingUser.username}'s Profile`)
|
|
||||||
.setColor(supportbot.Embed.Colours.General)
|
|
||||||
.setThumbnail(viewingUser.displayAvatarURL())
|
|
||||||
.addFields(
|
|
||||||
{ name: "Bio", value: bio || "No bio set.", inline: false },
|
|
||||||
{ name: "Timezone", value: timezone || "No timezone set.", inline: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isStaff) {
|
const profileContainer = buildProfileContainer({
|
||||||
profileEmbed.addFields({
|
viewingUser,
|
||||||
name: "Clocked In Status",
|
viewingUserId,
|
||||||
value: clockedIn ? "✅ Clocked In" : "❌ Clocked Out",
|
bio,
|
||||||
inline: true
|
timezone,
|
||||||
|
clockedIn,
|
||||||
|
isStaff,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
const buttonRow = new ActionRowBuilder();
|
const buttonRows = buildProfileButtons({
|
||||||
if (isOwnProfile) {
|
isOwnProfile,
|
||||||
buttonRow.addComponents(
|
isStaff,
|
||||||
new ButtonBuilder()
|
clockedIn,
|
||||||
.setCustomId("editProfile")
|
});
|
||||||
.setLabel("Edit Profile")
|
|
||||||
.setStyle(ButtonStyle.Secondary)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isStaff) {
|
|
||||||
buttonRow.addComponents(
|
|
||||||
new ButtonBuilder()
|
|
||||||
.setCustomId("clockInOut")
|
|
||||||
.setLabel(clockedIn ? "Clock Out" : "Clock In")
|
|
||||||
.setStyle(ButtonStyle.Secondary)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isStaff && supportbot.Ticket.ClaimTickets.Enabled) {
|
|
||||||
buttonRow.addComponents(
|
|
||||||
new ButtonBuilder()
|
|
||||||
.setCustomId("viewTicketStats")
|
|
||||||
.setLabel("View Ticket Stats")
|
|
||||||
.setStyle(ButtonStyle.Secondary)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await interaction.editReply({
|
await interaction.editReply({
|
||||||
embeds: [profileEmbed],
|
flags: MessageFlags.IsComponentsV2,
|
||||||
components: buttonRow.components.length ? [buttonRow] : []
|
components: [profileContainer, ...buttonRows],
|
||||||
});
|
});
|
||||||
|
|
||||||
const filter = i =>
|
const filter = (i) =>
|
||||||
["editProfile", "clockInOut", "viewTicketStats", "showOpenTickets", "backToStats", "backToProfile"]
|
[
|
||||||
.includes(i.customId) &&
|
"editProfile",
|
||||||
i.user.id === interactionUserId;
|
"clockInOut",
|
||||||
|
"viewTicketStats",
|
||||||
|
"showOpenTickets",
|
||||||
|
"backToStats",
|
||||||
|
"backToProfile",
|
||||||
|
].includes(i.customId) && i.user.id === interactionUserId;
|
||||||
|
|
||||||
const collector = interaction.channel.createMessageComponentCollector({ filter, time: 60000 });
|
const collector = interaction.channel.createMessageComponentCollector({
|
||||||
|
filter,
|
||||||
|
time: 60000,
|
||||||
|
});
|
||||||
|
|
||||||
collector.on("collect", async i => {
|
collector.on("collect", async (i) => {
|
||||||
if (i.customId === "editProfile") {
|
if (i.customId === "editProfile") {
|
||||||
const modal = new ModalBuilder()
|
const modal = new ModalBuilder()
|
||||||
.setCustomId("editProfileModal")
|
.setCustomId("editProfileModal")
|
||||||
@@ -139,7 +290,7 @@ module.exports = new Command({
|
|||||||
.setStyle(TextInputStyle.Paragraph)
|
.setStyle(TextInputStyle.Paragraph)
|
||||||
.setPlaceholder("Enter your bio")
|
.setPlaceholder("Enter your bio")
|
||||||
.setValue(String(bio || ""))
|
.setValue(String(bio || ""))
|
||||||
.setRequired(false)
|
.setRequired(false),
|
||||||
),
|
),
|
||||||
new ActionRowBuilder().addComponents(
|
new ActionRowBuilder().addComponents(
|
||||||
new TextInputBuilder()
|
new TextInputBuilder()
|
||||||
@@ -148,8 +299,8 @@ module.exports = new Command({
|
|||||||
.setStyle(TextInputStyle.Short)
|
.setStyle(TextInputStyle.Short)
|
||||||
.setPlaceholder("Enter your timezone")
|
.setPlaceholder("Enter your timezone")
|
||||||
.setValue(String(timezone || ""))
|
.setValue(String(timezone || ""))
|
||||||
.setRequired(false)
|
.setRequired(false),
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
return i.showModal(modal);
|
return i.showModal(modal);
|
||||||
@@ -157,140 +308,220 @@ module.exports = new Command({
|
|||||||
|
|
||||||
if (i.customId === "clockInOut") {
|
if (i.customId === "clockInOut") {
|
||||||
const newStatus = !clockedIn;
|
const newStatus = !clockedIn;
|
||||||
db.setClockedIn(viewingUserId, newStatus);
|
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;
|
clockedIn = newStatus;
|
||||||
profileEmbed = updateClockedInStatus(profileEmbed, newStatus);
|
|
||||||
|
|
||||||
if (newStatus) clockedInUsers.add(viewingUserId);
|
if (newStatus) clockedInUsers.add(viewingUserId);
|
||||||
else clockedInUsers.delete(viewingUserId);
|
else clockedInUsers.delete(viewingUserId);
|
||||||
|
|
||||||
const updatedRow = new ActionRowBuilder().addComponents(
|
const updatedProfileContainer = buildProfileContainer({
|
||||||
new ButtonBuilder()
|
viewingUser,
|
||||||
.setCustomId("editProfile")
|
viewingUserId,
|
||||||
.setLabel("Edit Profile")
|
bio,
|
||||||
.setStyle(ButtonStyle.Secondary)
|
timezone,
|
||||||
);
|
clockedIn,
|
||||||
if (isStaff) {
|
isStaff,
|
||||||
updatedRow.addComponents(
|
});
|
||||||
new ButtonBuilder()
|
|
||||||
.setCustomId("clockInOut")
|
|
||||||
.setLabel(newStatus ? "Clock Out" : "Clock In")
|
|
||||||
.setStyle(ButtonStyle.Secondary)
|
|
||||||
);
|
|
||||||
if (supportbot.Ticket.ClaimTickets.Enabled) {
|
|
||||||
updatedRow.addComponents(
|
|
||||||
new ButtonBuilder()
|
|
||||||
.setCustomId("viewTicketStats")
|
|
||||||
.setLabel("View Ticket Stats")
|
|
||||||
.setStyle(ButtonStyle.Secondary)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await i.update({ embeds: [profileEmbed], components: [updatedRow] });
|
const updatedButtonRows = buildProfileButtons({
|
||||||
|
isOwnProfile,
|
||||||
|
isStaff,
|
||||||
|
clockedIn,
|
||||||
|
});
|
||||||
|
|
||||||
const clockEmbed = new EmbedBuilder()
|
await i.update({
|
||||||
.setTitle(newStatus ? "Clocked In!" : "Clocked Out!")
|
flags: MessageFlags.IsComponentsV2,
|
||||||
.setDescription(newStatus ? "You have successfully clocked in." : "You have successfully clocked out.")
|
components: [updatedProfileContainer, ...updatedButtonRows],
|
||||||
.setColor(supportbot.Embed.Colours.General);
|
});
|
||||||
|
|
||||||
return interaction.followUp({ embeds: [clockEmbed], ephemeral: true });
|
return interaction.followUp({
|
||||||
|
content: newStatus
|
||||||
|
? "You have successfully clocked in."
|
||||||
|
: "You have successfully clocked out.",
|
||||||
|
flags: MessageFlags.Ephemeral,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (i.customId === "viewTicketStats") {
|
if (i.customId === "viewTicketStats") {
|
||||||
const TicketData = JSON.parse(fs.readFileSync("./Data/TicketData.json", "utf8"));
|
let TicketData = { tickets: [] };
|
||||||
const ticketsClaimed = TicketData.tickets.filter(t => t.claimedBy === viewingUserId);
|
|
||||||
const ticketsOpen = ticketsClaimed.filter(t => t.open);
|
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;
|
const totalTickets = ticketsClaimed.length;
|
||||||
|
|
||||||
let totalResponseTime = 0;
|
let totalResponseTime = 0;
|
||||||
let responseCount = 0;
|
let responseCount = 0;
|
||||||
ticketsClaimed.forEach(t => {
|
|
||||||
|
ticketsClaimed.forEach((t) => {
|
||||||
if (t.claimedAt && t.createdAt) {
|
if (t.claimedAt && t.createdAt) {
|
||||||
totalResponseTime += new Date(t.claimedAt) - new Date(t.createdAt);
|
totalResponseTime += new Date(t.claimedAt) - new Date(t.createdAt);
|
||||||
responseCount++;
|
responseCount++;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const avgMinutes = responseCount ? Math.round(totalResponseTime / responseCount / 60000) : 0;
|
const avgMinutes = responseCount
|
||||||
|
? Math.round(totalResponseTime / responseCount / 60000)
|
||||||
|
: 0;
|
||||||
|
|
||||||
const statsEmbed = new EmbedBuilder()
|
const statsContainer = buildTicketStatsContainer(
|
||||||
.setTitle(msgconfig.TicketStats.Title)
|
viewingUserId,
|
||||||
.setDescription(`> Ticket stats for <@${viewingUserId}>`)
|
ticketsClaimed,
|
||||||
.setColor(supportbot.Embed.Colours.General)
|
ticketsOpen,
|
||||||
.addFields(
|
avgMinutes,
|
||||||
{ name: msgconfig.TicketStats.OpenTickets, value: `${ticketsOpen.length}`, inline: true },
|
|
||||||
{ name: msgconfig.TicketStats.TotalTickets, value: `${totalTickets}`, inline: true },
|
|
||||||
{ name: msgconfig.TicketStats.ResponseTime, value: `${avgMinutes} minutes`, inline: false }
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const statsRow = new ActionRowBuilder().addComponents(
|
const statsRow = new ActionRowBuilder().addComponents(
|
||||||
new ButtonBuilder().setCustomId("showOpenTickets").setLabel("Show Open Tickets").setStyle(ButtonStyle.Secondary),
|
new ButtonBuilder()
|
||||||
new ButtonBuilder().setCustomId("backToProfile").setLabel("Back to Profile").setStyle(ButtonStyle.Secondary)
|
.setCustomId("showOpenTickets")
|
||||||
|
.setLabel("Show Open Tickets")
|
||||||
|
.setStyle(ButtonStyle.Secondary),
|
||||||
|
new ButtonBuilder()
|
||||||
|
.setCustomId("backToProfile")
|
||||||
|
.setLabel("Back to Profile")
|
||||||
|
.setStyle(ButtonStyle.Secondary),
|
||||||
);
|
);
|
||||||
|
|
||||||
return i.update({ embeds: [statsEmbed], components: [statsRow] });
|
return i.update({
|
||||||
|
flags: MessageFlags.IsComponentsV2,
|
||||||
|
components: [statsContainer, statsRow],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (i.customId === "showOpenTickets") {
|
if (i.customId === "showOpenTickets") {
|
||||||
const TicketData = JSON.parse(fs.readFileSync("./Data/TicketData.json", "utf8"));
|
let TicketData = { tickets: [] };
|
||||||
const ticketsOpen = TicketData.tickets.filter(t => t.claimedBy === viewingUserId && t.open);
|
|
||||||
|
|
||||||
const openEmbed = new EmbedBuilder()
|
try {
|
||||||
.setTitle("Active Tickets")
|
TicketData = JSON.parse(fs.readFileSync("./Data/TicketData.json", "utf8"));
|
||||||
.setDescription(ticketsOpen.map(t => `<#${t.id}>`).join("\n") || "No active tickets.")
|
} catch (error) {
|
||||||
.setColor(supportbot.Embed.Colours.General);
|
console.error("Error reading TicketData.json:", error);
|
||||||
|
}
|
||||||
|
|
||||||
const backRow = new ActionRowBuilder().addComponents(
|
const ticketsOpen = TicketData.tickets.filter(
|
||||||
new ButtonBuilder().setCustomId("backToStats").setLabel("Back to Stats").setStyle(ButtonStyle.Secondary)
|
(t) => t.claimedBy === viewingUserId && t.open,
|
||||||
);
|
);
|
||||||
|
|
||||||
return i.update({ embeds: [openEmbed], components: [backRow] });
|
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") {
|
if (i.customId === "backToStats") {
|
||||||
|
let TicketData = { tickets: [] };
|
||||||
|
|
||||||
const TicketData = JSON.parse(fs.readFileSync("./Data/TicketData.json", "utf8"));
|
try {
|
||||||
const ticketsClaimed = TicketData.tickets.filter(t => t.claimedBy === viewingUserId);
|
TicketData = JSON.parse(fs.readFileSync("./Data/TicketData.json", "utf8"));
|
||||||
const ticketsOpen = ticketsClaimed.filter(t => t.open);
|
} catch (error) {
|
||||||
const totalTickets = ticketsClaimed.length;
|
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 totalResponseTime = 0;
|
||||||
let responseCount = 0;
|
let responseCount = 0;
|
||||||
ticketsClaimed.forEach(t => {
|
|
||||||
|
ticketsClaimed.forEach((t) => {
|
||||||
if (t.claimedAt && t.createdAt) {
|
if (t.claimedAt && t.createdAt) {
|
||||||
totalResponseTime += new Date(t.claimedAt) - new Date(t.createdAt);
|
totalResponseTime += new Date(t.claimedAt) - new Date(t.createdAt);
|
||||||
responseCount++;
|
responseCount++;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const avgMinutes = responseCount ? Math.round(totalResponseTime / responseCount / 60000) : 0;
|
|
||||||
|
|
||||||
const statsEmbed = new EmbedBuilder()
|
const avgMinutes = responseCount
|
||||||
.setTitle(msgconfig.TicketStats.Title)
|
? Math.round(totalResponseTime / responseCount / 60000)
|
||||||
.setDescription(`> Ticket stats for <@${viewingUserId}>`)
|
: 0;
|
||||||
.setColor(supportbot.Embed.Colours.General)
|
|
||||||
.addFields(
|
const statsContainer = buildTicketStatsContainer(
|
||||||
{ name: msgconfig.TicketStats.OpenTickets, value: `${ticketsOpen.length}`, inline: true },
|
viewingUserId,
|
||||||
{ name: msgconfig.TicketStats.TotalTickets, value: `${totalTickets}`, inline: true },
|
ticketsClaimed,
|
||||||
{ name: msgconfig.TicketStats.ResponseTime, value: `${avgMinutes} minutes`, inline: false }
|
ticketsOpen,
|
||||||
|
avgMinutes,
|
||||||
);
|
);
|
||||||
|
|
||||||
const statsRow = new ActionRowBuilder().addComponents(
|
const statsRow = new ActionRowBuilder().addComponents(
|
||||||
new ButtonBuilder().setCustomId("showOpenTickets").setLabel("Show Open Tickets").setStyle(ButtonStyle.Secondary),
|
new ButtonBuilder()
|
||||||
new ButtonBuilder().setCustomId("backToProfile").setLabel("Back to Profile").setStyle(ButtonStyle.Secondary)
|
.setCustomId("showOpenTickets")
|
||||||
|
.setLabel("Show Open Tickets")
|
||||||
|
.setStyle(ButtonStyle.Secondary),
|
||||||
|
new ButtonBuilder()
|
||||||
|
.setCustomId("backToProfile")
|
||||||
|
.setLabel("Back to Profile")
|
||||||
|
.setStyle(ButtonStyle.Secondary),
|
||||||
);
|
);
|
||||||
|
|
||||||
return i.update({ embeds: [statsEmbed], components: [statsRow] });
|
return i.update({
|
||||||
|
flags: MessageFlags.IsComponentsV2,
|
||||||
|
components: [statsContainer, statsRow],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (i.customId === "backToProfile") {
|
if (i.customId === "backToProfile") {
|
||||||
|
const updatedProfileContainer = buildProfileContainer({
|
||||||
|
viewingUser,
|
||||||
|
viewingUserId,
|
||||||
|
bio,
|
||||||
|
timezone,
|
||||||
|
clockedIn,
|
||||||
|
isStaff,
|
||||||
|
});
|
||||||
|
|
||||||
|
const updatedButtonRows = buildProfileButtons({
|
||||||
|
isOwnProfile,
|
||||||
|
isStaff,
|
||||||
|
clockedIn,
|
||||||
|
});
|
||||||
|
|
||||||
return i.update({
|
return i.update({
|
||||||
embeds: [profileEmbed],
|
flags: MessageFlags.IsComponentsV2,
|
||||||
components: buttonRow.components.length ? [buttonRow] : []
|
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;
|
module.exports.clockedInUsers = clockedInUsers;
|
||||||
+167
-78
@@ -2,116 +2,205 @@
|
|||||||
// Translate Command
|
// Translate Command
|
||||||
|
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
|
|
||||||
const Discord = require("discord.js");
|
const Discord = require("discord.js");
|
||||||
const yaml = require("js-yaml");
|
const yaml = require("js-yaml");
|
||||||
|
|
||||||
const supportbot = yaml.load(
|
const { translate } = require("@vitalets/google-translate-api");
|
||||||
fs.readFileSync("./Configs/supportbot.yml", "utf8")
|
|
||||||
);
|
|
||||||
|
|
||||||
const cmdconfig = yaml.load(
|
const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8"));
|
||||||
fs.readFileSync("./Configs/commands.yml", "utf8")
|
const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8"));
|
||||||
);
|
const msgconfig = yaml.load(fs.readFileSync("./Configs/messages.yml", "utf8"));
|
||||||
|
|
||||||
const msgconfig = yaml.load(
|
|
||||||
fs.readFileSync("./Configs/messages.yml", "utf8")
|
|
||||||
)
|
|
||||||
|
|
||||||
const Command = require("../../Structures/Command.js");
|
const Command = require("../../Structures/Command.js");
|
||||||
const translate = require("@vitalets/google-translate-api");
|
|
||||||
|
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({
|
module.exports = new Command({
|
||||||
name: cmdconfig.Translate.Command,
|
name: cmdconfig.Translate.Command,
|
||||||
description: cmdconfig.Translate.Description,
|
description: cmdconfig.Translate.Description,
|
||||||
type: Discord.ApplicationCommandType.ChatInput,
|
type: Discord.ApplicationCommandType.ChatInput,
|
||||||
|
|
||||||
options: [
|
options: [
|
||||||
{
|
{
|
||||||
type: Discord.ApplicationCommandOptionType.String,
|
type: Discord.ApplicationCommandOptionType.String,
|
||||||
name: "language",
|
name: "from",
|
||||||
description: "A 2 letter language code",
|
description: "Language to translate FROM",
|
||||||
required: true,
|
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,
|
type: Discord.ApplicationCommandOptionType.String,
|
||||||
name: "text",
|
name: "text",
|
||||||
description: "The text to translate",
|
description: "Text to translate",
|
||||||
required: true,
|
required: true,
|
||||||
}, // The input text for the translation
|
},
|
||||||
],
|
],
|
||||||
permissions: cmdconfig.Translate.Permission, // The permission the user/role at least requires
|
|
||||||
|
permissions: cmdconfig.Translate.Permission,
|
||||||
|
|
||||||
async run(interaction) {
|
async run(interaction) {
|
||||||
let disableCommand = true;
|
const { getRole, getChannel } = interaction.client;
|
||||||
|
|
||||||
const { getRole } = interaction.client;
|
const Staff = await getRole(supportbot.Roles.StaffMember.Staff, interaction.guild);
|
||||||
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(
|
const Admin = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild);
|
||||||
"Some roles seem to be missing!\nPlease check for errors when starting the bot."
|
|
||||||
);
|
|
||||||
|
|
||||||
const NoPerms = new Discord.EmbedBuilder()
|
if (!Staff || !Admin) {
|
||||||
.setTitle("Invalid Permissions!")
|
return interaction.reply({
|
||||||
.setDescription(
|
content: "Some roles seem to be missing!",
|
||||||
`${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(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.Translate.TranslateLog,
|
|
||||||
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.EmbedBuilder()
|
|
||||||
.setColor(supportbot.Embed.Colours.Warn)
|
|
||||||
.setTitle("Valid Language Codes")
|
|
||||||
.setURL(url)
|
|
||||||
.setDescription(
|
|
||||||
"Click the title to see which 2 letter language codes are valid."
|
|
||||||
),
|
|
||||||
],
|
|
||||||
flags: Discord.MessageFlags.Ephemeral,
|
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,
|
||||||
|
),
|
||||||
|
],
|
||||||
});
|
});
|
||||||
if (!result) return;
|
}
|
||||||
|
|
||||||
// Embed containing language code, original text and translated text
|
await interaction.deferReply({ flags: Discord.MessageFlags.Ephemeral });
|
||||||
let transembed = new Discord.EmbedBuilder()
|
|
||||||
.setAuthor({ name: interaction.user.tag, iconURL: interaction.user.displayAvatarURL({ dynamic: true }) })
|
|
||||||
.setColor(supportbot.Embed.Colours.Success)
|
|
||||||
.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
|
const fromLang = interaction.options.getString("from");
|
||||||
await interaction.reply({ embeds: [transembed] });
|
const toLang = interaction.options.getString("to");
|
||||||
|
const text = interaction.options.getString("text");
|
||||||
|
|
||||||
// Send embed [transembed] to logging channel
|
const translateLog = await getChannel(supportbot.Translate?.TranslateLog, interaction.guild);
|
||||||
await translatelog.send({ embeds: [transembed] });
|
|
||||||
},
|
|
||||||
|
|
||||||
|
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(() => {});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
const fs = require("fs");
|
|
||||||
const {
|
|
||||||
EmbedBuilder,
|
|
||||||
ApplicationCommandOptionType,
|
|
||||||
ApplicationCommandType,
|
|
||||||
ChannelType,
|
|
||||||
MessageFlags,
|
|
||||||
} = require("discord.js");
|
|
||||||
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 Command = require("../../Structures/Command.js");
|
|
||||||
const db = require("../../Structures/Database.js");
|
|
||||||
|
|
||||||
module.exports = new Command({
|
|
||||||
name: cmdconfig.ForceAddUser.Command,
|
|
||||||
description: cmdconfig.ForceAddUser.Description,
|
|
||||||
type: ApplicationCommandType.ChatInput,
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
name: "user",
|
|
||||||
description: "The user to forcefully add",
|
|
||||||
type: ApplicationCommandOptionType.User,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
permissions: cmdconfig.ForceAddUser.Permission,
|
|
||||||
|
|
||||||
async run(interaction) {
|
|
||||||
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: Discord.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 if (supportbot.Ticket.TicketType === "channels") {
|
|
||||||
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: Discord.MessageFlags.Ephemeral,
|
|
||||||
});
|
|
||||||
|
|
||||||
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 (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: Discord.MessageFlags.Ephemeral,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
+438
-413
@@ -1,14 +1,400 @@
|
|||||||
|
|
||||||
|
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const Discord = require("discord.js");
|
const Discord = require("discord.js");
|
||||||
const yaml = require("js-yaml");
|
const yaml = require("js-yaml");
|
||||||
|
|
||||||
const db = require("../../Structures/Database.js");
|
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 supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8"));
|
||||||
const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8"));
|
const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8"));
|
||||||
const msgconfig = yaml.load(fs.readFileSync("./Configs/messages.yml", "utf8"));
|
const msgconfig = yaml.load(fs.readFileSync("./Configs/messages.yml", "utf8"));
|
||||||
|
|
||||||
const Command = require("../../Structures/Command.js");
|
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({
|
module.exports = new Command({
|
||||||
name: cmdconfig.CloseTicket.Command,
|
name: cmdconfig.CloseTicket.Command,
|
||||||
@@ -19,450 +405,89 @@ module.exports = new Command({
|
|||||||
name: "reason",
|
name: "reason",
|
||||||
description: "Ticket Close Reason",
|
description: "Ticket Close Reason",
|
||||||
type: Discord.ApplicationCommandOptionType.String,
|
type: Discord.ApplicationCommandOptionType.String,
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
permissions: cmdconfig.CloseTicket.Permission,
|
permissions: cmdconfig.CloseTicket.Permission,
|
||||||
|
|
||||||
async run(interaction) {
|
async run(interaction) {
|
||||||
const { getRole, getChannel } = interaction.client;
|
const { getRole } = interaction.client;
|
||||||
|
|
||||||
if (supportbot.Ticket.Close.StaffOnly) {
|
if (supportbot.Ticket.Close.StaffOnly) {
|
||||||
let SupportStaff = await getRole(supportbot.Roles.StaffMember.Staff, interaction.guild);
|
const SupportStaff = await getRole(supportbot.Roles.StaffMember.Staff, interaction.guild);
|
||||||
let Admin = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild);
|
const Admin = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild);
|
||||||
|
|
||||||
if (!SupportStaff || !Admin) {
|
if (!SupportStaff || !Admin) {
|
||||||
return interaction.reply("Some roles seem to be missing! Please check for errors when starting the bot.");
|
return interaction.reply({
|
||||||
|
content: "Some roles seem to be missing! Please check for errors when starting the bot.",
|
||||||
|
flags: Discord.MessageFlags.Ephemeral,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const NoPerms = new Discord.EmbedBuilder()
|
if (
|
||||||
.setTitle("Invalid Permissions!")
|
!interaction.member.roles.cache.has(SupportStaff.id) &&
|
||||||
.setDescription(`${msgconfig.Error.IncorrectPerms}\n\nRole Required: \`${supportbot.Roles.StaffMember.Staff}\` or \`${supportbot.Roles.StaffMember.Admin}\``)
|
!interaction.member.roles.cache.has(Admin.id)
|
||||||
.setColor(supportbot.Embed.Colours.Warn);
|
) {
|
||||||
|
return interaction.reply({
|
||||||
if (!interaction.member.roles.cache.has(SupportStaff.id) && !interaction.member.roles.cache.has(Admin.id)) {
|
flags: Discord.MessageFlags.IsComponentsV2 | Discord.MessageFlags.Ephemeral,
|
||||||
return interaction.reply({ embeds: [NoPerms] });
|
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;
|
const isThread = interaction.channel.type === Discord.ChannelType.PrivateThread;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(supportbot.Ticket.TicketType === "threads" && !isThread) ||
|
(supportbot.Ticket.TicketType === "threads" && !isThread) ||
|
||||||
(supportbot.Ticket.TicketType === "channels" && interaction.channel.type !== Discord.ChannelType.GuildText)
|
(supportbot.Ticket.TicketType === "channels" &&
|
||||||
|
interaction.channel.type !== Discord.ChannelType.GuildText)
|
||||||
) {
|
) {
|
||||||
const NotTicketChannel = new Discord.EmbedBuilder()
|
return interaction.reply({
|
||||||
.setTitle("Invalid Channel!")
|
flags: Discord.MessageFlags.IsComponentsV2 | Discord.MessageFlags.Ephemeral,
|
||||||
.setDescription(`This command can only be used in a ${supportbot.Ticket.TicketType === "threads" ? "ticket thread" : "ticket channel"}.`)
|
components: [
|
||||||
.setColor(supportbot.Embed.Colours.Warn);
|
buildContainer("Invalid Channel!", [
|
||||||
|
`This command can only be used in a ${
|
||||||
return interaction.reply({ embeds: [NotTicketChannel], ephemeral: true });
|
supportbot.Ticket.TicketType === "threads" ? "ticket thread" : "ticket channel"
|
||||||
|
}.`,
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await interaction.deferReply();
|
await interaction.deferReply({ flags: Discord.MessageFlags.Ephemeral });
|
||||||
|
|
||||||
const ticket = db.getTicket(interaction.channel.id);
|
const ticket = db.getTicket(interaction.channel.id);
|
||||||
|
|
||||||
if (!ticket) {
|
if (!ticket) {
|
||||||
const Exists = new Discord.EmbedBuilder()
|
return interaction.editReply({
|
||||||
.setTitle("No Ticket Found!")
|
flags: Discord.MessageFlags.IsComponentsV2,
|
||||||
.setDescription(msgconfig.Error.NoValidTicket)
|
components: [buildContainer("No Ticket Found!", [msgconfig.Error.NoValidTicket])],
|
||||||
.setColor(supportbot.Embed.Colours.Warn);
|
});
|
||||||
return interaction.followUp({ embeds: [Exists] });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let reason = interaction.options?.getString("reason") || "No Reason Provided.";
|
const reason = interaction.options?.getString("reason") || "No Reason Provided.";
|
||||||
const ticketUserId = ticket.user;
|
|
||||||
|
|
||||||
if (supportbot.Ticket.ReviewSystem.Enabled) {
|
const confirmed = await collectCloseConfirmation(interaction, ticket, reason);
|
||||||
const rating = await collectReviewRating(interaction, ticketUserId);
|
if (!confirmed) {
|
||||||
const comment = await collectReviewComment(interaction, ticketUserId);
|
return;
|
||||||
|
|
||||||
const reviewChannel = await getChannel(supportbot.Ticket.ReviewSystem.Channel, interaction.guild);
|
|
||||||
const reviewer = supportbot.Ticket.ClaimTickets ? `<@${ticket.claimedBy}>` : null;
|
|
||||||
|
|
||||||
const reviewEmbed = new Discord.EmbedBuilder()
|
|
||||||
.setTitle(msgconfig.ReviewSystem.ReviewEmbed.Title)
|
|
||||||
.addFields(
|
|
||||||
{
|
|
||||||
name: msgconfig.ReviewSystem.ReviewEmbed.RatingTitle,
|
|
||||||
value: `${msgconfig.ReviewSystem.ReviewEmbed.ReviewEmoji.repeat(rating)}`,
|
|
||||||
inline: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: msgconfig.ReviewSystem.ReviewEmbed.CommentTitle,
|
|
||||||
value: `\`\`\`${comment}\`\`\``,
|
|
||||||
inline: false
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.setColor(msgconfig.ReviewSystem.ReviewEmbed.Color);
|
|
||||||
|
|
||||||
if (supportbot.Ticket.ClaimTickets.Enabled) {
|
|
||||||
reviewEmbed.setDescription(
|
|
||||||
`**${msgconfig.ReviewSystem.ReviewEmbed.ReviewedStaffTitle}** ${reviewer}\n**${msgconfig.ReviewSystem.ReviewEmbed.ReviewedByTitle}** <@${ticketUserId}>` || `N/A`
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
reviewEmbed.setDescription(
|
|
||||||
`**${msgconfig.ReviewSystem.ReviewEmbed.ReviewedByTitle}** <@${ticketUserId}>`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (reviewChannel) {
|
if (supportbot.Ticket?.ReviewSystem?.Enabled === true) {
|
||||||
await reviewChannel.send({ embeds: [reviewEmbed] });
|
const reviewData = await collectReview(interaction, ticket);
|
||||||
}
|
|
||||||
|
|
||||||
if (!interaction.replied && !interaction.deferred) {
|
if (reviewData) {
|
||||||
await interaction.followUp({ embeds: [reviewEmbed] });
|
await postReview(interaction, ticket, reviewData);
|
||||||
}
|
}
|
||||||
|
|
||||||
await handleCloseTicket(interaction, reason, ticket);
|
await handleCloseTicket(interaction, reason, ticket);
|
||||||
} else {
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await handleCloseTicket(interaction, reason, ticket);
|
await handleCloseTicket(interaction, reason, ticket);
|
||||||
}
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
async function collectReviewRating(interaction, ticketUserId) {
|
|
||||||
const reviewPrompt = new Discord.EmbedBuilder()
|
|
||||||
.setTitle(msgconfig.ReviewSystem.Rate.Title)
|
|
||||||
.setDescription(`${msgconfig.ReviewSystem.Rate.Description}`)
|
|
||||||
.setColor(supportbot.Embed.Colours.General);
|
|
||||||
|
|
||||||
const stars = new Discord.ActionRowBuilder().addComponents(
|
|
||||||
new Discord.StringSelectMenuBuilder()
|
|
||||||
.setCustomId("starRating")
|
|
||||||
.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" },
|
|
||||||
])
|
|
||||||
);
|
|
||||||
|
|
||||||
await interaction.followUp({
|
|
||||||
content: `<@${ticketUserId}>`,
|
|
||||||
embeds: [reviewPrompt],
|
|
||||||
components: [stars],
|
|
||||||
flags: Discord.MessageFlags.Ephemeral,
|
|
||||||
});
|
|
||||||
|
|
||||||
const starRating = await interaction.channel.awaitMessageComponent({
|
|
||||||
componentType: Discord.ComponentType.StringSelect,
|
|
||||||
filter: (i) => i.customId === "starRating" && i.user.id === ticketUserId,
|
|
||||||
});
|
|
||||||
await starRating.deferUpdate();
|
|
||||||
return starRating.values[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
async function collectReviewComment(interaction, ticketUserId) {
|
|
||||||
const commentPrompt = new Discord.EmbedBuilder()
|
|
||||||
.setTitle(msgconfig.ReviewSystem.Comment.Title)
|
|
||||||
.setDescription(`${msgconfig.ReviewSystem.Comment.Description}`)
|
|
||||||
.setColor(supportbot.Embed.Colours.General);
|
|
||||||
|
|
||||||
await interaction.followUp({
|
|
||||||
content: `<@${ticketUserId}>`,
|
|
||||||
embeds: [commentPrompt],
|
|
||||||
flags: Discord.MessageFlags.Ephemeral,
|
|
||||||
});
|
|
||||||
|
|
||||||
const filter = (response) => response.author.id === ticketUserId;
|
|
||||||
|
|
||||||
const commentCollection = await interaction.channel.awaitMessages({ filter, max: 1 });
|
|
||||||
const comment = commentCollection.first()?.content;
|
|
||||||
return comment && comment.toLowerCase() !== "no" ? comment : "No Comment Provided";
|
|
||||||
}
|
|
||||||
async function handleCloseTicket(interaction, reason, ticket) {
|
|
||||||
const { getChannel } = interaction.client;
|
|
||||||
|
|
||||||
let tUser = interaction.client.users.cache.get(ticket.user_id);
|
|
||||||
let transcriptChannel = await getChannel(supportbot.Ticket.Log.TicketDataLog, interaction.guild);
|
|
||||||
|
|
||||||
if (!transcriptChannel) {
|
|
||||||
console.log("Transcript channel missing or inaccessible");
|
|
||||||
return interaction.followUp("Error: Transcript log channel is missing or bot lacks permission.");
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
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 || "No content",
|
|
||||||
username: msg.author.username,
|
|
||||||
userId: msg.author.id,
|
|
||||||
avatar: msg.author.displayAvatarURL(),
|
|
||||||
timestamp: msg.createdAt.toISOString(),
|
|
||||||
attachments: Array.from(msg.attachments.values()).map(att => ({
|
|
||||||
url: att.url,
|
|
||||||
name: att.name
|
|
||||||
})),
|
|
||||||
embeds: msg.embeds.map(embed => ({
|
|
||||||
title: embed.title,
|
|
||||||
description: embed.description,
|
|
||||||
fields: embed.fields
|
|
||||||
}))
|
|
||||||
}));
|
|
||||||
|
|
||||||
db.updateTicketStatus(interaction.channel.id, "closed");
|
|
||||||
|
|
||||||
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 || ticket.user_id}\`)` +
|
|
||||||
`> **Closed by:** <@${interaction.user.id}>\n` +
|
|
||||||
`> **Message Count:** ${transcriptData.length}`
|
|
||||||
)
|
|
||||||
.addFields({ name: "Reason", value: `\`\`\`${reason}\`\`\``, inline: true });
|
|
||||||
|
|
||||||
const html = createTranscriptHTML(
|
|
||||||
{
|
|
||||||
id: interaction.channel.id,
|
|
||||||
messages: transcriptData,
|
|
||||||
name: interaction.channel.name
|
|
||||||
},
|
|
||||||
reason
|
|
||||||
);
|
|
||||||
|
|
||||||
const transcriptDir = "./Data/Transcripts";
|
|
||||||
|
|
||||||
if (!fs.existsSync(transcriptDir)) {
|
|
||||||
fs.mkdirSync(transcriptDir, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileName = `${interaction.channel.id}-transcript.html`;
|
|
||||||
fs.writeFileSync(`./Data/Transcripts/${fileName}`, html);
|
|
||||||
|
|
||||||
await transcriptChannel.send({
|
|
||||||
embeds: [transcriptEmbed],
|
|
||||||
files: [{
|
|
||||||
attachment: `./Data/Transcripts/${fileName}`,
|
|
||||||
name: `SPOILER_${fileName}`,
|
|
||||||
spoiler: true
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
|
|
||||||
if (interaction.channel.type === Discord.ChannelType.GuildText ||
|
|
||||||
interaction.channel.type === Discord.ChannelType.PrivateThread) {
|
|
||||||
await interaction.channel.delete();
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Error handling ticket close:", err);
|
|
||||||
interaction.followUp("An error occurred while closing the ticket.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseMarkdown(content) {
|
|
||||||
content = content.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
|
|
||||||
|
|
||||||
content = content.replace(/_(.*?)_/g, '<em>$1</em>');
|
|
||||||
|
|
||||||
content = content.replace(/`(.*?)`/g, '<code>$1</code>');
|
|
||||||
|
|
||||||
content = content.replace(/\[(.*?)\]\((.*?)\)/g, '<a href="$2" target="_blank">$1</a>');
|
|
||||||
|
|
||||||
content = content.replace(/```(.*?)```/gs, '<pre><code>$1</code></pre>');
|
|
||||||
|
|
||||||
return content;
|
|
||||||
}
|
|
||||||
|
|
||||||
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>Ticket Transcript</title>
|
|
||||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.css" rel="stylesheet">
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
background: linear-gradient(135deg, #0f2027, #203a43, #2c5364);
|
|
||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
||||||
color: #e5e7eb;
|
|
||||||
margin: 0;
|
|
||||||
padding: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
max-width: 1000px;
|
|
||||||
margin: auto;
|
|
||||||
padding: 25px;
|
|
||||||
border-radius: 20px;
|
|
||||||
backdrop-filter: blur(18px) saturate(180%);
|
|
||||||
background: rgba(17, 25, 40, 0.75);
|
|
||||||
box-shadow: 0 8px 32px rgba(0,0,0,0.6);
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 2rem;
|
|
||||||
font-weight: 700;
|
|
||||||
text-align: center;
|
|
||||||
color: #22c55e; /* green accent */
|
|
||||||
margin-bottom: 20px;
|
|
||||||
text-shadow: 0 0 4px rgba(34,197,94,0.4); /* subtle glow */
|
|
||||||
}
|
|
||||||
|
|
||||||
.ticket-info {
|
|
||||||
margin-bottom: 25px;
|
|
||||||
padding: 15px;
|
|
||||||
border-radius: 12px;
|
|
||||||
background: rgba(255, 255, 255, 0.05);
|
|
||||||
border: 1px solid rgba(255,255,255,0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ticket-info p {
|
|
||||||
margin: 6px 0;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message {
|
|
||||||
background: rgba(255, 255, 255, 0.05);
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
||||||
border-radius: 15px;
|
|
||||||
padding: 16px;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
transition: all 0.2s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message:hover {
|
|
||||||
background: rgba(255, 255, 255, 0.08);
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 6px 20px rgba(0,0,0,0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar {
|
|
||||||
width: 45px;
|
|
||||||
height: 45px;
|
|
||||||
border-radius: 50%;
|
|
||||||
margin-right: 12px;
|
|
||||||
border: 2px solid rgba(34,197,94,0.6);
|
|
||||||
}
|
|
||||||
|
|
||||||
.username {
|
|
||||||
font-weight: 600;
|
|
||||||
color: #22c55e;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timestamp {
|
|
||||||
margin-left: auto;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
color: #9ca3af;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
word-break: break-word;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
line-height: 1.4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.embed {
|
|
||||||
margin-top: 12px;
|
|
||||||
border-left: 4px solid #22c55e;
|
|
||||||
padding-left: 12px;
|
|
||||||
border-radius: 6px;
|
|
||||||
background: rgba(34,197,94,0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.embed strong {
|
|
||||||
color: #f9fafb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.attachment {
|
|
||||||
display: inline-block;
|
|
||||||
background: rgba(34,197,94,0.1);
|
|
||||||
border: 1px solid #22c55e;
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 6px 12px;
|
|
||||||
margin: 6px 0;
|
|
||||||
color: #22c55e;
|
|
||||||
text-decoration: none;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.attachment:hover {
|
|
||||||
background: rgba(34,197,94,0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
code, pre {
|
|
||||||
background: rgba(0,0,0,0.4);
|
|
||||||
color: #f8fafc;
|
|
||||||
padding: 4px 8px;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-family: 'Consolas', 'Monaco', monospace;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<h1>Ticket Transcript</h1>
|
|
||||||
<div class="ticket-info">
|
|
||||||
<p><strong>Channel:</strong> ${ticket.name}</p>
|
|
||||||
<p><strong>Ticket ID:</strong> ${ticket.id}</p>
|
|
||||||
<p><strong>Message Count:</strong> ${ticket.messages.length}</p>
|
|
||||||
<p><strong>Close Reason:</strong> ${reason}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="messages">
|
|
||||||
${ticket.messages.map(msg => `
|
|
||||||
<div class="message">
|
|
||||||
<div class="message-header">
|
|
||||||
<img src="${msg.avatar}" alt="Avatar" class="avatar">
|
|
||||||
<span class="username">${msg.username}</span>
|
|
||||||
<span class="timestamp">${new Date(msg.timestamp).toLocaleString()}</span>
|
|
||||||
</div>
|
|
||||||
<div class="content">
|
|
||||||
${parseMarkdown(msg.content)}
|
|
||||||
|
|
||||||
${msg.embeds.map(embed => `
|
|
||||||
<div class="embed">
|
|
||||||
${embed.title ? `<div class="font-bold">${embed.title}</div>` : ''}
|
|
||||||
${embed.description ? `<div>${embed.description}</div>` : ''}
|
|
||||||
${embed.fields.map(field => `
|
|
||||||
<div class="mt-2">
|
|
||||||
<strong>${field.name}:</strong>
|
|
||||||
<div>${field.value}</div>
|
|
||||||
</div>
|
|
||||||
`).join('')}
|
|
||||||
</div>
|
|
||||||
`).join('')}
|
|
||||||
|
|
||||||
${msg.attachments.map(att => `
|
|
||||||
<a href="${att.url}" class="attachment" target="_blank">📎 ${att.name}</a>
|
|
||||||
`).join('')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`).join('')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
const fs = require("fs");
|
|
||||||
const {
|
|
||||||
EmbedBuilder,
|
|
||||||
ApplicationCommandOptionType,
|
|
||||||
ApplicationCommandType,
|
|
||||||
ChannelType,
|
|
||||||
MessageFlags,
|
|
||||||
} = require("discord.js");
|
|
||||||
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 Command = require("../../Structures/Command.js");
|
|
||||||
const db = require("../../Structures/Database.js");
|
|
||||||
|
|
||||||
module.exports = new Command({
|
|
||||||
name: cmdconfig.ForceAddUser.Command,
|
|
||||||
description: cmdconfig.ForceAddUser.Description,
|
|
||||||
type: ApplicationCommandType.ChatInput,
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
name: "user",
|
|
||||||
description: "The user to forcefully add",
|
|
||||||
type: ApplicationCommandOptionType.User,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
permissions: cmdconfig.ForceAddUser.Permission,
|
|
||||||
|
|
||||||
async run(interaction) {
|
|
||||||
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 if (supportbot.Ticket.TicketType === "channels") {
|
|
||||||
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,
|
|
||||||
});
|
|
||||||
|
|
||||||
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 (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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
// SupportBot | Emerald Services
|
|
||||||
// Remove user from a ticket
|
|
||||||
|
|
||||||
const {
|
|
||||||
EmbedBuilder,
|
|
||||||
ApplicationCommandOptionType,
|
|
||||||
ApplicationCommandType,
|
|
||||||
ChannelType,
|
|
||||||
MessageFlags,
|
|
||||||
} = require("discord.js");
|
|
||||||
const yaml = require("js-yaml");
|
|
||||||
const fs = require("fs");
|
|
||||||
|
|
||||||
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 Command = require("../../Structures/Command.js");
|
|
||||||
const db = require("../../Structures/Database.js");
|
|
||||||
|
|
||||||
module.exports = new Command({
|
|
||||||
name: cmdconfig.RemoveUser.Command,
|
|
||||||
description: cmdconfig.RemoveUser.Description,
|
|
||||||
type: ApplicationCommandType.ChatInput,
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
name: "user",
|
|
||||||
description: "The user to remove",
|
|
||||||
type: ApplicationCommandOptionType.User,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
permissions: cmdconfig.RemoveUser.Permission,
|
|
||||||
|
|
||||||
async run(interaction) {
|
|
||||||
const { getRole } = interaction.client;
|
|
||||||
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("Missing staff/admin roles in config.");
|
|
||||||
}
|
|
||||||
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(supportbot.Embed.Colours.Warn);
|
|
||||||
return interaction.reply({ embeds: [NoPerms], flags: MessageFlags.Ephemeral });
|
|
||||||
}
|
|
||||||
|
|
||||||
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 });
|
|
||||||
|
|
||||||
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 });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
+657
-182
File diff suppressed because it is too large.
Load diff
+194
-80
@@ -1,16 +1,19 @@
|
|||||||
|
// SupportBot | Emerald Services
|
||||||
|
// Ticket Panel Command
|
||||||
|
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const {
|
const {
|
||||||
EmbedBuilder,
|
|
||||||
ApplicationCommandType,
|
ApplicationCommandType,
|
||||||
ActionRowBuilder,
|
ActionRowBuilder,
|
||||||
ButtonBuilder,
|
ButtonBuilder,
|
||||||
|
ButtonStyle,
|
||||||
MessageFlags,
|
MessageFlags,
|
||||||
ContainerBuilder,
|
ContainerBuilder,
|
||||||
TextDisplayBuilder,
|
TextDisplayBuilder,
|
||||||
MediaGalleryBuilder,
|
MediaGalleryBuilder,
|
||||||
SeparatorBuilder,
|
SeparatorBuilder,
|
||||||
ThumbnailBuilder,
|
SectionBuilder,
|
||||||
SectionBuilder
|
StringSelectMenuBuilder,
|
||||||
} = require("discord.js");
|
} = require("discord.js");
|
||||||
const yaml = require("js-yaml");
|
const yaml = require("js-yaml");
|
||||||
|
|
||||||
@@ -21,6 +24,25 @@ const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8"));
|
|||||||
const Command = require("../../Structures/Command.js");
|
const Command = require("../../Structures/Command.js");
|
||||||
const db = require("../../Structures/Database.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({
|
module.exports = new Command({
|
||||||
name: cmdconfig.TicketPanel.Command,
|
name: cmdconfig.TicketPanel.Command,
|
||||||
description: cmdconfig.TicketPanel.Description,
|
description: cmdconfig.TicketPanel.Description,
|
||||||
@@ -34,26 +56,33 @@ module.exports = new Command({
|
|||||||
"\u001b[31m",
|
"\u001b[31m",
|
||||||
`[TICKET PANEL]`,
|
`[TICKET PANEL]`,
|
||||||
"\u001b[33m",
|
"\u001b[33m",
|
||||||
"Ticket Panel is not setup, You can set this in", `\u001b[31m`, '/Configs/ticket-panel.yml', `\n `,
|
"Ticket Panel is not setup, You can set this in",
|
||||||
|
`\u001b[31m`,
|
||||||
|
"/Configs/ticket-panel.yml",
|
||||||
|
`\n `,
|
||||||
);
|
);
|
||||||
|
|
||||||
return interaction.reply({
|
return interaction.reply({
|
||||||
content: "Ticket Panel is not set up. Please configure it in `/Configs/ticket-panel.yml`.",
|
content: getPanelMessage(
|
||||||
flags: MessageFlags.Ephemeral
|
"NotConfigured",
|
||||||
|
"Ticket Panel is not set up. Please configure it in `/Configs/ticket-panel.yml`.",
|
||||||
|
),
|
||||||
|
flags: MessageFlags.Ephemeral,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const { getChannel } = interaction.client;
|
const { getChannel } = interaction.client;
|
||||||
|
|
||||||
const channel = await getChannel(
|
const channel = await getChannel(supportbot.Ticket.TicketHome, interaction.guild);
|
||||||
supportbot.Ticket.TicketHome,
|
|
||||||
interaction.guild
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!channel) {
|
if (!channel) {
|
||||||
console.log(`[TICKET PANEL] ${channelName} channel not found. Please check your config file.`);
|
console.log(`[TICKET PANEL] TicketHome channel not found. Please check your config file.`);
|
||||||
return interaction.reply({
|
return interaction.reply({
|
||||||
content: `${channelName} channel not found. Please check your config file.`,
|
content: getPanelMessage(
|
||||||
flags: MessageFlags.Ephemeral
|
"InvalidChannel",
|
||||||
|
"TicketHome channel not found. Please check your config file.",
|
||||||
|
),
|
||||||
|
flags: MessageFlags.Ephemeral,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,101 +90,178 @@ module.exports = new Command({
|
|||||||
|
|
||||||
if (panelRow) {
|
if (panelRow) {
|
||||||
try {
|
try {
|
||||||
await channel.messages.fetch(panelRow.message_id);
|
const oldMessage = await channel.messages.fetch(panelRow.message_id);
|
||||||
return interaction.reply({
|
if (oldMessage) {
|
||||||
content: "Ticket panel message already exists.",
|
await oldMessage.delete().catch(() => {});
|
||||||
flags: MessageFlags.Ephemeral
|
}
|
||||||
});
|
} catch (err) {
|
||||||
} catch (err) {}
|
console.log(`[TICKET PANEL] Previous panel message not found, creating a new one.`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const panelContainer = new ContainerBuilder()
|
const departmentSystem = supportbot.Ticket?.DepartmentSystem || {};
|
||||||
|
const prioritySystem = supportbot.Ticket?.PrioritySystem || {};
|
||||||
|
const usingThreads = supportbot.Ticket?.TicketType === "threads";
|
||||||
|
|
||||||
if (panelconfig.Style.Color) {
|
const departmentsEnabled = departmentSystem.Enabled === true && !usingThreads;
|
||||||
panelContainer.setAccentColor(
|
const prioritiesEnabled = prioritySystem.Enabled === true && !usingThreads;
|
||||||
parseInt(panelconfig.Style.Color.replace("#", ""), 16)
|
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(
|
const panelTitle = new TextDisplayBuilder().setContent(
|
||||||
panelconfig.Style.Title,
|
panelconfig.Style?.Title || "## SupportBot Ticket Creation",
|
||||||
);
|
);
|
||||||
|
|
||||||
const topSeperator = new SeparatorBuilder()
|
|
||||||
.setDivider(true)
|
|
||||||
|
|
||||||
if (panelconfig.Settings.TopTitle) {
|
|
||||||
panelContainer.addTextDisplayComponents(panelTitle)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (panelconfig.Settings.TopDivider) {
|
|
||||||
panelContainer.addSeparatorComponents(topSeperator)
|
|
||||||
}
|
|
||||||
|
|
||||||
const panelDesc = new TextDisplayBuilder().setContent(
|
const panelDesc = new TextDisplayBuilder().setContent(
|
||||||
panelconfig.Style.Description,
|
panelconfig.Style?.Description || "Click on the button below to create a support ticket.",
|
||||||
);
|
);
|
||||||
|
|
||||||
if (panelconfig.Style.Layout === "3") {
|
if (showTopTitle) {
|
||||||
panelContainer.addTextDisplayComponents(panelDesc)
|
panelContainer.addTextDisplayComponents(panelTitle);
|
||||||
}
|
}
|
||||||
|
|
||||||
const createTicketButton = new ButtonBuilder()
|
if (showTopDivider) {
|
||||||
.setCustomId("createticket")
|
panelContainer.addSeparatorComponents(new SeparatorBuilder().setDivider(true));
|
||||||
.setLabel(panelconfig.Button.Text)
|
|
||||||
.setEmoji(panelconfig.Button.Emoji)
|
|
||||||
.setStyle(panelconfig.Button.Color);
|
|
||||||
|
|
||||||
const middleSection = new SectionBuilder()
|
|
||||||
.addTextDisplayComponents(panelDesc)
|
|
||||||
.setButtonAccessory(createTicketButton)
|
|
||||||
|
|
||||||
if (panelconfig.Style.Layout === "1") {
|
|
||||||
panelContainer.addSectionComponents(middleSection)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const panelImage = new MediaGalleryBuilder()
|
if (layout === "1" || layout === "3") {
|
||||||
.addItems([
|
panelContainer.addTextDisplayComponents(panelDesc);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showImage && layout === "2") {
|
||||||
|
const panelImage = new MediaGalleryBuilder().addItems([
|
||||||
{
|
{
|
||||||
media: {
|
media: {
|
||||||
url: panelconfig.Style.Image,
|
url: panelconfig.Style.Image,
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
])
|
]);
|
||||||
|
|
||||||
if (panelconfig.Settings.Image) {
|
panelContainer.addMediaGalleryComponents(panelImage);
|
||||||
panelContainer.addMediaGalleryComponents(panelImage)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// START OF STYLE 2 - TEXT UNDER THE IMAGE
|
if (layout === "1") {
|
||||||
|
const middleSection = new SectionBuilder().addTextDisplayComponents(panelDesc);
|
||||||
|
|
||||||
const bottomSeperator = new SeparatorBuilder()
|
if (!shouldUseDepartments) {
|
||||||
.setDivider(true)
|
middleSection.setButtonAccessory(openerComponent);
|
||||||
|
panelContainer.addSectionComponents(middleSection);
|
||||||
if (panelconfig.Settings.BottomTitle) {
|
} else {
|
||||||
panelContainer.addTextDisplayComponents(panelTitle)
|
panelContainer.addSectionComponents(middleSection);
|
||||||
|
panelContainer.addActionRowComponents(
|
||||||
|
new ActionRowBuilder().addComponents(openerComponent),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (panelconfig.Settings.BottomDivider) {
|
if (showImage && layout === "3") {
|
||||||
panelContainer.addSeparatorComponents(bottomSeperator)
|
const panelImage = new MediaGalleryBuilder().addItems([
|
||||||
|
{
|
||||||
|
media: {
|
||||||
|
url: panelconfig.Style.Image,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
panelContainer.addMediaGalleryComponents(panelImage);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (panelconfig.Style.Layout === "2") {
|
if (showBottomTitle) {
|
||||||
panelContainer.addSectionComponents(middleSection)
|
panelContainer.addTextDisplayComponents(panelTitle);
|
||||||
}
|
}
|
||||||
|
|
||||||
// START OF STYLE 3
|
if (showBottomDivider) {
|
||||||
|
panelContainer.addSeparatorComponents(new SeparatorBuilder().setDivider(true));
|
||||||
|
}
|
||||||
|
|
||||||
const buttonRow = new ActionRowBuilder();
|
if (layout === "2") {
|
||||||
const ticketButtonRow = new ButtonBuilder()
|
const middleSection = new SectionBuilder().addTextDisplayComponents(panelDesc);
|
||||||
.setCustomId("createticket")
|
|
||||||
.setLabel(panelconfig.Button.Text)
|
|
||||||
.setEmoji(panelconfig.Button.Emoji)
|
|
||||||
.setStyle(panelconfig.Button.Color);
|
|
||||||
buttonRow.addComponents(ticketButtonRow)
|
|
||||||
|
|
||||||
if (panelconfig.Style.Layout === "3") {
|
if (!shouldUseDepartments) {
|
||||||
panelContainer.addActionRowComponents(buttonRow)
|
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 {
|
try {
|
||||||
@@ -167,14 +273,22 @@ module.exports = new Command({
|
|||||||
db.saveTicketPanel(message.id, channel.id);
|
db.saveTicketPanel(message.id, channel.id);
|
||||||
|
|
||||||
return interaction.reply({
|
return interaction.reply({
|
||||||
content: "Ticket panel message has been sent!",
|
content: shouldUseDepartments
|
||||||
flags: MessageFlags.Ephemeral
|
? getPanelMessage(
|
||||||
|
"PanelSentWithDepartments",
|
||||||
|
"Ticket panel message with department selection has been sent!",
|
||||||
|
)
|
||||||
|
: getPanelMessage("PanelSent", "Ticket panel message has been sent!"),
|
||||||
|
flags: MessageFlags.Ephemeral,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("Error sending message:", e);
|
console.log("Error sending message:", e);
|
||||||
return interaction.reply({
|
return interaction.reply({
|
||||||
content: "An error occurred while sending the ticket panel message.",
|
content: getPanelMessage(
|
||||||
flags: MessageFlags.Ephemeral
|
"SendError",
|
||||||
|
"An error occurred while sending the ticket panel message.",
|
||||||
|
),
|
||||||
|
flags: MessageFlags.Ephemeral,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
+28
-13
@@ -49,24 +49,34 @@ AddUser:
|
|||||||
Permission: ["SendMessages"]
|
Permission: ["SendMessages"]
|
||||||
Enabled: true
|
Enabled: true
|
||||||
|
|
||||||
ForceAddUser:
|
|
||||||
Command: "forceadd"
|
|
||||||
Description: "Add a user to a support ticket by bypassing the invite system."
|
|
||||||
Permission: ["SendMessages"]
|
|
||||||
Enabled: true
|
|
||||||
|
|
||||||
RemoveUser:
|
|
||||||
Command: "remove"
|
|
||||||
Description: "Remove a user from a support ticket."
|
|
||||||
Permission: ["SendMessages"]
|
|
||||||
Enabled: true
|
|
||||||
|
|
||||||
CloseTicket:
|
CloseTicket:
|
||||||
Command: "close"
|
Command: "close"
|
||||||
Description: "Close a support ticket"
|
Description: "Close a support ticket"
|
||||||
Permission: ["SendMessages"]
|
Permission: ["SendMessages"]
|
||||||
Enabled: true
|
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:
|
Suggestion:
|
||||||
Command: "suggest"
|
Command: "suggest"
|
||||||
Description: "Create a suggestion."
|
Description: "Create a suggestion."
|
||||||
@@ -89,7 +99,7 @@ Translate:
|
|||||||
Command: "translate"
|
Command: "translate"
|
||||||
Description: "Translate something to english"
|
Description: "Translate something to english"
|
||||||
Permission: ["SendMessages"]
|
Permission: ["SendMessages"]
|
||||||
Enabled: false
|
Enabled: true
|
||||||
|
|
||||||
UserInfo:
|
UserInfo:
|
||||||
Command: "userinfo"
|
Command: "userinfo"
|
||||||
@@ -155,3 +165,8 @@ HelpCommandDesc: "Get a list of all the commands."
|
|||||||
EmbedCommandDesc: "Send a message as an embed."
|
EmbedCommandDesc: "Send a message as an embed."
|
||||||
SuggestCommandDesc: "Suggest a feature for the server."
|
SuggestCommandDesc: "Suggest a feature for the server."
|
||||||
UserInfoCommandDesc: "Get info about a user."
|
UserInfoCommandDesc: "Get info about a user."
|
||||||
|
TicketManageDesc: "Manage an existing support ticket."
|
||||||
|
TicketManageCloseDesc: "Close a support ticket."
|
||||||
|
TicketManageForceAddDesc: "Add a user to a support ticket by bypassing the invite system."
|
||||||
|
TicketManageForceRemoveDesc: "Remove a user from a support ticket by bypassing the normal restrictions."
|
||||||
|
TicketManageForceRenameDesc: "Rename a support ticket by bypassing the normal restrictions."
|
||||||
+16
-11
@@ -173,6 +173,22 @@ Ticket:
|
|||||||
Title: "Ticket Details"
|
Title: "Ticket Details"
|
||||||
Description: "Questions by <@%user%>" # Use <@%user%> to display the username.
|
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
|
# Review Messages
|
||||||
# ----------------------------
|
# ----------------------------
|
||||||
@@ -202,17 +218,6 @@ ReviewSystem:
|
|||||||
ReviewEmoji: "⭐"
|
ReviewEmoji: "⭐"
|
||||||
Color: "#f7a943"
|
Color: "#f7a943"
|
||||||
|
|
||||||
# ----------------------------
|
|
||||||
# Profile Messages
|
|
||||||
# ----------------------------
|
|
||||||
|
|
||||||
TicketStats:
|
|
||||||
OpenTickets: "✅ Open Tickets"
|
|
||||||
TotalTickets: "🎫 Total Tickets"
|
|
||||||
ResponseTime: "⏱️ Average Response Time"
|
|
||||||
|
|
||||||
Title: "Your Ticket Stats"
|
|
||||||
|
|
||||||
# ----------------------------
|
# ----------------------------
|
||||||
# Mod Command Messages
|
# Mod Command Messages
|
||||||
# ----------------------------
|
# ----------------------------
|
||||||
|
|||||||
+57
-21
@@ -1,39 +1,75 @@
|
|||||||
# -------------------------
|
# -------------------------
|
||||||
# SupportBot AI
|
# SupportBot AI
|
||||||
#
|
#
|
||||||
# In order to use the SupportBot AI, You need to setup an OpenAI Account.
|
# 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/
|
# this can be done at https://platform.openai.com/ or https://groq.com/
|
||||||
# -------------------------
|
# -------------------------
|
||||||
|
|
||||||
Enabled: true
|
Enabled: true
|
||||||
|
|
||||||
General:
|
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"
|
Name: "SupportBot AI"
|
||||||
# What would you like to call your AI
|
|
||||||
Model: "gpt-4o-mini"
|
|
||||||
# You can see a list of OpenAI Models here: https://platform.openai.com/docs/models
|
|
||||||
|
|
||||||
Tokens: 400
|
|
||||||
# Max tokens you can set is 400
|
|
||||||
# For more information about tokens, go to https://openai.com/api/pricing/
|
|
||||||
OpenAI_Key: "OPEN_AI_API_KEY"
|
|
||||||
# To create an OpenAI API key, go to https://platform.openai.com/api-keys
|
|
||||||
|
|
||||||
|
|
||||||
PastebinAPI_Key: "PASTEBIN_API_KEY"
|
PastebinAPI_Key: "PASTEBIN_API_KEY"
|
||||||
# You can get your Pastebin API Key from https://pastebin.com
|
|
||||||
PastebinAPI_URL: "https://pastebin.com/api/api_post.php"
|
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:
|
Channels:
|
||||||
AIChannel: "AI_BOT_CHANNEL"
|
AIChannel: "CHANNEL_ID"
|
||||||
# This is the channel where AI will respond in without needing to mention the bot.
|
|
||||||
|
|
||||||
Embed:
|
Embed:
|
||||||
Color: 0x0cc559 # 0x<hexcode>
|
Color: "#45b463"
|
||||||
Footer: "SupportBot | Developed by Emerald Development"
|
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:
|
Messages:
|
||||||
MessageTooLong: "The response was too long, so I've posted it here: {pastebin}"
|
ErrorResponse: "I couldn't generate a response."
|
||||||
ErrorResponse: "I didn't receive a proper response from OpenAI. Please try again later."
|
AIError: "There was an error contacting the AI provider."
|
||||||
OpenAIError: "I'm having some trouble talking to SupportBot AI's Model, OpenAI. Please try again later."
|
AIDisabled: "AI is currently disabled."
|
||||||
AIDisabled: "I am disabled, You have enable me in `supportbot-ai.yml`"
|
MessageTooLong: "My response was too long, so I uploaded it here: {pastebin}"
|
||||||
|
SuggestTicketText: "-# Need more help? React with 🎫 to open a ticket."
|
||||||
|
SuggestTicketReaction : "🎫"
|
||||||
+116
-46
@@ -8,12 +8,11 @@
|
|||||||
# Installed with MIT License
|
# Installed with MIT License
|
||||||
#
|
#
|
||||||
# Discord Support: https://discord.gg/emeraldsrv
|
# Discord Support: https://discord.gg/emeraldsrv
|
||||||
# Official Documentation: https://docs.emeraldsrv.dev
|
# Website: https://emeraldsrv.dev
|
||||||
# Addons Marketplace: https://community.emeraldsrv.dev
|
|
||||||
#
|
#
|
||||||
# SupportBot Configuration (supportbot.yml)
|
# SupportBot Configuration (supportbot.yml)
|
||||||
|
|
||||||
SupportBot_Version: "v8.4"
|
SupportBot_Version: "v26.0.0"
|
||||||
|
|
||||||
# -------------------------
|
# -------------------------
|
||||||
# General
|
# General
|
||||||
@@ -66,12 +65,12 @@ Embed:
|
|||||||
Roles:
|
Roles:
|
||||||
AutoRole:
|
AutoRole:
|
||||||
Enabled: true
|
Enabled: true
|
||||||
Role: "1111111111111111"
|
Role: "ROLE_ID"
|
||||||
|
|
||||||
StaffMember:
|
StaffMember:
|
||||||
Staff: "1111111111111111"
|
Staff: "ROLE_ID"
|
||||||
Admin: "1111111111111111"
|
Admin: "ROLE_ID"
|
||||||
Moderator: "1111111111111111"
|
Moderator: "ROLE_ID"
|
||||||
|
|
||||||
Mod:
|
Mod:
|
||||||
AllowSupportStaff: false
|
AllowSupportStaff: false
|
||||||
@@ -84,11 +83,11 @@ Roles:
|
|||||||
|
|
||||||
Welcome:
|
Welcome:
|
||||||
Enabled: true
|
Enabled: true
|
||||||
Channel: "1111111111111111"
|
Channel: "CHANNEL_ID"
|
||||||
|
|
||||||
Leave:
|
Leave:
|
||||||
Enabled: true
|
Enabled: true
|
||||||
Channel: "1111111111111111"
|
Channel: "CHANNEL_ID"
|
||||||
|
|
||||||
# ----------------------------
|
# ----------------------------
|
||||||
# Suggestions
|
# Suggestions
|
||||||
@@ -96,8 +95,8 @@ Leave:
|
|||||||
|
|
||||||
Suggestions:
|
Suggestions:
|
||||||
Mode: "Channel" # Channel or Forum
|
Mode: "Channel" # Channel or Forum
|
||||||
ForumChannel: "1111111111111111"
|
ForumChannel: "FORUM_CHANNEL_ID" # A forum channel, disabled if mode is set to Channel.
|
||||||
Channel: "1111111111111111"
|
Channel: "CHANNEL_ID" # A text channel, disabled if mode is set to Forum.
|
||||||
|
|
||||||
UpvoteEmoji: "✅"
|
UpvoteEmoji: "✅"
|
||||||
DownvoteEmoji: "❌"
|
DownvoteEmoji: "❌"
|
||||||
@@ -130,10 +129,10 @@ Suggestions:
|
|||||||
# 4 - Red
|
# 4 - Red
|
||||||
|
|
||||||
Tags:
|
Tags:
|
||||||
Created: "1111111111111111"
|
Created: "TAG_ID"
|
||||||
Considering: "1111111111111111"
|
Considering: "TAG_ID"
|
||||||
Solved: "1111111111111111"
|
Solved: "TAG_ID"
|
||||||
Denied: "1111111111111111"
|
Denied: "TAG_ID"
|
||||||
|
|
||||||
# -------------------------
|
# -------------------------
|
||||||
# Tickets
|
# Tickets
|
||||||
@@ -141,30 +140,34 @@ Suggestions:
|
|||||||
|
|
||||||
VoiceTickets:
|
VoiceTickets:
|
||||||
Name: "%username%'s VC"
|
Name: "%username%'s VC"
|
||||||
Category: "1111111111111111"
|
Category: "CATEGORY_ID"
|
||||||
|
|
||||||
Ticket:
|
Ticket:
|
||||||
TicketType: "threads"
|
TicketType: "channels"
|
||||||
|
|
||||||
# Available TicketTypes:
|
# Available TicketTypes:
|
||||||
# channels - Tickets will be created as channels in the TicketChannelsCategory
|
# channels - Tickets will be created as channels in the TicketChannelsCategory
|
||||||
# threads - Tickets will be created as threads through the "TicketHome" channel.
|
# threads - Tickets will be created as threads through the "TicketHome" channel.
|
||||||
|
|
||||||
TicketHome: "1111111111111111"
|
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?
|
# What is TicketHome?
|
||||||
# - Channel where the Ticket creation panel is sent.
|
# - Channel where the Ticket creation panel is sent.
|
||||||
# - Channel where ticket threads are made. (only if ticket threads is enabled above.)
|
# - Channel where ticket threads are made. (only if ticket threads is enabled above.)
|
||||||
|
|
||||||
# For TicketType: channels
|
# For TicketType: channels
|
||||||
TicketChannelsCategory: "1111111111111111"
|
TicketChannelsCategory: "CATEGORY_ID"
|
||||||
TicketChannelsCategory2: "1111111111111111" # This will be used if the first category has reached the max channel limit.
|
TicketChannelsCategory2: "CATEGORY_ID" # This will be used if the first category has reached the max channel limit.
|
||||||
TicketArchiveCategory: "1111111111111111"
|
TicketArchiveCategory: "CATEGORY_ID"
|
||||||
# ---
|
# ---
|
||||||
|
|
||||||
Channel: "ticket-" # The prefix for ticket channel names
|
Channel: "ticket-" # The prefix for ticket channel names
|
||||||
TicketsPerUser: 100
|
TicketsPerUser: 100
|
||||||
DMTranscripts: true
|
|
||||||
TicketReason: true
|
TicketReason: true
|
||||||
# If ticket reason is set to false, It will not prompt to create a reason
|
# If ticket reason is set to false, It will not prompt to create a reason
|
||||||
|
|
||||||
@@ -181,24 +184,12 @@ Ticket:
|
|||||||
StaffOnly: true
|
StaffOnly: true
|
||||||
# Set to false is anyone is able to close a ticket.
|
# Set to false is anyone is able to close a ticket.
|
||||||
|
|
||||||
Confirmation_Button: "Confirm Close"
|
Invites: # Invites is for ticket threads.
|
||||||
Confirmation_Emoji: "🗑️"
|
|
||||||
Confirmation_Style: "2"
|
|
||||||
|
|
||||||
Title: "Are you sure you want to close this ticket?"
|
|
||||||
|
|
||||||
# Available Button Styles:
|
|
||||||
# 1 - Blurple
|
|
||||||
# 2 - Grey
|
|
||||||
# 3 - Green
|
|
||||||
# 4 - Red
|
|
||||||
|
|
||||||
Invites:
|
|
||||||
StaffOnly: true
|
StaffOnly: true
|
||||||
|
|
||||||
ClaimTickets:
|
ClaimTickets:
|
||||||
Enabled: true
|
Enabled: false
|
||||||
Channel: "1111111111111111"
|
Channel: "CHANNEL_ID"
|
||||||
# Channel where the ticket assignments are sent for staff to claim.
|
# Channel where the ticket assignments are sent for staff to claim.
|
||||||
|
|
||||||
ClaimAcceptTime: 120000
|
ClaimAcceptTime: 120000
|
||||||
@@ -221,20 +212,100 @@ Ticket:
|
|||||||
|
|
||||||
ReviewSystem:
|
ReviewSystem:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
Channel: "1111111111111111"
|
Channel: "CHANNEL_ID"
|
||||||
UseModal: true
|
UseModal: true
|
||||||
|
|
||||||
Questions:
|
Questions: # Default questions asked when a ticket is created, these are used if departments are disabled.
|
||||||
Enabled: true # Set to false to disable ticket questions
|
Enabled: false
|
||||||
List:
|
List:
|
||||||
- "Please provide your account email?"
|
- "Please provide your account email?"
|
||||||
- "What is the nature of your issue?"
|
- "What is the nature of your issue?"
|
||||||
- "Have you tried resolving the issue yourself?"
|
- "Have you tried resolving the issue yourself?"
|
||||||
|
|
||||||
Log:
|
Log:
|
||||||
TicketDataLog: "1111111111111111"
|
TicketDataLog: "CHANNEL_ID"
|
||||||
TickeDataTitle: "Ticket Data"
|
TickeDataTitle: "Ticket Data"
|
||||||
TicketBlacklistLog: "1111111111111111"
|
TicketBlacklistLog: "CHANNEL_ID"
|
||||||
|
|
||||||
|
AIMode:
|
||||||
|
Enabled: true
|
||||||
|
ThreadName: "Chat with SupportBot AI"
|
||||||
|
WelcomeMessage: "Hi! I’m SupportBot AI. You can chat with me here about your issue."
|
||||||
|
|
||||||
|
# -------------------------
|
||||||
|
# Ticket Departments
|
||||||
|
# -------------------------
|
||||||
|
|
||||||
|
DepartmentSystem:
|
||||||
|
Enabled: false
|
||||||
|
Placeholder: "Choose a ticket department"
|
||||||
|
DefaultDepartment: "general"
|
||||||
|
|
||||||
|
Departments:
|
||||||
|
general:
|
||||||
|
Name: "General Support"
|
||||||
|
Emoji: "🎫"
|
||||||
|
Category: "CATEGORY_ID"
|
||||||
|
Role: "ROLE_ID"
|
||||||
|
ChannelPrefix: "support-"
|
||||||
|
DefaultPriority: "normal"
|
||||||
|
Questions:
|
||||||
|
- "What do you need help with?"
|
||||||
|
- "When did the issue start?"
|
||||||
|
|
||||||
|
purchase:
|
||||||
|
Name: "Purchase Support"
|
||||||
|
Emoji: "💳"
|
||||||
|
Category: "CATEGORY_ID"
|
||||||
|
Role: "ROLE_ID"
|
||||||
|
ChannelPrefix: "purchase-"
|
||||||
|
DefaultPriority: "high"
|
||||||
|
Questions:
|
||||||
|
- "What is your order ID?"
|
||||||
|
- "What product did you purchase?"
|
||||||
|
- "What payment issue are you having?"
|
||||||
|
|
||||||
|
reports:
|
||||||
|
Name: "Player Report"
|
||||||
|
Emoji: "🚨"
|
||||||
|
Category: "CATEGORY_ID"
|
||||||
|
Role: "ROLE_ID"
|
||||||
|
ChannelPrefix: "report-"
|
||||||
|
DefaultPriority: "normal"
|
||||||
|
Questions:
|
||||||
|
- "Who are you reporting?"
|
||||||
|
- "What happened?"
|
||||||
|
- "Do you have evidence?"
|
||||||
|
|
||||||
|
appeals:
|
||||||
|
Name: "Ban Appeals"
|
||||||
|
Emoji: "⚖️"
|
||||||
|
Category: "CATEGORY_ID"
|
||||||
|
Role: "ROLE_ID"
|
||||||
|
ChannelPrefix: "appeals-"
|
||||||
|
DefaultPriority: "low"
|
||||||
|
Questions:
|
||||||
|
- "What punishment are you appealing?"
|
||||||
|
- "Why should it be reviewed?"
|
||||||
|
- "Anything else staff should know?"
|
||||||
|
|
||||||
|
PrioritySystem:
|
||||||
|
Enabled: false
|
||||||
|
DefaultPriority: "Normal"
|
||||||
|
AllowUsersToChooseOnOpen: true
|
||||||
|
Priorities:
|
||||||
|
low:
|
||||||
|
Name: "Low"
|
||||||
|
Emoji: "🟢"
|
||||||
|
normal:
|
||||||
|
Name: "Normal"
|
||||||
|
Emoji: "🟡"
|
||||||
|
high:
|
||||||
|
Name: "High"
|
||||||
|
Emoji: "🟠"
|
||||||
|
urgent:
|
||||||
|
Name: "Urgent"
|
||||||
|
Emoji: "🔴"
|
||||||
|
|
||||||
# -------------------------
|
# -------------------------
|
||||||
# Buttons
|
# Buttons
|
||||||
@@ -258,7 +329,6 @@ Buttons:
|
|||||||
Unlock: "Unlock Ticket"
|
Unlock: "Unlock Ticket"
|
||||||
Unlock_Emoji: "🔓"
|
Unlock_Emoji: "🔓"
|
||||||
Unlock_Style: "2"
|
Unlock_Style: "2"
|
||||||
|
|
||||||
# Available Button Styles
|
# Available Button Styles
|
||||||
# 1 - Blurple
|
# 1 - Blurple
|
||||||
# 2 - Grey
|
# 2 - Grey
|
||||||
@@ -287,12 +357,12 @@ SelectMenus:
|
|||||||
# -------------------------
|
# -------------------------
|
||||||
|
|
||||||
MessageDelete:
|
MessageDelete:
|
||||||
Channel: "1111111111111111"
|
Channel: "CHANNEL_ID"
|
||||||
Colour: "#d92525"
|
Colour: "#d92525"
|
||||||
|
|
||||||
MessageUpdate:
|
MessageUpdate:
|
||||||
Channel: "1111111111111111"
|
Channel: "CHANNEL_ID"
|
||||||
Colour: "#f2a024"
|
Colour: "#f2a024"
|
||||||
|
|
||||||
Translate:
|
Translate:
|
||||||
TranslateLog: "1111111111111111"
|
TranslateLog: "CHANNEL_ID"
|
||||||
+14
-12
@@ -19,17 +19,11 @@
|
|||||||
|
|
||||||
Panel: true
|
Panel: true
|
||||||
|
|
||||||
# ----------------------------------------
|
|
||||||
# Ticket Panel Message Configuration #2b2d31
|
|
||||||
# ----------------------------------------
|
|
||||||
|
|
||||||
Style:
|
Style:
|
||||||
Title: "## SupportBot Ticket Creation"
|
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!"
|
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"
|
Image: "https://i.imgur.com/dejQvkg.png"
|
||||||
Color: "#45b463"
|
Color: "#45b463"
|
||||||
# Leave Color empty to remove the color from the embed/container.
|
|
||||||
|
|
||||||
Layout: "3"
|
Layout: "3"
|
||||||
|
|
||||||
# Available Layout Styles
|
# Available Layout Styles
|
||||||
@@ -40,7 +34,8 @@ Style:
|
|||||||
Button:
|
Button:
|
||||||
Text: "Create Ticket"
|
Text: "Create Ticket"
|
||||||
Emoji: "🎫"
|
Emoji: "🎫"
|
||||||
Color: "3"
|
Color: "2"
|
||||||
|
CustomId: "createticket"
|
||||||
|
|
||||||
# Available Button Styles
|
# Available Button Styles
|
||||||
# 1 - Blurple
|
# 1 - Blurple
|
||||||
@@ -48,15 +43,22 @@ Button:
|
|||||||
# 3 - Red
|
# 3 - Red
|
||||||
# 4 - Green
|
# 4 - Green
|
||||||
|
|
||||||
|
DepartmentMenu:
|
||||||
|
CustomId: "ticketdepartmentselect"
|
||||||
|
Placeholder: "Choose a ticket department"
|
||||||
|
DefaultDescription: "Open a support ticket"
|
||||||
|
DefaultEmoji: "🎫"
|
||||||
|
|
||||||
Settings:
|
Settings:
|
||||||
Image: true
|
Image: true
|
||||||
|
|
||||||
# Text and divider will appear above the image.
|
|
||||||
TopTitle: true
|
TopTitle: true
|
||||||
TopDivider: true
|
TopDivider: true
|
||||||
|
|
||||||
# Text and divider will appear below the image.
|
|
||||||
BottomTitle: false
|
BottomTitle: false
|
||||||
BottomDivider: false
|
BottomDivider: false
|
||||||
|
|
||||||
# Note: If Image is set to false, the bottom and top settings will look the samme.
|
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."
|
||||||
@@ -1 +1 @@
|
|||||||
{}
|
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
@@ -36,9 +36,10 @@ function getWelcomeThumbnail(client, member, iconCfg) {
|
|||||||
|
|
||||||
function buildModernComponents(client, member) {
|
function buildModernComponents(client, member) {
|
||||||
const container = new ContainerBuilder()
|
const container = new ContainerBuilder()
|
||||||
.setAccentColor(
|
|
||||||
parseInt(msgconfig.Welcome.Modern.Colour.replace("#", ""), 16)
|
if (msgconfig.Welcome.Modern.Colour) {
|
||||||
);
|
container .setAccentColor(parseInt(msgconfig.Welcome.Modern.Colour.replace("#", ""), 16));
|
||||||
|
}
|
||||||
|
|
||||||
const section = new SectionBuilder().addTextDisplayComponents(
|
const section = new SectionBuilder().addTextDisplayComponents(
|
||||||
new TextDisplayBuilder().setContent(
|
new TextDisplayBuilder().setContent(
|
||||||
|
|||||||
+404
-99
@@ -2,7 +2,9 @@ const fs = require("fs");
|
|||||||
const Discord = require("discord.js");
|
const Discord = require("discord.js");
|
||||||
const yaml = require("js-yaml");
|
const yaml = require("js-yaml");
|
||||||
|
|
||||||
const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8"));
|
const supportbot = yaml.load(
|
||||||
|
fs.readFileSync("./Configs/supportbot.yml", "utf8"),
|
||||||
|
);
|
||||||
const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8"));
|
const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8"));
|
||||||
const msgconfig = yaml.load(fs.readFileSync("./Configs/messages.yml", "utf8"));
|
const msgconfig = yaml.load(fs.readFileSync("./Configs/messages.yml", "utf8"));
|
||||||
|
|
||||||
@@ -10,10 +12,9 @@ const Event = require("../Structures/Event.js");
|
|||||||
const db = require("../Structures/Database.js");
|
const db = require("../Structures/Database.js");
|
||||||
|
|
||||||
module.exports = new Event("interactionCreate", async (client, interaction) => {
|
module.exports = new Event("interactionCreate", async (client, interaction) => {
|
||||||
|
|
||||||
if (interaction.type === Discord.InteractionType.ApplicationCommand) {
|
if (interaction.type === Discord.InteractionType.ApplicationCommand) {
|
||||||
const command = client.commands.find(
|
const command = client.commands.find(
|
||||||
(cmd) => cmd.name.toLowerCase() === interaction.commandName
|
(cmd) => cmd.name.toLowerCase() === interaction.commandName,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (interaction.user.bot || !interaction.guild) return;
|
if (interaction.user.bot || !interaction.guild) return;
|
||||||
@@ -32,7 +33,7 @@ module.exports = new Event("interactionCreate", async (client, interaction) => {
|
|||||||
|
|
||||||
const ValidPerms = new Discord.EmbedBuilder()
|
const ValidPerms = new Discord.EmbedBuilder()
|
||||||
.setDescription(
|
.setDescription(
|
||||||
"> :x: `Invalid Permissions` Do you have the correct permissions to execute this command?"
|
"> :x: `Invalid Permissions` Do you have the correct permissions to execute this command?",
|
||||||
)
|
)
|
||||||
.setColor(supportbot.Embed.Colours.Error);
|
.setColor(supportbot.Embed.Colours.Error);
|
||||||
|
|
||||||
@@ -43,7 +44,10 @@ module.exports = new Event("interactionCreate", async (client, interaction) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (command.name === cmdconfig.OpenTicket.Command && supportbot.Ticket.TicketReason) {
|
if (
|
||||||
|
command.name === cmdconfig.OpenTicket.Command &&
|
||||||
|
supportbot.Ticket.TicketReason
|
||||||
|
) {
|
||||||
const modal = new Discord.ModalBuilder()
|
const modal = new Discord.ModalBuilder()
|
||||||
.setCustomId("ticketReasonModal")
|
.setCustomId("ticketReasonModal")
|
||||||
.setTitle("Ticket Reason")
|
.setTitle("Ticket Reason")
|
||||||
@@ -53,13 +57,18 @@ module.exports = new Event("interactionCreate", async (client, interaction) => {
|
|||||||
.setCustomId("reasonInput")
|
.setCustomId("reasonInput")
|
||||||
.setLabel("Reason")
|
.setLabel("Reason")
|
||||||
.setStyle(Discord.TextInputStyle.Paragraph)
|
.setStyle(Discord.TextInputStyle.Paragraph)
|
||||||
.setRequired(true)
|
.setRequired(true),
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
await interaction.showModal(modal);
|
await interaction.showModal(modal);
|
||||||
} else {
|
} else {
|
||||||
console.log("\u001b[32m", "[Executed]", "\u001b[37;1m", `${interaction.user.username} has executed ${command.name}`);
|
console.log(
|
||||||
|
"\u001b[32m",
|
||||||
|
"[Executed]",
|
||||||
|
"\u001b[37;1m",
|
||||||
|
`${interaction.user.username} has executed ${command.name}`,
|
||||||
|
);
|
||||||
await command.run(interaction);
|
await command.run(interaction);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -72,16 +81,29 @@ module.exports = new Event("interactionCreate", async (client, interaction) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (interaction.isModalSubmit()) {
|
if (interaction.isModalSubmit()) {
|
||||||
if (interaction.customId === "ticketReasonModal") {
|
if (interaction.customId.startsWith("ticketReasonModal")) {
|
||||||
const reason = interaction.fields.getTextInputValue("reasonInput");
|
const reason = interaction.fields.getTextInputValue("reasonInput");
|
||||||
interaction.customId = `createticket-${reason}`;
|
const department = interaction.customId.includes(":")
|
||||||
|
? interaction.customId.split(":")[1]
|
||||||
|
: supportbot.Ticket.DepartmentSystem?.DefaultDepartment || "general";
|
||||||
|
|
||||||
const cmd = client.commands.get(cmdconfig.OpenTicket.Command);
|
const cmd = client.commands.get(cmdconfig.OpenTicket.Command);
|
||||||
if (cmd) {
|
if (!cmd) {
|
||||||
interaction.reason = reason;
|
return interaction.reply({
|
||||||
await cmd.run(interaction);
|
content: "Open ticket command not found.",
|
||||||
} else {
|
flags: Discord.MessageFlags.Ephemeral,
|
||||||
console.error("Command not found.");
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interaction.reason = reason;
|
||||||
|
interaction.department = department;
|
||||||
|
interaction.priority =
|
||||||
|
supportbot.Ticket.DepartmentSystem?.Departments?.[department]
|
||||||
|
?.DefaultPriority ||
|
||||||
|
supportbot.Ticket.PrioritySystem?.DefaultPriority ||
|
||||||
|
"medium";
|
||||||
|
|
||||||
|
return await cmd.run(interaction);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,13 +141,52 @@ module.exports = new Event("interactionCreate", async (client, interaction) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (interaction.isStringSelectMenu()) {
|
if (interaction.isStringSelectMenu()) {
|
||||||
|
if (interaction.customId === "ticketdepartmentselect") {
|
||||||
|
const selectedDepartment = interaction.values[0];
|
||||||
|
|
||||||
|
if (supportbot.Ticket.TicketReason) {
|
||||||
|
const modal = new Discord.ModalBuilder()
|
||||||
|
.setCustomId(`ticketReasonModal:${selectedDepartment}`)
|
||||||
|
.setTitle("Ticket Reason")
|
||||||
|
.addComponents(
|
||||||
|
new Discord.ActionRowBuilder().addComponents(
|
||||||
|
new Discord.TextInputBuilder()
|
||||||
|
.setCustomId("reasonInput")
|
||||||
|
.setLabel("Reason")
|
||||||
|
.setStyle(Discord.TextInputStyle.Paragraph)
|
||||||
|
.setRequired(true),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return await interaction.showModal(modal);
|
||||||
|
}
|
||||||
|
|
||||||
|
const cmd = client.commands.get(cmdconfig.OpenTicket.Command);
|
||||||
|
if (!cmd) {
|
||||||
|
return interaction.reply({
|
||||||
|
content: "Open ticket command not found.",
|
||||||
|
flags: Discord.MessageFlags.Ephemeral,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
interaction.department = selectedDepartment;
|
||||||
|
interaction.priority =
|
||||||
|
supportbot.Ticket.DepartmentSystem?.Departments?.[selectedDepartment]
|
||||||
|
?.DefaultPriority ||
|
||||||
|
supportbot.Ticket.PrioritySystem?.DefaultPriority ||
|
||||||
|
"medium";
|
||||||
|
|
||||||
|
return await cmd.run(interaction);
|
||||||
|
}
|
||||||
|
|
||||||
if (interaction.customId === "ticketcontrolpanel") {
|
if (interaction.customId === "ticketcontrolpanel") {
|
||||||
const selectedOption = interaction.values[0];
|
const selectedOption = interaction.values[0];
|
||||||
const { getRole, getChannel } = interaction.client;
|
const { getRole, getChannel } = interaction.client;
|
||||||
switch (selectedOption) {
|
switch (selectedOption) {
|
||||||
|
|
||||||
case "ticketclose":
|
case "ticketclose":
|
||||||
const closeCommand = client.commands.get(cmdconfig.CloseTicket.Command);
|
const closeCommand = client.commands.get(
|
||||||
|
cmdconfig.CloseTicket.Command,
|
||||||
|
);
|
||||||
if (closeCommand) {
|
if (closeCommand) {
|
||||||
await closeCommand.run(interaction);
|
await closeCommand.run(interaction);
|
||||||
} else {
|
} else {
|
||||||
@@ -138,15 +199,19 @@ module.exports = new Event("interactionCreate", async (client, interaction) => {
|
|||||||
if (!ticketInfo) {
|
if (!ticketInfo) {
|
||||||
return interaction.reply({
|
return interaction.reply({
|
||||||
content: "⚠️ This channel isn’t a valid ticket.",
|
content: "⚠️ This channel isn’t a valid ticket.",
|
||||||
flags: Discord.MessageFlags.Ephemeral
|
flags: Discord.MessageFlags.Ephemeral,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = interaction.guild.members.cache.get(ticketInfo.user_id) || ticketInfo.user_id;
|
const user =
|
||||||
|
interaction.guild.members.cache.get(ticketInfo.user_id) ||
|
||||||
|
ticketInfo.user_id;
|
||||||
const username = user?.user?.username || "Unknown User";
|
const username = user?.user?.username || "Unknown User";
|
||||||
|
|
||||||
const VCcategory = interaction.guild.channels.cache.find(
|
const VCcategory = interaction.guild.channels.cache.find(
|
||||||
(c) => c.name === supportbot.VoiceTickets.Category || c.id === supportbot.VoiceTickets.Category
|
(c) =>
|
||||||
|
c.name === supportbot.VoiceTickets.Category ||
|
||||||
|
c.id === supportbot.VoiceTickets.Category,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!VCcategory) {
|
if (!VCcategory) {
|
||||||
@@ -163,7 +228,7 @@ module.exports = new Event("interactionCreate", async (client, interaction) => {
|
|||||||
permissionOverwrites: [
|
permissionOverwrites: [
|
||||||
{
|
{
|
||||||
id: interaction.guild.id,
|
id: interaction.guild.id,
|
||||||
deny: [Discord.PermissionsBitField.Flags.Connect]
|
deny: [Discord.PermissionsBitField.Flags.Connect],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: user.id || user, // works if user is a Member or just ID
|
id: user.id || user, // works if user is a Member or just ID
|
||||||
@@ -188,7 +253,9 @@ module.exports = new Event("interactionCreate", async (client, interaction) => {
|
|||||||
.setEmoji(supportbot.Buttons.General.Delete)
|
.setEmoji(supportbot.Buttons.General.Delete)
|
||||||
.setStyle(supportbot.Buttons.General.DeleteStyle);
|
.setStyle(supportbot.Buttons.General.DeleteStyle);
|
||||||
|
|
||||||
const VCDeleteRow = new Discord.ActionRowBuilder().addComponents(VCDeleteButton);
|
const VCDeleteRow = new Discord.ActionRowBuilder().addComponents(
|
||||||
|
VCDeleteButton,
|
||||||
|
);
|
||||||
|
|
||||||
await interaction.reply({
|
await interaction.reply({
|
||||||
embeds: [vcmadeembed],
|
embeds: [vcmadeembed],
|
||||||
@@ -210,8 +277,10 @@ module.exports = new Event("interactionCreate", async (client, interaction) => {
|
|||||||
.setLabel(supportbot.Buttons.Tickets.Unarchive)
|
.setLabel(supportbot.Buttons.Tickets.Unarchive)
|
||||||
.setEmoji(supportbot.Buttons.Tickets.Unarchive_Emoji)
|
.setEmoji(supportbot.Buttons.Tickets.Unarchive_Emoji)
|
||||||
.setStyle(supportbot.Buttons.Tickets.Unarchive_Style);
|
.setStyle(supportbot.Buttons.Tickets.Unarchive_Style);
|
||||||
const ArchivedOptions = new Discord.ActionRowBuilder()
|
const ArchivedOptions = new Discord.ActionRowBuilder().addComponents(
|
||||||
.addComponents(CloseButton, unarchiveButton);
|
CloseButton,
|
||||||
|
unarchiveButton,
|
||||||
|
);
|
||||||
await interaction.channel.send({
|
await interaction.channel.send({
|
||||||
embeds: [arcembed],
|
embeds: [arcembed],
|
||||||
components: [ArchivedOptions],
|
components: [ArchivedOptions],
|
||||||
@@ -221,14 +290,26 @@ module.exports = new Event("interactionCreate", async (client, interaction) => {
|
|||||||
}
|
}
|
||||||
if (supportbot.Ticket.TicketType === "channels") {
|
if (supportbot.Ticket.TicketType === "channels") {
|
||||||
const { getRole, getChannel } = interaction.client;
|
const { getRole, getChannel } = interaction.client;
|
||||||
await interaction.channel.setParent(supportbot.Ticket.TicketArchiveCategory);
|
await interaction.channel.setParent(
|
||||||
|
supportbot.Ticket.TicketArchiveCategory,
|
||||||
|
);
|
||||||
const tickets = db.getAllTickets();
|
const tickets = db.getAllTickets();
|
||||||
const ticketInfo = tickets.find(t => t.ticket_id === interaction.channel.id);
|
const ticketInfo = tickets.find(
|
||||||
|
(t) => t.ticket_id === interaction.channel.id,
|
||||||
|
);
|
||||||
if (ticketInfo) {
|
if (ticketInfo) {
|
||||||
const user = interaction.guild.members.cache.get(ticketInfo.user_id) || ticketInfo.user_id;
|
const user =
|
||||||
const claimedBy = interaction.guild.members.cache.get(ticketInfo.claimed_by) || ticketInfo.claimed_by;
|
interaction.guild.members.cache.get(ticketInfo.user_id) ||
|
||||||
const Admin = await interaction.guild.roles.fetch(supportbot.Roles.StaffMember.Admin);
|
ticketInfo.user_id;
|
||||||
const Staff = await interaction.guild.roles.fetch(supportbot.Roles.StaffMember.Staff);
|
const claimedBy =
|
||||||
|
interaction.guild.members.cache.get(ticketInfo.claimed_by) ||
|
||||||
|
ticketInfo.claimed_by;
|
||||||
|
const Admin = await interaction.guild.roles.fetch(
|
||||||
|
supportbot.Roles.StaffMember.Admin,
|
||||||
|
);
|
||||||
|
const Staff = await interaction.guild.roles.fetch(
|
||||||
|
supportbot.Roles.StaffMember.Staff,
|
||||||
|
);
|
||||||
if (user && user.id) {
|
if (user && user.id) {
|
||||||
await interaction.channel.permissionOverwrites.edit(user.id, {
|
await interaction.channel.permissionOverwrites.edit(user.id, {
|
||||||
ViewChannel: false,
|
ViewChannel: false,
|
||||||
@@ -237,20 +318,26 @@ module.exports = new Event("interactionCreate", async (client, interaction) => {
|
|||||||
}
|
}
|
||||||
if (supportbot.ClaimTickets === true) {
|
if (supportbot.ClaimTickets === true) {
|
||||||
if (claimedBy && claimedBy.id) {
|
if (claimedBy && claimedBy.id) {
|
||||||
await interaction.channel.permissionOverwrites.edit(claimedBy.id, {
|
await interaction.channel.permissionOverwrites.edit(
|
||||||
|
claimedBy.id,
|
||||||
|
{
|
||||||
ViewChannel: true,
|
ViewChannel: true,
|
||||||
SendMessages: true,
|
SendMessages: true,
|
||||||
ReadMessageHistory: true,
|
ReadMessageHistory: true,
|
||||||
});
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (supportbot.ClaimTickets === false) {
|
if (supportbot.ClaimTickets === false) {
|
||||||
if (Staff && Staff.id) {
|
if (Staff && Staff.id) {
|
||||||
await interaction.channel.permissionOverwrites.edit(claimedBy.id, {
|
await interaction.channel.permissionOverwrites.edit(
|
||||||
|
claimedBy.id,
|
||||||
|
{
|
||||||
ViewChannel: true,
|
ViewChannel: true,
|
||||||
SendMessages: true,
|
SendMessages: true,
|
||||||
ReadMessageHistory: true,
|
ReadMessageHistory: true,
|
||||||
});
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (Admin && Admin.id) {
|
if (Admin && Admin.id) {
|
||||||
@@ -260,11 +347,14 @@ module.exports = new Event("interactionCreate", async (client, interaction) => {
|
|||||||
ReadMessageHistory: true,
|
ReadMessageHistory: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
await interaction.channel.permissionOverwrites.edit(interaction.guild.roles.everyone.id, {
|
await interaction.channel.permissionOverwrites.edit(
|
||||||
|
interaction.guild.roles.everyone.id,
|
||||||
|
{
|
||||||
ViewChannel: false,
|
ViewChannel: false,
|
||||||
SendMessages: false,
|
SendMessages: false,
|
||||||
ReadMessageHistory: false,
|
ReadMessageHistory: false,
|
||||||
});
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -278,8 +368,9 @@ module.exports = new Event("interactionCreate", async (client, interaction) => {
|
|||||||
.setLabel(supportbot.Buttons.Tickets.Unlock)
|
.setLabel(supportbot.Buttons.Tickets.Unlock)
|
||||||
.setEmoji(supportbot.Buttons.Tickets.Unlock_Emoji)
|
.setEmoji(supportbot.Buttons.Tickets.Unlock_Emoji)
|
||||||
.setStyle(supportbot.Buttons.Tickets.Unlock_Style);
|
.setStyle(supportbot.Buttons.Tickets.Unlock_Style);
|
||||||
const LockedOptions = new Discord.ActionRowBuilder()
|
const LockedOptions = new Discord.ActionRowBuilder().addComponents(
|
||||||
.addComponents(unlockButton);
|
unlockButton,
|
||||||
|
);
|
||||||
await interaction.channel.send({
|
await interaction.channel.send({
|
||||||
embeds: [lockembed],
|
embeds: [lockembed],
|
||||||
components: [LockedOptions],
|
components: [LockedOptions],
|
||||||
@@ -316,14 +407,13 @@ module.exports = new Event("interactionCreate", async (client, interaction) => {
|
|||||||
.setCustomId("renameTicketModal")
|
.setCustomId("renameTicketModal")
|
||||||
.setTitle("Rename Channel")
|
.setTitle("Rename Channel")
|
||||||
.addComponents(
|
.addComponents(
|
||||||
new Discord.ActionRowBuilder()
|
new Discord.ActionRowBuilder().addComponents(
|
||||||
.addComponents(
|
|
||||||
new Discord.TextInputBuilder()
|
new Discord.TextInputBuilder()
|
||||||
.setCustomId("renameTicket")
|
.setCustomId("renameTicket")
|
||||||
.setLabel("New Channel Name")
|
.setLabel("New Channel Name")
|
||||||
.setStyle(Discord.TextInputStyle.Short)
|
.setStyle(Discord.TextInputStyle.Short)
|
||||||
.setRequired(true)
|
.setRequired(true),
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
await interaction.showModal(modal);
|
await interaction.showModal(modal);
|
||||||
break;
|
break;
|
||||||
@@ -331,16 +421,22 @@ module.exports = new Event("interactionCreate", async (client, interaction) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (interaction.customId === 'deleteSupportVC') {
|
if (interaction.customId === "deleteSupportVC") {
|
||||||
const ticketData = db.getAllTickets();
|
const ticketData = db.getAllTickets();
|
||||||
const ticketInfo = ticketData.find(t => t.ticket_id === interaction.channel.id);
|
const ticketInfo = ticketData.find(
|
||||||
|
(t) => t.ticket_id === interaction.channel.id,
|
||||||
|
);
|
||||||
if (ticketInfo && ticketInfo.voiceChannelId) {
|
if (ticketInfo && ticketInfo.voiceChannelId) {
|
||||||
try {
|
try {
|
||||||
const voiceChannel = await interaction.guild.channels.fetch(ticketInfo.voiceChannelId).catch(() => null);
|
const voiceChannel = await interaction.guild.channels
|
||||||
|
.fetch(ticketInfo.voiceChannelId)
|
||||||
|
.catch(() => null);
|
||||||
if (voiceChannel) {
|
if (voiceChannel) {
|
||||||
await voiceChannel.delete();
|
await voiceChannel.delete();
|
||||||
const successEmbed = new Discord.EmbedBuilder()
|
const successEmbed = new Discord.EmbedBuilder()
|
||||||
.setDescription('✅ Support voice channel has been successfully deleted!')
|
.setDescription(
|
||||||
|
"✅ Support voice channel has been successfully deleted!",
|
||||||
|
)
|
||||||
.setColor(supportbot.Embed.Colours.Success);
|
.setColor(supportbot.Embed.Colours.Success);
|
||||||
await interaction.reply({
|
await interaction.reply({
|
||||||
embeds: [successEmbed],
|
embeds: [successEmbed],
|
||||||
@@ -348,7 +444,9 @@ module.exports = new Event("interactionCreate", async (client, interaction) => {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const errorEmbed = new Discord.EmbedBuilder()
|
const errorEmbed = new Discord.EmbedBuilder()
|
||||||
.setDescription('❌ The associated voice channel could not be found.')
|
.setDescription(
|
||||||
|
"❌ The associated voice channel could not be found.",
|
||||||
|
)
|
||||||
.setColor(supportbot.Embed.Colours.Error);
|
.setColor(supportbot.Embed.Colours.Error);
|
||||||
await interaction.reply({
|
await interaction.reply({
|
||||||
embeds: [errorEmbed],
|
embeds: [errorEmbed],
|
||||||
@@ -356,9 +454,11 @@ module.exports = new Event("interactionCreate", async (client, interaction) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error deleting the Voice Channel:', error);
|
console.error("Error deleting the Voice Channel:", error);
|
||||||
const errorEmbed = new Discord.EmbedBuilder()
|
const errorEmbed = new Discord.EmbedBuilder()
|
||||||
.setDescription('❌ **There was an error trying to delete this voice channel. Please try again later.**')
|
.setDescription(
|
||||||
|
"❌ **There was an error trying to delete this voice channel. Please try again later.**",
|
||||||
|
)
|
||||||
.setColor(supportbot.Embed.Colours.Error);
|
.setColor(supportbot.Embed.Colours.Error);
|
||||||
await interaction.reply({
|
await interaction.reply({
|
||||||
embeds: [errorEmbed],
|
embeds: [errorEmbed],
|
||||||
@@ -367,7 +467,9 @@ module.exports = new Event("interactionCreate", async (client, interaction) => {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const errorEmbed = new Discord.EmbedBuilder()
|
const errorEmbed = new Discord.EmbedBuilder()
|
||||||
.setDescription('⚠️ This button is not linked to a valid voice channel.')
|
.setDescription(
|
||||||
|
"⚠️ This button is not linked to a valid voice channel.",
|
||||||
|
)
|
||||||
.setColor(supportbot.Embed.Colours.Warn);
|
.setColor(supportbot.Embed.Colours.Warn);
|
||||||
await interaction.reply({
|
await interaction.reply({
|
||||||
embeds: [errorEmbed],
|
embeds: [errorEmbed],
|
||||||
@@ -377,20 +479,153 @@ module.exports = new Event("interactionCreate", async (client, interaction) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (interaction.isButton()) {
|
if (interaction.isButton()) {
|
||||||
|
if (interaction.customId.startsWith("ticket_ai_enable:")) {
|
||||||
|
const ticketId = interaction.customId.split(":")[1];
|
||||||
|
const ticket = db.getTicket(ticketId);
|
||||||
|
|
||||||
|
if (!ticket) {
|
||||||
|
return interaction.reply({
|
||||||
|
content: msgconfig.Ticket.AIMode.TicketNotFound,
|
||||||
|
flags: Discord.MessageFlags.Ephemeral,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const usingThreads = supportbot.Ticket.TicketType === "threads";
|
||||||
|
|
||||||
|
if (usingThreads) {
|
||||||
|
db.setTicketAIState(ticketId, {
|
||||||
|
enabled: true,
|
||||||
|
aiChannelId: interaction.channel.id,
|
||||||
|
aiType: "thread",
|
||||||
|
enabledBy: interaction.user.id,
|
||||||
|
enabledAt: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const embed = new Discord.EmbedBuilder()
|
||||||
|
.setDescription(msgconfig.Ticket.AIMode.EnabledMessage)
|
||||||
|
.setColor(supportbot.Embed.Colours.Success);
|
||||||
|
|
||||||
|
return interaction.reply({
|
||||||
|
embeds: [embed],
|
||||||
|
flags: Discord.MessageFlags.Ephemeral,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let aiChannelId = ticket.aiChannelId;
|
||||||
|
|
||||||
|
if (!aiChannelId) {
|
||||||
|
const threadName =
|
||||||
|
supportbot.Ticket.AIMode?.ThreadName || "Chat with SupportBot AI";
|
||||||
|
|
||||||
|
const aiThread = await interaction.channel.threads.create({
|
||||||
|
name: threadName,
|
||||||
|
autoArchiveDuration: 60,
|
||||||
|
reason: "ticket ai mode thread",
|
||||||
|
});
|
||||||
|
|
||||||
|
await aiThread.members.add(interaction.user.id).catch(() => {});
|
||||||
|
|
||||||
|
aiChannelId = aiThread.id;
|
||||||
|
|
||||||
|
const welcomeEmbed = new Discord.EmbedBuilder()
|
||||||
|
.setTitle(msgconfig.Ticket.AIMode.ThreadWelcomeTitle)
|
||||||
|
.setDescription(msgconfig.Ticket.AIMode.ThreadWelcomeMessage)
|
||||||
|
.setColor(supportbot.Embed.Colours.General);
|
||||||
|
|
||||||
|
await aiThread.send({ embeds: [welcomeEmbed] });
|
||||||
|
}
|
||||||
|
|
||||||
|
db.setTicketAIState(ticketId, {
|
||||||
|
enabled: true,
|
||||||
|
aiChannelId,
|
||||||
|
aiType: "thread",
|
||||||
|
enabledBy: interaction.user.id,
|
||||||
|
enabledAt: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const embed = new Discord.EmbedBuilder()
|
||||||
|
.setDescription(msgconfig.Ticket.AIMode.EnabledMessage)
|
||||||
|
.setColor(supportbot.Embed.Colours.Success);
|
||||||
|
|
||||||
|
return interaction.reply({
|
||||||
|
embeds: [embed],
|
||||||
|
flags: Discord.MessageFlags.Ephemeral,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error enabling ticket AI:", error);
|
||||||
|
|
||||||
|
return interaction.reply({
|
||||||
|
content: msgconfig.Ticket.AIMode.EnableError,
|
||||||
|
flags: Discord.MessageFlags.Ephemeral,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (interaction.customId.startsWith("ticket_ai_disable:")) {
|
||||||
|
const ticketId = interaction.customId.split(":")[1];
|
||||||
|
const ticket = db.getTicket(ticketId);
|
||||||
|
|
||||||
|
if (!ticket) {
|
||||||
|
return interaction.reply({
|
||||||
|
content: msgconfig.Ticket.AIMode.TicketNotFound,
|
||||||
|
flags: Discord.MessageFlags.Ephemeral,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
db.setTicketAIState(ticketId, {
|
||||||
|
enabled: false,
|
||||||
|
aiChannelId: null,
|
||||||
|
aiType: null,
|
||||||
|
enabledBy: interaction.user.id,
|
||||||
|
enabledAt: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const embed = new Discord.EmbedBuilder()
|
||||||
|
.setDescription(msgconfig.Ticket.AIMode.DisabledMessage)
|
||||||
|
.setColor(supportbot.Embed.Colours.Success);
|
||||||
|
|
||||||
|
return interaction.reply({
|
||||||
|
embeds: [embed],
|
||||||
|
flags: Discord.MessageFlags.Ephemeral,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error disabling ticket AI:", error);
|
||||||
|
|
||||||
|
return interaction.reply({
|
||||||
|
content: msgconfig.Ticket.AIMode.DisableError,
|
||||||
|
flags: Discord.MessageFlags.Ephemeral,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (interaction.customId === "unarchiveTicket") {
|
if (interaction.customId === "unarchiveTicket") {
|
||||||
if (supportbot.Ticket.TicketType === "threads") {
|
if (supportbot.Ticket.TicketType === "threads") {
|
||||||
await interaction.channel.setArchived(false);
|
await interaction.channel.setArchived(false);
|
||||||
}
|
}
|
||||||
if (supportbot.Ticket.TicketType === "channels") {
|
if (supportbot.Ticket.TicketType === "channels") {
|
||||||
const { getRole, getChannel } = interaction.client;
|
const { getRole, getChannel } = interaction.client;
|
||||||
await interaction.channel.setParent(supportbot.Ticket.TicketChannelsCategory);
|
await interaction.channel.setParent(
|
||||||
|
supportbot.Ticket.TicketChannelsCategory,
|
||||||
|
);
|
||||||
const ticketData = db.getAllTickets();
|
const ticketData = db.getAllTickets();
|
||||||
const ticketInfo = ticketData.find(t => t.ticket_id === interaction.channel.id);
|
const ticketInfo = ticketData.find(
|
||||||
|
(t) => t.ticket_id === interaction.channel.id,
|
||||||
|
);
|
||||||
if (ticketInfo) {
|
if (ticketInfo) {
|
||||||
const user = interaction.guild.members.cache.get(ticketInfo.user_id) || ticketInfo.user_id;
|
const user =
|
||||||
const claimedBy = interaction.guild.members.cache.get(ticketInfo.claimed_by) || ticketInfo.claimed_by;
|
interaction.guild.members.cache.get(ticketInfo.user_id) ||
|
||||||
const Admin = await interaction.guild.roles.fetch(supportbot.Roles.StaffMember.Admin);
|
ticketInfo.user_id;
|
||||||
const Staff = await interaction.guild.roles.fetch(supportbot.Roles.StaffMember.Staff);
|
const claimedBy =
|
||||||
|
interaction.guild.members.cache.get(ticketInfo.claimed_by) ||
|
||||||
|
ticketInfo.claimed_by;
|
||||||
|
const Admin = await interaction.guild.roles.fetch(
|
||||||
|
supportbot.Roles.StaffMember.Admin,
|
||||||
|
);
|
||||||
|
const Staff = await interaction.guild.roles.fetch(
|
||||||
|
supportbot.Roles.StaffMember.Staff,
|
||||||
|
);
|
||||||
if (user && user.id) {
|
if (user && user.id) {
|
||||||
await interaction.channel.permissionOverwrites.edit(user.id, {
|
await interaction.channel.permissionOverwrites.edit(user.id, {
|
||||||
ViewChannel: true,
|
ViewChannel: true,
|
||||||
@@ -400,20 +635,26 @@ module.exports = new Event("interactionCreate", async (client, interaction) => {
|
|||||||
}
|
}
|
||||||
if (supportbot.ClaimTickets === true) {
|
if (supportbot.ClaimTickets === true) {
|
||||||
if (claimedBy && claimedBy.id) {
|
if (claimedBy && claimedBy.id) {
|
||||||
await interaction.channel.permissionOverwrites.edit(claimedBy.id, {
|
await interaction.channel.permissionOverwrites.edit(
|
||||||
|
claimedBy.id,
|
||||||
|
{
|
||||||
ViewChannel: true,
|
ViewChannel: true,
|
||||||
SendMessages: true,
|
SendMessages: true,
|
||||||
ReadMessageHistory: true,
|
ReadMessageHistory: true,
|
||||||
});
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (supportbot.ClaimTickets === false) {
|
if (supportbot.ClaimTickets === false) {
|
||||||
if (Staff && Staff.id) {
|
if (Staff && Staff.id) {
|
||||||
await interaction.channel.permissionOverwrites.edit(claimedBy.id, {
|
await interaction.channel.permissionOverwrites.edit(
|
||||||
|
claimedBy.id,
|
||||||
|
{
|
||||||
ViewChannel: true,
|
ViewChannel: true,
|
||||||
SendMessages: true,
|
SendMessages: true,
|
||||||
ReadMessageHistory: true,
|
ReadMessageHistory: true,
|
||||||
});
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (Admin && Admin.id) {
|
if (Admin && Admin.id) {
|
||||||
@@ -423,11 +664,14 @@ module.exports = new Event("interactionCreate", async (client, interaction) => {
|
|||||||
ReadMessageHistory: true,
|
ReadMessageHistory: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
await interaction.channel.permissionOverwrites.edit(interaction.guild.roles.everyone.id, {
|
await interaction.channel.permissionOverwrites.edit(
|
||||||
|
interaction.guild.roles.everyone.id,
|
||||||
|
{
|
||||||
ViewChannel: false,
|
ViewChannel: false,
|
||||||
SendMessages: false,
|
SendMessages: false,
|
||||||
ReadMessageHistory: false,
|
ReadMessageHistory: false,
|
||||||
});
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const unrcembed = new Discord.EmbedBuilder()
|
const unrcembed = new Discord.EmbedBuilder()
|
||||||
@@ -450,8 +694,8 @@ module.exports = new Event("interactionCreate", async (client, interaction) => {
|
|||||||
.setCustomId("reasonInput")
|
.setCustomId("reasonInput")
|
||||||
.setLabel("Reason")
|
.setLabel("Reason")
|
||||||
.setStyle(Discord.TextInputStyle.Paragraph)
|
.setStyle(Discord.TextInputStyle.Paragraph)
|
||||||
.setRequired(true)
|
.setRequired(true),
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
await interaction.showModal(modal);
|
await interaction.showModal(modal);
|
||||||
} else {
|
} else {
|
||||||
@@ -480,8 +724,8 @@ module.exports = new Event("interactionCreate", async (client, interaction) => {
|
|||||||
.setCustomId("reasonInput")
|
.setCustomId("reasonInput")
|
||||||
.setLabel("Reason")
|
.setLabel("Reason")
|
||||||
.setStyle(Discord.TextInputStyle.Paragraph)
|
.setStyle(Discord.TextInputStyle.Paragraph)
|
||||||
.setRequired(true)
|
.setRequired(true),
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
await interaction.showModal(modal);
|
await interaction.showModal(modal);
|
||||||
} else {
|
} else {
|
||||||
@@ -497,7 +741,8 @@ module.exports = new Event("interactionCreate", async (client, interaction) => {
|
|||||||
|
|
||||||
if (interaction.customId.startsWith("claimticket-")) {
|
if (interaction.customId.startsWith("claimticket-")) {
|
||||||
const ticketChannelId = interaction.customId.split("-")[1];
|
const ticketChannelId = interaction.customId.split("-")[1];
|
||||||
const ticketChannel = interaction.guild.channels.cache.get(ticketChannelId);
|
const ticketChannel =
|
||||||
|
interaction.guild.channels.cache.get(ticketChannelId);
|
||||||
if (!ticketChannel) {
|
if (!ticketChannel) {
|
||||||
return interaction.reply({
|
return interaction.reply({
|
||||||
content: "Ticket channel not found.",
|
content: "Ticket channel not found.",
|
||||||
@@ -506,13 +751,19 @@ module.exports = new Event("interactionCreate", async (client, interaction) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { getRole } = interaction.client;
|
const { getRole } = interaction.client;
|
||||||
const Staff = await getRole(supportbot.Roles.StaffMember.Staff, interaction.guild);
|
const Staff = await getRole(
|
||||||
const Admin = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild);
|
supportbot.Roles.StaffMember.Staff,
|
||||||
|
interaction.guild,
|
||||||
|
);
|
||||||
|
const Admin = await getRole(
|
||||||
|
supportbot.Roles.StaffMember.Admin,
|
||||||
|
interaction.guild,
|
||||||
|
);
|
||||||
|
|
||||||
const RolePerms = new Discord.EmbedBuilder()
|
const RolePerms = new Discord.EmbedBuilder()
|
||||||
.setTitle("Invalid Permissions!")
|
.setTitle("Invalid Permissions!")
|
||||||
.setDescription(
|
.setDescription(
|
||||||
`${msgconfig.Error.IncorrectPerms}\n\nRole Required: \`${supportbot.Roles.StaffMember.Staff}\` or \`${supportbot.Roles.StaffMember.Admin}\``
|
`${msgconfig.Error.IncorrectPerms}\n\nRole Required: \`${supportbot.Roles.StaffMember.Staff}\` or \`${supportbot.Roles.StaffMember.Admin}\``,
|
||||||
)
|
)
|
||||||
.setColor(supportbot.Embed.Colours.Warn);
|
.setColor(supportbot.Embed.Colours.Warn);
|
||||||
|
|
||||||
@@ -523,11 +774,17 @@ module.exports = new Event("interactionCreate", async (client, interaction) => {
|
|||||||
const NoPermsClaim = new Discord.EmbedBuilder()
|
const NoPermsClaim = new Discord.EmbedBuilder()
|
||||||
.setTitle(msgconfig.Ticket.ClaimTickets.NoPermsToClaimTitle)
|
.setTitle(msgconfig.Ticket.ClaimTickets.NoPermsToClaimTitle)
|
||||||
.setDescription(
|
.setDescription(
|
||||||
msgconfig.Ticket.ClaimTickets.NoPermsToClaim.replace("%channel%", ticketChannel.name)
|
msgconfig.Ticket.ClaimTickets.NoPermsToClaim.replace(
|
||||||
|
"%channel%",
|
||||||
|
ticketChannel.name,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.setColor(supportbot.Embed.Colours.Error);
|
.setColor(supportbot.Embed.Colours.Error);
|
||||||
|
|
||||||
if (!interaction.member.roles.cache.has(Staff.id) && !interaction.member.roles.cache.has(Admin.id)) {
|
if (
|
||||||
|
!interaction.member.roles.cache.has(Staff.id) &&
|
||||||
|
!interaction.member.roles.cache.has(Admin.id)
|
||||||
|
) {
|
||||||
return interaction.reply({ embeds: [NoPermsClaim], ephemeral: true });
|
return interaction.reply({ embeds: [NoPermsClaim], ephemeral: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -544,12 +801,17 @@ module.exports = new Event("interactionCreate", async (client, interaction) => {
|
|||||||
SendMessages: true,
|
SendMessages: true,
|
||||||
ReadMessageHistory: true,
|
ReadMessageHistory: true,
|
||||||
});
|
});
|
||||||
await ticketChannel.permissionOverwrites.create(interaction.guild.roles.everyone, {
|
await ticketChannel.permissionOverwrites.create(
|
||||||
|
interaction.guild.roles.everyone,
|
||||||
|
{
|
||||||
ViewChannel: false,
|
ViewChannel: false,
|
||||||
});
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const pingclaimedstaff = await ticketChannel.send(`<@${interaction.user.id}>`);
|
const pingclaimedstaff = await ticketChannel.send(
|
||||||
|
`<@${interaction.user.id}>`,
|
||||||
|
);
|
||||||
setTimeout(() => pingclaimedstaff.delete(), 2000);
|
setTimeout(() => pingclaimedstaff.delete(), 2000);
|
||||||
|
|
||||||
const ticketInfo = db.getTicket(ticketChannelId);
|
const ticketInfo = db.getTicket(ticketChannelId);
|
||||||
@@ -562,21 +824,28 @@ module.exports = new Event("interactionCreate", async (client, interaction) => {
|
|||||||
const claimedEmbed = new Discord.EmbedBuilder(claimMessage.embeds[0].data)
|
const claimedEmbed = new Discord.EmbedBuilder(claimMessage.embeds[0].data)
|
||||||
.setTitle(msgconfig.Ticket.ClaimTickets.ClaimedTitle)
|
.setTitle(msgconfig.Ticket.ClaimTickets.ClaimedTitle)
|
||||||
.setDescription(
|
.setDescription(
|
||||||
msgconfig.Ticket.ClaimTickets.ClaimMessage_Edit
|
msgconfig.Ticket.ClaimTickets.ClaimMessage_Edit.replace(
|
||||||
.replace("%user%", `${interaction.user.id}`)
|
"%user%",
|
||||||
.replace("%channel%", `${ticketChannel.id}`)
|
`${interaction.user.id}`,
|
||||||
|
).replace("%channel%", `${ticketChannel.id}`),
|
||||||
)
|
)
|
||||||
.setColor(supportbot.Embed.Colours.Success);
|
.setColor(supportbot.Embed.Colours.Success);
|
||||||
|
|
||||||
const successfullyClaimed = new Discord.EmbedBuilder()
|
const successfullyClaimed = new Discord.EmbedBuilder()
|
||||||
.setTitle(msgconfig.Ticket.ClaimTickets.ClaimedTitle)
|
.setTitle(msgconfig.Ticket.ClaimTickets.ClaimedTitle)
|
||||||
.setDescription(msgconfig.Ticket.ClaimTickets.Claimed
|
.setDescription(
|
||||||
.replace("%channel%", `${ticketChannel.id}`)
|
msgconfig.Ticket.ClaimTickets.Claimed.replace(
|
||||||
|
"%channel%",
|
||||||
|
`${ticketChannel.id}`,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.setColor(supportbot.Embed.Colours.Success);
|
.setColor(supportbot.Embed.Colours.Success);
|
||||||
|
|
||||||
await claimMessage.edit({ embeds: [claimedEmbed], components: [] });
|
await claimMessage.edit({ embeds: [claimedEmbed], components: [] });
|
||||||
await interaction.reply({ embeds: [successfullyClaimed], ephemeral: true });
|
await interaction.reply({
|
||||||
|
embeds: [successfullyClaimed],
|
||||||
|
ephemeral: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// SUGGESTION INTERACTIONS [START]
|
// SUGGESTION INTERACTIONS [START]
|
||||||
@@ -588,7 +857,10 @@ module.exports = new Event("interactionCreate", async (client, interaction) => {
|
|||||||
) {
|
) {
|
||||||
const suggestion = suggestions[interaction.message.id];
|
const suggestion = suggestions[interaction.message.id];
|
||||||
if (!suggestion)
|
if (!suggestion)
|
||||||
return interaction.reply({ content: "Suggestion not found.", ephemeral: true });
|
return interaction.reply({
|
||||||
|
content: "Suggestion not found.",
|
||||||
|
ephemeral: true,
|
||||||
|
});
|
||||||
|
|
||||||
const userId = interaction.user.id;
|
const userId = interaction.user.id;
|
||||||
const hasUpvoted = suggestion.upvotes.includes(userId);
|
const hasUpvoted = suggestion.upvotes.includes(userId);
|
||||||
@@ -600,29 +872,42 @@ module.exports = new Event("interactionCreate", async (client, interaction) => {
|
|||||||
} else {
|
} else {
|
||||||
suggestion.upvotes.push(userId);
|
suggestion.upvotes.push(userId);
|
||||||
if (hasDownvoted) {
|
if (hasDownvoted) {
|
||||||
suggestion.downvotes = suggestion.downvotes.filter((id) => id !== userId);
|
suggestion.downvotes = suggestion.downvotes.filter(
|
||||||
|
(id) => id !== userId,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (interaction.customId === "downvote") {
|
} else if (interaction.customId === "downvote") {
|
||||||
if (hasDownvoted) {
|
if (hasDownvoted) {
|
||||||
suggestion.downvotes = suggestion.downvotes.filter((id) => id !== userId);
|
suggestion.downvotes = suggestion.downvotes.filter(
|
||||||
|
(id) => id !== userId,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
suggestion.downvotes.push(userId);
|
suggestion.downvotes.push(userId);
|
||||||
if (hasUpvoted) {
|
if (hasUpvoted) {
|
||||||
suggestion.upvotes = suggestion.upvotes.filter((id) => id !== userId);
|
suggestion.upvotes = suggestion.upvotes.filter(
|
||||||
|
(id) => id !== userId,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (interaction.customId === "removevote") {
|
} else if (interaction.customId === "removevote") {
|
||||||
suggestion.upvotes = suggestion.upvotes.filter((id) => id !== userId);
|
suggestion.upvotes = suggestion.upvotes.filter((id) => id !== userId);
|
||||||
suggestion.downvotes = suggestion.downvotes.filter((id) => id !== userId);
|
suggestion.downvotes = suggestion.downvotes.filter(
|
||||||
|
(id) => id !== userId,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fs.writeFileSync("./Data/SuggestionData.json", JSON.stringify(suggestions, null, 2));
|
fs.writeFileSync(
|
||||||
|
"./Data/SuggestionData.json",
|
||||||
|
JSON.stringify(suggestions, null, 2),
|
||||||
|
);
|
||||||
|
|
||||||
const upvoteCount = suggestion.upvotes.length;
|
const upvoteCount = suggestion.upvotes.length;
|
||||||
const downvoteCount = suggestion.downvotes.length;
|
const downvoteCount = suggestion.downvotes.length;
|
||||||
|
|
||||||
const updatedEmbed = new Discord.EmbedBuilder(interaction.message.embeds[0].data).setFields([
|
const updatedEmbed = new Discord.EmbedBuilder(
|
||||||
|
interaction.message.embeds[0].data,
|
||||||
|
).setFields([
|
||||||
{ name: "Suggestion", value: suggestion.suggestion, inline: true },
|
{ name: "Suggestion", value: suggestion.suggestion, inline: true },
|
||||||
{ name: "From", value: `<@${suggestion.author}>` },
|
{ name: "From", value: `<@${suggestion.author}>` },
|
||||||
{
|
{
|
||||||
@@ -639,25 +924,30 @@ module.exports = new Event("interactionCreate", async (client, interaction) => {
|
|||||||
|
|
||||||
await interaction.update({ embeds: [updatedEmbed] });
|
await interaction.update({ embeds: [updatedEmbed] });
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleInvites(interaction, enable) {
|
async function handleInvites(interaction, enable) {
|
||||||
if (supportbot.Ticket.Invites.StaffOnly) {
|
if (supportbot.Ticket.Invites.StaffOnly) {
|
||||||
const { getRole, getChannel } = interaction.client;
|
const { getRole, getChannel } = interaction.client;
|
||||||
const SupportStaff = await getRole(supportbot.Roles.StaffMember.Staff, interaction.guild);
|
const SupportStaff = await getRole(
|
||||||
const Admin = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild);
|
supportbot.Roles.StaffMember.Staff,
|
||||||
|
interaction.guild,
|
||||||
|
);
|
||||||
|
const Admin = await getRole(
|
||||||
|
supportbot.Roles.StaffMember.Admin,
|
||||||
|
interaction.guild,
|
||||||
|
);
|
||||||
|
|
||||||
if (!SupportStaff || !Admin) {
|
if (!SupportStaff || !Admin) {
|
||||||
return interaction.reply(
|
return interaction.reply(
|
||||||
"Some roles seem to be missing!\nPlease check for errors when starting the bot."
|
"Some roles seem to be missing!\nPlease check for errors when starting the bot.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const NoPerms = new Discord.EmbedBuilder()
|
const NoPerms = new Discord.EmbedBuilder()
|
||||||
.setTitle("Invalid Permissions!")
|
.setTitle("Invalid Permissions!")
|
||||||
.setDescription(
|
.setDescription(
|
||||||
`${msgconfig.Error.IncorrectPerms}\n\nRole Required: \`${supportbot.Roles.StaffMember.Staff}\` or \`${supportbot.Roles.StaffMember.Admin}\``
|
`${msgconfig.Error.IncorrectPerms}\n\nRole Required: \`${supportbot.Roles.StaffMember.Staff}\` or \`${supportbot.Roles.StaffMember.Admin}\``,
|
||||||
)
|
)
|
||||||
.setColor(supportbot.Embed.Colours.Warn);
|
.setColor(supportbot.Embed.Colours.Warn);
|
||||||
|
|
||||||
@@ -671,8 +961,12 @@ module.exports = new Event("interactionCreate", async (client, interaction) => {
|
|||||||
|
|
||||||
await interaction.channel.setInvitable(enable);
|
await interaction.channel.setInvitable(enable);
|
||||||
|
|
||||||
const message = enable ? msgconfig.Ticket.EnableInvites : msgconfig.Ticket.DisableInvites;
|
const message = enable
|
||||||
const embed = new Discord.EmbedBuilder().setDescription(message).setColor(supportbot.Embed.Colours.General);
|
? msgconfig.Ticket.EnableInvites
|
||||||
|
: msgconfig.Ticket.DisableInvites;
|
||||||
|
const embed = new Discord.EmbedBuilder()
|
||||||
|
.setDescription(message)
|
||||||
|
.setColor(supportbot.Embed.Colours.General);
|
||||||
|
|
||||||
await interaction.reply({
|
await interaction.reply({
|
||||||
embeds: [embed],
|
embeds: [embed],
|
||||||
@@ -680,7 +974,10 @@ module.exports = new Event("interactionCreate", async (client, interaction) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (interaction.type === Discord.InteractionType.ModalSubmit && interaction.customId === "editProfileModal") {
|
if (
|
||||||
|
interaction.type === Discord.InteractionType.ModalSubmit &&
|
||||||
|
interaction.customId === "editProfileModal"
|
||||||
|
) {
|
||||||
const userId = interaction.user.id;
|
const userId = interaction.user.id;
|
||||||
const profilePath = `./Data/Profiles/${userId}.json`;
|
const profilePath = `./Data/Profiles/${userId}.json`;
|
||||||
|
|
||||||
@@ -710,7 +1007,11 @@ module.exports = new Event("interactionCreate", async (client, interaction) => {
|
|||||||
.setThumbnail(interaction.user.displayAvatarURL())
|
.setThumbnail(interaction.user.displayAvatarURL())
|
||||||
.addFields(
|
.addFields(
|
||||||
{ name: "Bio", value: newBio || "No bio set.", inline: false },
|
{ name: "Bio", value: newBio || "No bio set.", inline: false },
|
||||||
{ name: "Timezone", value: newTimezone || "No timezone set.", inline: true }
|
{
|
||||||
|
name: "Timezone",
|
||||||
|
value: newTimezone || "No timezone set.",
|
||||||
|
inline: true,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (profileData.clockedIn !== undefined) {
|
if (profileData.clockedIn !== undefined) {
|
||||||
@@ -721,11 +1022,15 @@ module.exports = new Event("interactionCreate", async (client, interaction) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await interaction.reply({ embeds: [updatedProfileEmbed], ephemeral: true });
|
await interaction.reply({
|
||||||
|
embeds: [updatedProfileEmbed],
|
||||||
|
ephemeral: true,
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to update profile data: ", error);
|
console.error("Failed to update profile data: ", error);
|
||||||
await interaction.reply({
|
await interaction.reply({
|
||||||
content: "There was an error while updating your profile. Please try again later.",
|
content:
|
||||||
|
"There was an error while updating your profile. Please try again later.",
|
||||||
flags: Discord.MessageFlags.Ephemeral,
|
flags: Discord.MessageFlags.Ephemeral,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+512
-116
@@ -2,165 +2,561 @@ const fs = require("fs");
|
|||||||
const Discord = require("discord.js");
|
const Discord = require("discord.js");
|
||||||
const yaml = require("js-yaml");
|
const yaml = require("js-yaml");
|
||||||
const axios = require("axios");
|
const axios = require("axios");
|
||||||
const { OpenAI } = require('openai');
|
|
||||||
|
|
||||||
// Load configuration files with validation
|
const supportbotai = yaml.load(
|
||||||
function loadConfig(path) {
|
fs.readFileSync("./Configs/supportbot-ai.yml", "utf8"),
|
||||||
try {
|
);
|
||||||
return yaml.load(fs.readFileSync(path, "utf8"));
|
const supportbot = yaml.load(
|
||||||
} catch (error) {
|
fs.readFileSync("./Configs/supportbot.yml", "utf8"),
|
||||||
console.error(`Error loading config file at ${path}:`, error);
|
);
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const supportbotai = yaml.load(fs.readFileSync("./Configs/supportbot-ai.yml"));
|
|
||||||
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"));
|
|
||||||
// Validate configuration values
|
|
||||||
function validateConfig(config, keys) {
|
|
||||||
keys.forEach(key => {
|
|
||||||
if (!config[key]) {
|
|
||||||
throw new Error(`Missing configuration key: ${key}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const CHANNELS = [supportbotai.Channels.AIChannel];
|
|
||||||
|
|
||||||
// Initialize Discord client
|
|
||||||
const client = new Discord.Client({
|
|
||||||
intents: ['Guilds', 'GuildMembers', 'GuildMessages']
|
|
||||||
});
|
|
||||||
|
|
||||||
// Load custom Event structure
|
|
||||||
const Event = require("../Structures/Event.js");
|
const Event = require("../Structures/Event.js");
|
||||||
|
const AIDatabase = require("../Structures/AIDatabase.js");
|
||||||
|
const db = require("../Structures/Database.js");
|
||||||
|
|
||||||
// Initialize OpenAI client
|
const openai = require("./Models/openai.js");
|
||||||
const openai = new OpenAI({
|
const groq = require("./Models/groq.js");
|
||||||
apiKey: supportbotai.General.OpenAI_Key,
|
|
||||||
});
|
|
||||||
|
|
||||||
const PASTEBIN_API_KEY = supportbotai.General.PastebinAPI_Key;
|
const PASTEBIN_API_KEY = supportbotai.General.PastebinAPI_Key;
|
||||||
const PASTEBIN_API_URL = supportbotai.General.PastebinAPI_URL;
|
const PASTEBIN_API_URL = supportbotai.General.PastebinAPI_URL;
|
||||||
|
|
||||||
// Function to create a paste on Pastebin
|
const CHANNELS = [supportbotai.Channels.AIChannel].filter(Boolean);
|
||||||
|
|
||||||
async function createPaste(content) {
|
async function createPaste(content) {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post(PASTEBIN_API_URL, new URLSearchParams({
|
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_dev_key: PASTEBIN_API_KEY,
|
||||||
api_option: 'paste',
|
api_option: "paste",
|
||||||
api_paste_code: content,
|
api_paste_code: content,
|
||||||
api_paste_private: 1, // 0 = public, 1 = unlisted, 2 = private
|
api_paste_private: 1,
|
||||||
api_paste_expire_date: '1D', // Expire in 1 day
|
api_paste_expire_date: "1D",
|
||||||
api_paste_format: 'text'
|
api_paste_format: "text",
|
||||||
}));
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Pastebin Error:', error);
|
console.error("Pastebin Error:", error);
|
||||||
throw error;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Event handler for message creation
|
function getProvider() {
|
||||||
module.exports = new Event("messageCreate", async (client, message) => {
|
const provider = String(
|
||||||
if (message.author.bot || !message.guild) return;
|
supportbotai.General.Provider || "openai",
|
||||||
if (!CHANNELS.includes(message.channelId) && !message.mentions.users.has(client.user.id)) return;
|
).toLowerCase();
|
||||||
|
|
||||||
if (supportbotai.Enabled) {
|
switch (provider) {
|
||||||
|
case "openai":
|
||||||
|
return openai;
|
||||||
|
|
||||||
const sendTypingInterval = setInterval(() => {
|
case "groq":
|
||||||
message.channel.sendTyping();
|
return groq;
|
||||||
}, 5000);
|
|
||||||
|
|
||||||
let conversation = [{
|
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",
|
role: "system",
|
||||||
content: "SupportBot is your friendly neighbourhood support bot."
|
content: buildSystemPrompt(currentMessage),
|
||||||
}];
|
},
|
||||||
|
];
|
||||||
|
|
||||||
let prevMessages = await message.channel.messages.fetch({ limit: 10 });
|
if (!memoryEnabled) return conversation;
|
||||||
prevMessages = prevMessages.reverse().filter(msg => !msg.author.bot || msg.author.id === client.user.id);
|
|
||||||
|
|
||||||
prevMessages.forEach((msg) => {
|
const recentMessages =
|
||||||
const username = msg.author.username.replace(/\s+/g, '_').replace(/[^\w\s]/gi, '');
|
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({
|
conversation.push({
|
||||||
role: msg.author.id === client.user.id ? "assistant" : "user",
|
role: "system",
|
||||||
name: username,
|
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,
|
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 {
|
try {
|
||||||
const response = await openai.chat.completions.create({
|
const memoryConfig = supportbotai.Memory || {};
|
||||||
model: supportbotai.General.Model,
|
const memoryEnabled = memoryConfig.Enabled !== false;
|
||||||
messages: conversation,
|
|
||||||
max_tokens: supportbotai.General.Tokens,
|
if (memoryEnabled) {
|
||||||
|
AIDatabase.addMessage({
|
||||||
|
guildId: message.guild.id,
|
||||||
|
channelId: message.channel.id,
|
||||||
|
userId: message.author.id,
|
||||||
|
role: "user",
|
||||||
|
content: message.content,
|
||||||
});
|
});
|
||||||
|
|
||||||
clearInterval(sendTypingInterval);
|
rememberUserFacts(message);
|
||||||
|
}
|
||||||
|
|
||||||
if (response.choices && response.choices.length > 0) {
|
const ticket = getTicketContextForMessage(message);
|
||||||
const replyContent = response.choices[0].message.content;
|
|
||||||
|
|
||||||
const SupportBotAI = new Discord.EmbedBuilder()
|
const conversation = buildConversationFromMemory(message);
|
||||||
.setAuthor({ name: supportbotai.General.Name, iconURL: client.user.displayAvatarURL() })
|
|
||||||
.setDescription(replyContent)
|
|
||||||
.setColor(supportbotai.Embed.Color)
|
|
||||||
.setFooter({ text: supportbotai.Embed.Footer, iconURL: message.author.displayAvatarURL() })
|
|
||||||
.setTimestamp();
|
|
||||||
|
|
||||||
if (replyContent.length > 2000) {
|
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);
|
const pasteLink = await createPaste(replyContent);
|
||||||
|
|
||||||
const SupportBotAIPastebin = new Discord.EmbedBuilder()
|
if (pasteLink) {
|
||||||
.setAuthor({ name: supportbotai.General.Name, iconURL: client.user.displayAvatarURL() })
|
const embed = new Discord.EmbedBuilder()
|
||||||
.setDescription(supportbotai.Messages.MessageTooLong.replace("{pastebin}", pasteLink))
|
.setDescription(
|
||||||
.setColor(supportbotai.Embed.Color)
|
(
|
||||||
.setFooter({ text: supportbotai.Embed.Footer, iconURL: message.author.displayAvatarURL() })
|
supportbotai.Messages?.MessageTooLong ||
|
||||||
.setTimestamp();
|
"My response was too long, so I uploaded it here: {pastebin}"
|
||||||
|
).replace("{pastebin}", pasteLink),
|
||||||
|
)
|
||||||
|
.setColor(supportbotai.Embed.Color);
|
||||||
|
|
||||||
message.reply({ embeds: [SupportBotAIPastebin] });
|
return message.reply({ embeds: [embed] });
|
||||||
} else {
|
|
||||||
message.reply({ embeds: [SupportBotAI] });
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
const SupportBotAIErrorResponse = new Discord.EmbedBuilder()
|
|
||||||
.setAuthor({ name: supportbotai.General.Name, iconURL: client.user.displayAvatarURL() })
|
|
||||||
.setDescription(supportbotai.Messages.ErrorResponse)
|
|
||||||
.setColor(supportbotai.Embed.Color)
|
|
||||||
.setFooter({ text: supportbotai.Embed.Footer, iconURL: message.author.displayAvatarURL() })
|
|
||||||
.setTimestamp();
|
|
||||||
|
|
||||||
message.reply({ embeds: [SupportBotAIErrorResponse], ephemeral: true });
|
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) {
|
} catch (error) {
|
||||||
clearInterval(sendTypingInterval);
|
clearInterval(typingInterval);
|
||||||
|
|
||||||
const SupportBotAIErrorResponse = new Discord.EmbedBuilder()
|
console.error("AI Error:", error);
|
||||||
.setAuthor({ name: supportbotai.General.Name, iconURL: client.user.displayAvatarURL() })
|
|
||||||
.setDescription(supportbotai.Messages.OpenAIError)
|
|
||||||
.setColor(supportbot.Embed.Colours.Error)
|
|
||||||
.setFooter({ text: supportbotai.Embed.Footer, iconURL: message.author.displayAvatarURL() })
|
|
||||||
.setTimestamp();
|
|
||||||
|
|
||||||
console.error('OpenAI Error:', error);
|
const embed = new Discord.EmbedBuilder()
|
||||||
message.reply({ embeds: [SupportBotAIErrorResponse], ephemeral: true });
|
.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(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
if (supportbotai.Enabled === false) {
|
|
||||||
const AIDisabled = new Discord.EmbedBuilder()
|
|
||||||
.setAuthor({ name: supportbotai.General.Name, iconURL: client.user.displayAvatarURL() })
|
|
||||||
.setDescription(supportbotai.Messages.AIDisabled)
|
|
||||||
.setColor(supportbot.Embed.Colours.Error)
|
|
||||||
.setFooter({ text: supportbotai.Embed.Footer, iconURL: message.author.displayAvatarURL() })
|
|
||||||
.setTimestamp();
|
|
||||||
|
|
||||||
message.reply({ embeds: [AIDisabled], ephemeral: true })
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
});
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
+66
-68
@@ -2,14 +2,9 @@ const fs = require("fs");
|
|||||||
const Discord = require("discord.js");
|
const Discord = require("discord.js");
|
||||||
const yaml = require("js-yaml");
|
const yaml = require("js-yaml");
|
||||||
const Event = require("../Structures/Event.js");
|
const Event = require("../Structures/Event.js");
|
||||||
const gradient = require("gradient-string");
|
|
||||||
const figlet = require("figlet");
|
|
||||||
|
|
||||||
// Load configs
|
// Load configs
|
||||||
const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8"));
|
const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8"));
|
||||||
const panel = yaml.load(fs.readFileSync("./Configs/ticket-panel.yml", "utf8"));
|
|
||||||
const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8"));
|
|
||||||
const msgconfig = yaml.load(fs.readFileSync("./Configs/messages.yml", "utf8"));
|
|
||||||
|
|
||||||
const ACTIVITY_TYPES = {
|
const ACTIVITY_TYPES = {
|
||||||
PLAYING: Discord.ActivityType.Playing,
|
PLAYING: Discord.ActivityType.Playing,
|
||||||
@@ -20,65 +15,76 @@ const ACTIVITY_TYPES = {
|
|||||||
|
|
||||||
const setActivity = (client, type, status) => {
|
const setActivity = (client, type, status) => {
|
||||||
const activityType = ACTIVITY_TYPES[type?.toUpperCase()];
|
const activityType = ACTIVITY_TYPES[type?.toUpperCase()];
|
||||||
if (activityType) {
|
if (!activityType) return;
|
||||||
|
|
||||||
client.user.setPresence({
|
client.user.setPresence({
|
||||||
activities: [{ name: status || "Online", type: activityType }],
|
activities: [{ name: status || "Online", type: activityType }],
|
||||||
status: type?.toLowerCase() || "online"
|
status: "online"
|
||||||
});
|
});
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
// 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) => {
|
const logStartup = async (client) => {
|
||||||
console.clear();
|
console.clear();
|
||||||
|
|
||||||
// Animated loading
|
|
||||||
const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
||||||
for (let i = 0; i < frames.length * 2; i++) {
|
|
||||||
process.stdout.write(`\r${frames[i % frames.length]} Initializing SupportBot...`);
|
|
||||||
await sleep(100);
|
|
||||||
}
|
|
||||||
console.clear();
|
|
||||||
|
|
||||||
// Main title
|
|
||||||
console.log("\n" + gradient.pastel.multiline(figlet.textSync("SupportBot", { font: "ANSI Shadow", horizontalLayout: "fitted" })));
|
|
||||||
|
|
||||||
// Extract bot details safely
|
|
||||||
const botName = supportbot?.General?.Name || "SupportBot";
|
const botName = supportbot?.General?.Name || "SupportBot";
|
||||||
const botVersion = supportbot.SupportBot_Version;
|
const botVersion = supportbot?.SupportBot_Version || "Unknown";
|
||||||
|
|
||||||
console.log("\n╔═══════════════════════════════════════════════════════════════╗");
|
|
||||||
console.log("║ System Information ║");
|
|
||||||
console.log("╠═══════════════════════════════════════════════════════════════╣");
|
|
||||||
console.log(`║ Bot Name : ${botName.padEnd(45)} ║`);
|
|
||||||
console.log(`║ Version : ${botVersion.padEnd(45)} ║`);
|
|
||||||
console.log(`║ Node.js : ${process.version.padEnd(45)} ║`);
|
|
||||||
console.log(`║ Discord.js : v${Discord.version.padEnd(44)} ║`);
|
|
||||||
console.log("╚═══════════════════════════════════════════════════════════════╝\n");
|
|
||||||
|
|
||||||
// Links section
|
|
||||||
console.log(gradient.summer("┏━━━━━━━━━━━━━━━━━━━ Important Links ━━━━━━━━━━━━━━━━━━━┓"));
|
|
||||||
console.log(gradient.summer("┃ ┃"));
|
|
||||||
console.log(gradient.summer("┃ ") + "📚 Docs : " + gradient.cristal("https://emerald-services.gitbook.io/ ┃"));
|
|
||||||
console.log(gradient.summer("┃ ") + "🤝 Discord : " + gradient.cristal("https://dsc.gg/emerald-dev ┃"));
|
|
||||||
console.log(gradient.summer("┃ ┃"));
|
|
||||||
console.log(gradient.summer("┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛\n"));
|
|
||||||
|
|
||||||
// Bot Invite
|
|
||||||
const inviteUrl = `https://discord.com/api/oauth2/authorize?client_id=${client.user.id}&permissions=8&scope=bot%20applications.commands`;
|
const inviteUrl = `https://discord.com/api/oauth2/authorize?client_id=${client.user.id}&permissions=8&scope=bot%20applications.commands`;
|
||||||
console.log(gradient.teen("╔════════════════════════ Bot Invite ════════════════════════╗"));
|
|
||||||
console.log(gradient.teen("║ ║"));
|
line();
|
||||||
console.log(gradient.teen("║ ") + gradient.pastel(inviteUrl.padEnd(56)) + gradient.teen("║"));
|
console.log(`${c.bold}${c.green}SupportBot${c.reset}`);
|
||||||
console.log(gradient.teen("║ ║"));
|
console.log(`${c.gray}Emerald Services${c.reset}`);
|
||||||
console.log(gradient.teen("╚════════════════════════════════════════════════════════════╝\n"));
|
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 checkConfigs = async (client) => {
|
||||||
const guild = client.guilds.cache.first();
|
const guild = client.guilds.cache.first();
|
||||||
if (!guild) return;
|
if (!guild) return;
|
||||||
|
|
||||||
console.log(gradient.morning("┏━━━━━━━━━━━━━━━━━━━━ Config Check ━━━━━━━━━━━━━━━━━━━━━┓"));
|
section("Config Check");
|
||||||
|
|
||||||
const roles = [
|
const roles = [
|
||||||
supportbot?.Roles?.StaffMember?.Admin,
|
supportbot?.Roles?.StaffMember?.Admin,
|
||||||
@@ -87,38 +93,30 @@ const checkConfigs = async (client) => {
|
|||||||
].filter(Boolean);
|
].filter(Boolean);
|
||||||
|
|
||||||
const channels = [
|
const channels = [
|
||||||
supportbot?.Suggestions?.Channel,
|
supportbot?.Ticket?.TicketHome
|
||||||
supportbot?.Ticket?.Log?.TicketLog,
|
|
||||||
supportbot?.Ticket?.Log?.TranscriptLog,
|
|
||||||
supportbot?.Ticket?.TicketHome,
|
|
||||||
supportbot?.Welcome?.Channel,
|
|
||||||
supportbot?.Leave?.Channel,
|
|
||||||
supportbot?.Translate?.Log
|
|
||||||
].filter(Boolean);
|
].filter(Boolean);
|
||||||
|
|
||||||
const [missingRoles, missingChannels] = await Promise.all([
|
const [roleResults, channelResults] = await Promise.all([
|
||||||
Promise.all(roles.map((role) => client.getRole(role, guild))),
|
Promise.all(roles.map((role) => client.getRole(role, guild))),
|
||||||
Promise.all(channels.map((channel) => client.getChannel(channel, guild)))
|
Promise.all(channels.map((channel) => client.getChannel(channel, guild)))
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const missing = {
|
const missingRoles = roles.filter((_, i) => !roleResults[i]);
|
||||||
roles: roles.filter((_, i) => !missingRoles[i]),
|
const missingChannels = channels.filter((_, i) => !channelResults[i]);
|
||||||
channels: channels.filter((_, i) => !missingChannels[i])
|
|
||||||
};
|
|
||||||
|
|
||||||
if (missing.roles.length || missing.channels.length) {
|
if (!missingRoles.length && !missingChannels.length) {
|
||||||
console.log(gradient.morning("┃ ⚠️ - Configuration Issues Found: ┃"));
|
status("All configurations validated successfully", true);
|
||||||
if (missing.roles.length) {
|
|
||||||
console.log(gradient.morning("┃ • Missing Roles: ") + gradient.fruit(missing.roles.join(", ")));
|
|
||||||
}
|
|
||||||
if (missing.channels.length) {
|
|
||||||
console.log(gradient.morning("┃ • Missing Channels: ") + gradient.fruit(missing.channels.join(", ")));
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
console.log(gradient.morning("┃ ✅ - All configurations validated successfully ┃"));
|
if (missingRoles.length) {
|
||||||
|
status("Missing roles", false, missingRoles.join(", "));
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(gradient.morning("┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛\n"));
|
if (missingChannels.length) {
|
||||||
|
status("Missing channels", false, missingChannels.join(", "));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("");
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = new Event("clientReady", async (client) => {
|
module.exports = new Event("clientReady", async (client) => {
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2025 Emerald Services
|
Copyright (c) 2026 Emerald Services
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
@@ -28,8 +28,8 @@
|
|||||||
|
|
||||||
| SupportBot | Node.JS | Discord.JS | Supported |
|
| SupportBot | Node.JS | Discord.JS | Supported |
|
||||||
|------------|---------|------------|------------|
|
|------------|---------|------------|------------|
|
||||||
| 8.x | 20.x | 14.x. | ✅ Yes
|
| 8.x | 22.x | 14.x. | ✅ Yes
|
||||||
| 7.7.3 | 16.x | 13.x. | ✅ Yes
|
| 7.7.3 | 16.x | 13.x. | ❌ No
|
||||||
| 7.x | 16.x | 13.x. | ❌ No
|
| 7.x | 16.x | 13.x. | ❌ No
|
||||||
| 6.x | 14.x | 13.x. | ❌ No
|
| 6.x | 14.x | 13.x. | ❌ No
|
||||||
| 5.x | 12.x | 13.x. | ❌ No
|
| 5.x | 12.x | 13.x. | ❌ No
|
||||||
@@ -74,7 +74,7 @@ Released under the [MIT](https://opensource.org/licenses/MIT) license.
|
|||||||
|
|
||||||
- [Official addons repository](https://github.com/Emerald-Services/Addons/)
|
- [Official addons repository](https://github.com/Emerald-Services/Addons/)
|
||||||
- [Syphers Addons](https://github.com/SypherRed/SB_Addons_Unofficial)
|
- [Syphers Addons](https://github.com/SypherRed/SB_Addons_Unofficial)
|
||||||
- [BuiltByBit](https://builtbybit.com/supportbot)
|
- [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)
|
- [Polymart](https://polymart.org/resource/supportbot-1-discord-ticket-bot.518)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
},
|
||||||
|
};
|
||||||
+67
-104
@@ -1,13 +1,32 @@
|
|||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const Discord = require("discord.js");
|
const Discord = require("discord.js");
|
||||||
const { GatewayIntentBits, Partials } = require('discord.js');
|
const { GatewayIntentBits, Partials } = require("discord.js");
|
||||||
const yaml = require("js-yaml");
|
const yaml = require("js-yaml");
|
||||||
const { Command, Event } = require('./Addon.js');
|
const { Command, Event } = require("./Addon.js");
|
||||||
|
|
||||||
const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8"));
|
const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8"));
|
||||||
const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.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 = []) {
|
function walk(dir, ext = ".js", fileList = []) {
|
||||||
const files = fs.readdirSync(dir);
|
const files = fs.readdirSync(dir);
|
||||||
@@ -30,9 +49,10 @@ class Client extends Discord.Client {
|
|||||||
GatewayIntentBits.Guilds,
|
GatewayIntentBits.Guilds,
|
||||||
GatewayIntentBits.GuildMembers,
|
GatewayIntentBits.GuildMembers,
|
||||||
GatewayIntentBits.GuildMessages,
|
GatewayIntentBits.GuildMessages,
|
||||||
GatewayIntentBits.MessageContent
|
GatewayIntentBits.GuildMessageReactions,
|
||||||
|
GatewayIntentBits.MessageContent,
|
||||||
],
|
],
|
||||||
partials: [Partials.Message, Partials.Channel]
|
partials: [Partials.Message, Partials.Channel, Partials.Reaction],
|
||||||
});
|
});
|
||||||
|
|
||||||
this.commands = new Discord.Collection();
|
this.commands = new Discord.Collection();
|
||||||
@@ -43,14 +63,14 @@ class Client extends Discord.Client {
|
|||||||
return guild.channels.cache.find(
|
return guild.channels.cache.find(
|
||||||
(c) =>
|
(c) =>
|
||||||
(c.type === Discord.ChannelType.GuildText || c.type === Discord.ChannelType.GuildNews) &&
|
(c.type === Discord.ChannelType.GuildText || c.type === Discord.ChannelType.GuildNews) &&
|
||||||
(c.id === channel || (c.name && c.name.toLowerCase() === channel.toLowerCase()))
|
(c.id === channel || (c.name && c.name.toLowerCase() === channel.toLowerCase())),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getRole(role, guild) {
|
async getRole(role, guild) {
|
||||||
if (!role) return null;
|
if (!role) return null;
|
||||||
return guild.roles.cache.find(
|
return guild.roles.cache.find(
|
||||||
(r) => r.id === role || (r.name && r.name.toLowerCase() === role.toLowerCase())
|
(r) => r.id === role || (r.name && r.name.toLowerCase() === role.toLowerCase()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,88 +79,42 @@ class Client extends Discord.Client {
|
|||||||
return guild.channels.cache.find(
|
return guild.channels.cache.find(
|
||||||
(c) =>
|
(c) =>
|
||||||
c.type === Discord.ChannelType.GuildCategory &&
|
c.type === Discord.ChannelType.GuildCategory &&
|
||||||
(c.id === category || (c.name && c.name.toLowerCase() === category.toLowerCase()))
|
(c.id === category || (c.name && c.name.toLowerCase() === category.toLowerCase())),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async start(token) {
|
async start(token) {
|
||||||
let tempCommandFiles = walk("./Commands");
|
const commandFiles = walk("./Commands");
|
||||||
|
|
||||||
if (cmdconfig.Suggestion.Enabled === false) {
|
const allCommands = commandFiles.map((file) => ({
|
||||||
tempCommandFiles = tempCommandFiles.filter(item => path.basename(item) !== "suggest.js");
|
file,
|
||||||
|
command: require(`../${file}`),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const enabledCommands = allCommands.filter(({ command }) => {
|
||||||
|
const matchingConfig = Object.values(cmdconfig).find(
|
||||||
|
(entry) =>
|
||||||
|
entry &&
|
||||||
|
typeof entry === "object" &&
|
||||||
|
entry.Command &&
|
||||||
|
String(entry.Command).toLowerCase() === String(command.name).toLowerCase(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!matchingConfig) {
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cmdconfig.Suggestion.Enabled === false) {
|
return matchingConfig.Enabled !== false;
|
||||||
tempCommandFiles = tempCommandFiles.filter(item => path.basename(item) !== "suggestadmin.js");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cmdconfig.Help.Enabled === false) {
|
|
||||||
tempCommandFiles = tempCommandFiles.filter(item => path.basename(item) !== "help.js");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cmdconfig.Info.Enabled === false) {
|
|
||||||
tempCommandFiles = tempCommandFiles.filter(item => path.basename(item) !== "info.js");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cmdconfig.OpenTicket.Enabled === false) {
|
|
||||||
tempCommandFiles = tempCommandFiles.filter(item => path.basename(item) !== "ticket.js");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cmdconfig.CloseTicket.Enabled === false) {
|
|
||||||
tempCommandFiles = tempCommandFiles.filter(item => path.basename(item) !== "close.js");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cmdconfig.Embed.Enabled === false) {
|
|
||||||
tempCommandFiles = tempCommandFiles.filter(item => path.basename(item) !== "embed.js");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cmdconfig.Translate.Enabled === false) {
|
|
||||||
tempCommandFiles = tempCommandFiles.filter(item => path.basename(item) !== "translate.js");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cmdconfig.UserInfo.Enabled === false) {
|
|
||||||
tempCommandFiles = tempCommandFiles.filter(item => path.basename(item) !== "userinfo.js");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cmdconfig.Ping.Enabled === false) {
|
|
||||||
tempCommandFiles = tempCommandFiles.filter(item => path.basename(item) !== "Ping.js");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cmdconfig.AddUser.Enabled === false) {
|
|
||||||
tempCommandFiles = tempCommandFiles.filter(item => path.basename(item) !== "addUser.js");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cmdconfig.RemoveUser.Enabled === false) {
|
|
||||||
tempCommandFiles = tempCommandFiles.filter(item => path.basename(item) !== "removeUser.js");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cmdconfig.ForceAddUser.Enabled === false) {
|
|
||||||
tempCommandFiles = tempCommandFiles.filter(item => path.basename(item) !== "forceaddUser.js");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cmdconfig.Profile.Enabled === false) {
|
|
||||||
tempCommandFiles = tempCommandFiles.filter(item => path.basename(item) !== "profile.js");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cmdconfig.Settings.Enabled === false) {
|
|
||||||
tempCommandFiles = tempCommandFiles.filter(item => path.basename(item) !== "settings.js");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cmdconfig.Mod.Enabled === false) {
|
|
||||||
tempCommandFiles = tempCommandFiles.filter(item => path.basename(item) !== "mod.js");
|
|
||||||
}
|
|
||||||
|
|
||||||
const commandFiles = tempCommandFiles;
|
|
||||||
const commands = commandFiles.map((file) => require(`../${file}`));
|
|
||||||
|
|
||||||
console.log(`\u001b[33m`, "――――――――――――――――――――――――――――――――――――――――――――");
|
|
||||||
|
|
||||||
commands.forEach((cmd) => {
|
|
||||||
console.log(`\u001b[32m`, `[CMD]`, `\u001b[37;1m`, `${cmd.name}`, `\u001b[32;1m`, "Loaded");
|
|
||||||
this.commands.set(cmd.name, cmd);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`\u001b[33m`, "▬▬▬▬▬▬▬ Commands ▬▬▬▬▬▬▬");
|
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) {
|
if (supportbot.General.Addons.Enabled) {
|
||||||
const addonFiles = fs.readdirSync("./Addons").filter((file) => file.endsWith(".js"));
|
const addonFiles = fs.readdirSync("./Addons").filter((file) => file.endsWith(".js"));
|
||||||
@@ -151,18 +125,10 @@ class Client extends Discord.Client {
|
|||||||
return addon;
|
return addon;
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(" ");
|
section("Addons");
|
||||||
console.log(`\u001b[33m`, "▬▬▬▬▬▬▬ Addons ▬▬▬▬▬▬▬");
|
|
||||||
|
|
||||||
addons.forEach((addon) => {
|
addons.forEach((addon) => {
|
||||||
console.log(
|
console.log(`${c.green}✓${c.reset} ${c.white}${addon.name}${c.reset}`);
|
||||||
`\u001b[32m`,
|
|
||||||
`[ADDON]`,
|
|
||||||
`\u001b[37;1m`,
|
|
||||||
`${addon.name}`,
|
|
||||||
`\u001b[32;1m`,
|
|
||||||
"Loaded"
|
|
||||||
);
|
|
||||||
|
|
||||||
if (addon instanceof Command) {
|
if (addon instanceof Command) {
|
||||||
this.commands.set(addon.name, addon);
|
this.commands.set(addon.name, addon);
|
||||||
@@ -176,37 +142,34 @@ class Client extends Discord.Client {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`\u001b[33m`, "▬▬▬▬▬▬▬ Addons ▬▬▬▬▬▬▬");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.once("clientReady", async () => {
|
this.once("clientReady", async () => {
|
||||||
await this.guilds.cache.first()?.commands.set(this.commands);
|
await this.guilds.cache.first()?.commands.set(this.commands);
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
"\u001b[32m",
|
`\n${c.green}✓${c.reset} ${c.white}Slash Commands Registered${c.reset} ${c.gray}for ${this.guilds.cache.first().name}${c.reset}`,
|
||||||
`[SUCCESS]`,
|
|
||||||
`\u001b[37;1m`,
|
|
||||||
`Slash Commands Registered for ${this.guilds.cache.first().name}`
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(" ");
|
section("Events");
|
||||||
console.log(`\u001b[33m`, "▬▬▬▬▬▬▬ Events ▬▬▬▬▬▬▬");
|
|
||||||
|
|
||||||
fs.readdirSync("./Events").filter((file) => file.endsWith(".js")).forEach((file) => {
|
fs.readdirSync("./Events")
|
||||||
|
.filter((file) => file.endsWith(".js"))
|
||||||
|
.forEach((file) => {
|
||||||
const event = require(`../Events/${file}`);
|
const event = require(`../Events/${file}`);
|
||||||
console.log(`\u001b[32m`, `[EVENT]`, `\u001b[37;1m`, `${event.event}`, `\u001b[32;1m`, "Loaded");
|
console.log(
|
||||||
|
`${c.green}✓${c.reset} ${c.white}${event.event}${c.reset} ${c.gray}(${file})${c.reset}`,
|
||||||
|
);
|
||||||
this.on(event.event, (...args) => event.run(this, ...args));
|
this.on(event.event, (...args) => event.run(this, ...args));
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`\u001b[33m`, "▬▬▬▬▬▬▬ Events ▬▬▬▬▬▬▬");
|
console.log("");
|
||||||
|
|
||||||
if (process.argv[2] !== "test") {
|
if (process.argv[2] !== "test") {
|
||||||
await this.login(token);
|
await this.login(token);
|
||||||
} else {
|
} else {
|
||||||
console.log(
|
console.log(`${c.red}RUNNING IN TEST MODE, IF NOT INTENDED, PLEASE USE npm start${c.reset}`);
|
||||||
`\u001b[31;1m`,
|
|
||||||
"RUNNING IN TEST MODE, IF NOT INTENDED, PLEASE USE npm start"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+352
-99
@@ -1,30 +1,33 @@
|
|||||||
// SupportBot | Emerald Services
|
// SupportBot | Emerald Services
|
||||||
// Database Structure
|
|
||||||
|
|
||||||
const Database = require("better-sqlite3");
|
const Database = require("better-sqlite3");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
|
||||||
const db = new Database(path.join(__dirname, "../Data/supportbot.db"));
|
const db = new Database(path.join(__dirname, "../Data/supportbot.db"));
|
||||||
|
|
||||||
db.prepare(`
|
db.prepare(
|
||||||
|
`
|
||||||
CREATE TABLE IF NOT EXISTS suggestions (
|
CREATE TABLE IF NOT EXISTS suggestions (
|
||||||
thread_id TEXT PRIMARY KEY,
|
thread_id TEXT PRIMARY KEY,
|
||||||
author_id TEXT,
|
author_id TEXT,
|
||||||
text TEXT,
|
text TEXT,
|
||||||
status TEXT DEFAULT 'Created'
|
status TEXT DEFAULT 'Created'
|
||||||
)
|
)
|
||||||
`).run();
|
`,
|
||||||
|
).run();
|
||||||
|
|
||||||
db.prepare(`
|
db.prepare(
|
||||||
|
`
|
||||||
CREATE TABLE IF NOT EXISTS profiles (
|
CREATE TABLE IF NOT EXISTS profiles (
|
||||||
user_id TEXT PRIMARY KEY,
|
user_id TEXT PRIMARY KEY,
|
||||||
bio TEXT,
|
bio TEXT,
|
||||||
timezone TEXT,
|
timezone TEXT,
|
||||||
clockedIn INTEGER DEFAULT 0
|
clockedIn INTEGER DEFAULT 0
|
||||||
)
|
)
|
||||||
`).run();
|
`,
|
||||||
|
).run();
|
||||||
|
|
||||||
db.prepare(`
|
db.prepare(
|
||||||
|
`
|
||||||
CREATE TABLE IF NOT EXISTS tickets (
|
CREATE TABLE IF NOT EXISTS tickets (
|
||||||
ticket_id TEXT PRIMARY KEY,
|
ticket_id TEXT PRIMARY KEY,
|
||||||
user_id TEXT,
|
user_id TEXT,
|
||||||
@@ -34,170 +37,380 @@ db.prepare(`
|
|||||||
subject TEXT,
|
subject TEXT,
|
||||||
description TEXT
|
description TEXT
|
||||||
)
|
)
|
||||||
`).run();
|
`,
|
||||||
|
).run();
|
||||||
|
|
||||||
|
// Safe column upgrades
|
||||||
|
const safeAlter = (sql) => {
|
||||||
try {
|
try {
|
||||||
db.prepare("ALTER TABLE tickets ADD COLUMN voiceChannelId TEXT").run();
|
db.prepare(sql).run();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!String(e).includes("duplicate column name")) {
|
if (!String(e).includes("duplicate column name")) throw e;
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
db.prepare(`
|
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 (
|
CREATE TABLE IF NOT EXISTS ticket_panel (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
message_id TEXT,
|
message_id TEXT,
|
||||||
channel_id TEXT,
|
channel_id TEXT,
|
||||||
created_at INTEGER
|
created_at INTEGER
|
||||||
)
|
)
|
||||||
`).run();
|
`,
|
||||||
|
).run();
|
||||||
|
|
||||||
db.prepare(`
|
db.prepare(
|
||||||
|
`
|
||||||
CREATE TABLE IF NOT EXISTS ticket_users (
|
CREATE TABLE IF NOT EXISTS ticket_users (
|
||||||
ticket_id TEXT,
|
ticket_id TEXT,
|
||||||
user_id TEXT,
|
user_id TEXT,
|
||||||
PRIMARY KEY(ticket_id, user_id)
|
PRIMARY KEY(ticket_id, user_id)
|
||||||
)
|
)
|
||||||
`).run();
|
`,
|
||||||
|
).run();
|
||||||
|
|
||||||
db.prepare(`
|
db.prepare(
|
||||||
|
`
|
||||||
CREATE TABLE IF NOT EXISTS settings (
|
CREATE TABLE IF NOT EXISTS settings (
|
||||||
id INTEGER PRIMARY KEY,
|
id INTEGER PRIMARY KEY,
|
||||||
data TEXT
|
data TEXT
|
||||||
)
|
)
|
||||||
`).run();
|
`,
|
||||||
|
).run();
|
||||||
|
|
||||||
module.exports = {
|
function ensureProfile(userId) {
|
||||||
addSuggestion(threadId, authorId, text) {
|
const existing = db
|
||||||
db.prepare(`
|
.prepare(
|
||||||
INSERT INTO suggestions (thread_id, author_id, text)
|
`
|
||||||
VALUES (@threadId, @authorId, @text)
|
SELECT user_id FROM profiles WHERE user_id = ?
|
||||||
ON CONFLICT(thread_id) DO NOTHING
|
`,
|
||||||
`).run({ threadId, authorId, text });
|
)
|
||||||
},
|
.get(userId);
|
||||||
|
|
||||||
setSuggestionStatus(threadId, status) {
|
if (!existing) {
|
||||||
db.prepare(`
|
db.prepare(
|
||||||
UPDATE suggestions SET status=@status WHERE thread_id=@threadId
|
`
|
||||||
`).run({ threadId, status });
|
|
||||||
},
|
|
||||||
|
|
||||||
getSuggestion(threadId) {
|
|
||||||
return db.prepare(`
|
|
||||||
SELECT * FROM suggestions WHERE thread_id=@threadId
|
|
||||||
`).get({ threadId });
|
|
||||||
},
|
|
||||||
|
|
||||||
// ----- Profiles -----
|
|
||||||
getProfile(userId) {
|
|
||||||
const row = db.prepare(`SELECT * FROM profiles WHERE user_id=?`).get(userId);
|
|
||||||
if (!row) {
|
|
||||||
db.prepare(`
|
|
||||||
INSERT INTO profiles (user_id, bio, timezone, clockedIn)
|
INSERT INTO profiles (user_id, bio, timezone, clockedIn)
|
||||||
VALUES (?, '', '', 0)
|
VALUES (?, '', '', 0)
|
||||||
`).run(userId);
|
`,
|
||||||
return { user_id: userId, bio: "", timezone: "", clockedIn: 0 };
|
).run(userId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return row;
|
|
||||||
},
|
|
||||||
|
|
||||||
updateProfile(userId, data) {
|
function normaliseTicketRow(row) {
|
||||||
const { bio = "", timezone = "", clockedIn = 0 } = data;
|
if (!row) return null;
|
||||||
db.prepare(`
|
|
||||||
INSERT INTO profiles (user_id, bio, timezone, clockedIn)
|
|
||||||
VALUES (@userId, @bio, @timezone, @clockedIn)
|
|
||||||
ON CONFLICT(user_id) DO UPDATE SET
|
|
||||||
bio = excluded.bio,
|
|
||||||
timezone = excluded.timezone,
|
|
||||||
clockedIn = excluded.clockedIn
|
|
||||||
`).run({ userId, bio, timezone, clockedIn });
|
|
||||||
},
|
|
||||||
|
|
||||||
setClockedIn(userId, value) {
|
let questionAnswers = [];
|
||||||
db.prepare(`UPDATE profiles SET clockedIn=? WHERE user_id=?`)
|
|
||||||
.run(value ? 1 : 0, userId);
|
|
||||||
},
|
|
||||||
|
|
||||||
|
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) {
|
addTicket(ticket) {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
db.prepare(`
|
|
||||||
INSERT INTO tickets (ticket_id, user_id, subject, description, created_at, updated_at)
|
db.prepare(
|
||||||
VALUES (@ticket_id, @user_id, @subject, @description, @created_at, @updated_at)
|
`
|
||||||
|
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
|
ON CONFLICT(ticket_id) DO NOTHING
|
||||||
`).run({
|
`,
|
||||||
|
).run({
|
||||||
ticket_id: ticket.id,
|
ticket_id: ticket.id,
|
||||||
user_id: ticket.user,
|
user_id: ticket.user,
|
||||||
subject: ticket.reason || "",
|
subject: ticket.reason || "",
|
||||||
description: ticket.description || "",
|
description: ticket.description || "",
|
||||||
created_at: now,
|
created_at: now,
|
||||||
updated_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) {
|
updateTicketVoice(ticketId, voiceChannelId) {
|
||||||
db.prepare(`
|
db.prepare(
|
||||||
UPDATE tickets SET voiceChannelId=@voiceChannelId, updated_at=@updatedAt WHERE ticket_id=@ticketId
|
`
|
||||||
`).run({ ticketId, voiceChannelId, updatedAt: Date.now() });
|
UPDATE tickets
|
||||||
},
|
SET voiceChannelId=@voiceChannelId, updated_at=@updatedAt
|
||||||
|
WHERE ticket_id=@ticketId
|
||||||
getTicketVoice(ticketId) {
|
`,
|
||||||
return db.prepare(`SELECT voiceChannelId FROM tickets WHERE ticket_id=?`).get(ticketId);
|
).run({
|
||||||
},
|
ticketId,
|
||||||
|
voiceChannelId,
|
||||||
deleteTicketVoice(ticketId) {
|
updatedAt: Date.now(),
|
||||||
db.prepare(`
|
});
|
||||||
UPDATE tickets SET voiceChannelId=NULL, updated_at=@updatedAt WHERE ticket_id=@ticketId
|
|
||||||
`).run({ ticketId, updatedAt: Date.now() });
|
|
||||||
},
|
},
|
||||||
|
|
||||||
updateTicketStatus(ticketId, status) {
|
updateTicketStatus(ticketId, status) {
|
||||||
db.prepare(`
|
db.prepare(
|
||||||
UPDATE tickets SET status=@status, updated_at=@updatedAt WHERE ticket_id=@ticketId
|
`
|
||||||
`).run({ status, updatedAt: Date.now(), ticketId });
|
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) {
|
getTicket(ticketId) {
|
||||||
return db.prepare(`
|
const row = db
|
||||||
|
.prepare(
|
||||||
|
`
|
||||||
SELECT * FROM tickets WHERE ticket_id=@ticketId
|
SELECT * FROM tickets WHERE ticket_id=@ticketId
|
||||||
`).get({ 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() {
|
getAllTickets() {
|
||||||
return db.prepare(`SELECT * FROM tickets`).all();
|
return db.prepare(`SELECT * FROM tickets`).all().map(normaliseTicketRow);
|
||||||
},
|
},
|
||||||
|
|
||||||
saveTicketPanel(messageId, channelId) {
|
saveTicketPanel(messageId, channelId) {
|
||||||
db.prepare(`
|
db.prepare(
|
||||||
|
`
|
||||||
INSERT INTO ticket_panel (message_id, channel_id, created_at)
|
INSERT INTO ticket_panel (message_id, channel_id, created_at)
|
||||||
VALUES (@messageId, @channelId, @createdAt)
|
VALUES (@messageId, @channelId, @createdAt)
|
||||||
`).run({ messageId, channelId, createdAt: Date.now() });
|
`,
|
||||||
|
).run({ messageId, channelId, createdAt: Date.now() });
|
||||||
},
|
},
|
||||||
|
|
||||||
getTicketPanel() {
|
getTicketPanel() {
|
||||||
return db.prepare(`
|
return db
|
||||||
|
.prepare(
|
||||||
|
`
|
||||||
SELECT * FROM ticket_panel ORDER BY id DESC LIMIT 1
|
SELECT * FROM ticket_panel ORDER BY id DESC LIMIT 1
|
||||||
`).get();
|
`,
|
||||||
|
)
|
||||||
|
.get();
|
||||||
},
|
},
|
||||||
|
|
||||||
addUserToTicket(ticketId, userId) {
|
addUserToTicket(ticketId, userId) {
|
||||||
db.prepare(`
|
db.prepare(
|
||||||
|
`
|
||||||
INSERT INTO ticket_users (ticket_id, user_id)
|
INSERT INTO ticket_users (ticket_id, user_id)
|
||||||
VALUES (?, ?)
|
VALUES (?, ?)
|
||||||
ON CONFLICT(ticket_id, user_id) DO NOTHING
|
ON CONFLICT(ticket_id, user_id) DO NOTHING
|
||||||
`).run(ticketId, userId);
|
`,
|
||||||
|
).run(ticketId, userId);
|
||||||
},
|
},
|
||||||
|
|
||||||
removeUserFromTicket(ticketId, userId) {
|
removeUserFromTicket(ticketId, userId) {
|
||||||
db.prepare(`
|
db.prepare(
|
||||||
|
`
|
||||||
DELETE FROM ticket_users WHERE ticket_id=? AND user_id=?
|
DELETE FROM ticket_users WHERE ticket_id=? AND user_id=?
|
||||||
`).run(ticketId, userId);
|
`,
|
||||||
|
).run(ticketId, userId);
|
||||||
},
|
},
|
||||||
|
|
||||||
getTicketUsers(ticketId) {
|
getTicketUsers(ticketId) {
|
||||||
return db.prepare(`
|
return db
|
||||||
|
.prepare(
|
||||||
|
`
|
||||||
SELECT user_id FROM ticket_users WHERE ticket_id=?
|
SELECT user_id FROM ticket_users WHERE ticket_id=?
|
||||||
`).all(ticketId).map(r => r.user_id);
|
`,
|
||||||
|
)
|
||||||
|
.all(ticketId)
|
||||||
|
.map((r) => r.user_id);
|
||||||
},
|
},
|
||||||
|
|
||||||
getSettings() {
|
getSettings() {
|
||||||
@@ -210,12 +423,52 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
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) {
|
saveSettings(obj) {
|
||||||
const data = JSON.stringify(obj);
|
const data = JSON.stringify(obj);
|
||||||
db.prepare(`
|
db.prepare(
|
||||||
|
`
|
||||||
INSERT INTO settings (id, data)
|
INSERT INTO settings (id, data)
|
||||||
VALUES (1, @data)
|
VALUES (1, @data)
|
||||||
ON CONFLICT(id) DO UPDATE SET data = excluded.data
|
ON CONFLICT(id) DO UPDATE SET data = excluded.data
|
||||||
`).run({ data });
|
`,
|
||||||
}
|
).run({ data });
|
||||||
|
},
|
||||||
};
|
};
|
||||||
@@ -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, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/"/g, """);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseMarkdown(content = "") {
|
||||||
|
let safe = escapeHtml(content);
|
||||||
|
|
||||||
|
safe = safe.replace(/```([\s\S]*?)```/g, "<pre><code>$1</code></pre>");
|
||||||
|
safe = safe.replace(/`(.*?)`/g, "<code>$1</code>");
|
||||||
|
safe = safe.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>");
|
||||||
|
safe = safe.replace(/\*(.*?)\*/g, "<em>$1</em>");
|
||||||
|
safe = safe.replace(/_(.*?)_/g, "<em>$1</em>");
|
||||||
|
safe = safe.replace(
|
||||||
|
/\[(.*?)\]\((.*?)\)/g,
|
||||||
|
'<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>',
|
||||||
|
);
|
||||||
|
|
||||||
|
return safe.replace(/\n/g, "<br>");
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(iso) {
|
||||||
|
try {
|
||||||
|
return new Date(iso).toLocaleString("en-GB", {
|
||||||
|
dateStyle: "medium",
|
||||||
|
timeStyle: "short",
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return iso;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFileTypeInfo(att) {
|
||||||
|
const fileName = String(att.name || "attachment").toLowerCase();
|
||||||
|
|
||||||
|
if (/\.(png|jpg|jpeg|gif|webp|bmp|svg)$/i.test(fileName)) {
|
||||||
|
return "image";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/\.(mp4|webm|mov|m4v)$/i.test(fileName)) {
|
||||||
|
return "video";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/\.(mp3|wav|ogg|m4a|flac)$/i.test(fileName)) {
|
||||||
|
return "audio";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "file";
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderAttachment(att) {
|
||||||
|
const url = escapeHtml(att.url || "");
|
||||||
|
const name = escapeHtml(att.name || "attachment");
|
||||||
|
const type = getFileTypeInfo(att);
|
||||||
|
|
||||||
|
if (type === "image") {
|
||||||
|
return `
|
||||||
|
<div class="attachment">
|
||||||
|
<a href="${url}" target="_blank" rel="noopener noreferrer" class="attachment-file">📎 ${name}</a>
|
||||||
|
<div class="attachment-preview attachment-image">
|
||||||
|
<img src="${url}" alt="${name}" loading="lazy">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === "video") {
|
||||||
|
return `
|
||||||
|
<div class="attachment">
|
||||||
|
<a href="${url}" target="_blank" rel="noopener noreferrer" class="attachment-file">📎 ${name}</a>
|
||||||
|
<div class="attachment-preview">
|
||||||
|
<video controls preload="metadata">
|
||||||
|
<source src="${url}">
|
||||||
|
</video>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === "audio") {
|
||||||
|
return `
|
||||||
|
<div class="attachment">
|
||||||
|
<a href="${url}" target="_blank" rel="noopener noreferrer" class="attachment-file">📎 ${name}</a>
|
||||||
|
<div class="attachment-preview">
|
||||||
|
<audio controls preload="metadata">
|
||||||
|
<source src="${url}">
|
||||||
|
</audio>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="attachment">
|
||||||
|
<a href="${url}" target="_blank" rel="noopener noreferrer" class="attachment-file">📎 ${name}</a>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderEmbeds(embeds = []) {
|
||||||
|
return embeds
|
||||||
|
.map((embed) => {
|
||||||
|
const fields = (embed.fields || [])
|
||||||
|
.map(
|
||||||
|
(field) => `
|
||||||
|
<div class="embed-field">
|
||||||
|
<div class="embed-field-name">${escapeHtml(field.name || "Field")}</div>
|
||||||
|
<div class="embed-field-value">${parseMarkdown(field.value || "")}</div>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
)
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="discord-embed">
|
||||||
|
${embed.title ? `<div class="discord-embed-title">${escapeHtml(embed.title)}</div>` : ""}
|
||||||
|
${embed.description ? `<div class="discord-embed-description">${parseMarkdown(embed.description)}</div>` : ""}
|
||||||
|
${fields}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
})
|
||||||
|
.join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
||||||
@@ -41,26 +41,26 @@ logTypes.forEach((type) => {
|
|||||||
|
|
||||||
function logToFile(type, data) {
|
function logToFile(type, data) {
|
||||||
const date = new Date().toISOString().split("T")[0];
|
const date = new Date().toISOString().split("T")[0];
|
||||||
const file = path.join(`./logs/${type}`, `${type}-${date}.log`);
|
const file = path.join(`./Logs/${type}`, `${type}-${date}.log`);
|
||||||
fs.appendFileSync(file, `[${new Date().toISOString()}] ${data}\n`);
|
fs.appendFileSync(file, `[${new Date().toISOString()}] ${data}\n`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const origLog = console.log;
|
const origLog = console.log;
|
||||||
console.log = (...args) => {
|
console.log = (...args) => {
|
||||||
origLog(...args);
|
origLog(...args);
|
||||||
logToFile("output", args.join(" "));
|
logToFile("Output", args.join(" "));
|
||||||
};
|
};
|
||||||
|
|
||||||
const origWarn = console.warn;
|
const origWarn = console.warn;
|
||||||
console.warn = (...args) => {
|
console.warn = (...args) => {
|
||||||
origWarn(...args);
|
origWarn(...args);
|
||||||
logToFile("warn", args.join(" "));
|
logToFile("Warn", args.join(" "));
|
||||||
};
|
};
|
||||||
|
|
||||||
const origError = console.error;
|
const origError = console.error;
|
||||||
console.error = (...args) => {
|
console.error = (...args) => {
|
||||||
origError(...args);
|
origError(...args);
|
||||||
logToFile("error", args.join(" "));
|
logToFile("Error", args.join(" "));
|
||||||
};
|
};
|
||||||
|
|
||||||
process.on("unhandledRejection", (reason) => {
|
process.on("unhandledRejection", (reason) => {
|
||||||
|
|||||||
+3
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "supportbot",
|
"name": "supportbot",
|
||||||
"version": "8.4",
|
"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.",
|
"description": "SupportBot is an open-sourced support system for discord. It's completely free to use with high amounts of customization to match your own brand.",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -18,10 +18,12 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://community.emeraldsrv.dev",
|
"homepage": "https://community.emeraldsrv.dev",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@vitalets/google-translate-api": "^9.2.1",
|
||||||
"axios": "^1.7.2",
|
"axios": "^1.7.2",
|
||||||
"better-sqlite3": "^12.2.0",
|
"better-sqlite3": "^12.2.0",
|
||||||
"discord.js": "^14.13.0",
|
"discord.js": "^14.13.0",
|
||||||
"figlet": "^1.9.3",
|
"figlet": "^1.9.3",
|
||||||
|
"google-translate-api": "^2.3.0",
|
||||||
"gradient-string": "^3.0.0",
|
"gradient-string": "^3.0.0",
|
||||||
"js": "^0.1.0",
|
"js": "^0.1.0",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
|
|||||||
Reference in new issue
Block a user