Compare commits
101
Commits
v8.1-Pre-1
...
26.2-pre1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
004b638dbf | ||
|
|
69371ae619 | ||
|
|
b145d5fa01 | ||
|
|
8c9fcb32e0 | ||
|
|
17d45de378 | ||
|
|
b96c60912c | ||
|
|
e97e281bda | ||
|
|
00cebb7bb5 | ||
|
|
014f263bf1 | ||
|
|
942d71040b | ||
|
|
92d21e7fc0 | ||
|
|
07e4705a6a | ||
|
|
6320296f0f | ||
|
|
f4b14e6c01 | ||
|
|
7e22831d08 | ||
|
|
9332fc9254 | ||
|
|
018c949104 | ||
|
|
b55976508e | ||
|
|
264dcb3053 | ||
|
|
22c33c0f7a | ||
|
|
57d420a9f4 | ||
|
|
f8cc08ba3e | ||
|
|
60d5c4cb1f | ||
|
|
0288e2f4de | ||
|
|
33d5a79759 | ||
|
|
30b89a3cf9 | ||
|
|
427dc2d040 | ||
|
|
033f32d596 | ||
|
|
d5fa42b55d | ||
|
|
4bb35b1e50 | ||
|
|
d451857d7b | ||
|
|
78b804176a | ||
|
|
15b780ab62 | ||
|
|
9448c4e3d6 | ||
|
|
4af9759700 | ||
|
|
835cbbe937 | ||
|
|
491bb9ba65 | ||
|
|
ced29535f9 | ||
|
|
d99e9a105b | ||
|
|
c72c7c5c34 | ||
|
|
23960bc30d | ||
|
|
abb48c281b | ||
|
|
7acb825713 | ||
|
|
dd2f329485 | ||
|
|
75577ec0bd | ||
|
|
7366bcda71 | ||
|
|
23e43cfad3 | ||
|
|
ad9a8085aa | ||
|
|
2f23e8f23d | ||
|
|
4c07b8a236 | ||
|
|
571bd586fa | ||
|
|
eac2d88873 | ||
|
|
93c793f393 | ||
|
|
1aa97b63f9 | ||
|
|
c1b76a2b84 | ||
|
|
a1429aa723 | ||
|
|
2de7c7b19c | ||
|
|
7383bd2c48 | ||
|
|
a05d65099b | ||
|
|
c6db929d12 | ||
|
|
dc88f6c51a | ||
|
|
d69fb0e26d | ||
|
|
4b2d53d3f4 | ||
|
|
e3fa8b6d72 | ||
|
|
5c6a9f7306 | ||
|
|
71e33dbf66 | ||
|
|
d468ab1575 | ||
|
|
74482af471 | ||
|
|
0b8b092cb9 | ||
|
|
3cdfaef942 | ||
|
|
fcf441a190 | ||
|
|
5a1c4ad99c | ||
|
|
d9cb72f22e | ||
|
|
58eb37124e | ||
|
|
afeeda3a8d | ||
|
|
fece90d2bd | ||
|
|
357a85fa3d | ||
|
|
f34158caa6 | ||
|
|
1bb343e49a | ||
|
|
dffa5d4161 | ||
|
|
13590b08a8 | ||
|
|
6e14ce209d | ||
|
|
72b8c2247e | ||
|
|
925504a928 | ||
|
|
236d67707f | ||
|
|
753f432c95 | ||
|
|
c824bc51b2 | ||
|
|
39ee540b55 | ||
|
|
903b214051 | ||
|
|
ac0daf5fe7 | ||
|
|
80e38a07e9 | ||
|
|
d2598e09f7 | ||
|
|
28aab47150 | ||
|
|
81bda67ae4 | ||
|
|
25bbd1ff85 | ||
|
|
164d695a5f | ||
|
|
57e4a3e77c | ||
|
|
a6ac532b2b | ||
|
|
4f14722e14 | ||
|
|
9a6eece7a1 | ||
|
|
48500fe8a4 |
No files matched your search
@@ -0,0 +1,2 @@
|
||||
Configs/supportbot.yml filter=supportbot-token
|
||||
Configs/api.yml filter=api-secrets
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
node_modules
|
||||
|
||||
# Dashboard UI source at repo root only (do not ignore public/dashboard/)
|
||||
/dashboard/
|
||||
|
||||
# Local git hooks + secret-stripping scripts (run npm run git:setup)
|
||||
scripts/
|
||||
|
||||
# Local secrets — use *.example as templates
|
||||
Configs/api.yml
|
||||
+1595
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,111 @@
|
||||
Command:
|
||||
Name: "applicationsetup"
|
||||
Description: "Posts the application panel"
|
||||
Permission:
|
||||
- "ManageGuild"
|
||||
|
||||
Application:
|
||||
ReviewChannelId: "1481380207305691287"
|
||||
CooldownHours: 24
|
||||
OneOpenApplicationPerUserPerType: true
|
||||
|
||||
Panel:
|
||||
Title: "Applications"
|
||||
Description: "Choose an application type from the dropdown below."
|
||||
Placeholder: "Select an application type"
|
||||
|
||||
Review:
|
||||
Title: "New Application"
|
||||
AcceptLabel: "Accept"
|
||||
RejectLabel: "Reject"
|
||||
|
||||
Applications:
|
||||
staff:
|
||||
Name: "Staff Team"
|
||||
Description: "Apply for the staff team"
|
||||
AcceptedRoleId: "" # Leave "" blank to disable
|
||||
Questions:
|
||||
Username:
|
||||
Label: "What is your Discord username?"
|
||||
Placeholder: "ExampleUser"
|
||||
Required: true
|
||||
Style: "Short"
|
||||
MinLength: 2
|
||||
MaxLength: 50
|
||||
|
||||
Age:
|
||||
Label: "How old are you?"
|
||||
Placeholder: "18"
|
||||
Required: true
|
||||
Style: "Short"
|
||||
MinLength: 1
|
||||
MaxLength: 3
|
||||
|
||||
Timezone:
|
||||
Label: "What is your timezone?"
|
||||
Placeholder: "GMT / BST / EST"
|
||||
Required: true
|
||||
Style: "Short"
|
||||
MinLength: 2
|
||||
MaxLength: 30
|
||||
|
||||
Experience:
|
||||
Label: "What experience do you have?"
|
||||
Placeholder: "Tell us about your experience"
|
||||
Required: true
|
||||
Style: "Paragraph"
|
||||
MinLength: 10
|
||||
MaxLength: 1000
|
||||
|
||||
Reason:
|
||||
Label: "Why should we accept you?"
|
||||
Placeholder: "Tell us why"
|
||||
Required: true
|
||||
Style: "Paragraph"
|
||||
MinLength: 10
|
||||
MaxLength: 1000
|
||||
|
||||
developer:
|
||||
Name: "Developer"
|
||||
Description: "Apply for the development team"
|
||||
AcceptedRoleId: ""
|
||||
Questions:
|
||||
Username:
|
||||
Label: "What is your Discord username?"
|
||||
Placeholder: "ExampleUser"
|
||||
Required: true
|
||||
Style: "Short"
|
||||
MinLength: 2
|
||||
MaxLength: 50
|
||||
|
||||
Languages:
|
||||
Label: "What languages do you know?"
|
||||
Placeholder: "JavaScript, Python, Lua"
|
||||
Required: true
|
||||
Style: "Paragraph"
|
||||
MinLength: 3
|
||||
MaxLength: 500
|
||||
|
||||
Projects:
|
||||
Label: "What projects have you worked on?"
|
||||
Placeholder: "Describe your work"
|
||||
Required: true
|
||||
Style: "Paragraph"
|
||||
MinLength: 10
|
||||
MaxLength: 1000
|
||||
|
||||
Availability:
|
||||
Label: "How active can you be?"
|
||||
Placeholder: "How much time can you commit?"
|
||||
Required: true
|
||||
Style: "Paragraph"
|
||||
MinLength: 5
|
||||
MaxLength: 500
|
||||
|
||||
Reason:
|
||||
Label: "Why do you want this role?"
|
||||
Placeholder: "Tell us why"
|
||||
Required: true
|
||||
Style: "Paragraph"
|
||||
MinLength: 10
|
||||
MaxLength: 1000
|
||||
@@ -0,0 +1,11 @@
|
||||
|
||||
# SupportBot | Emerald Services
|
||||
# Discord Stats addon | Created by Xiled Emerald
|
||||
|
||||
Guild_ID: "GUILD_ID_HERE"
|
||||
|
||||
StatsCategory: "Server Statistics"
|
||||
|
||||
UserCount_Name: "Total Members:"
|
||||
ChannelCount_Name: "Total Channels:"
|
||||
RoleCount_Name: "Total Roles:"
|
||||
@@ -0,0 +1,16 @@
|
||||
# Minecraft Server Configuration
|
||||
|
||||
Java:
|
||||
IP: "play.sapling.gg"
|
||||
Port: 25565
|
||||
DisplayName: "Java Edition"
|
||||
Bedrock:
|
||||
IP: "play.sapling.gg"
|
||||
Port: 19132
|
||||
DisplayName: "Bedrock Edition"
|
||||
|
||||
Embed:
|
||||
Color: "#57F287"
|
||||
OfflineColor: "#ED4245"
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
# SupportBot Temporary Voice Channels Configuration
|
||||
# Addon Version 1.0
|
||||
|
||||
Enabled: true
|
||||
|
||||
# The ID of the voice channel that users join to create a new one
|
||||
JoinToCreateChannelID: "CHANNEL_ID"
|
||||
|
||||
# The category ID where new channels should be created (Optional)
|
||||
# If left blank, it will create it in the same category as the JoinToCreate channel
|
||||
CategoryID: "CATEGORY_ID"
|
||||
|
||||
# The name pattern for new channels
|
||||
|
||||
# Placeholders:
|
||||
# {user}: Username
|
||||
ChannelNamePattern: "🔊 {user}'s Channel"
|
||||
|
||||
# Should the channel be deleted when the last person leaves?
|
||||
AutoDelete: true
|
||||
|
||||
# Voice Channel Hub (Control Panel)
|
||||
Hub:
|
||||
Enabled: true
|
||||
SendOnCreate: true
|
||||
LayoutStyle: "classic" # classic | modern
|
||||
|
||||
# Classic Layout Settings (Standard Embed + Buttons)
|
||||
Classic:
|
||||
Title: "Voice Channel Hub"
|
||||
Description: "Manage your voice channel using the buttons below."
|
||||
Color: "#57F287"
|
||||
|
||||
# Modern Layout Settings (V2 Containers)
|
||||
Modern:
|
||||
Title: "Voice Channel Controller"
|
||||
Body: "Control temporary voice channel using this control panel."
|
||||
Color: "#57F287"
|
||||
@@ -0,0 +1,28 @@
|
||||
# +==================================================================================+
|
||||
# | |
|
||||
# | __ __ _ _ _ _ _ |
|
||||
# | \ \ / /__ _ _ ___| |__ | | ___ _ _ / \ __| | __| | ___ _ __ |
|
||||
# | \ \ / / _ \| | | |/ __| '_ \| |/ _ \ | | | / _ \ / _` |/ _` |/ _ \| '_ \ |
|
||||
# | \ V / (_) | |_| | (__| | | | | __/ |_| | / ___ \ (_| | (_| | (_) | | | | |
|
||||
# | \_/ \___/ \__,_|\___|_| |_|_|\___|\__, | /_/ \_\__,_|\__,_|\___/|_| |_| |
|
||||
# | |___/ |
|
||||
# | |
|
||||
# | Vouchley Addon for SupportBot |
|
||||
# | Created by Emerald Services |
|
||||
# | |
|
||||
# +==================================================================================+
|
||||
|
||||
Vouchley:
|
||||
API_Key: "REQUEST_API_KEY_BELOW"
|
||||
|
||||
# Vouchley API Documentation: https://www.vouchley.com/docs/api/getting-started
|
||||
# To use the API you will need an API key. To get this create a ticket in https://discord.gg/buzz
|
||||
|
||||
Embed:
|
||||
Colour: 0x6063fc # 0x<hexcode> without the #
|
||||
Ephermal: true # Make the profile searches hidden so only the interacting user can see them.
|
||||
|
||||
Command:
|
||||
Name: "vouchley" # The name of the command (/vouchley)
|
||||
Description: "Search a Vouchley Profile." # The description of the command (/vouchley)
|
||||
Permissions: ["SendMessages"]
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"applications": []
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
const Discord = require("discord.js");
|
||||
const { Command } = require("../Structures/Addon.js");
|
||||
const fs = require("fs");
|
||||
const yaml = require("js-yaml");
|
||||
const axios = require("axios");
|
||||
|
||||
// Load configs
|
||||
const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8"));
|
||||
const minecraftConfig = yaml.load(fs.readFileSync("./Addons/Configs/minecraft.yml", "utf8"));
|
||||
|
||||
module.exports = new Command({
|
||||
name: "serverip",
|
||||
description: "Display the Minecraft server IP and status",
|
||||
options: [
|
||||
{
|
||||
name: "type",
|
||||
description: "The edition of Minecraft (Java or Bedrock)",
|
||||
type: Discord.ApplicationCommandOptionType.String,
|
||||
required: true,
|
||||
choices: [
|
||||
{ name: "Java", value: "java" },
|
||||
{ name: "Bedrock", value: "bedrock" },
|
||||
],
|
||||
},
|
||||
],
|
||||
permissions: ["SendMessages"],
|
||||
|
||||
async run(interaction) {
|
||||
const type = interaction.options.getString("type");
|
||||
const config = type === "java" ? minecraftConfig.Java : minecraftConfig.Bedrock;
|
||||
|
||||
await interaction.deferReply();
|
||||
|
||||
try {
|
||||
const response = await axios.get(`https://api.mcstatus.io/v2/status/${type}/${config.IP}:${config.Port}`);
|
||||
const data = response.data;
|
||||
|
||||
if (!data.online) {
|
||||
throw new Error("Server is offline");
|
||||
}
|
||||
|
||||
const embed = new Discord.EmbedBuilder()
|
||||
.setTitle(`${config.DisplayName} Status`)
|
||||
.setTimestamp();
|
||||
|
||||
embed.setColor(minecraftConfig.Embed.Color || supportbot.Embed.Colours.General)
|
||||
.setDescription(`The server is currently **Online**!`)
|
||||
.addFields(
|
||||
{ name: "IP Address", value: `\`${config.IP}\``, inline: true },
|
||||
{ name: "Port", value: `\`${config.Port}\``, inline: true },
|
||||
{ name: "Players", value: `\`${data.players.online}/${data.players.max}\``, inline: true },
|
||||
{ name: "Version", value: `\`${type === "java" ? data.version.name_clean : data.version.name || "Unknown"}\``, inline: true }
|
||||
);
|
||||
|
||||
if (data.motd && data.motd.clean) {
|
||||
embed.addFields({ name: "MOTD", value: `\`\`\`${data.motd.clean}\`\`\`` });
|
||||
}
|
||||
|
||||
await interaction.editReply({ embeds: [embed] });
|
||||
} catch (error) {
|
||||
console.error("Error fetching Minecraft status:", error);
|
||||
|
||||
const config = interaction.options.getString("type") === "java" ? minecraftConfig.Java : minecraftConfig.Bedrock;
|
||||
|
||||
const embed = new Discord.EmbedBuilder()
|
||||
.setTitle(`${config.DisplayName} Status`)
|
||||
.setTimestamp()
|
||||
.setColor(minecraftConfig.Embed.OfflineColor || "#FF0000")
|
||||
.setDescription(`The server is currently **Offline**.`)
|
||||
.addFields(
|
||||
{ name: "IP Address", value: `\`${config.IP}\``, inline: true },
|
||||
{ name: "Port", value: `\`${config.Port}\``, inline: true }
|
||||
);
|
||||
|
||||
await interaction.editReply({ embeds: [embed] });
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
# SupportBot Sanction Addon
|
||||
# Created by ItsNyoty
|
||||
|
||||
LogChannel: "1275426007980965908"
|
||||
# The ID of the channel where the logs will be sent.
|
||||
@@ -0,0 +1,157 @@
|
||||
const Discord = require('discord.js');
|
||||
const fs = require('fs');
|
||||
const yaml = require('js-yaml');
|
||||
const { Command } = require('../Structures/Addon.js');
|
||||
const supportbot = yaml.load(fs.readFileSync('./Configs/supportbot.yml', 'utf8'));
|
||||
const sactionaddon = yaml.load(fs.readFileSync('./Addons/Configs/sactionaddon.yml', 'utf8'));
|
||||
|
||||
module.exports = new Command({
|
||||
name: 'sanction',
|
||||
description: 'Sanction a user (kick, ban, or timeout).',
|
||||
options: [
|
||||
{
|
||||
name: 'action',
|
||||
type: 3,
|
||||
description: 'The action to perform (kick, ban, timeout)',
|
||||
required: true,
|
||||
choices: [
|
||||
{ name: 'Kick', value: 'kick' },
|
||||
{ name: 'Ban', value: 'ban' },
|
||||
{ name: 'Timeout', value: 'timeout' }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'user',
|
||||
type: 6,
|
||||
description: 'The user to sanction',
|
||||
required: true
|
||||
},
|
||||
{
|
||||
name: 'reason',
|
||||
type: 3,
|
||||
description: 'Reason for the sanction',
|
||||
required: false
|
||||
},
|
||||
{
|
||||
name: 'duration',
|
||||
type: 3,
|
||||
description: 'Duration of the timeout (e.g., "10m", "2h", "1d")',
|
||||
required: false
|
||||
}
|
||||
],
|
||||
permissions: [],
|
||||
|
||||
async run(interaction) {
|
||||
const action = interaction.options.getString('action');
|
||||
const user = interaction.options.getUser('user');
|
||||
const reason = interaction.options.getString('reason') || 'No reason provided';
|
||||
const duration = interaction.options.getString('duration');
|
||||
const staffRoleId = supportbot.Roles.StaffMember.Staff;
|
||||
|
||||
const member = interaction.guild.members.cache.get(interaction.user.id);
|
||||
if (!member || !member.roles.cache.has(staffRoleId)) {
|
||||
return interaction.reply({ content: 'You do not have permission to use this command.', ephemeral: true });
|
||||
}
|
||||
|
||||
const targetMember = interaction.guild.members.cache.get(user.id);
|
||||
if (!targetMember) {
|
||||
return interaction.reply({ content: 'The user is not a member of this guild.', ephemeral: true });
|
||||
}
|
||||
|
||||
try {
|
||||
let actionMessage = '';
|
||||
let logEmbed = new Discord.EmbedBuilder().setColor(supportbot.Embed.Colours.General);
|
||||
|
||||
switch (action) {
|
||||
case 'kick':
|
||||
if (!targetMember.kickable) return interaction.reply({ content: 'I cannot kick this user.', ephemeral: true });
|
||||
await targetMember.kick(reason);
|
||||
actionMessage = `Successfully kicked ${user.tag} for: ${reason}`;
|
||||
logEmbed
|
||||
.setTitle('User Kicked')
|
||||
.setDescription(`**User:** ${user.tag}\n**Reason:** ${reason}`)
|
||||
.addFields(
|
||||
{ name: 'Action By', value: interaction.user.tag, inline: true },
|
||||
{ name: 'Action', value: 'Kick', inline: true },
|
||||
{ name: 'Timestamp', value: new Date().toISOString(), inline: true }
|
||||
);
|
||||
break;
|
||||
|
||||
case 'ban':
|
||||
if (!targetMember.bannable) return interaction.reply({ content: 'I cannot ban this user.', ephemeral: true });
|
||||
await targetMember.ban({ reason });
|
||||
actionMessage = `Successfully banned ${user.tag} for: ${reason}`;
|
||||
logEmbed
|
||||
.setTitle('User Banned')
|
||||
.setDescription(`**User:** ${user.tag}\n**Reason:** ${reason}`)
|
||||
.addFields(
|
||||
{ name: 'Action By', value: interaction.user.tag, inline: true },
|
||||
{ name: 'Action', value: 'Ban', inline: true },
|
||||
{ name: 'Timestamp', value: new Date().toISOString(), inline: true }
|
||||
);
|
||||
break;
|
||||
|
||||
case 'timeout':
|
||||
// Ensure a valid duration is provided
|
||||
if (!duration) return interaction.reply({ content: 'You must specify a duration for the timeout.', ephemeral: true });
|
||||
|
||||
// Convert duration to milliseconds
|
||||
const parsedDuration = ms(duration);
|
||||
if (!parsedDuration) return interaction.reply({ content: 'Invalid duration format. Use "1s", "5m", "2h", or "1d".', ephemeral: true });
|
||||
|
||||
// Apply the timeout
|
||||
await targetMember.timeout(parsedDuration, reason);
|
||||
actionMessage = `Successfully timed out ${user.tag} for ${duration} with reason: ${reason}`;
|
||||
logEmbed
|
||||
.setTitle('User Timed Out')
|
||||
.setDescription(`**User:** ${user.tag}\n**Duration:** ${duration}\n**Reason:** ${reason}`)
|
||||
.addFields(
|
||||
{ name: 'Action By', value: interaction.user.tag, inline: true },
|
||||
{ name: 'Action', value: 'Timeout', inline: true },
|
||||
{ name: 'Timestamp', value: new Date().toISOString(), inline: true }
|
||||
);
|
||||
break;
|
||||
|
||||
default:
|
||||
return interaction.reply({ content: 'Invalid action specified.', ephemeral: true });
|
||||
}
|
||||
|
||||
// Send the action message
|
||||
interaction.reply({ content: actionMessage, ephemeral: true });
|
||||
|
||||
// Log the action to the logging channel
|
||||
const logChannel = interaction.guild.channels.cache.get(sactionaddon.LogChannel);
|
||||
if (logChannel) {
|
||||
logChannel.send({ embeds: [logEmbed] });
|
||||
} else {
|
||||
console.error('Logging channel not found.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error executing sanction command:', error);
|
||||
interaction.reply({ content: 'An error occurred while executing the command.', ephemeral: true });
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Converts a time duration string to milliseconds.
|
||||
* @param {string} duration - The duration string (e.g., "10m", "2h", "1d").
|
||||
* @returns {number} - The duration in milliseconds.
|
||||
*/
|
||||
function ms(duration) {
|
||||
const regex = /^(\d+)([smhd])$/;
|
||||
const match = regex.exec(duration);
|
||||
|
||||
if (!match) return null;
|
||||
|
||||
const value = parseInt(match[1], 10);
|
||||
const unit = match[2];
|
||||
|
||||
switch (unit) {
|
||||
case 's': return value * 1000; // seconds
|
||||
case 'm': return value * 60 * 1000; // minutes
|
||||
case 'h': return value * 60 * 60 * 1000; // hours
|
||||
case 'd': return value * 24 * 60 * 60 * 1000; // days
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
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: "tags",
|
||||
description: "Retrieve a saved tag.",
|
||||
options: [
|
||||
{
|
||||
name: 'name',
|
||||
type: Discord.ApplicationCommandOptionType.String,
|
||||
description: 'The name of the tag',
|
||||
required: true,
|
||||
autocomplete: true
|
||||
},
|
||||
],
|
||||
permissions: ["SendMessages"],
|
||||
|
||||
async autocomplete(interaction) {
|
||||
const tagsPath = './Addons/Data/tags.json';
|
||||
const focusedValue = interaction.options.getFocused().toLowerCase();
|
||||
|
||||
if (!fs.existsSync(tagsPath)) {
|
||||
return interaction.respond([]);
|
||||
}
|
||||
|
||||
const tags = JSON.parse(fs.readFileSync(tagsPath, 'utf8'));
|
||||
const choices = Object.keys(tags)
|
||||
.filter(tag => tag.toLowerCase().includes(focusedValue))
|
||||
.slice(0, 25) // Discord has a limit of 25 choices
|
||||
.map(tag => ({ name: tag, value: tag }));
|
||||
|
||||
return interaction.respond(choices);
|
||||
},
|
||||
|
||||
async run(interaction) {
|
||||
const tagsPath = './Addons/Data/tags.json';
|
||||
const tagName = interaction.options.getString('name').toLowerCase();
|
||||
|
||||
if (!fs.existsSync(tagsPath)) {
|
||||
const embed = new Discord.EmbedBuilder()
|
||||
.setDescription('No tags have been created yet.')
|
||||
.setColor(supportbot.Embed.Colours.General);
|
||||
return interaction.reply({ embeds: [embed], ephemeral: true });
|
||||
}
|
||||
|
||||
const tags = JSON.parse(fs.readFileSync(tagsPath, 'utf8'));
|
||||
|
||||
if (tags[tagName]) {
|
||||
const tag = tags[tagName];
|
||||
|
||||
if (tag.embed) {
|
||||
// For embeds, Discord automatically handles line breaks correctly
|
||||
const embed = new Discord.EmbedBuilder()
|
||||
.setDescription(tag.content)
|
||||
.setColor(supportbot.Embed.Colours.General);
|
||||
|
||||
if (tag.image) {
|
||||
embed.setImage(tag.image);
|
||||
}
|
||||
|
||||
return interaction.reply({ embeds: [embed], ephemeral: tag.ephemeral || false });
|
||||
} else {
|
||||
// For plain text, we need to keep the line breaks
|
||||
const response = {
|
||||
content: tag.content,
|
||||
ephemeral: tag.ephemeral || false
|
||||
};
|
||||
|
||||
if (tag.image) {
|
||||
response.files = [tag.image];
|
||||
}
|
||||
|
||||
return interaction.reply(response);
|
||||
}
|
||||
} else {
|
||||
const embed = new Discord.EmbedBuilder()
|
||||
.setDescription(`Tag "${tagName}" not found.`)
|
||||
.setColor(supportbot.Embed.Colours.General);
|
||||
return interaction.reply({ embeds: [embed], ephemeral: true });
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,76 @@
|
||||
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: "tagscreate",
|
||||
description: "Create a new tag.",
|
||||
options: [
|
||||
{
|
||||
name: 'name',
|
||||
type: Discord.ApplicationCommandOptionType.String,
|
||||
description: 'The name of the tag',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'content',
|
||||
type: Discord.ApplicationCommandOptionType.String,
|
||||
description: 'The content of the tag (use ; or /n for new lines)',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'image',
|
||||
type: Discord.ApplicationCommandOptionType.Attachment,
|
||||
description: 'The image URL of the tag',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'embed',
|
||||
type: Discord.ApplicationCommandOptionType.Boolean,
|
||||
description: 'Do you want this tag to send as an embed',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'ephemeral',
|
||||
type: Discord.ApplicationCommandOptionType.Boolean,
|
||||
description: 'Do you want this tag to send as ephemeral',
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
permissions: ["SendMessages"],
|
||||
|
||||
async run(interaction) {
|
||||
const tagName = interaction.options.getString('name').toLowerCase();
|
||||
let tagContent = interaction.options.getString('content');
|
||||
const tagImage = interaction.options.getAttachment('image');
|
||||
const isEmbed = interaction.options.getBoolean('embed');
|
||||
const isEphemeral = interaction.options.getBoolean('ephemeral');
|
||||
const tagsPath = './Addons/Data/tags.json';
|
||||
|
||||
// Process line breaks in content - support both ; and /n notations
|
||||
tagContent = tagContent.replace(/;/g, '\n').replace(/\/n/g, '\n');
|
||||
|
||||
let tags = {};
|
||||
|
||||
if (fs.existsSync(tagsPath)) {
|
||||
tags = JSON.parse(fs.readFileSync(tagsPath, 'utf8'));
|
||||
}
|
||||
|
||||
tags[tagName] = {
|
||||
content: tagContent,
|
||||
image: tagImage ? tagImage.url : null,
|
||||
embed: isEmbed || false,
|
||||
ephemeral: isEphemeral || false,
|
||||
};
|
||||
|
||||
fs.writeFileSync(tagsPath, JSON.stringify(tags, null, 4));
|
||||
|
||||
const embed = new Discord.EmbedBuilder()
|
||||
.setDescription(`Tag "${tagName}" created successfully.`)
|
||||
.setColor(supportbot.Embed.Colours.General);
|
||||
|
||||
return interaction.reply({ embeds: [embed], ephemeral: true });
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
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: "tagsdelete",
|
||||
description: "Delete an existing tag.",
|
||||
options: [
|
||||
{
|
||||
name: 'name',
|
||||
type: Discord.ApplicationCommandOptionType.String,
|
||||
description: 'The name of the tag to delete',
|
||||
required: true,
|
||||
autocomplete: true
|
||||
},
|
||||
],
|
||||
permissions: ["SendMessages"],
|
||||
|
||||
async autocomplete(interaction) {
|
||||
const tagsPath = './Addons/Data/tags.json';
|
||||
const focusedValue = interaction.options.getFocused().toLowerCase();
|
||||
|
||||
if (!fs.existsSync(tagsPath)) {
|
||||
return interaction.respond([]);
|
||||
}
|
||||
|
||||
const tags = JSON.parse(fs.readFileSync(tagsPath, 'utf8'));
|
||||
const choices = Object.keys(tags)
|
||||
.filter(tag => tag.toLowerCase().includes(focusedValue))
|
||||
.slice(0, 25) // Discord has a limit of 25 choices
|
||||
.map(tag => ({ name: tag, value: tag }));
|
||||
|
||||
return interaction.respond(choices);
|
||||
},
|
||||
|
||||
async run(interaction) {
|
||||
const tagsPath = './Addons/Data/tags.json';
|
||||
const tagName = interaction.options.getString('name').toLowerCase();
|
||||
|
||||
if (!fs.existsSync(tagsPath)) {
|
||||
const embed = new Discord.EmbedBuilder()
|
||||
.setDescription('No tags have been created yet.')
|
||||
.setColor(supportbot.Embed.Colours.General);
|
||||
return interaction.reply({ embeds: [embed], ephemeral: true });
|
||||
}
|
||||
|
||||
let tags = JSON.parse(fs.readFileSync(tagsPath, 'utf8'));
|
||||
|
||||
if (!tags[tagName]) {
|
||||
const embed = new Discord.EmbedBuilder()
|
||||
.setDescription(`Tag "${tagName}" not found.`)
|
||||
.setColor(supportbot.Embed.Colours.General);
|
||||
return interaction.reply({ embeds: [embed], ephemeral: true });
|
||||
}
|
||||
|
||||
delete tags[tagName];
|
||||
|
||||
fs.writeFileSync(tagsPath, JSON.stringify(tags, null, 4));
|
||||
|
||||
const embed = new Discord.EmbedBuilder()
|
||||
.setDescription(`Tag "${tagName}" deleted successfully.`)
|
||||
.setColor(supportbot.Embed.Colours.General);
|
||||
|
||||
return interaction.reply({ embeds: [embed], ephemeral: true });
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
PayPal:
|
||||
ClientID: "clientID_Here"
|
||||
ClientSecret: "ClientSecret_Here"
|
||||
Mode: "live" # or "live"
|
||||
Email: "Email_Here" # Add your PayPal email here
|
||||
Embed:
|
||||
Colours:
|
||||
General: "#0099ff"
|
||||
Success: "#00ff00" # Add more colors if needed
|
||||
Error: "#ff0000" # Add more colors if needed
|
||||
Footer: "PayPal Invoice" # Add footer text here
|
||||
Logo: "https://your-logo-url.com/logo.png" # Add logo URL if used in invoice
|
||||
Note: "Thank you for your business!" # Add a note if used in invoice
|
||||
TOS: "Please review our terms and conditions. https://domain.com/tos" # Add terms of service if used in invoice
|
||||
@@ -0,0 +1,146 @@
|
||||
const { EmbedBuilder, ButtonBuilder, ButtonStyle, ActionRowBuilder, ApplicationCommandOptionType } = require('discord.js');
|
||||
const fs = require('fs');
|
||||
const yaml = require('js-yaml');
|
||||
const paypal = require('paypal-rest-sdk');
|
||||
const { Command } = require('../Structures/Addon.js'); // Adjust the path as needed
|
||||
|
||||
// Load the PayPal configuration
|
||||
const config = yaml.load(fs.readFileSync("./Addons/Configs/paypal.yml", "utf8"));
|
||||
const supportbot = config;
|
||||
|
||||
// Configure PayPal SDK
|
||||
paypal.configure({
|
||||
mode: supportbot.PayPal.Mode, // sandbox or live
|
||||
client_id: supportbot.PayPal.ClientID,
|
||||
client_secret: supportbot.PayPal.ClientSecret
|
||||
});
|
||||
|
||||
module.exports = new Command({
|
||||
name: "invoice",
|
||||
description: "Generate a PayPal invoice for a user.",
|
||||
options: [
|
||||
{
|
||||
name: "user",
|
||||
description: "The user to notify.",
|
||||
type: ApplicationCommandOptionType.User,
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: "price",
|
||||
description: "The amount to invoice.",
|
||||
type: ApplicationCommandOptionType.String,
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: "service",
|
||||
description: "The service provided.",
|
||||
type: ApplicationCommandOptionType.String,
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: "email",
|
||||
description: "The email to invoice.",
|
||||
type: ApplicationCommandOptionType.String,
|
||||
required: true,
|
||||
}
|
||||
],
|
||||
permissions: ["Administrator"],
|
||||
|
||||
async run(interaction) {
|
||||
const user = interaction.options.getUser("user");
|
||||
const price = interaction.options.getString("price");
|
||||
const service = interaction.options.getString("service");
|
||||
const email = interaction.options.getString("email");
|
||||
|
||||
// Validate the price format
|
||||
if (isNaN(price) || parseFloat(price) <= 0) {
|
||||
const errorEmbed = new EmbedBuilder()
|
||||
.setDescription(`The price provided is invalid.`)
|
||||
.setColor(supportbot.Embed.Colours.Error)
|
||||
.setFooter({ text: supportbot.Embed.Footer });
|
||||
return interaction.reply({ embeds: [errorEmbed], ephemeral: true });
|
||||
}
|
||||
|
||||
const invoiceData = {
|
||||
"merchant_info": {
|
||||
"email": supportbot.PayPal.Email,
|
||||
"business_name": interaction.guild.name
|
||||
},
|
||||
"billing_info": [{
|
||||
"email": email
|
||||
}],
|
||||
"items": [{
|
||||
"name": service,
|
||||
"quantity": 1,
|
||||
"unit_price": {
|
||||
"currency": "USD",
|
||||
"value": price
|
||||
}
|
||||
}],
|
||||
"logo_url": supportbot.PayPal.Logo,
|
||||
"note": supportbot.PayPal.Note,
|
||||
"terms": supportbot.PayPal.TOS,
|
||||
"payment_term": {
|
||||
"term_type": "NET_45"
|
||||
},
|
||||
"tax_inclusive": false,
|
||||
"total_amount": {
|
||||
"currency": "USD",
|
||||
"value": price
|
||||
}
|
||||
};
|
||||
|
||||
paypal.invoice.create(invoiceData, function (error, invoice) {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
const errorEmbed = new EmbedBuilder()
|
||||
.setTitle(`PayPal Error`)
|
||||
.setDescription(`There was an error creating the invoice. Please check your configuration and try again.`)
|
||||
.setColor(supportbot.Embed.Colours.Error)
|
||||
.setFooter({ text: supportbot.Embed.Footer });
|
||||
return interaction.reply({ embeds: [errorEmbed], ephemeral: true });
|
||||
}
|
||||
|
||||
const invoiceId = invoice.id;
|
||||
paypal.invoice.send(invoiceId, function (error) {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
const errorEmbed = new EmbedBuilder()
|
||||
.setTitle(`PayPal Error`)
|
||||
.setDescription(`There was an error sending the invoice. Please check your configuration and try again.`)
|
||||
.setColor(supportbot.Embed.Colours.Error)
|
||||
.setFooter({ text: supportbot.Embed.Footer });
|
||||
return interaction.reply({ embeds: [errorEmbed], ephemeral: true });
|
||||
}
|
||||
|
||||
const invoiceEmbed = new EmbedBuilder()
|
||||
.setTitle(`Payment System | ${interaction.guild.name}`)
|
||||
.addFields(
|
||||
{ name: 'Service', value: service, inline: true },
|
||||
{ name: 'Amount Due', value: `$${price}`, inline: true },
|
||||
{ name: 'Currency', value: 'USD', inline: true },
|
||||
{ name: 'Invoice ID', value: `\`${invoiceId}\``, inline: false }
|
||||
)
|
||||
.setColor(supportbot.Embed.Colours.General)
|
||||
.setFooter({ text: supportbot.Embed.Footer })
|
||||
.setTimestamp();
|
||||
|
||||
// Create a button with a link to the invoice
|
||||
const invoiceButton = new ButtonBuilder()
|
||||
.setLabel('View Invoice')
|
||||
.setStyle(ButtonStyle.Link)
|
||||
.setURL(`https://www.paypal.com/invoice/payerView/details/${invoiceId}`);
|
||||
|
||||
// Create the action row
|
||||
const actionRow = new ActionRowBuilder()
|
||||
.addComponents(invoiceButton);
|
||||
|
||||
// Send the embed with the button
|
||||
interaction.reply({
|
||||
embeds: [invoiceEmbed],
|
||||
components: [actionRow]
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,227 @@
|
||||
const Discord = require("discord.js");
|
||||
const { Event } = require("../Structures/Addon.js");
|
||||
const fs = require("fs");
|
||||
const yaml = require("js-yaml");
|
||||
|
||||
const tempVCConfig = yaml.load(fs.readFileSync("./Addons/Configs/tempvc.yml", "utf8"));
|
||||
|
||||
const tempChannels = new Map();
|
||||
|
||||
const voiceStateUpdate = new Event("voiceStateUpdate", async (client, oldState, newState) => {
|
||||
if (!tempVCConfig.Enabled) return;
|
||||
|
||||
const member = newState.member;
|
||||
const guild = newState.guild;
|
||||
|
||||
if (newState.channelId === tempVCConfig.JoinToCreateChannelID) {
|
||||
console.log(`[TempVC] ${member.user.tag} joined the Join-to-Create channel.`);
|
||||
}
|
||||
|
||||
if (newState.channelId === tempVCConfig.JoinToCreateChannelID) {
|
||||
try {
|
||||
const parentCategory = tempVCConfig.CategoryID || newState.channel.parentId;
|
||||
const channelName = tempVCConfig.ChannelNamePattern.replace("{user}", member.user.username);
|
||||
|
||||
const newChannel = await guild.channels.create({
|
||||
name: channelName,
|
||||
type: Discord.ChannelType.GuildVoice,
|
||||
parent: parentCategory,
|
||||
permissionOverwrites: [
|
||||
{
|
||||
id: member.id,
|
||||
allow: [Discord.PermissionFlagsBits.ManageChannels, Discord.PermissionFlagsBits.MoveMembers],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
tempChannels.set(newChannel.id, member.id);
|
||||
await member.voice.setChannel(newChannel);
|
||||
|
||||
console.log(`[TempVC] Created channel "${channelName}" for ${member.user.tag}`);
|
||||
|
||||
if (tempVCConfig.Hub.Enabled && tempVCConfig.Hub.SendOnCreate) {
|
||||
try {
|
||||
const { ActionRowBuilder, ButtonBuilder, EmbedBuilder, MessageFlags, ButtonStyle } = Discord;
|
||||
const layoutStyle = tempVCConfig.Hub.LayoutStyle || "classic";
|
||||
|
||||
if (layoutStyle === "classic") {
|
||||
const config = tempVCConfig.Hub.Classic;
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle(config.Title)
|
||||
.setDescription(config.Description)
|
||||
.setColor(config.Color);
|
||||
|
||||
const buttons = new ActionRowBuilder().addComponents(
|
||||
new ButtonBuilder().setCustomId("tempvc_lock").setLabel("Lock").setEmoji("🔒").setStyle(ButtonStyle.Secondary),
|
||||
new ButtonBuilder().setCustomId("tempvc_unlock").setLabel("Unlock").setEmoji("🔓").setStyle(ButtonStyle.Secondary),
|
||||
new ButtonBuilder().setCustomId("tempvc_rename").setLabel("Rename").setEmoji("✏️").setStyle(ButtonStyle.Secondary),
|
||||
new ButtonBuilder().setCustomId("tempvc_limit").setLabel("Limit").setEmoji("👥").setStyle(ButtonStyle.Secondary),
|
||||
new ButtonBuilder().setCustomId("tempvc_claim").setLabel("Claim Ownership").setEmoji("👑").setStyle(ButtonStyle.Primary)
|
||||
);
|
||||
|
||||
await newChannel.send({ embeds: [embed], components: [buttons] });
|
||||
} else {
|
||||
const config = tempVCConfig.Hub.Modern;
|
||||
const colorInt = parseInt(config.Color.replace("#", ""), 16);
|
||||
|
||||
const container = {
|
||||
type: 11,
|
||||
accent_color: colorInt,
|
||||
children: [
|
||||
{
|
||||
type: 12,
|
||||
children: [
|
||||
{ type: 13, content: `## ${config.Title}` },
|
||||
{ type: 13, content: config.Body }
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 15,
|
||||
divider: true
|
||||
},
|
||||
{
|
||||
type: 1,
|
||||
components: [
|
||||
{ type: 2, custom_id: "tempvc_lock", label: "Lock", emoji: { name: "🔒" }, style: 2 },
|
||||
{ type: 2, custom_id: "tempvc_unlock", label: "Unlock", emoji: { name: "🔓" }, style: 2 },
|
||||
{ type: 2, custom_id: "tempvc_rename", label: "Rename", emoji: { name: "✏️" }, style: 2 },
|
||||
{ type: 2, custom_id: "tempvc_limit", label: "Limit", emoji: { name: "👥" }, style: 2 }
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 1,
|
||||
components: [
|
||||
{ type: 2, custom_id: "tempvc_claim", label: "Claim Ownership", emoji: { name: "👑" }, style: 1 }
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
await newChannel.send({
|
||||
flags: MessageFlags.IsComponentsV2,
|
||||
components: [container]
|
||||
});
|
||||
}
|
||||
} catch (hubError) {
|
||||
console.error("[TempVC] Error sending Hub:", hubError);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[TempVC] Error creating temporary channel:", error);
|
||||
}
|
||||
}
|
||||
|
||||
if (oldState.channelId && tempChannels.has(oldState.channelId)) {
|
||||
const oldChannel = oldState.channel;
|
||||
|
||||
if (oldChannel && oldChannel.members.size === 0) {
|
||||
try {
|
||||
await oldChannel.delete();
|
||||
tempChannels.delete(oldState.channelId);
|
||||
console.log(`[TempVC] Deleted empty temporary channel "${oldChannel.name}"`);
|
||||
} catch (error) {
|
||||
if (error.code !== 10003) {
|
||||
console.error("[TempVC] Error deleting temporary channel:", error);
|
||||
}
|
||||
tempChannels.delete(oldState.channelId);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const interactionEvent = new Event("interactionCreate", async (client, interaction) => {
|
||||
if (!interaction.isButton() && !interaction.isModalSubmit()) return;
|
||||
if (!interaction.customId.startsWith("tempvc_")) return;
|
||||
|
||||
const channel = interaction.channel;
|
||||
if (!tempChannels.has(channel.id)) {
|
||||
return interaction.reply({ content: "This channel is no longer a temporary voice channel.", flags: Discord.MessageFlags.Ephemeral });
|
||||
}
|
||||
|
||||
const ownerId = tempChannels.get(channel.id);
|
||||
const isOwner = interaction.user.id === ownerId || interaction.member.permissions.has(Discord.PermissionFlagsBits.Administrator);
|
||||
|
||||
if (interaction.customId === "tempvc_claim") {
|
||||
const currentOwnerInVC = channel.members.has(ownerId);
|
||||
if (currentOwnerInVC && interaction.user.id !== ownerId) {
|
||||
return interaction.reply({ content: "The owner is still in the voice channel!", flags: Discord.MessageFlags.Ephemeral });
|
||||
}
|
||||
|
||||
tempChannels.set(channel.id, interaction.user.id);
|
||||
await channel.permissionOverwrites.edit(interaction.user.id, {
|
||||
ManageChannels: true,
|
||||
MoveMembers: true,
|
||||
});
|
||||
|
||||
return interaction.reply({ content: "You are now the owner of this voice channel!", flags: Discord.MessageFlags.Ephemeral });
|
||||
}
|
||||
|
||||
if (!isOwner) {
|
||||
return interaction.reply({ content: "Only the channel owner can use these controls.", flags: Discord.MessageFlags.Ephemeral });
|
||||
}
|
||||
|
||||
try {
|
||||
switch (interaction.customId) {
|
||||
case "tempvc_lock":
|
||||
await channel.permissionOverwrites.edit(interaction.guild.id, { Connect: false });
|
||||
await interaction.reply({ content: "Channel locked! 🔒", flags: Discord.MessageFlags.Ephemeral });
|
||||
break;
|
||||
case "tempvc_unlock":
|
||||
await channel.permissionOverwrites.edit(interaction.guild.id, { Connect: null });
|
||||
await interaction.reply({ content: "Channel unlocked! 🔓", flags: Discord.MessageFlags.Ephemeral });
|
||||
break;
|
||||
case "tempvc_rename":
|
||||
const modal = new Discord.ModalBuilder()
|
||||
.setCustomId("tempvc_modal_rename")
|
||||
.setTitle("Rename Channel")
|
||||
.addComponents(
|
||||
new Discord.ActionRowBuilder().addComponents(
|
||||
new Discord.TextInputBuilder()
|
||||
.setCustomId("name")
|
||||
.setLabel("New Name")
|
||||
.setStyle(Discord.TextInputStyle.Short)
|
||||
.setPlaceholder(channel.name)
|
||||
.setRequired(true)
|
||||
)
|
||||
);
|
||||
await interaction.showModal(modal);
|
||||
break;
|
||||
case "tempvc_limit":
|
||||
const limitModal = new Discord.ModalBuilder()
|
||||
.setCustomId("tempvc_modal_limit")
|
||||
.setTitle("Set User Limit")
|
||||
.addComponents(
|
||||
new Discord.ActionRowBuilder().addComponents(
|
||||
new Discord.TextInputBuilder()
|
||||
.setCustomId("limit")
|
||||
.setLabel("Limit (0-99)")
|
||||
.setStyle(Discord.TextInputStyle.Short)
|
||||
.setPlaceholder("0 for no limit")
|
||||
.setRequired(true)
|
||||
)
|
||||
);
|
||||
await interaction.showModal(limitModal);
|
||||
break;
|
||||
}
|
||||
|
||||
if (interaction.isModalSubmit()) {
|
||||
if (interaction.customId === "tempvc_modal_rename") {
|
||||
const newName = interaction.fields.getTextInputValue("name");
|
||||
await channel.setName(newName);
|
||||
await interaction.reply({ content: `Channel renamed to **${newName}**!`, flags: Discord.MessageFlags.Ephemeral });
|
||||
} else if (interaction.customId === "tempvc_modal_limit") {
|
||||
const limit = parseInt(interaction.fields.getTextInputValue("limit"));
|
||||
if (isNaN(limit) || limit < 0 || limit > 99) {
|
||||
return interaction.reply({ content: "Please enter a valid number between 0 and 99.", flags: Discord.MessageFlags.Ephemeral });
|
||||
}
|
||||
await channel.setUserLimit(limit);
|
||||
await interaction.reply({ content: `User limit set to **${limit === 0 ? "Unlimited" : limit}**!`, flags: Discord.MessageFlags.Ephemeral });
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[TempVC] Error handling interaction:", error);
|
||||
if (!interaction.replied) await interaction.reply({ content: "An error occurred.", flags: Discord.MessageFlags.Ephemeral });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = [voiceStateUpdate, interactionEvent];
|
||||
+1
-1
@@ -2,7 +2,7 @@ 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"));
|
||||
const supportbot = require("../Structures/ConfigStore").supportbot;
|
||||
|
||||
module.exports = new Command({
|
||||
name: "addon",
|
||||
|
||||
@@ -0,0 +1,652 @@
|
||||
const Discord = require("discord.js");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const yaml = require("js-yaml");
|
||||
|
||||
const { Command } = require("../Structures/Addon.js");
|
||||
|
||||
// Main bot config
|
||||
const supportbot = yaml.load(
|
||||
fs.readFileSync("./Configs/supportbot.yml", "utf8")
|
||||
);
|
||||
|
||||
// Addon config
|
||||
const addonConfig = yaml.load(
|
||||
fs.readFileSync("./Addons/Configs/applicationsystem.yml", "utf8")
|
||||
);
|
||||
|
||||
// Data file
|
||||
const dataPath = path.join(__dirname, "Data/applicationsystem.json");
|
||||
|
||||
function ensureDataFile() {
|
||||
const dir = path.dirname(dataPath);
|
||||
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
if (!fs.existsSync(dataPath)) {
|
||||
fs.writeFileSync(
|
||||
dataPath,
|
||||
JSON.stringify({ applications: [] }, null, 2),
|
||||
"utf8"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function readData() {
|
||||
ensureDataFile();
|
||||
|
||||
try {
|
||||
const raw = fs.readFileSync(dataPath, "utf8");
|
||||
const parsed = JSON.parse(raw);
|
||||
|
||||
if (!parsed || !Array.isArray(parsed.applications)) {
|
||||
return { applications: [] };
|
||||
}
|
||||
|
||||
return parsed;
|
||||
} catch (error) {
|
||||
console.error("[ApplicationSystem] Failed to read data file:", error);
|
||||
return { applications: [] };
|
||||
}
|
||||
}
|
||||
|
||||
function writeData(data) {
|
||||
ensureDataFile();
|
||||
fs.writeFileSync(dataPath, JSON.stringify(data, null, 2), "utf8");
|
||||
}
|
||||
|
||||
function now() {
|
||||
return Date.now();
|
||||
}
|
||||
|
||||
function createId() {
|
||||
return `app_${Date.now()}_${Math.floor(Math.random() * 100000)}`;
|
||||
}
|
||||
|
||||
function parseColor(value, fallback = 0x2b2d31) {
|
||||
if (typeof value === "number") return value;
|
||||
|
||||
if (typeof value === "string") {
|
||||
const cleaned = value.trim().replace("#", "");
|
||||
if (/^[0-9A-Fa-f]{6}$/.test(cleaned)) {
|
||||
return parseInt(cleaned, 16);
|
||||
}
|
||||
}
|
||||
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function getGeneralColour() {
|
||||
return parseColor(supportbot?.Embed?.Colours?.General, 0x2b2d31);
|
||||
}
|
||||
|
||||
function buildNoticeContainer(title, description, colour = 0x5865f2) {
|
||||
return new Discord.ContainerBuilder()
|
||||
.setAccentColor(colour)
|
||||
.addTextDisplayComponents(
|
||||
new Discord.TextDisplayBuilder().setContent(`## ${title}`)
|
||||
)
|
||||
.addSeparatorComponents(new Discord.SeparatorBuilder())
|
||||
.addTextDisplayComponents(
|
||||
new Discord.TextDisplayBuilder().setContent(description)
|
||||
);
|
||||
}
|
||||
|
||||
function buildPanelContainer() {
|
||||
const options = Object.entries(addonConfig.Applications || {})
|
||||
.slice(0, 25)
|
||||
.map(([key, app]) =>
|
||||
new Discord.StringSelectMenuOptionBuilder()
|
||||
.setLabel(String(app.Name || key).slice(0, 100))
|
||||
.setDescription(
|
||||
String(app.Description || "Apply for this position").slice(0, 100)
|
||||
)
|
||||
.setValue(key)
|
||||
);
|
||||
|
||||
return new Discord.ContainerBuilder()
|
||||
.setAccentColor(getGeneralColour())
|
||||
.addTextDisplayComponents(
|
||||
new Discord.TextDisplayBuilder().setContent(
|
||||
`## ${addonConfig.Panel?.Title || "Applications"}`
|
||||
)
|
||||
)
|
||||
.addSeparatorComponents(new Discord.SeparatorBuilder())
|
||||
.addTextDisplayComponents(
|
||||
new Discord.TextDisplayBuilder().setContent(
|
||||
addonConfig.Panel?.Description ||
|
||||
"Choose an application from the dropdown below."
|
||||
)
|
||||
)
|
||||
.addActionRowComponents(
|
||||
new Discord.ActionRowBuilder().addComponents(
|
||||
new Discord.StringSelectMenuBuilder()
|
||||
.setCustomId("appsystem_select")
|
||||
.setPlaceholder(
|
||||
addonConfig.Panel?.Placeholder || "Select an application type"
|
||||
)
|
||||
.addOptions(options)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function textInputStyle(style) {
|
||||
return style === "Paragraph"
|
||||
? Discord.TextInputStyle.Paragraph
|
||||
: Discord.TextInputStyle.Short;
|
||||
}
|
||||
|
||||
function sanitizeQuestionKey(key) {
|
||||
return String(key).toLowerCase().replace(/[^a-z0-9_]/g, "_");
|
||||
}
|
||||
|
||||
function formatQuestionTitle(key) {
|
||||
return String(key)
|
||||
.replace(/_/g, " ")
|
||||
.replace(/\b\w/g, (char) => char.toUpperCase());
|
||||
}
|
||||
|
||||
function buildApplicationModal(typeKey) {
|
||||
const applicationType = addonConfig.Applications?.[typeKey];
|
||||
if (!applicationType) return null;
|
||||
|
||||
const modal = new Discord.ModalBuilder()
|
||||
.setCustomId(`appsystem_modal_${typeKey}`)
|
||||
.setTitle(String(applicationType.Name || typeKey).slice(0, 45));
|
||||
|
||||
const questionEntries = Object.entries(applicationType.Questions || {}).slice(0, 5);
|
||||
|
||||
for (const [key, question] of questionEntries) {
|
||||
const input = new Discord.TextInputBuilder()
|
||||
.setCustomId(sanitizeQuestionKey(key))
|
||||
.setLabel(String(question.Label || key).slice(0, 45))
|
||||
.setPlaceholder(String(question.Placeholder || "Enter your answer").slice(0, 100))
|
||||
.setRequired(Boolean(question.Required))
|
||||
.setStyle(textInputStyle(question.Style))
|
||||
.setMinLength(Number(question.MinLength || 1))
|
||||
.setMaxLength(Number(question.MaxLength || 1000));
|
||||
|
||||
modal.addComponents(
|
||||
new Discord.ActionRowBuilder().addComponents(input)
|
||||
);
|
||||
}
|
||||
|
||||
return modal;
|
||||
}
|
||||
|
||||
function buildReviewContainer(application, reviewerText = null, locked = false) {
|
||||
const statusText =
|
||||
application.status === "accepted"
|
||||
? "Accepted"
|
||||
: application.status === "rejected"
|
||||
? "Rejected"
|
||||
: "Pending";
|
||||
|
||||
const answerLines = Object.entries(application.answers || {})
|
||||
.map(([key, value]) => `### ${formatQuestionTitle(key)}\n${value}`)
|
||||
.join("\n\n");
|
||||
|
||||
const body = [
|
||||
`**Applicant:** <@${application.userId}>`,
|
||||
`**Tag:** ${application.userTag}`,
|
||||
`**Application Type:** ${application.typeName}`,
|
||||
`**Application ID:** \`${application.id}\``,
|
||||
`**Status:** ${statusText}`,
|
||||
"",
|
||||
answerLines || "*No answers provided.*",
|
||||
].join("\n");
|
||||
|
||||
const container = new Discord.ContainerBuilder()
|
||||
.setAccentColor(getGeneralColour())
|
||||
.addTextDisplayComponents(
|
||||
new Discord.TextDisplayBuilder().setContent(
|
||||
`## ${addonConfig.Review?.Title || "New Application"}`
|
||||
)
|
||||
)
|
||||
.addSeparatorComponents(new Discord.SeparatorBuilder())
|
||||
.addTextDisplayComponents(
|
||||
new Discord.TextDisplayBuilder().setContent(body)
|
||||
);
|
||||
|
||||
if (reviewerText) {
|
||||
container
|
||||
.addSeparatorComponents(new Discord.SeparatorBuilder())
|
||||
.addTextDisplayComponents(
|
||||
new Discord.TextDisplayBuilder().setContent(reviewerText)
|
||||
);
|
||||
}
|
||||
|
||||
container.addActionRowComponents(
|
||||
new Discord.ActionRowBuilder().addComponents(
|
||||
new Discord.ButtonBuilder()
|
||||
.setCustomId(`appsystem_accept_${application.id}`)
|
||||
.setLabel(addonConfig.Review?.AcceptLabel || "Accept")
|
||||
.setStyle(Discord.ButtonStyle.Success)
|
||||
.setDisabled(locked),
|
||||
new Discord.ButtonBuilder()
|
||||
.setCustomId(`appsystem_reject_${application.id}`)
|
||||
.setLabel(addonConfig.Review?.RejectLabel || "Reject")
|
||||
.setStyle(Discord.ButtonStyle.Danger)
|
||||
.setDisabled(locked)
|
||||
)
|
||||
);
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
function getPendingApplication(data, guildId, userId, type) {
|
||||
return data.applications.find(
|
||||
(app) =>
|
||||
app.guildId === guildId &&
|
||||
app.userId === userId &&
|
||||
app.type === type &&
|
||||
app.status === "pending"
|
||||
);
|
||||
}
|
||||
|
||||
function getCooldownApplication(data, guildId, userId, type) {
|
||||
const cooldownMs =
|
||||
Number(addonConfig.Application?.CooldownHours || 24) * 60 * 60 * 1000;
|
||||
|
||||
return data.applications.find(
|
||||
(app) =>
|
||||
app.guildId === guildId &&
|
||||
app.userId === userId &&
|
||||
app.type === type &&
|
||||
now() - Number(app.createdAt || 0) < cooldownMs
|
||||
);
|
||||
}
|
||||
|
||||
function memberCanReview(member) {
|
||||
if (!member) return false;
|
||||
|
||||
if (member.permissions?.has(Discord.PermissionFlagsBits.ManageGuild)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function registerAddonInteractions(client) {
|
||||
if (client.__applicationSystemRegistered) return;
|
||||
client.__applicationSystemRegistered = true;
|
||||
|
||||
client.on("interactionCreate", async (interaction) => {
|
||||
try {
|
||||
if (!interaction.guild) return;
|
||||
|
||||
// Application dropdown select
|
||||
if (
|
||||
interaction.isStringSelectMenu() &&
|
||||
interaction.customId === "appsystem_select"
|
||||
) {
|
||||
const typeKey = interaction.values[0];
|
||||
const applicationType = addonConfig.Applications?.[typeKey];
|
||||
|
||||
if (!applicationType) {
|
||||
return interaction.reply({
|
||||
flags:
|
||||
Discord.MessageFlags.Ephemeral |
|
||||
Discord.MessageFlags.IsComponentsV2,
|
||||
components: [
|
||||
buildNoticeContainer(
|
||||
"Invalid Application",
|
||||
"That application type does not exist.",
|
||||
0xed4245
|
||||
),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
const data = readData();
|
||||
|
||||
if (addonConfig.Application?.OneOpenApplicationPerUserPerType) {
|
||||
const existing = getPendingApplication(
|
||||
data,
|
||||
interaction.guild.id,
|
||||
interaction.user.id,
|
||||
typeKey
|
||||
);
|
||||
|
||||
if (existing) {
|
||||
return interaction.reply({
|
||||
flags:
|
||||
Discord.MessageFlags.Ephemeral |
|
||||
Discord.MessageFlags.IsComponentsV2,
|
||||
components: [
|
||||
buildNoticeContainer(
|
||||
"Application Already Open",
|
||||
`You already have a pending ${applicationType.Name} application.`,
|
||||
0xed4245
|
||||
),
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const cooldownHit = getCooldownApplication(
|
||||
data,
|
||||
interaction.guild.id,
|
||||
interaction.user.id,
|
||||
typeKey
|
||||
);
|
||||
|
||||
if (cooldownHit && cooldownHit.status !== "pending") {
|
||||
return interaction.reply({
|
||||
flags:
|
||||
Discord.MessageFlags.Ephemeral |
|
||||
Discord.MessageFlags.IsComponentsV2,
|
||||
components: [
|
||||
buildNoticeContainer(
|
||||
"Please Wait",
|
||||
`You must wait ${addonConfig.Application?.CooldownHours || 24} hours before applying again for ${applicationType.Name}.`,
|
||||
0xfee75c
|
||||
),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
const modal = buildApplicationModal(typeKey);
|
||||
|
||||
if (!modal) {
|
||||
return interaction.reply({
|
||||
flags:
|
||||
Discord.MessageFlags.Ephemeral |
|
||||
Discord.MessageFlags.IsComponentsV2,
|
||||
components: [
|
||||
buildNoticeContainer(
|
||||
"Modal Error",
|
||||
"The application form could not be created.",
|
||||
0xed4245
|
||||
),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
return interaction.showModal(modal);
|
||||
}
|
||||
|
||||
// Application modal submit
|
||||
if (
|
||||
interaction.isModalSubmit() &&
|
||||
interaction.customId.startsWith("appsystem_modal_")
|
||||
) {
|
||||
const typeKey = interaction.customId.replace("appsystem_modal_", "");
|
||||
const applicationType = addonConfig.Applications?.[typeKey];
|
||||
|
||||
if (!applicationType) {
|
||||
return interaction.reply({
|
||||
flags:
|
||||
Discord.MessageFlags.Ephemeral |
|
||||
Discord.MessageFlags.IsComponentsV2,
|
||||
components: [
|
||||
buildNoticeContainer(
|
||||
"Invalid Application",
|
||||
"That application type no longer exists.",
|
||||
0xed4245
|
||||
),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
const reviewChannel = interaction.guild.channels.cache.get(
|
||||
addonConfig.Application?.ReviewChannelId
|
||||
);
|
||||
|
||||
if (!reviewChannel) {
|
||||
return interaction.reply({
|
||||
flags:
|
||||
Discord.MessageFlags.Ephemeral |
|
||||
Discord.MessageFlags.IsComponentsV2,
|
||||
components: [
|
||||
buildNoticeContainer(
|
||||
"Configuration Error",
|
||||
"The review channel was not found. Please contact staff.",
|
||||
0xed4245
|
||||
),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
const data = readData();
|
||||
|
||||
if (addonConfig.Application?.OneOpenApplicationPerUserPerType) {
|
||||
const existing = getPendingApplication(
|
||||
data,
|
||||
interaction.guild.id,
|
||||
interaction.user.id,
|
||||
typeKey
|
||||
);
|
||||
|
||||
if (existing) {
|
||||
return interaction.reply({
|
||||
flags:
|
||||
Discord.MessageFlags.Ephemeral |
|
||||
Discord.MessageFlags.IsComponentsV2,
|
||||
components: [
|
||||
buildNoticeContainer(
|
||||
"Application Already Open",
|
||||
`You already have a pending ${applicationType.Name} application.`,
|
||||
0xed4245
|
||||
),
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const answers = {};
|
||||
const questionEntries = Object.entries(applicationType.Questions || {}).slice(0, 5);
|
||||
|
||||
for (const [key] of questionEntries) {
|
||||
const inputKey = sanitizeQuestionKey(key);
|
||||
answers[inputKey] = interaction.fields.getTextInputValue(inputKey);
|
||||
}
|
||||
|
||||
const application = {
|
||||
id: createId(),
|
||||
type: typeKey,
|
||||
typeName: applicationType.Name || typeKey,
|
||||
guildId: interaction.guild.id,
|
||||
userId: interaction.user.id,
|
||||
userTag: interaction.user.tag || interaction.user.username,
|
||||
status: "pending",
|
||||
createdAt: now(),
|
||||
reviewerId: null,
|
||||
reviewedAt: null,
|
||||
reviewMessageId: null,
|
||||
answers,
|
||||
};
|
||||
|
||||
const reviewMessage = await reviewChannel.send({
|
||||
flags: Discord.MessageFlags.IsComponentsV2,
|
||||
components: [buildReviewContainer(application, null, false)],
|
||||
});
|
||||
|
||||
application.reviewMessageId = reviewMessage.id;
|
||||
data.applications.push(application);
|
||||
writeData(data);
|
||||
|
||||
return interaction.reply({
|
||||
flags:
|
||||
Discord.MessageFlags.Ephemeral |
|
||||
Discord.MessageFlags.IsComponentsV2,
|
||||
components: [
|
||||
buildNoticeContainer(
|
||||
"Application Sent",
|
||||
`Your ${applicationType.Name} application has been submitted successfully.`,
|
||||
0x57f287
|
||||
),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
// Review buttons
|
||||
if (
|
||||
interaction.isButton() &&
|
||||
(interaction.customId.startsWith("appsystem_accept_") ||
|
||||
interaction.customId.startsWith("appsystem_reject_"))
|
||||
) {
|
||||
if (!memberCanReview(interaction.member)) {
|
||||
return interaction.reply({
|
||||
flags:
|
||||
Discord.MessageFlags.Ephemeral |
|
||||
Discord.MessageFlags.IsComponentsV2,
|
||||
components: [
|
||||
buildNoticeContainer(
|
||||
"No Permission",
|
||||
"You do not have permission to review applications.",
|
||||
0xed4245
|
||||
),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
const isAccept = interaction.customId.startsWith("appsystem_accept_");
|
||||
const applicationId = interaction.customId.replace(
|
||||
isAccept ? "appsystem_accept_" : "appsystem_reject_",
|
||||
""
|
||||
);
|
||||
|
||||
const data = readData();
|
||||
const application = data.applications.find((app) => app.id === applicationId);
|
||||
|
||||
if (!application) {
|
||||
return interaction.reply({
|
||||
flags:
|
||||
Discord.MessageFlags.Ephemeral |
|
||||
Discord.MessageFlags.IsComponentsV2,
|
||||
components: [
|
||||
buildNoticeContainer(
|
||||
"Not Found",
|
||||
"This application no longer exists in storage.",
|
||||
0xed4245
|
||||
),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
if (application.status !== "pending") {
|
||||
return interaction.reply({
|
||||
flags:
|
||||
Discord.MessageFlags.Ephemeral |
|
||||
Discord.MessageFlags.IsComponentsV2,
|
||||
components: [
|
||||
buildNoticeContainer(
|
||||
"Already Reviewed",
|
||||
`This application has already been ${application.status}.`,
|
||||
0xfee75c
|
||||
),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
application.status = isAccept ? "accepted" : "rejected";
|
||||
application.reviewerId = interaction.user.id;
|
||||
application.reviewedAt = now();
|
||||
writeData(data);
|
||||
|
||||
const reviewerText = [
|
||||
`### Review Result`,
|
||||
`**Decision:** ${isAccept ? "Accepted" : "Rejected"}`,
|
||||
`**Reviewer:** <@${interaction.user.id}>`,
|
||||
`**Reviewed At:** <t:${Math.floor(application.reviewedAt / 1000)}:F>`,
|
||||
].join("\n");
|
||||
|
||||
await interaction.update({
|
||||
flags: Discord.MessageFlags.IsComponentsV2,
|
||||
components: [
|
||||
buildReviewContainer(application, reviewerText, true),
|
||||
],
|
||||
});
|
||||
|
||||
const applicationType = addonConfig.Applications?.[application.type];
|
||||
|
||||
if (isAccept && applicationType?.AcceptedRoleId) {
|
||||
const member = await interaction.guild.members
|
||||
.fetch(application.userId)
|
||||
.catch(() => null);
|
||||
|
||||
if (member) {
|
||||
await member.roles.add(applicationType.AcceptedRoleId).catch(() => null);
|
||||
}
|
||||
}
|
||||
|
||||
const applicantUser = await interaction.client.users
|
||||
.fetch(application.userId)
|
||||
.catch(() => null);
|
||||
|
||||
if (applicantUser) {
|
||||
await applicantUser
|
||||
.send({
|
||||
flags: Discord.MessageFlags.IsComponentsV2,
|
||||
components: [
|
||||
buildNoticeContainer(
|
||||
isAccept ? "Application Accepted" : "Application Rejected",
|
||||
isAccept
|
||||
? `Congratulations! Your ${application.typeName} application in **${interaction.guild.name}** has been accepted.`
|
||||
: `Your ${application.typeName} application in **${interaction.guild.name}** has been rejected.`,
|
||||
isAccept ? 0x57f287 : 0xed4245
|
||||
),
|
||||
],
|
||||
})
|
||||
.catch(() => null);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[ApplicationSystem] Interaction error:", error);
|
||||
|
||||
if (interaction.deferred || interaction.replied) {
|
||||
return;
|
||||
}
|
||||
|
||||
return interaction
|
||||
.reply({
|
||||
flags:
|
||||
Discord.MessageFlags.Ephemeral |
|
||||
Discord.MessageFlags.IsComponentsV2,
|
||||
components: [
|
||||
buildNoticeContainer(
|
||||
"Error",
|
||||
"Something went wrong while handling that application interaction.",
|
||||
0xed4245
|
||||
),
|
||||
],
|
||||
})
|
||||
.catch(() => null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = new Command({
|
||||
name: addonConfig.Command?.Name || "applicationsetup",
|
||||
description:
|
||||
addonConfig.Command?.Description || "Posts the application panel",
|
||||
options: [],
|
||||
permissions: addonConfig.Command?.Permission || ["ManageGuild"],
|
||||
|
||||
async run(interaction) {
|
||||
registerAddonInteractions(interaction.client);
|
||||
|
||||
await interaction.channel.send({
|
||||
flags: Discord.MessageFlags.IsComponentsV2,
|
||||
components: [buildPanelContainer()],
|
||||
});
|
||||
|
||||
return interaction.reply({
|
||||
flags:
|
||||
Discord.MessageFlags.Ephemeral |
|
||||
Discord.MessageFlags.IsComponentsV2,
|
||||
components: [
|
||||
buildNoticeContainer(
|
||||
"Application Panel Posted",
|
||||
"The application panel has been posted in this channel.",
|
||||
0x57f287
|
||||
),
|
||||
],
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,83 @@
|
||||
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: "calculator",
|
||||
description: "Use a calculator to solve your math problems.",
|
||||
options: [],
|
||||
permissions: ["SendMessages"],
|
||||
|
||||
async run(interaction) {
|
||||
let expression = "";
|
||||
let history = "";
|
||||
|
||||
const createButton = (label, customId) => {
|
||||
return new Discord.ButtonBuilder()
|
||||
.setLabel(label)
|
||||
.setCustomId(customId)
|
||||
.setStyle(Discord.ButtonStyle.Secondary);
|
||||
};
|
||||
|
||||
const createRow = (buttons) => {
|
||||
return new Discord.ActionRowBuilder().addComponents(buttons);
|
||||
};
|
||||
|
||||
const rows = [
|
||||
createRow([createButton('7', '7'), createButton('8', '8'), createButton('9', '9'), createButton('/', '/')]),
|
||||
createRow([createButton('4', '4'), createButton('5', '5'), createButton('6', '6'), createButton('*', '*')]),
|
||||
createRow([createButton('1', '1'), createButton('2', '2'), createButton('3', '3'), createButton('-', '-')]),
|
||||
createRow([createButton('0', '0'), createButton('.', '.'), createButton('=', '='), createButton('+', '+')]),
|
||||
createRow([createButton('Clear', 'clear')])
|
||||
];
|
||||
|
||||
const embed = new Discord.EmbedBuilder()
|
||||
.setTitle('Calculator')
|
||||
.setDescription('Calculator')
|
||||
.setColor(supportbot.Embed.Colours.General);
|
||||
|
||||
const message = await interaction.reply({
|
||||
embeds: [embed],
|
||||
components: rows,
|
||||
fetchReply: true
|
||||
});
|
||||
|
||||
const filter = i => i.user.id === interaction.user.id;
|
||||
const collector = message.createMessageComponentCollector({ filter, time: 60000 });
|
||||
|
||||
collector.on('collect', async i => {
|
||||
if (i.customId === 'clear') {
|
||||
expression = "";
|
||||
history = "";
|
||||
} else if (i.customId === '=') {
|
||||
try {
|
||||
const result = eval(expression).toString();
|
||||
history += `${expression} = ${result}\n`;
|
||||
expression = result;
|
||||
} catch {
|
||||
expression = "Error";
|
||||
}
|
||||
} else {
|
||||
if (history.endsWith(`${expression} = ${expression}\n`)) {
|
||||
history += `\n`;
|
||||
expression = i.customId;
|
||||
} else {
|
||||
expression += i.customId;
|
||||
}
|
||||
}
|
||||
|
||||
await i.update({
|
||||
embeds: [new Discord.EmbedBuilder().setTitle('Calculator').setDescription(`\`\`\`${history}${expression}\`\`\``).setColor(supportbot.Embed.Colours.General)],
|
||||
components: rows
|
||||
});
|
||||
});
|
||||
|
||||
collector.on('end', collected => {
|
||||
message.edit({
|
||||
components: []
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,139 @@
|
||||
// SupportBot | Emerald Services
|
||||
// Discord Stats addon | Created by Xiled Emerald
|
||||
|
||||
const fs = require("fs");
|
||||
|
||||
const Discord = require("discord.js");
|
||||
const yaml = require("js-yaml");
|
||||
const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8"));
|
||||
|
||||
// If you need to read something from the main config!
|
||||
const cmdconfig = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8"));
|
||||
|
||||
// This is for your Addon Configuration
|
||||
const config = yaml.load(fs.readFileSync('./Addons/Configs/discord-stats.yml', 'utf8'));
|
||||
|
||||
const { Command, Event } = require("../Structures/Addon");
|
||||
|
||||
module.exports.commands = [
|
||||
|
||||
new Command({
|
||||
name: "stats",
|
||||
description: "Create the discord statistic channels",
|
||||
options: [],
|
||||
permissions: ["SEND_MESSAGES"],
|
||||
async run(interaction) {
|
||||
const guild = interaction.guild;
|
||||
const statsCategory = guild.channels.cache.find(
|
||||
(category) =>
|
||||
category.name === config.StatsCategory ||
|
||||
category.id === config.StatsCategory
|
||||
);
|
||||
|
||||
if (!statsCategory) {
|
||||
const errorEmbed = new Discord.MessageEmbed()
|
||||
.setTitle("Invalid Category")
|
||||
.setDescription(
|
||||
`${supportbot.InvalidChannel}\n\n**Category Required:** \`${config.StatsCategory}\``
|
||||
)
|
||||
.setColor(supportbot.ErrorColour);
|
||||
|
||||
interaction.reply({
|
||||
embeds: [errorEmbed],
|
||||
ephemeral: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const guildName = guild.name;
|
||||
const userCount = guild.memberCount;
|
||||
const roleCount = guild.roles.cache.size;
|
||||
const channelCount = guild.channels.cache.size;
|
||||
|
||||
try {
|
||||
const guildNameChannel = await guild.channels.create(guildName, {
|
||||
type: "GUILD_VOICE",
|
||||
});
|
||||
|
||||
const userCountChannel = await guild.channels.create(
|
||||
`${config.UserCount_Name} ${userCount}`,
|
||||
{
|
||||
type: "GUILD_VOICE",
|
||||
}
|
||||
);
|
||||
|
||||
const channelCountChannel = await guild.channels.create(
|
||||
`${config.ChannelCount_Name} ${channelCount}`,
|
||||
{
|
||||
type: "GUILD_VOICE",
|
||||
}
|
||||
);
|
||||
|
||||
const roleCountChannel = await guild.channels.create(
|
||||
`${config.RoleCount_Name} ${roleCount}`,
|
||||
{
|
||||
type: "GUILD_VOICE",
|
||||
}
|
||||
);
|
||||
|
||||
guildNameChannel.permissionOverwrites.edit(guild.id, {
|
||||
VIEW_CHANNEL: true,
|
||||
CONNECT: false,
|
||||
});
|
||||
|
||||
userCountChannel.permissionOverwrites.edit(guild.id, {
|
||||
VIEW_CHANNEL: true,
|
||||
CONNECT: false,
|
||||
});
|
||||
|
||||
channelCountChannel.permissionOverwrites.edit(guild.id, {
|
||||
VIEW_CHANNEL: true,
|
||||
CONNECT: false,
|
||||
});
|
||||
|
||||
roleCountChannel.permissionOverwrites.edit(guild.id, {
|
||||
VIEW_CHANNEL: true,
|
||||
CONNECT: false,
|
||||
});
|
||||
|
||||
userCountChannel.setParent(statsCategory.id);
|
||||
roleCountChannel.setParent(statsCategory.id);
|
||||
guildNameChannel.setParent(statsCategory.id);
|
||||
channelCountChannel.setParent(statsCategory.id);
|
||||
|
||||
const successEmbed = new Discord.MessageEmbed()
|
||||
.setDescription(":white_check_mark: **[DISCORD STATS]** Channels created successfully!")
|
||||
.setColor(supportbot.EmbedColour);
|
||||
|
||||
interaction.reply({
|
||||
embeds: [successEmbed],
|
||||
ephemeral: true,
|
||||
});
|
||||
|
||||
setInterval(() => {
|
||||
userCountChannel.setName(
|
||||
`${config.UserCount_Name} ${userCount}`
|
||||
);
|
||||
channelCountChannel.setName(
|
||||
`${config.ChannelCount_Name} ${channelCount}`
|
||||
);
|
||||
roleCountChannel.setName(
|
||||
`${config.RoleCount_Name} ${roleCount}`
|
||||
);
|
||||
}, 30000);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
const errorEmbed = new Discord.MessageEmbed()
|
||||
.setTitle("Error")
|
||||
.setDescription("An error occurred while creating the stats channels.")
|
||||
.setColor(supportbot.ErrorColour);
|
||||
|
||||
interaction.reply({
|
||||
embeds: [errorEmbed],
|
||||
ephemeral: true,
|
||||
});
|
||||
}
|
||||
},
|
||||
}),
|
||||
|
||||
];
|
||||
@@ -0,0 +1,292 @@
|
||||
const Discord = require("discord.js");
|
||||
const fs = require("fs");
|
||||
const yaml = require("js-yaml");
|
||||
const fetch = require("node-fetch");
|
||||
const { Command } = require('../Structures/Addon.js');
|
||||
|
||||
const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8"));
|
||||
const vouchleyaddon = yaml.load(fs.readFileSync("./Addons/Configs/vouchley.yml", "utf8"));
|
||||
|
||||
function formatURL(url) {
|
||||
if (!url) return 'https://www.vouchley.com';
|
||||
if (!url.startsWith('http://') && !url.startsWith('https://')) {
|
||||
return `https://${url}`;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
function formatDate(timestamp) {
|
||||
return `<t:${Math.floor(new Date(timestamp).getTime() / 1000)}:D>`;
|
||||
}
|
||||
|
||||
function createReviewEmbed(review, username, currentPage, totalReviews) {
|
||||
const embed = new Discord.EmbedBuilder()
|
||||
.setTitle(`Review for ${username}`)
|
||||
.setColor(vouchleyaddon.Embed.Colour);
|
||||
|
||||
const fields = [
|
||||
{
|
||||
name: "Rating",
|
||||
value: "⭐".repeat(Math.min(Math.max(review.rating, 1), 5)) || "No rating",
|
||||
inline: false
|
||||
},
|
||||
{
|
||||
name: "Platform",
|
||||
value: review.platform?.toString() || "Not specified",
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: "Date",
|
||||
value: formatDate(review.time_sent),
|
||||
inline: true
|
||||
}
|
||||
];
|
||||
|
||||
if (review.message && review.message.trim()) {
|
||||
fields.push({
|
||||
name: "Review",
|
||||
value: `\`\`\`${review.message.substring(0, 1024)}\`\`\``,
|
||||
inline: false
|
||||
});
|
||||
}
|
||||
|
||||
embed.addFields(fields);
|
||||
embed.setFooter({ text: `Review ${currentPage + 1} of ${totalReviews}` });
|
||||
|
||||
return embed;
|
||||
}
|
||||
|
||||
function createReviewNavigation(currentPage, totalReviews) {
|
||||
return new Discord.ActionRowBuilder()
|
||||
.addComponents(
|
||||
new Discord.ButtonBuilder()
|
||||
.setCustomId('prev_review')
|
||||
.setLabel('Previous')
|
||||
.setStyle(Discord.ButtonStyle.Primary)
|
||||
.setDisabled(currentPage === 0),
|
||||
new Discord.ButtonBuilder()
|
||||
.setCustomId('next_review')
|
||||
.setLabel('Next')
|
||||
.setStyle(Discord.ButtonStyle.Primary)
|
||||
.setDisabled(currentPage === totalReviews - 1),
|
||||
new Discord.ButtonBuilder()
|
||||
.setCustomId('back_to_profile')
|
||||
.setLabel('Back to Profile')
|
||||
.setStyle(Discord.ButtonStyle.Secondary)
|
||||
);
|
||||
}
|
||||
|
||||
const activeReviews = new Map();
|
||||
|
||||
module.exports = new Command({
|
||||
name: vouchleyaddon.Command.Name,
|
||||
description: vouchleyaddon.Command.Description,
|
||||
options: [
|
||||
{
|
||||
name: "search_type",
|
||||
description: "How do you want to search for the user?",
|
||||
type: 3,
|
||||
required: true,
|
||||
choices: [
|
||||
{ name: "Vouchley ID", value: "id" },
|
||||
{ name: "Vouchley Username", value: "username" },
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "query",
|
||||
description: "Enter the ID, username, or Discord ID to search",
|
||||
type: 3,
|
||||
required: true
|
||||
}
|
||||
],
|
||||
permissions: vouchleyaddon.Command.Permissions,
|
||||
|
||||
async run(interaction) {
|
||||
const searchType = interaction.options.getString("search_type");
|
||||
const query = interaction.options.getString("query");
|
||||
|
||||
try {
|
||||
let apiUrl;
|
||||
switch (searchType) {
|
||||
case "id":
|
||||
apiUrl = `https://www.vouchley.com/api/v1/user?id=${query}`;
|
||||
break;
|
||||
case "username":
|
||||
apiUrl = `https://www.vouchley.com/api/v1/user?username=${query}`;
|
||||
break;
|
||||
}
|
||||
|
||||
const response = await fetch(apiUrl, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${vouchleyaddon.Vouchley.API_Key}`,
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok || (data.error && data.error.code !== 200)) {
|
||||
return interaction.reply({
|
||||
content: `Error: ${data.error?.message || 'Failed to fetch user data'}`,
|
||||
ephemeral: vouchleyaddon.Embed.Ephemeral
|
||||
});
|
||||
}
|
||||
|
||||
const user = data.user;
|
||||
|
||||
const profileEmbed = new Discord.EmbedBuilder()
|
||||
.setTitle(`${user.username} - ${String(user.title || "N/A")}`)
|
||||
.setColor(vouchleyaddon.Embed.Colour)
|
||||
.setThumbnail(user.avatar_url)
|
||||
.addFields([
|
||||
{
|
||||
name: "Discord",
|
||||
value: `<@${String(user.discord_id)}> (\`${String(user.discord_id)}\`)`,
|
||||
inline: false
|
||||
},
|
||||
{
|
||||
name: "Average Rating",
|
||||
value: `${String(user.average_rating)} (\`${String(user.reviews.length)} Reviews\`)`,
|
||||
inline: false
|
||||
}
|
||||
]);
|
||||
|
||||
const profileButtons = [
|
||||
new Discord.ButtonBuilder()
|
||||
.setLabel(`Review ${user.username}`)
|
||||
.setURL(`https://www.vouchley.com/review?user=${user.username}`)
|
||||
.setStyle(Discord.ButtonStyle.Link)
|
||||
];
|
||||
|
||||
if (user.website) {
|
||||
profileButtons.push(
|
||||
new Discord.ButtonBuilder()
|
||||
.setLabel('Visit Website')
|
||||
.setURL(formatURL(user.website))
|
||||
.setStyle(Discord.ButtonStyle.Link)
|
||||
);
|
||||
}
|
||||
|
||||
if (user.reviews && user.reviews.length > 0) {
|
||||
profileButtons.push(
|
||||
new Discord.ButtonBuilder()
|
||||
.setCustomId('seereviews')
|
||||
.setLabel('See Reviews')
|
||||
.setEmoji("⭐")
|
||||
.setStyle(Discord.ButtonStyle.Secondary)
|
||||
);
|
||||
}
|
||||
|
||||
const profileRow = new Discord.ActionRowBuilder()
|
||||
.addComponents(profileButtons);
|
||||
|
||||
const message = await interaction.reply({
|
||||
embeds: [profileEmbed],
|
||||
components: [profileRow],
|
||||
fetchReply: true,
|
||||
ephemeral: vouchleyaddon.Embed.Ephemeral
|
||||
});
|
||||
|
||||
if (user.reviews && user.reviews.length > 0) {
|
||||
activeReviews.set(message.id, {
|
||||
reviews: user.reviews,
|
||||
currentPage: 0,
|
||||
username: user.username,
|
||||
profileEmbed,
|
||||
profileRow
|
||||
});
|
||||
|
||||
const collector = message.createMessageComponentCollector({
|
||||
time: 600000
|
||||
});
|
||||
|
||||
collector.on('collect', async i => {
|
||||
const reviewData = activeReviews.get(message.id);
|
||||
if (!reviewData) return;
|
||||
|
||||
switch (i.customId) {
|
||||
case 'seereviews': {
|
||||
const review = reviewData.reviews[reviewData.currentPage];
|
||||
const reviewEmbed = createReviewEmbed(
|
||||
review,
|
||||
reviewData.username,
|
||||
reviewData.currentPage,
|
||||
reviewData.reviews.length
|
||||
);
|
||||
const reviewNav = createReviewNavigation(
|
||||
reviewData.currentPage,
|
||||
reviewData.reviews.length
|
||||
);
|
||||
await i.update({
|
||||
embeds: [reviewEmbed],
|
||||
components: [reviewNav],
|
||||
ephemeral: vouchleyaddon.Embed.Ephemeral
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'prev_review': {
|
||||
if (reviewData.currentPage > 0) {
|
||||
reviewData.currentPage--;
|
||||
const review = reviewData.reviews[reviewData.currentPage];
|
||||
const reviewEmbed = createReviewEmbed(
|
||||
review,
|
||||
reviewData.username,
|
||||
reviewData.currentPage,
|
||||
reviewData.reviews.length
|
||||
);
|
||||
const reviewNav = createReviewNavigation(
|
||||
reviewData.currentPage,
|
||||
reviewData.reviews.length
|
||||
);
|
||||
await i.update({
|
||||
embeds: [reviewEmbed],
|
||||
components: [reviewNav],
|
||||
ephemeral: vouchleyaddon.Embed.Ephemeral
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'next_review': {
|
||||
if (reviewData.currentPage < reviewData.reviews.length - 1) {
|
||||
reviewData.currentPage++;
|
||||
const review = reviewData.reviews[reviewData.currentPage];
|
||||
const reviewEmbed = createReviewEmbed(
|
||||
review,
|
||||
reviewData.username,
|
||||
reviewData.currentPage,
|
||||
reviewData.reviews.length
|
||||
);
|
||||
const reviewNav = createReviewNavigation(
|
||||
reviewData.currentPage,
|
||||
reviewData.reviews.length
|
||||
);
|
||||
await i.update({ embeds: [reviewEmbed], components: [reviewNav] });
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'back_to_profile': {
|
||||
await i.update({
|
||||
embeds: [reviewData.profileEmbed],
|
||||
components: [reviewData.profileRow]
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
collector.on('end', () => {
|
||||
activeReviews.delete(message.id);
|
||||
});
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
if (!interaction.replied) {
|
||||
await interaction.reply({
|
||||
content: "There was an error fetching Vouchley user data.",
|
||||
ephemeral: vouchleyaddon.Embed.Ephemeral
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1,128 +0,0 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
our discord.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series
|
||||
of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or
|
||||
permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.0, available at
|
||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||
enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
https://www.contributor-covenant.org/faq. Translations are available at
|
||||
https://www.contributor-covenant.org/translations.
|
||||
@@ -1,92 +0,0 @@
|
||||
# Contributing
|
||||
|
||||
When contributing to this repository, please first discuss the change you wish to make via issue,
|
||||
email, or any other method with the owners of this repository before making a change.
|
||||
|
||||
Please note we have a code of conduct, please follow it in all your interactions with the project.
|
||||
|
||||
## Pull Request Process
|
||||
|
||||
1. Ensure any install or build dependencies are removed before the end of the layer when doing a
|
||||
build.
|
||||
2. Update the README.md with details of changes to the interface, this includes new environment
|
||||
variables, exposed ports, useful file locations and container parameters.
|
||||
3. Increase the version numbers in any examples files and the README.md to the new version that this
|
||||
Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/).
|
||||
4. You may merge the Pull Request in once you have the sign-off of two other developers, or if you
|
||||
do not have permission to do that, you may request the second reviewer to merge it for you.
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
### Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as
|
||||
contributors and maintainers pledge to making participation in our project and
|
||||
our community a harassment-free experience for everyone, regardless of age, body
|
||||
size, disability, ethnicity, gender identity and expression, level of experience,
|
||||
nationality, personal appearance, race, religion, or sexual identity and
|
||||
orientation.
|
||||
|
||||
### Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment
|
||||
include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
### Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable
|
||||
behavior and are expected to take appropriate and fair corrective action in
|
||||
response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or
|
||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||
permanently any contributor for other behaviors that they deem inappropriate,
|
||||
threatening, offensive, or harmful.
|
||||
|
||||
### Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces
|
||||
when an individual is representing the project or its community. Examples of
|
||||
representing a project or community include using an official project e-mail
|
||||
address, posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event. Representation of a project may be
|
||||
further defined and clarified by project maintainers.
|
||||
|
||||
### Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported by contacting the project team at [INSERT EMAIL ADDRESS]. All
|
||||
complaints will be reviewed and investigated and will result in a response that
|
||||
is deemed necessary and appropriate to the circumstances. The project team is
|
||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||
Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||
faith may face temporary or permanent repercussions as determined by other
|
||||
members of the project's leadership.
|
||||
|
||||
### Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||
available at [http://contributor-covenant.org/version/1/4][version]
|
||||
|
||||
[homepage]: http://contributor-covenant.org
|
||||
[version]: http://contributor-covenant.org/version/1/4/
|
||||
@@ -0,0 +1,122 @@
|
||||
// SupportBot | Emerald Services
|
||||
// AI Command (Consolidated)
|
||||
|
||||
const fs = require("fs");
|
||||
const { ApplicationCommandOptionType, ApplicationCommandType, EmbedBuilder, MessageFlags } = require("discord.js");
|
||||
const yaml = require("js-yaml");
|
||||
|
||||
const supportbot = require("../../Structures/ConfigStore").supportbot;
|
||||
const cmdconfig = require("../../Structures/ConfigStore").commands;
|
||||
const supportbotai = require("../../Structures/ConfigStore").supportbotAi;
|
||||
|
||||
const Command = require("../../Structures/Command.js");
|
||||
const AIDatabase = require("../../Structures/AIDatabase.js");
|
||||
|
||||
module.exports = new Command({
|
||||
name: cmdconfig.AI?.Command || "ai",
|
||||
description: cmdconfig.AI?.Description || "Manage SupportBot AI settings and memory.",
|
||||
type: ApplicationCommandType.ChatInput,
|
||||
options: [
|
||||
{
|
||||
name: cmdconfig.AI?.Train?.Command || "train",
|
||||
description: cmdconfig.AI?.Train?.Description || "Train the AI by adding knowledge to its database.",
|
||||
type: ApplicationCommandOptionType.Subcommand,
|
||||
options: [
|
||||
{
|
||||
name: "content",
|
||||
description: "The text/knowledge you want the bot to learn",
|
||||
type: ApplicationCommandOptionType.String,
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: cmdconfig.AI?.Knowledge?.Command || "knowledge",
|
||||
description: cmdconfig.AI?.Knowledge?.Description || "Manage the AI knowledge base.",
|
||||
type: ApplicationCommandOptionType.SubcommandGroup,
|
||||
options: [
|
||||
{
|
||||
name: cmdconfig.AI?.Knowledge?.Clear?.Command || "clear",
|
||||
description: cmdconfig.AI?.Knowledge?.Clear?.Description || "Clear all trained knowledge from the AI.",
|
||||
type: ApplicationCommandOptionType.Subcommand,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: cmdconfig.AI?.Memory?.Command || "memory",
|
||||
description: cmdconfig.AI?.Memory?.Description || "Manage the AI conversational memory.",
|
||||
type: ApplicationCommandOptionType.SubcommandGroup,
|
||||
options: [
|
||||
{
|
||||
name: cmdconfig.AI?.Memory?.Clear?.Command || "clear",
|
||||
description: cmdconfig.AI?.Memory?.Clear?.Description || "Clear recent conversation memory for this channel.",
|
||||
type: ApplicationCommandOptionType.Subcommand,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
permissions: cmdconfig.AI?.Permission || ["Administrator"],
|
||||
|
||||
async run(interaction) {
|
||||
if (!supportbotai.Enabled) {
|
||||
return interaction.reply({
|
||||
content: "SupportBot AI is currently disabled.",
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
|
||||
const subcommandGroup = interaction.options.getSubcommandGroup(false);
|
||||
const subcommand = interaction.options.getSubcommand(false);
|
||||
|
||||
try {
|
||||
if (subcommand === "train" || subcommand === cmdconfig.AI?.Train?.Command) {
|
||||
const content = interaction.options.getString("content");
|
||||
|
||||
AIDatabase.addKnowledge({
|
||||
sourceType: "user_command",
|
||||
sourceName: interaction.user.tag,
|
||||
title: "Trained Knowledge",
|
||||
content: content,
|
||||
});
|
||||
|
||||
const SuccessEmbed = new EmbedBuilder()
|
||||
.setDescription(`✅ **Knowledge Added!**\n\nThe AI has learned the following information:\n\`\`\`${content}\`\`\``)
|
||||
.setColor(supportbot.Embed.Colours.Success);
|
||||
|
||||
return interaction.reply({ embeds: [SuccessEmbed], flags: MessageFlags.Ephemeral });
|
||||
}
|
||||
|
||||
if (subcommandGroup === (cmdconfig.AI?.Knowledge?.Command || "knowledge")) {
|
||||
if (subcommand === (cmdconfig.AI?.Knowledge?.Clear?.Command || "clear")) {
|
||||
AIDatabase.clearKnowledge();
|
||||
|
||||
const SuccessEmbed = new EmbedBuilder()
|
||||
.setDescription(`✅ **Knowledge Cleared!**\n\nAll manually trained knowledge has been wiped from the database.`)
|
||||
.setColor(supportbot.Embed.Colours.Success);
|
||||
|
||||
return interaction.reply({ embeds: [SuccessEmbed], flags: MessageFlags.Ephemeral });
|
||||
}
|
||||
}
|
||||
|
||||
if (subcommandGroup === (cmdconfig.AI?.Memory?.Command || "memory")) {
|
||||
if (subcommand === (cmdconfig.AI?.Memory?.Clear?.Command || "clear")) {
|
||||
AIDatabase.clearConversations(interaction.channel.id);
|
||||
|
||||
const SuccessEmbed = new EmbedBuilder()
|
||||
.setDescription(`✅ **Memory Cleared!**\n\nThe AI's recent conversation memory in this channel has been wiped. It will no longer remember previous messages sent here.`)
|
||||
.setColor(supportbot.Embed.Colours.Success);
|
||||
|
||||
return interaction.reply({ embeds: [SuccessEmbed], flags: MessageFlags.Ephemeral });
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error("AI Command Error:", error);
|
||||
const ErrorEmbed = new EmbedBuilder()
|
||||
.setDescription(`❌ **Error:** Failed to process the AI command.`)
|
||||
.setColor(supportbot.Embed.Colours.Error);
|
||||
|
||||
return interaction.reply({ embeds: [ErrorEmbed], flags: MessageFlags.Ephemeral });
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -2,10 +2,10 @@ const fs = require("fs");
|
||||
const Discord = require("discord.js");
|
||||
const yaml = require("js-yaml");
|
||||
|
||||
const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8"));
|
||||
const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8"));
|
||||
const supportbot = require("../../Structures/ConfigStore").supportbot;
|
||||
const cmdconfig = require("../../Structures/ConfigStore").commands;
|
||||
|
||||
const Command = require("../Structures/Command.js");
|
||||
const Command = require("../../Structures/Command.js");
|
||||
|
||||
module.exports = new Command({
|
||||
name: cmdconfig.Help.Command,
|
||||
@@ -54,8 +54,8 @@ module.exports = new Command({
|
||||
iconURL: interaction.user.displayAvatarURL(),
|
||||
});
|
||||
|
||||
interaction.reply({
|
||||
ephemeral: true,
|
||||
await interaction.reply({
|
||||
flags: Discord.MessageFlags.Ephemeral,
|
||||
embeds: [HelpEmbed1],
|
||||
});
|
||||
},
|
||||
@@ -1,24 +1,13 @@
|
||||
// SupportBot | Emerald Services
|
||||
// Info Command
|
||||
|
||||
const fs = require("fs");
|
||||
|
||||
const Discord = require("discord.js");
|
||||
const yaml = require("js-yaml");
|
||||
|
||||
const supportbot = yaml.load(
|
||||
fs.readFileSync("./Configs/supportbot.yml", "utf8")
|
||||
);
|
||||
const supportbot = require("../../Structures/ConfigStore").supportbot;
|
||||
const cmdconfig = require("../../Structures/ConfigStore").commands;
|
||||
const msgconfig = require("../../Structures/ConfigStore").messages;
|
||||
|
||||
const cmdconfig = yaml.load(
|
||||
fs.readFileSync("./Configs/commands.yml", "utf8")
|
||||
);
|
||||
|
||||
const msgconfig = yaml.load(
|
||||
fs.readFileSync("./Configs/messages.yml", "utf8")
|
||||
);
|
||||
|
||||
const Command = require("../Structures/Command.js");
|
||||
const Command = require("../../Structures/Command.js");
|
||||
|
||||
module.exports = new Command({
|
||||
name: cmdconfig.Info.Command,
|
||||
@@ -5,12 +5,10 @@ const fs = require("fs");
|
||||
|
||||
const Discord = require("discord.js");
|
||||
const yaml = require("js-yaml");
|
||||
const supportbot = yaml.load(
|
||||
fs.readFileSync("./Configs/supportbot.yml", "utf8")
|
||||
);
|
||||
const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8"));
|
||||
const supportbot = require("../../Structures/ConfigStore").supportbot;
|
||||
const cmdconfig = require("../../Structures/ConfigStore").commands;
|
||||
|
||||
const Command = require("../Structures/Command.js");
|
||||
const Command = require("../../Structures/Command.js");
|
||||
|
||||
module.exports = new Command({
|
||||
name: cmdconfig.Ping.Command,
|
||||
@@ -19,15 +17,13 @@ module.exports = new Command({
|
||||
permissions: cmdconfig.Ping.Permission,
|
||||
|
||||
async run(interaction) {
|
||||
let disableCommand = true;
|
||||
|
||||
const PingEmbed = new Discord.EmbedBuilder()
|
||||
.setDescription(
|
||||
`:ping_pong: **Ping:** \`${interaction.client.ws.ping} ms\``
|
||||
`:ping_pong: **Ping:** \`${interaction.client.ws.ping} ms\``,
|
||||
)
|
||||
.setColor(supportbot.Embed.Colours.General);
|
||||
|
||||
interaction.reply({
|
||||
await interaction.reply({
|
||||
embeds: [PingEmbed],
|
||||
});
|
||||
},
|
||||
@@ -0,0 +1,527 @@
|
||||
const {
|
||||
ModalBuilder,
|
||||
TextInputBuilder,
|
||||
TextInputStyle,
|
||||
ApplicationCommandType,
|
||||
ActionRowBuilder,
|
||||
ButtonBuilder,
|
||||
ButtonStyle,
|
||||
ApplicationCommandOptionType,
|
||||
MessageFlags,
|
||||
ContainerBuilder,
|
||||
TextDisplayBuilder,
|
||||
SeparatorBuilder,
|
||||
ThumbnailBuilder,
|
||||
MediaGalleryBuilder,
|
||||
MediaGalleryItemBuilder,
|
||||
} = require("discord.js");
|
||||
const fs = require("fs");
|
||||
const yaml = require("js-yaml");
|
||||
const db = require("../../Structures/Database.js");
|
||||
const Command = require("../../Structures/Command.js");
|
||||
|
||||
const supportbot = require("../../Structures/ConfigStore").supportbot;
|
||||
const cmdconfig = require("../../Structures/ConfigStore").commands;
|
||||
const msgconfig = require("../../Structures/ConfigStore").messages;
|
||||
|
||||
const clockedInUsers = new Set();
|
||||
|
||||
function safeGetProfile(userId) {
|
||||
try {
|
||||
if (typeof db.getProfile !== "function") {
|
||||
console.error("db.getProfile is not a function");
|
||||
return {
|
||||
bio: "",
|
||||
timezone: "",
|
||||
clockedIn: false,
|
||||
};
|
||||
}
|
||||
|
||||
const profile = db.getProfile(userId);
|
||||
|
||||
return {
|
||||
bio: profile?.bio || "",
|
||||
timezone: profile?.timezone || "",
|
||||
clockedIn: Boolean(profile?.clockedIn),
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error getting profile:", error);
|
||||
return {
|
||||
bio: "",
|
||||
timezone: "",
|
||||
clockedIn: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function safeSetClockedIn(userId, value) {
|
||||
try {
|
||||
if (typeof db.setClockedIn !== "function") {
|
||||
console.error("db.setClockedIn is not a function");
|
||||
return false;
|
||||
}
|
||||
|
||||
db.setClockedIn(userId, value);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("Error setting clocked in status:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function buildProfileContainer({
|
||||
viewingUser,
|
||||
viewingUserId,
|
||||
bio,
|
||||
timezone,
|
||||
clockedIn,
|
||||
isStaff,
|
||||
}) {
|
||||
const container = new ContainerBuilder();
|
||||
|
||||
if (supportbot.Embed?.Colours?.General) {
|
||||
container.setAccentColor(
|
||||
parseInt(String(supportbot.Embed.Colours.General).replace("#", ""), 16),
|
||||
);
|
||||
}
|
||||
|
||||
const avatarUrl = viewingUser.displayAvatarURL({ size: 128, extension: "png" });
|
||||
|
||||
const gallery = new MediaGalleryBuilder().addItems(
|
||||
new MediaGalleryItemBuilder().setURL(avatarUrl)
|
||||
);
|
||||
|
||||
container.addMediaGalleryComponents(gallery);
|
||||
|
||||
container.addTextDisplayComponents(
|
||||
new TextDisplayBuilder().setContent(`## ${viewingUser.username}'s Profile`)
|
||||
);
|
||||
|
||||
container.addSeparatorComponents(new SeparatorBuilder().setDivider(true));
|
||||
|
||||
container.addTextDisplayComponents(
|
||||
new TextDisplayBuilder().setContent(
|
||||
[
|
||||
`**Bio:** ${bio || "No bio set."}`,
|
||||
`**Timezone:** ${timezone || "No timezone set."}`,
|
||||
...(isStaff
|
||||
? [`**Clocked In:** ${clockedIn ? "✅ Clocked In" : "❌ Clocked Out"}`]
|
||||
: []),
|
||||
].join("\n")
|
||||
)
|
||||
);
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
function buildProfileButtons({ isOwnProfile, isStaff, clockedIn }) {
|
||||
const row = new ActionRowBuilder();
|
||||
|
||||
if (isOwnProfile) {
|
||||
row.addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId("editProfile")
|
||||
.setLabel("Edit Profile")
|
||||
.setStyle(ButtonStyle.Secondary),
|
||||
);
|
||||
|
||||
if (isStaff) {
|
||||
row.addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId("clockInOut")
|
||||
.setLabel(clockedIn ? "Clock Out" : "Clock In")
|
||||
.setStyle(ButtonStyle.Secondary),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (isStaff && supportbot.Ticket?.ClaimTickets?.Enabled) {
|
||||
row.addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId("viewTicketStats")
|
||||
.setLabel("View Ticket Stats")
|
||||
.setStyle(ButtonStyle.Secondary),
|
||||
);
|
||||
}
|
||||
|
||||
return row.components.length ? [row] : [];
|
||||
}
|
||||
|
||||
function buildTicketStatsContainer(viewingUserId, ticketsClaimed, ticketsOpen, avgMinutes) {
|
||||
const container = new ContainerBuilder();
|
||||
|
||||
if (supportbot.Embed?.Colours?.General) {
|
||||
container.setAccentColor(
|
||||
parseInt(String(supportbot.Embed.Colours.General).replace("#", ""), 16),
|
||||
);
|
||||
}
|
||||
|
||||
container.addTextDisplayComponents(
|
||||
new TextDisplayBuilder().setContent(`# ${msgconfig.TicketStats.Title || "Ticket Stats"}`),
|
||||
);
|
||||
|
||||
container.addSeparatorComponents(new SeparatorBuilder().setDivider(true));
|
||||
|
||||
container.addTextDisplayComponents(
|
||||
new TextDisplayBuilder().setContent(
|
||||
[
|
||||
`> Ticket stats for <@${viewingUserId}>`,
|
||||
``,
|
||||
`**${msgconfig.TicketStats.OpenTickets || "Open Tickets"}:** ${ticketsOpen.length}`,
|
||||
`**${msgconfig.TicketStats.TotalTickets || "Total Tickets"}:** ${ticketsClaimed.length}`,
|
||||
`**${msgconfig.TicketStats.ResponseTime || "Average Response Time"}:** ${avgMinutes} minutes`,
|
||||
].join("\n"),
|
||||
),
|
||||
);
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
function buildOpenTicketsContainer(ticketsOpen) {
|
||||
const container = new ContainerBuilder();
|
||||
|
||||
if (supportbot.Embed?.Colours?.General) {
|
||||
container.setAccentColor(
|
||||
parseInt(String(supportbot.Embed.Colours.General).replace("#", ""), 16),
|
||||
);
|
||||
}
|
||||
|
||||
container.addTextDisplayComponents(new TextDisplayBuilder().setContent(`# Active Tickets`));
|
||||
|
||||
container.addSeparatorComponents(new SeparatorBuilder().setDivider(true));
|
||||
|
||||
container.addTextDisplayComponents(
|
||||
new TextDisplayBuilder().setContent(
|
||||
ticketsOpen.length ? ticketsOpen.map((t) => `<#${t.id}>`).join("\n") : "No active tickets.",
|
||||
),
|
||||
);
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
module.exports = new Command({
|
||||
name: cmdconfig.Profile.Command,
|
||||
description: cmdconfig.Profile.Description,
|
||||
type: ApplicationCommandType.ChatInput,
|
||||
permissions: cmdconfig.Profile.Permission,
|
||||
options: [
|
||||
{
|
||||
name: "user",
|
||||
description: "The user to view the profile of",
|
||||
type: ApplicationCommandOptionType.User,
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
|
||||
async run(interaction) {
|
||||
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
||||
|
||||
try {
|
||||
const viewingUser = interaction.options.getUser("user") || interaction.user;
|
||||
const viewingUserId = viewingUser.id;
|
||||
const interactionUserId = interaction.user.id;
|
||||
|
||||
const member = await interaction.guild.members.fetch(viewingUserId);
|
||||
const { getRole } = interaction.client;
|
||||
|
||||
const Staff = await getRole(supportbot.Roles.StaffMember.Staff, interaction.guild);
|
||||
const Admin = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild);
|
||||
|
||||
if (!Staff || !Admin) {
|
||||
return interaction.editReply({
|
||||
content: "Some roles seem to be missing!\nPlease check for errors when starting the bot.",
|
||||
});
|
||||
}
|
||||
|
||||
const isStaff = member.roles.cache.has(Staff.id) || member.roles.cache.has(Admin.id);
|
||||
const isOwnProfile = interactionUserId === viewingUserId;
|
||||
|
||||
if (typeof db.ensureProfile === "function") {
|
||||
db.ensureProfile(viewingUserId);
|
||||
}
|
||||
|
||||
let { bio, timezone, clockedIn } = safeGetProfile(viewingUserId);
|
||||
|
||||
const profileContainer = buildProfileContainer({
|
||||
viewingUser,
|
||||
viewingUserId,
|
||||
bio,
|
||||
timezone,
|
||||
clockedIn,
|
||||
isStaff,
|
||||
});
|
||||
|
||||
const buttonRows = buildProfileButtons({
|
||||
isOwnProfile,
|
||||
isStaff,
|
||||
clockedIn,
|
||||
});
|
||||
|
||||
await interaction.editReply({
|
||||
flags: MessageFlags.IsComponentsV2,
|
||||
components: [profileContainer, ...buttonRows],
|
||||
});
|
||||
|
||||
const filter = (i) =>
|
||||
[
|
||||
"editProfile",
|
||||
"clockInOut",
|
||||
"viewTicketStats",
|
||||
"showOpenTickets",
|
||||
"backToStats",
|
||||
"backToProfile",
|
||||
].includes(i.customId) && i.user.id === interactionUserId;
|
||||
|
||||
const collector = interaction.channel.createMessageComponentCollector({
|
||||
filter,
|
||||
time: 60000,
|
||||
});
|
||||
|
||||
collector.on("collect", async (i) => {
|
||||
if (i.customId === "editProfile") {
|
||||
const modal = new ModalBuilder()
|
||||
.setCustomId("editProfileModal")
|
||||
.setTitle("Edit Profile")
|
||||
.addComponents(
|
||||
new ActionRowBuilder().addComponents(
|
||||
new TextInputBuilder()
|
||||
.setCustomId("bio")
|
||||
.setLabel("Bio")
|
||||
.setStyle(TextInputStyle.Paragraph)
|
||||
.setPlaceholder("Enter your bio")
|
||||
.setValue(String(bio || ""))
|
||||
.setRequired(false),
|
||||
),
|
||||
new ActionRowBuilder().addComponents(
|
||||
new TextInputBuilder()
|
||||
.setCustomId("timezone")
|
||||
.setLabel("Timezone")
|
||||
.setStyle(TextInputStyle.Short)
|
||||
.setPlaceholder("Enter your timezone")
|
||||
.setValue(String(timezone || ""))
|
||||
.setRequired(false),
|
||||
),
|
||||
);
|
||||
|
||||
return i.showModal(modal);
|
||||
}
|
||||
|
||||
if (i.customId === "clockInOut") {
|
||||
const newStatus = !clockedIn;
|
||||
const saved = safeSetClockedIn(viewingUserId, newStatus);
|
||||
|
||||
if (!saved) {
|
||||
return i.reply({
|
||||
content:
|
||||
"Could not update clock in status because the profile database is not available.",
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
|
||||
clockedIn = newStatus;
|
||||
|
||||
if (newStatus) clockedInUsers.add(viewingUserId);
|
||||
else clockedInUsers.delete(viewingUserId);
|
||||
|
||||
const updatedProfileContainer = buildProfileContainer({
|
||||
viewingUser,
|
||||
viewingUserId,
|
||||
bio,
|
||||
timezone,
|
||||
clockedIn,
|
||||
isStaff,
|
||||
});
|
||||
|
||||
const updatedButtonRows = buildProfileButtons({
|
||||
isOwnProfile,
|
||||
isStaff,
|
||||
clockedIn,
|
||||
});
|
||||
|
||||
await i.update({
|
||||
flags: MessageFlags.IsComponentsV2,
|
||||
components: [updatedProfileContainer, ...updatedButtonRows],
|
||||
});
|
||||
|
||||
return interaction.followUp({
|
||||
content: newStatus
|
||||
? "You have successfully clocked in."
|
||||
: "You have successfully clocked out.",
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
|
||||
if (i.customId === "viewTicketStats") {
|
||||
let TicketData = { tickets: [] };
|
||||
|
||||
try {
|
||||
TicketData = JSON.parse(fs.readFileSync("./Data/TicketData.json", "utf8"));
|
||||
} catch (error) {
|
||||
console.error("Error reading TicketData.json:", error);
|
||||
}
|
||||
|
||||
const ticketsClaimed = TicketData.tickets.filter((t) => t.claimedBy === viewingUserId);
|
||||
const ticketsOpen = ticketsClaimed.filter((t) => t.open);
|
||||
const totalTickets = ticketsClaimed.length;
|
||||
|
||||
let totalResponseTime = 0;
|
||||
let responseCount = 0;
|
||||
|
||||
ticketsClaimed.forEach((t) => {
|
||||
if (t.claimedAt && t.createdAt) {
|
||||
totalResponseTime += new Date(t.claimedAt) - new Date(t.createdAt);
|
||||
responseCount++;
|
||||
}
|
||||
});
|
||||
|
||||
const avgMinutes = responseCount
|
||||
? Math.round(totalResponseTime / responseCount / 60000)
|
||||
: 0;
|
||||
|
||||
const statsContainer = buildTicketStatsContainer(
|
||||
viewingUserId,
|
||||
ticketsClaimed,
|
||||
ticketsOpen,
|
||||
avgMinutes,
|
||||
);
|
||||
|
||||
const statsRow = new ActionRowBuilder().addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId("showOpenTickets")
|
||||
.setLabel("Show Open Tickets")
|
||||
.setStyle(ButtonStyle.Secondary),
|
||||
new ButtonBuilder()
|
||||
.setCustomId("backToProfile")
|
||||
.setLabel("Back to Profile")
|
||||
.setStyle(ButtonStyle.Secondary),
|
||||
);
|
||||
|
||||
return i.update({
|
||||
flags: MessageFlags.IsComponentsV2,
|
||||
components: [statsContainer, statsRow],
|
||||
});
|
||||
}
|
||||
|
||||
if (i.customId === "showOpenTickets") {
|
||||
let TicketData = { tickets: [] };
|
||||
|
||||
try {
|
||||
TicketData = JSON.parse(fs.readFileSync("./Data/TicketData.json", "utf8"));
|
||||
} catch (error) {
|
||||
console.error("Error reading TicketData.json:", error);
|
||||
}
|
||||
|
||||
const ticketsOpen = TicketData.tickets.filter(
|
||||
(t) => t.claimedBy === viewingUserId && t.open,
|
||||
);
|
||||
|
||||
const openContainer = buildOpenTicketsContainer(ticketsOpen);
|
||||
|
||||
const backRow = new ActionRowBuilder().addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId("backToStats")
|
||||
.setLabel("Back to Stats")
|
||||
.setStyle(ButtonStyle.Secondary),
|
||||
);
|
||||
|
||||
return i.update({
|
||||
flags: MessageFlags.IsComponentsV2,
|
||||
components: [openContainer, backRow],
|
||||
});
|
||||
}
|
||||
|
||||
if (i.customId === "backToStats") {
|
||||
let TicketData = { tickets: [] };
|
||||
|
||||
try {
|
||||
TicketData = JSON.parse(fs.readFileSync("./Data/TicketData.json", "utf8"));
|
||||
} catch (error) {
|
||||
console.error("Error reading TicketData.json:", error);
|
||||
}
|
||||
|
||||
const ticketsClaimed = TicketData.tickets.filter((t) => t.claimedBy === viewingUserId);
|
||||
const ticketsOpen = ticketsClaimed.filter((t) => t.open);
|
||||
|
||||
let totalResponseTime = 0;
|
||||
let responseCount = 0;
|
||||
|
||||
ticketsClaimed.forEach((t) => {
|
||||
if (t.claimedAt && t.createdAt) {
|
||||
totalResponseTime += new Date(t.claimedAt) - new Date(t.createdAt);
|
||||
responseCount++;
|
||||
}
|
||||
});
|
||||
|
||||
const avgMinutes = responseCount
|
||||
? Math.round(totalResponseTime / responseCount / 60000)
|
||||
: 0;
|
||||
|
||||
const statsContainer = buildTicketStatsContainer(
|
||||
viewingUserId,
|
||||
ticketsClaimed,
|
||||
ticketsOpen,
|
||||
avgMinutes,
|
||||
);
|
||||
|
||||
const statsRow = new ActionRowBuilder().addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId("showOpenTickets")
|
||||
.setLabel("Show Open Tickets")
|
||||
.setStyle(ButtonStyle.Secondary),
|
||||
new ButtonBuilder()
|
||||
.setCustomId("backToProfile")
|
||||
.setLabel("Back to Profile")
|
||||
.setStyle(ButtonStyle.Secondary),
|
||||
);
|
||||
|
||||
return i.update({
|
||||
flags: MessageFlags.IsComponentsV2,
|
||||
components: [statsContainer, statsRow],
|
||||
});
|
||||
}
|
||||
|
||||
if (i.customId === "backToProfile") {
|
||||
const updatedProfileContainer = buildProfileContainer({
|
||||
viewingUser,
|
||||
viewingUserId,
|
||||
bio,
|
||||
timezone,
|
||||
clockedIn,
|
||||
isStaff,
|
||||
});
|
||||
|
||||
const updatedButtonRows = buildProfileButtons({
|
||||
isOwnProfile,
|
||||
isStaff,
|
||||
clockedIn,
|
||||
});
|
||||
|
||||
return i.update({
|
||||
flags: MessageFlags.IsComponentsV2,
|
||||
components: [updatedProfileContainer, ...updatedButtonRows],
|
||||
});
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error executing profile command:", error);
|
||||
|
||||
if (interaction.deferred || interaction.replied) {
|
||||
return interaction
|
||||
.editReply({
|
||||
content: "An error occurred while loading the profile.",
|
||||
components: [],
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
return interaction
|
||||
.reply({
|
||||
content: "An error occurred while loading the profile.",
|
||||
flags: MessageFlags.Ephemeral,
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
module.exports.clockedInUsers = clockedInUsers;
|
||||
@@ -0,0 +1,206 @@
|
||||
// SupportBot | Emerald Services
|
||||
// Translate Command
|
||||
|
||||
const fs = require("fs");
|
||||
const Discord = require("discord.js");
|
||||
const yaml = require("js-yaml");
|
||||
|
||||
const { translate } = require("@vitalets/google-translate-api");
|
||||
|
||||
const supportbot = require("../../Structures/ConfigStore").supportbot;
|
||||
const cmdconfig = require("../../Structures/ConfigStore").commands;
|
||||
const msgconfig = require("../../Structures/ConfigStore").messages;
|
||||
|
||||
const Command = require("../../Structures/Command.js");
|
||||
|
||||
const LANGUAGES = [
|
||||
{ name: "Auto Detect", value: "auto" },
|
||||
{ name: "English", value: "en" },
|
||||
{ name: "Spanish", value: "es" },
|
||||
{ name: "French", value: "fr" },
|
||||
{ name: "German", value: "de" },
|
||||
{ name: "Italian", value: "it" },
|
||||
{ name: "Portuguese", value: "pt" },
|
||||
{ name: "Dutch", value: "nl" },
|
||||
{ name: "Polish", value: "pl" },
|
||||
{ name: "Russian", value: "ru" },
|
||||
{ name: "Ukrainian", value: "uk" },
|
||||
{ name: "Turkish", value: "tr" },
|
||||
{ name: "Arabic", value: "ar" },
|
||||
{ name: "Hebrew", value: "he" },
|
||||
{ name: "Hindi", value: "hi" },
|
||||
{ name: "Japanese", value: "ja" },
|
||||
{ name: "Korean", value: "ko" },
|
||||
{ name: "Chinese (Simplified)", value: "zh-cn" },
|
||||
{ name: "Chinese (Traditional)", value: "zh-tw" },
|
||||
{ name: "Thai", value: "th" },
|
||||
{ name: "Vietnamese", value: "vi" },
|
||||
{ name: "Indonesian", value: "id" },
|
||||
{ name: "Greek", value: "el" },
|
||||
{ name: "Swedish", value: "sv" },
|
||||
{ name: "Romanian", value: "ro" },
|
||||
];
|
||||
|
||||
function buildContainer(title, lines = [], color = null) {
|
||||
const container = new Discord.ContainerBuilder();
|
||||
|
||||
if (color) {
|
||||
container.setAccentColor(parseInt(color.replace("#", ""), 16));
|
||||
}
|
||||
|
||||
container.addTextDisplayComponents(new Discord.TextDisplayBuilder().setContent(`## ${title}`));
|
||||
|
||||
container.addSeparatorComponents(new Discord.SeparatorBuilder().setDivider(true));
|
||||
|
||||
container.addTextDisplayComponents(new Discord.TextDisplayBuilder().setContent(lines.join("\n")));
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
function trim(text, max = 1800) {
|
||||
if (text.length <= max) return text;
|
||||
return text.substring(0, max) + "...";
|
||||
}
|
||||
|
||||
module.exports = new Command({
|
||||
name: cmdconfig.Translate.Command,
|
||||
description: cmdconfig.Translate.Description,
|
||||
type: Discord.ApplicationCommandType.ChatInput,
|
||||
|
||||
options: [
|
||||
{
|
||||
type: Discord.ApplicationCommandOptionType.String,
|
||||
name: "from",
|
||||
description: "Language to translate FROM",
|
||||
required: true,
|
||||
choices: LANGUAGES,
|
||||
},
|
||||
{
|
||||
type: Discord.ApplicationCommandOptionType.String,
|
||||
name: "to",
|
||||
description: "Language to translate TO",
|
||||
required: true,
|
||||
choices: LANGUAGES.filter((l) => l.value !== "auto"),
|
||||
},
|
||||
{
|
||||
type: Discord.ApplicationCommandOptionType.String,
|
||||
name: "text",
|
||||
description: "Text to translate",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
|
||||
permissions: cmdconfig.Translate.Permission,
|
||||
|
||||
async run(interaction) {
|
||||
const { getRole, getChannel } = interaction.client;
|
||||
|
||||
const Staff = await getRole(supportbot.Roles.StaffMember.Staff, interaction.guild);
|
||||
|
||||
const Admin = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild);
|
||||
|
||||
if (!Staff || !Admin) {
|
||||
return interaction.reply({
|
||||
content: "Some roles seem to be missing!",
|
||||
flags: Discord.MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
!interaction.member.roles.cache.has(Staff.id) &&
|
||||
!interaction.member.roles.cache.has(Admin.id)
|
||||
) {
|
||||
return interaction.reply({
|
||||
flags: Discord.MessageFlags.IsComponentsV2 | Discord.MessageFlags.Ephemeral,
|
||||
components: [
|
||||
buildContainer(
|
||||
"Invalid Permissions",
|
||||
[
|
||||
msgconfig.Error.IncorrectPerms,
|
||||
"",
|
||||
`Required role: \`${supportbot.Roles.StaffMember.Staff}\` or \`${supportbot.Roles.StaffMember.Admin}\``,
|
||||
],
|
||||
supportbot.Embed.Colours.Warn,
|
||||
),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
await interaction.deferReply({ flags: Discord.MessageFlags.Ephemeral });
|
||||
|
||||
const fromLang = interaction.options.getString("from");
|
||||
const toLang = interaction.options.getString("to");
|
||||
const text = interaction.options.getString("text");
|
||||
|
||||
const translateLog = await getChannel(supportbot.Translate?.TranslateLog, interaction.guild);
|
||||
|
||||
let result;
|
||||
|
||||
try {
|
||||
result = await translate(text, {
|
||||
from: fromLang === "auto" ? undefined : fromLang,
|
||||
to: toLang,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Translate error:", err);
|
||||
|
||||
return interaction.editReply({
|
||||
flags: Discord.MessageFlags.IsComponentsV2,
|
||||
components: [
|
||||
buildContainer(
|
||||
"Translation Failed",
|
||||
["The translation service returned an error."],
|
||||
supportbot.Embed.Colours.Warn,
|
||||
),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
const detectedLanguage = result.from?.language?.iso || result.from?.language || "unknown";
|
||||
|
||||
const container = buildContainer(
|
||||
"Translation",
|
||||
[
|
||||
`**Requested By:** <@${interaction.user.id}>`,
|
||||
`**From:** \`${fromLang === "auto" ? detectedLanguage : fromLang}\``,
|
||||
`**To:** \`${toLang}\``,
|
||||
"",
|
||||
"**Original Text**",
|
||||
trim(text),
|
||||
"",
|
||||
"**Translated Text**",
|
||||
trim(result.text),
|
||||
],
|
||||
supportbot.Embed.Colours.Success,
|
||||
);
|
||||
|
||||
await interaction.editReply({
|
||||
flags: Discord.MessageFlags.IsComponentsV2,
|
||||
components: [container],
|
||||
});
|
||||
|
||||
if (translateLog) {
|
||||
const logContainer = buildContainer(
|
||||
"Translation Log",
|
||||
[
|
||||
`User: <@${interaction.user.id}>`,
|
||||
`Channel: <#${interaction.channel.id}>`,
|
||||
`From: ${fromLang}`,
|
||||
`To: ${toLang}`,
|
||||
"",
|
||||
trim(text),
|
||||
"",
|
||||
trim(result.text),
|
||||
],
|
||||
supportbot.Embed.Colours.General,
|
||||
);
|
||||
|
||||
translateLog
|
||||
.send({
|
||||
flags: Discord.MessageFlags.IsComponentsV2,
|
||||
components: [logContainer],
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,147 @@
|
||||
const fs = require("fs");
|
||||
const Discord = require("discord.js");
|
||||
const yaml = require("js-yaml");
|
||||
const path = require("path");
|
||||
|
||||
const supportbot = require("../../Structures/ConfigStore").supportbot;
|
||||
const cmdconfig = require("../../Structures/ConfigStore").commands;
|
||||
const msgconfig = require("../../Structures/ConfigStore").messages;
|
||||
|
||||
const Command = require("../../Structures/Command.js");
|
||||
const DB = require("../../Structures/Database.js");
|
||||
|
||||
module.exports = new Command({
|
||||
name: "suggest",
|
||||
description: cmdconfig.Suggestion?.Description || "Submit a new suggestion",
|
||||
type: Discord.ApplicationCommandType.ChatInput,
|
||||
options: [
|
||||
{
|
||||
name: "text",
|
||||
description: "Your suggestion text",
|
||||
type: Discord.ApplicationCommandOptionType.String,
|
||||
required: true
|
||||
}
|
||||
],
|
||||
|
||||
async run(interaction) {
|
||||
const suggestion = interaction.options.getString("text");
|
||||
const mode = supportbot.Suggestions.Mode || "Channel";
|
||||
|
||||
const embed = new Discord.EmbedBuilder()
|
||||
.addFields(
|
||||
{ name: "Suggestion", value: suggestion, inline: true },
|
||||
{ name: "From", value: `<@${interaction.user.id}>` },
|
||||
{ name: `${supportbot.Suggestions.UpvoteEmoji} ${supportbot.Suggestions.UpvoteTitle}`, value: "0", inline: true },
|
||||
{ name: `${supportbot.Suggestions.DownvoteEmoji} ${supportbot.Suggestions.DownvoteTitle}`, value: "0", inline: true }
|
||||
)
|
||||
.setThumbnail(interaction.user.displayAvatarURL())
|
||||
.setColor(supportbot.Embed.Colours.General);
|
||||
|
||||
const row = new Discord.ActionRowBuilder().addComponents(
|
||||
new Discord.ButtonBuilder()
|
||||
.setCustomId("upvote")
|
||||
.setEmoji(supportbot.Suggestions.UpvoteEmoji)
|
||||
.setStyle(supportbot.Suggestions.Buttons.Upvote),
|
||||
new Discord.ButtonBuilder()
|
||||
.setCustomId("downvote")
|
||||
.setEmoji(supportbot.Suggestions.DownvoteEmoji)
|
||||
.setStyle(supportbot.Suggestions.Buttons.Downvote),
|
||||
new Discord.ButtonBuilder()
|
||||
.setCustomId("removevote")
|
||||
.setLabel(supportbot.Suggestions.Buttons.RemoveVote_Title)
|
||||
.setStyle(supportbot.Suggestions.Buttons.RemoveVote)
|
||||
);
|
||||
|
||||
if (mode === "Forum") {
|
||||
const forum = await interaction.guild.channels
|
||||
.fetch(supportbot.Suggestions.ForumChannel)
|
||||
.catch(() => null);
|
||||
|
||||
if (!forum || forum.type !== Discord.ChannelType.GuildForum) {
|
||||
return interaction.reply({
|
||||
embeds: [
|
||||
new Discord.EmbedBuilder()
|
||||
.setTitle("⚠ Missing Forum Channel")
|
||||
.setDescription(msgconfig.Error.MissingChannel)
|
||||
.setColor(supportbot.Embed.Colours.Error)
|
||||
],
|
||||
flags: Discord.MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
|
||||
const appliedTags = supportbot.Suggestions.Tags?.Created
|
||||
? [supportbot.Suggestions.Tags.Created]
|
||||
: [];
|
||||
|
||||
const thread = await forum.threads.create({
|
||||
name: suggestion.slice(0, 100),
|
||||
message: {
|
||||
content: "\u200B",
|
||||
embeds: [embed],
|
||||
components: [row]
|
||||
},
|
||||
appliedTags,
|
||||
autoArchiveDuration: Discord.ThreadAutoArchiveDuration.OneDay,
|
||||
reason: "New Suggestion"
|
||||
});
|
||||
|
||||
embed.setFooter({
|
||||
text: `${supportbot.Embed.Footer} | Suggestion ID: ${thread.id}`,
|
||||
iconURL: interaction.user.displayAvatarURL()
|
||||
});
|
||||
const starter = await thread.fetchStarterMessage();
|
||||
await starter.edit({ embeds: [embed], components: [row] });
|
||||
|
||||
DB.add(thread.id, interaction.user.id, suggestion);
|
||||
|
||||
return interaction.reply({
|
||||
embeds: [
|
||||
new Discord.EmbedBuilder()
|
||||
.setTitle(msgconfig.Suggestions.Sent_Title)
|
||||
.setDescription(`${msgconfig.Suggestions.Sent}`)
|
||||
.addFields({ name: 'Posted in:', value: `<#${forum.id}>` })
|
||||
.setColor(supportbot.Embed.Colours.Success)
|
||||
],
|
||||
flags: Discord.MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
|
||||
// ---------- Channel Mode ----------
|
||||
const channel = await interaction.guild.channels
|
||||
.fetch(supportbot.Suggestions.Channel)
|
||||
.catch(() => null);
|
||||
|
||||
if (!channel) {
|
||||
return interaction.reply({
|
||||
embeds: [
|
||||
new Discord.EmbedBuilder()
|
||||
.setTitle("⚠ Missing Suggestion Channel")
|
||||
.setDescription(msgconfig.Error.MissingChannel)
|
||||
.setColor(supportbot.Embed.Colours.Error)
|
||||
],
|
||||
flags: Discord.MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
|
||||
const message = await channel.send({ embeds: [embed], components: [row] });
|
||||
|
||||
embed.setFooter({
|
||||
text: `${supportbot.Embed.Footer} | Suggestion ID: ${message.id}`,
|
||||
iconURL: interaction.user.displayAvatarURL()
|
||||
});
|
||||
await message.edit({ embeds: [embed], components: [row] });
|
||||
|
||||
DB.add(message.id, interaction.user.id, suggestion);
|
||||
|
||||
return interaction.reply({
|
||||
embeds: [
|
||||
new Discord.EmbedBuilder()
|
||||
.setTitle(msgconfig.Suggestions.Sent_Title)
|
||||
.setDescription(`${msgconfig.Suggestions.Sent}`)
|
||||
.addFields({ name: 'Posted in:', value: `<#${channel.id}>` })
|
||||
.setColor(supportbot.Embed.Colours.Success)
|
||||
],
|
||||
flags: Discord.MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,183 @@
|
||||
const Discord = require("discord.js");
|
||||
const fs = require("fs");
|
||||
const yaml = require("js-yaml");
|
||||
|
||||
const supportbot = require("../../Structures/ConfigStore").supportbot;
|
||||
const msgconfig = require("../../Structures/ConfigStore").messages;
|
||||
const cmdconfig = require("../../Structures/ConfigStore").commands;
|
||||
const SuggestionDB = require("../../Structures/Database.js");
|
||||
const Command = require("../../Structures/Command.js");
|
||||
|
||||
module.exports = new Command({
|
||||
name: cmdconfig.SuggestionAdmin.Command,
|
||||
description: cmdconfig.SuggestionAdmin.Description,
|
||||
type: Discord.ApplicationCommandType.ChatInput,
|
||||
permissions: cmdconfig.SuggestionAdmin.Permission,
|
||||
options: [
|
||||
{
|
||||
name: "status",
|
||||
description: "New status",
|
||||
type: Discord.ApplicationCommandOptionType.String,
|
||||
required: true,
|
||||
choices: [
|
||||
{ name: "Solved", value: "Solved" },
|
||||
{ name: "Denied", value: "Denied" },
|
||||
{ name: "Considering", value: "Considering" }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "id",
|
||||
description: "Message ID (required in Channel mode)",
|
||||
type: Discord.ApplicationCommandOptionType.String,
|
||||
required: false
|
||||
}
|
||||
],
|
||||
|
||||
async run(interaction) {
|
||||
const status = interaction.options.getString("status");
|
||||
const idOption = interaction.options.getString("id");
|
||||
const mode = supportbot.Suggestions.Mode || "Channel";
|
||||
|
||||
const { getRole } = interaction.client;
|
||||
const staffRole = await getRole(supportbot.Roles.StaffMember.Staff, interaction.guild);
|
||||
const adminRole = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild);
|
||||
if (
|
||||
!interaction.member.roles.cache.has(staffRole?.id) &&
|
||||
!interaction.member.roles.cache.has(adminRole?.id)
|
||||
) {
|
||||
return interaction.reply({
|
||||
embeds: [
|
||||
new Discord.EmbedBuilder()
|
||||
.setTitle("Invalid Permissions!")
|
||||
.setDescription(
|
||||
`${msgconfig.Error.IncorrectPerms}\nRequired: ${supportbot.Roles.StaffMember.Staff} or ${supportbot.Roles.StaffMember.Admin}`
|
||||
)
|
||||
.setColor(supportbot.Embed.Colours.Warn)
|
||||
],
|
||||
flags: Discord.MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
|
||||
if (mode === "Forum") {
|
||||
const thread = interaction.channel;
|
||||
if (!thread.isThread() || thread.parentId !== supportbot.Suggestions.ForumChannel) {
|
||||
return interaction.reply({
|
||||
embeds: [
|
||||
new Discord.EmbedBuilder()
|
||||
.setTitle("Not a Suggestion Thread")
|
||||
.setDescription(msgconfig.Error.MissingChannel)
|
||||
.setColor(supportbot.Embed.Colours.Error)
|
||||
],
|
||||
flags: Discord.MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
|
||||
const tagMap = {
|
||||
Solved: supportbot.Suggestions.Tags.Solved,
|
||||
Denied: supportbot.Suggestions.Tags.Denied,
|
||||
Considering: supportbot.Suggestions.Tags.Considering
|
||||
};
|
||||
const tagId = tagMap[status];
|
||||
if (!tagId) {
|
||||
return interaction.reply({
|
||||
embeds: [
|
||||
new Discord.EmbedBuilder()
|
||||
.setTitle("Tag Missing")
|
||||
.setDescription("No forum tag configured for this status.")
|
||||
.setColor(supportbot.Embed.Colours.Error)
|
||||
],
|
||||
flags: Discord.MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
|
||||
await thread.setAppliedTags([tagId]).catch(() => null);
|
||||
SuggestionDB.setStatus(thread.id, status);
|
||||
|
||||
return interaction.reply({
|
||||
embeds: [
|
||||
new Discord.EmbedBuilder()
|
||||
.setTitle("Suggestion Updated")
|
||||
.setDescription(`Status set to **${status}**.`)
|
||||
.setColor(supportbot.Embed.Colours.Success)
|
||||
],
|
||||
flags: Discord.MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
|
||||
if (!idOption) {
|
||||
return interaction.reply({
|
||||
embeds: [
|
||||
new Discord.EmbedBuilder()
|
||||
.setTitle("Message ID Required")
|
||||
.setDescription("Provide the suggestion’s message ID when in Channel mode.")
|
||||
.setColor(supportbot.Embed.Colours.Error)
|
||||
],
|
||||
flags: Discord.MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
|
||||
const channel = await interaction.guild.channels
|
||||
.fetch(supportbot.Suggestions.Channel)
|
||||
.catch(() => null);
|
||||
if (!channel) {
|
||||
return interaction.reply({
|
||||
embeds: [
|
||||
new Discord.EmbedBuilder()
|
||||
.setTitle("Missing Suggestion Channel")
|
||||
.setDescription(msgconfig.Error.MissingChannel)
|
||||
.setColor(supportbot.Embed.Colours.Error)
|
||||
],
|
||||
flags: Discord.MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
|
||||
const targetMsg = await channel.messages.fetch(idOption).catch(() => null);
|
||||
if (!targetMsg) {
|
||||
return interaction.reply({
|
||||
embeds: [
|
||||
new Discord.EmbedBuilder()
|
||||
.setTitle("Message Not Found")
|
||||
.setDescription("Could not fetch a suggestion with that ID.")
|
||||
.setColor(supportbot.Embed.Colours.Error)
|
||||
],
|
||||
flags: Discord.MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
|
||||
const oldEmbed = targetMsg.embeds[0];
|
||||
if (!oldEmbed) {
|
||||
return interaction.reply({
|
||||
embeds: [
|
||||
new Discord.EmbedBuilder()
|
||||
.setTitle("No Embed Found")
|
||||
.setDescription("The target message has no suggestion embed.")
|
||||
.setColor(supportbot.Embed.Colours.Error)
|
||||
],
|
||||
flags: Discord.MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
|
||||
const colorMap = {
|
||||
Solved: supportbot.Embed.Colours.Success,
|
||||
Denied: supportbot.Embed.Colours.Error,
|
||||
Considering: supportbot.Embed.Colours.Warn
|
||||
};
|
||||
|
||||
const newEmbed = Discord.EmbedBuilder.from(oldEmbed)
|
||||
.setColor(colorMap[status])
|
||||
.addFields({ name: "Status", value: status, inline: true });
|
||||
|
||||
await targetMsg.edit({ embeds: [newEmbed], components: targetMsg.components });
|
||||
SuggestionDB.setStatus(idOption, status);
|
||||
|
||||
return interaction.reply({
|
||||
embeds: [
|
||||
new Discord.EmbedBuilder()
|
||||
.setTitle("Suggestion Updated")
|
||||
.setDescription(`Suggestion **${idOption}** marked as **${status}**.`)
|
||||
.setColor(supportbot.Embed.Colours.Success)
|
||||
],
|
||||
flags: Discord.MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,493 @@
|
||||
|
||||
|
||||
const fs = require("fs");
|
||||
const Discord = require("discord.js");
|
||||
const yaml = require("js-yaml");
|
||||
|
||||
const db = require("../../Structures/Database.js");
|
||||
const TicketManager = require("../../Structures/TicketManager.js");
|
||||
const Command = require("../../Structures/Command.js");
|
||||
|
||||
const supportbot = require("../../Structures/ConfigStore").supportbot;
|
||||
const cmdconfig = require("../../Structures/ConfigStore").commands;
|
||||
const msgconfig = require("../../Structures/ConfigStore").messages;
|
||||
|
||||
function getDepartmentConfig(departmentKey) {
|
||||
return supportbot.Ticket?.DepartmentSystem?.Departments?.[departmentKey] || null;
|
||||
}
|
||||
|
||||
function getTicketUserId(ticket) {
|
||||
return ticket?.user || ticket?.user_id || null;
|
||||
}
|
||||
|
||||
function buildContainer(title, lines = []) {
|
||||
const container = new Discord.ContainerBuilder();
|
||||
|
||||
if (supportbot.Embed?.Colours?.General) {
|
||||
container.setAccentColor(
|
||||
parseInt(String(supportbot.Embed.Colours.General).replace("#", ""), 16),
|
||||
);
|
||||
}
|
||||
|
||||
container.addTextDisplayComponents(new Discord.TextDisplayBuilder().setContent(`## ${title}`));
|
||||
|
||||
container.addSeparatorComponents(new Discord.SeparatorBuilder().setDivider(true));
|
||||
|
||||
if (lines.length > 0) {
|
||||
container.addTextDisplayComponents(
|
||||
new Discord.TextDisplayBuilder().setContent(lines.join("\n")),
|
||||
);
|
||||
}
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
function buildReviewSummaryContainer(ticket, rating, comment) {
|
||||
const departmentConfig = ticket.department ? getDepartmentConfig(ticket.department) : null;
|
||||
const departmentName = departmentConfig?.Name || ticket.department || null;
|
||||
const departmentEmoji = departmentConfig?.Emoji || "🎫";
|
||||
|
||||
const claimedByEnabled = supportbot.Ticket?.ClaimTickets?.Enabled === true;
|
||||
const claimedBy = claimedByEnabled && ticket.claimedBy ? `<@${ticket.claimedBy}>` : null;
|
||||
|
||||
const ticketUserId = getTicketUserId(ticket);
|
||||
const lines = [
|
||||
`**${msgconfig.ReviewSystem.ReviewEmbed.ReviewedByTitle}** ${ticketUserId ? `<@${ticketUserId}>` : "Unknown User"}`,
|
||||
];
|
||||
|
||||
if (claimedBy) {
|
||||
lines.unshift(`**${msgconfig.ReviewSystem.ReviewEmbed.ReviewedStaffTitle}** ${claimedBy}`);
|
||||
}
|
||||
|
||||
if (supportbot.Ticket?.DepartmentSystem?.Enabled === true && departmentName) {
|
||||
lines.push(`**Department** ${departmentEmoji} ${departmentName}`);
|
||||
}
|
||||
|
||||
lines.push("");
|
||||
lines.push(`**${msgconfig.ReviewSystem.ReviewEmbed.RatingTitle}**`);
|
||||
lines.push(`${msgconfig.ReviewSystem.ReviewEmbed.ReviewEmoji.repeat(Number(rating))}`);
|
||||
lines.push("");
|
||||
lines.push(`**${msgconfig.ReviewSystem.ReviewEmbed.CommentTitle}**`);
|
||||
lines.push(comment || "No Comment Provided");
|
||||
|
||||
const container = new Discord.ContainerBuilder();
|
||||
|
||||
if (msgconfig.ReviewSystem?.ReviewEmbed?.Color) {
|
||||
container.setAccentColor(
|
||||
parseInt(String(msgconfig.ReviewSystem.ReviewEmbed.Color).replace("#", ""), 16),
|
||||
);
|
||||
} else if (supportbot.Embed?.Colours?.General) {
|
||||
container.setAccentColor(
|
||||
parseInt(String(supportbot.Embed.Colours.General).replace("#", ""), 16),
|
||||
);
|
||||
}
|
||||
|
||||
container.addTextDisplayComponents(
|
||||
new Discord.TextDisplayBuilder().setContent(`## ${msgconfig.ReviewSystem.ReviewEmbed.Title}`),
|
||||
);
|
||||
|
||||
container.addSeparatorComponents(new Discord.SeparatorBuilder().setDivider(true));
|
||||
|
||||
container.addTextDisplayComponents(new Discord.TextDisplayBuilder().setContent(lines.join("\n")));
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
async function collectReview(interaction, ticket) {
|
||||
const ticketUserId = getTicketUserId(ticket);
|
||||
if (!ticketUserId) return null;
|
||||
|
||||
const ratingContainer = buildContainer(msgconfig.ReviewSystem.Rate.Title, [
|
||||
msgconfig.ReviewSystem.Rate.Description,
|
||||
"",
|
||||
]);
|
||||
|
||||
const ratingRow = new Discord.ActionRowBuilder().addComponents(
|
||||
new Discord.StringSelectMenuBuilder()
|
||||
.setCustomId(`closeReviewRating:${interaction.id}`)
|
||||
.setPlaceholder("Select a rating")
|
||||
.addOptions([
|
||||
{ label: msgconfig.ReviewSystem.Stars.One, value: "1" },
|
||||
{ label: msgconfig.ReviewSystem.Stars.Two, value: "2" },
|
||||
{ label: msgconfig.ReviewSystem.Stars.Three, value: "3" },
|
||||
{ label: msgconfig.ReviewSystem.Stars.Four, value: "4" },
|
||||
{ label: msgconfig.ReviewSystem.Stars.Five, value: "5" },
|
||||
]),
|
||||
);
|
||||
|
||||
ratingContainer.addActionRowComponents(ratingRow);
|
||||
|
||||
const ratingPrompt = await interaction.followUp({
|
||||
flags: Discord.MessageFlags.IsComponentsV2,
|
||||
components: [ratingContainer],
|
||||
});
|
||||
|
||||
let rating = null;
|
||||
|
||||
try {
|
||||
const ratingInteraction = await ratingPrompt.awaitMessageComponent({
|
||||
componentType: Discord.ComponentType.StringSelect,
|
||||
time: 120000,
|
||||
filter: (i) =>
|
||||
i.customId === `closeReviewRating:${interaction.id}` && i.user.id === ticketUserId,
|
||||
});
|
||||
|
||||
rating = ratingInteraction.values[0];
|
||||
await ratingInteraction.deferUpdate().catch(() => {});
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
const commentContainer = buildContainer(msgconfig.ReviewSystem.Comment.Title, [
|
||||
msgconfig.ReviewSystem.Comment.Description,
|
||||
"",
|
||||
]);
|
||||
|
||||
const commentRow = new Discord.ActionRowBuilder().addComponents(
|
||||
new Discord.ButtonBuilder()
|
||||
.setCustomId(`closeReviewWrite:${interaction.id}`)
|
||||
.setLabel("Write Comment")
|
||||
.setStyle(Discord.ButtonStyle.Secondary),
|
||||
new Discord.ButtonBuilder()
|
||||
.setCustomId(`closeReviewSkip:${interaction.id}`)
|
||||
.setLabel("Skip Comment")
|
||||
.setStyle(Discord.ButtonStyle.Danger),
|
||||
);
|
||||
|
||||
commentContainer.addActionRowComponents(commentRow);
|
||||
|
||||
const commentPrompt = await interaction.followUp({
|
||||
flags: Discord.MessageFlags.IsComponentsV2,
|
||||
components: [commentContainer],
|
||||
});
|
||||
|
||||
let comment = "No Comment Provided";
|
||||
|
||||
try {
|
||||
const buttonInteraction = await commentPrompt.awaitMessageComponent({
|
||||
componentType: Discord.ComponentType.Button,
|
||||
time: 120000,
|
||||
filter: (i) =>
|
||||
(i.customId === `closeReviewWrite:${interaction.id}` ||
|
||||
i.customId === `closeReviewSkip:${interaction.id}`) &&
|
||||
i.user.id === ticketUserId,
|
||||
});
|
||||
|
||||
if (buttonInteraction.customId === `closeReviewSkip:${interaction.id}`) {
|
||||
await buttonInteraction.deferUpdate().catch(() => {});
|
||||
return { rating, comment };
|
||||
}
|
||||
|
||||
await buttonInteraction
|
||||
.update({
|
||||
flags: Discord.MessageFlags.IsComponentsV2,
|
||||
components: [
|
||||
buildContainer(msgconfig.ReviewSystem.Comment.Title, [
|
||||
`Requested from: <@${ticketUserId}>`,
|
||||
"",
|
||||
"Please send your review comment in the channel now.",
|
||||
"Type `no` to skip.",
|
||||
]),
|
||||
],
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
const collected = await interaction.channel.awaitMessages({
|
||||
max: 1,
|
||||
time: 120000,
|
||||
filter: (m) => m.author.id === ticketUserId,
|
||||
});
|
||||
|
||||
const message = collected.first();
|
||||
|
||||
if (message) {
|
||||
const rawComment = message.content?.trim();
|
||||
if (rawComment && rawComment.toLowerCase() !== "no") {
|
||||
comment = rawComment;
|
||||
}
|
||||
|
||||
await message.delete().catch(() => {});
|
||||
}
|
||||
} catch {
|
||||
return { rating, comment };
|
||||
}
|
||||
|
||||
return { rating, comment };
|
||||
}
|
||||
|
||||
function getTranscriptAttachmentFromResult(result) {
|
||||
if (!result) return null;
|
||||
|
||||
if (typeof result === "string") {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (result.filePath) return result.filePath;
|
||||
if (result.attachment) return result.attachment;
|
||||
if (result.path) return result.path;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function maybeDMTranscript(interaction, ticket, transcriptResult) {
|
||||
if (supportbot.Ticket?.UserManagement?.DMTranscript !== true) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ticketUserId = getTicketUserId(ticket);
|
||||
if (!ticketUserId) return;
|
||||
|
||||
const ticketUser = await interaction.client.users.fetch(ticketUserId).catch(() => null);
|
||||
|
||||
if (!ticketUser) return;
|
||||
|
||||
const attachmentPath = getTranscriptAttachmentFromResult(transcriptResult);
|
||||
if (!attachmentPath) return;
|
||||
|
||||
try {
|
||||
await ticketUser.send({
|
||||
content: `Here is a copy of your ticket transcript from \`${interaction.guild.name}\`.`,
|
||||
files: [attachmentPath],
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to DM transcript to user:", error);
|
||||
}
|
||||
}
|
||||
|
||||
async function collectCloseConfirmation(interaction, ticket, reason) {
|
||||
const ticketUserId = getTicketUserId(ticket);
|
||||
|
||||
const lines = [
|
||||
"Are you sure you want to close this ticket?",
|
||||
"",
|
||||
`**Ticket:** ${interaction.channel.name}`,
|
||||
`**Reason:** ${reason || "No Reason Provided."}`,
|
||||
];
|
||||
|
||||
if (ticketUserId) {
|
||||
lines.push(`**Ticket User:** <@${ticketUserId}>`);
|
||||
}
|
||||
|
||||
if (supportbot.Ticket?.ReviewSystem?.Enabled === true) {
|
||||
lines.push("");
|
||||
lines.push("A review will be requested before the ticket is closed.");
|
||||
}
|
||||
|
||||
const confirmContainer = buildContainer("Close Confirmation", lines);
|
||||
|
||||
const confirmRow = new Discord.ActionRowBuilder().addComponents(
|
||||
new Discord.ButtonBuilder()
|
||||
.setCustomId(`confirmClose:${interaction.id}`)
|
||||
.setLabel("Confirm Close")
|
||||
.setStyle(Discord.ButtonStyle.Danger),
|
||||
new Discord.ButtonBuilder()
|
||||
.setCustomId(`cancelClose:${interaction.id}`)
|
||||
.setLabel("Cancel")
|
||||
.setStyle(Discord.ButtonStyle.Secondary),
|
||||
);
|
||||
|
||||
confirmContainer.addActionRowComponents(confirmRow);
|
||||
|
||||
const confirmMessage = await interaction.editReply({
|
||||
flags: Discord.MessageFlags.IsComponentsV2,
|
||||
components: [confirmContainer],
|
||||
});
|
||||
|
||||
try {
|
||||
const buttonInteraction = await confirmMessage.awaitMessageComponent({
|
||||
componentType: Discord.ComponentType.Button,
|
||||
time: 120000,
|
||||
filter: (i) =>
|
||||
(i.customId === `confirmClose:${interaction.id}` ||
|
||||
i.customId === `cancelClose:${interaction.id}`) &&
|
||||
i.user.id === interaction.user.id,
|
||||
});
|
||||
|
||||
if (buttonInteraction.customId === `cancelClose:${interaction.id}`) {
|
||||
await buttonInteraction
|
||||
.update({
|
||||
flags: Discord.MessageFlags.IsComponentsV2,
|
||||
components: [
|
||||
buildContainer("Close Cancelled", ["The ticket close process has been cancelled."]),
|
||||
],
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
await buttonInteraction.deferUpdate().catch(() => {});
|
||||
return true;
|
||||
} catch {
|
||||
await interaction
|
||||
.editReply({
|
||||
flags: Discord.MessageFlags.IsComponentsV2,
|
||||
components: [buildContainer("Close Cancelled", ["The close confirmation timed out."])],
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function postReview(interaction, ticket, reviewData) {
|
||||
const { getChannel } = interaction.client;
|
||||
const reviewChannel = await getChannel(supportbot.Ticket.ReviewSystem.Channel, interaction.guild);
|
||||
|
||||
if (!reviewChannel || !reviewData) return;
|
||||
|
||||
const reviewContainer = buildReviewSummaryContainer(
|
||||
ticket,
|
||||
reviewData.rating,
|
||||
reviewData.comment,
|
||||
);
|
||||
|
||||
await reviewChannel
|
||||
.send({
|
||||
flags: Discord.MessageFlags.IsComponentsV2,
|
||||
components: [reviewContainer],
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
async function handleCloseTicket(interaction, reason, ticket) {
|
||||
try {
|
||||
if (typeof db.updateTicketStatus === "function") {
|
||||
db.updateTicketStatus(interaction.channel.id, "closed");
|
||||
}
|
||||
|
||||
const transcriptResult = await TicketManager.createTranscript(interaction, ticket, reason);
|
||||
|
||||
await maybeDMTranscript(interaction, ticket, transcriptResult);
|
||||
|
||||
await interaction
|
||||
.editReply({
|
||||
flags: Discord.MessageFlags.IsComponentsV2,
|
||||
components: [
|
||||
buildContainer("Ticket Closed", [
|
||||
`**Ticket:** ${interaction.channel.name}`,
|
||||
`**Closed By:** <@${interaction.user.id}>`,
|
||||
`**Reason:** ${reason || "No Reason Provided."}`,
|
||||
]),
|
||||
],
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
await interaction.channel.delete().catch(() => {});
|
||||
} catch (err) {
|
||||
console.error("Error handling ticket close:", err);
|
||||
|
||||
if (interaction.deferred || interaction.replied) {
|
||||
await interaction
|
||||
.editReply({
|
||||
content: "An error occurred while closing the ticket.",
|
||||
components: [],
|
||||
})
|
||||
.catch(() => {});
|
||||
return;
|
||||
}
|
||||
|
||||
await interaction
|
||||
.reply({
|
||||
content: "An error occurred while closing the ticket.",
|
||||
flags: Discord.MessageFlags.Ephemeral,
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new Command({
|
||||
name: cmdconfig.CloseTicket.Command,
|
||||
description: cmdconfig.CloseTicket.Description,
|
||||
type: Discord.ApplicationCommandType.ChatInput,
|
||||
options: [
|
||||
{
|
||||
name: "reason",
|
||||
description: "Ticket Close Reason",
|
||||
type: Discord.ApplicationCommandOptionType.String,
|
||||
},
|
||||
],
|
||||
permissions: cmdconfig.CloseTicket.Permission,
|
||||
|
||||
async run(interaction) {
|
||||
const { getRole } = interaction.client;
|
||||
|
||||
if (supportbot.Ticket.Close.StaffOnly) {
|
||||
const SupportStaff = await getRole(supportbot.Roles.StaffMember.Staff, interaction.guild);
|
||||
const Admin = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild);
|
||||
|
||||
if (!SupportStaff || !Admin) {
|
||||
return interaction.reply({
|
||||
content: "Some roles seem to be missing! Please check for errors when starting the bot.",
|
||||
flags: Discord.MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
!interaction.member.roles.cache.has(SupportStaff.id) &&
|
||||
!interaction.member.roles.cache.has(Admin.id)
|
||||
) {
|
||||
return interaction.reply({
|
||||
flags: Discord.MessageFlags.IsComponentsV2 | Discord.MessageFlags.Ephemeral,
|
||||
components: [
|
||||
buildContainer("Invalid Permissions!", [
|
||||
msgconfig.Error.IncorrectPerms,
|
||||
"",
|
||||
`Role Required: \`${supportbot.Roles.StaffMember.Staff}\` or \`${supportbot.Roles.StaffMember.Admin}\``,
|
||||
]),
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const isThread = interaction.channel.type === Discord.ChannelType.PrivateThread;
|
||||
|
||||
if (
|
||||
(supportbot.Ticket.TicketType === "threads" && !isThread) ||
|
||||
(supportbot.Ticket.TicketType === "channels" &&
|
||||
interaction.channel.type !== Discord.ChannelType.GuildText)
|
||||
) {
|
||||
return interaction.reply({
|
||||
flags: Discord.MessageFlags.IsComponentsV2 | Discord.MessageFlags.Ephemeral,
|
||||
components: [
|
||||
buildContainer("Invalid Channel!", [
|
||||
`This command can only be used in a ${
|
||||
supportbot.Ticket.TicketType === "threads" ? "ticket thread" : "ticket channel"
|
||||
}.`,
|
||||
]),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
await interaction.deferReply({ flags: Discord.MessageFlags.Ephemeral });
|
||||
|
||||
const ticket = db.getTicket(interaction.channel.id);
|
||||
|
||||
if (!ticket) {
|
||||
return interaction.editReply({
|
||||
flags: Discord.MessageFlags.IsComponentsV2,
|
||||
components: [buildContainer("No Ticket Found!", [msgconfig.Error.NoValidTicket])],
|
||||
});
|
||||
}
|
||||
|
||||
const reason = interaction.options?.getString("reason") || "No Reason Provided.";
|
||||
|
||||
const confirmed = await collectCloseConfirmation(interaction, ticket, reason);
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (supportbot.Ticket?.ReviewSystem?.Enabled === true) {
|
||||
const reviewData = await collectReview(interaction, ticket);
|
||||
|
||||
if (reviewData) {
|
||||
await postReview(interaction, ticket, reviewData);
|
||||
}
|
||||
|
||||
await handleCloseTicket(interaction, reason, ticket);
|
||||
return;
|
||||
}
|
||||
|
||||
await handleCloseTicket(interaction, reason, ticket);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,664 @@
|
||||
const fs = require("fs");
|
||||
const yaml = require("js-yaml");
|
||||
const {
|
||||
EmbedBuilder,
|
||||
ApplicationCommandOptionType,
|
||||
ApplicationCommandType,
|
||||
ChannelType,
|
||||
MessageFlags,
|
||||
ModalBuilder,
|
||||
ActionRowBuilder,
|
||||
TextInputBuilder,
|
||||
TextInputStyle,
|
||||
} = require("discord.js");
|
||||
|
||||
const supportbot = require("../../Structures/ConfigStore").supportbot;
|
||||
const msgconfig = require("../../Structures/ConfigStore").messages;
|
||||
const cmdconfig = require("../../Structures/ConfigStore").commands;
|
||||
|
||||
const TicketManager = require("../../Structures/TicketManager.js");
|
||||
const Command = require("../../Structures/Command.js");
|
||||
const db = require("../../Structures/Database.js");
|
||||
|
||||
function getDepartments() {
|
||||
return supportbot.Ticket?.DepartmentSystem?.Departments || {};
|
||||
}
|
||||
|
||||
function getPriorities() {
|
||||
return supportbot.Ticket?.PrioritySystem?.Priorities || {};
|
||||
}
|
||||
|
||||
function prioritiesEnabled() {
|
||||
return (
|
||||
supportbot.Ticket?.TicketType !== "threads" &&
|
||||
supportbot.Ticket?.PrioritySystem?.Enabled === true
|
||||
);
|
||||
}
|
||||
|
||||
function departmentsEnabled() {
|
||||
return (
|
||||
supportbot.Ticket?.TicketType !== "threads" &&
|
||||
supportbot.Ticket?.DepartmentSystem?.Enabled === true
|
||||
);
|
||||
}
|
||||
|
||||
function buildTicketChannelName(ticketNumberID, priorityKey, departmentConfig) {
|
||||
const defaultPrefix = supportbot.Ticket?.ChannelPrefix || supportbot.Ticket?.Channel || "ticket-";
|
||||
|
||||
const departmentPrefix = departmentConfig?.ChannelPrefix || defaultPrefix;
|
||||
|
||||
const safePrefix = String(departmentPrefix).trim() || "ticket-";
|
||||
const baseName = `${safePrefix}${ticketNumberID}`;
|
||||
|
||||
if (!prioritiesEnabled() || !priorityKey) {
|
||||
return baseName;
|
||||
}
|
||||
|
||||
return `${baseName}-${String(priorityKey).toLowerCase()}`;
|
||||
}
|
||||
|
||||
module.exports = new Command({
|
||||
name: cmdconfig.TicketManage?.Command || "ticketmanage",
|
||||
description: cmdconfig.TicketManage?.Description || "Manage tickets",
|
||||
type: ApplicationCommandType.ChatInput,
|
||||
permissions: cmdconfig.TicketManage?.Permission || ["ManageChannels"],
|
||||
|
||||
options: [
|
||||
{
|
||||
name: "transfer",
|
||||
description: "Transfer a ticket to another department",
|
||||
type: ApplicationCommandOptionType.Subcommand,
|
||||
options: [
|
||||
{
|
||||
name: "department",
|
||||
description: "Department to transfer this ticket to",
|
||||
type: ApplicationCommandOptionType.String,
|
||||
required: true,
|
||||
choices: Object.entries(getDepartments())
|
||||
.slice(0, 25)
|
||||
.map(([key, dept]) => ({
|
||||
name: dept.Name || key,
|
||||
value: key,
|
||||
})),
|
||||
},
|
||||
{
|
||||
name: "priority",
|
||||
description: "Optional new priority",
|
||||
type: ApplicationCommandOptionType.String,
|
||||
required: false,
|
||||
choices: Object.entries(getPriorities())
|
||||
.slice(0, 25)
|
||||
.map(([key, priority]) => ({
|
||||
name: priority.Name || key,
|
||||
value: key,
|
||||
})),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "priority",
|
||||
description: "Change a ticket priority",
|
||||
type: ApplicationCommandOptionType.Subcommand,
|
||||
options: [
|
||||
{
|
||||
name: "level",
|
||||
description: "New priority level",
|
||||
type: ApplicationCommandOptionType.String,
|
||||
required: true,
|
||||
choices: Object.entries(getPriorities())
|
||||
.slice(0, 25)
|
||||
.map(([key, priority]) => ({
|
||||
name: priority.Name || key,
|
||||
value: key,
|
||||
})),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: cmdconfig.TicketManage?.ForceAdd?.Command || "forceadd",
|
||||
description:
|
||||
cmdconfig.TicketManage?.ForceAdd?.Description || "Force add a user to the current ticket",
|
||||
type: ApplicationCommandOptionType.Subcommand,
|
||||
options: [
|
||||
{
|
||||
name: "user",
|
||||
description: "The user to add",
|
||||
type: ApplicationCommandOptionType.User,
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: cmdconfig.TicketManage?.ForceRemove?.Command || "forceremove",
|
||||
description:
|
||||
cmdconfig.TicketManage?.ForceRemove?.Description || "Remove a user from the current ticket",
|
||||
type: ApplicationCommandOptionType.Subcommand,
|
||||
options: [
|
||||
{
|
||||
name: "user",
|
||||
description: "The user to remove",
|
||||
type: ApplicationCommandOptionType.User,
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: cmdconfig.TicketManage?.Rename?.Command || "rename",
|
||||
description: cmdconfig.TicketManage?.Rename?.Description || "Rename the current ticket",
|
||||
type: ApplicationCommandOptionType.Subcommand,
|
||||
options: [],
|
||||
},
|
||||
{
|
||||
name: cmdconfig.TicketManage?.Close?.Command || "close",
|
||||
description: cmdconfig.TicketManage?.Close?.Description || "Close the current ticket",
|
||||
type: ApplicationCommandOptionType.Subcommand,
|
||||
options: [
|
||||
{
|
||||
name: "reason",
|
||||
description: "Reason for closing the ticket",
|
||||
type: ApplicationCommandOptionType.String,
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
async run(interaction) {
|
||||
try {
|
||||
const subcommand = interaction.options.getSubcommand(false);
|
||||
const ticket = db.getTicket(interaction.channel.id);
|
||||
|
||||
if (!ticket) {
|
||||
return interaction.reply({
|
||||
content: "This channel is not a ticket.",
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
|
||||
const { getRole } = interaction.client;
|
||||
|
||||
let SupportStaff = await getRole(supportbot.Roles.StaffMember.Staff, interaction.guild);
|
||||
let Admin = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild);
|
||||
let Moderator = await getRole(supportbot.Roles.StaffMember.Moderator, interaction.guild);
|
||||
|
||||
if (!SupportStaff || !Admin || !Moderator) {
|
||||
const missingRolesEmbed = new EmbedBuilder()
|
||||
.setDescription(msgconfig.Error.InvalidChannel || "Required roles are missing!")
|
||||
.setColor(supportbot.Embed.Colours.Warn);
|
||||
|
||||
return interaction.reply({
|
||||
embeds: [missingRolesEmbed],
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
|
||||
const NoPerms = new EmbedBuilder()
|
||||
.setDescription(
|
||||
msgconfig.Error.IncorrectPerms || "You do not have the correct permissions!",
|
||||
)
|
||||
.setColor(supportbot.Embed.Colours.Warn);
|
||||
|
||||
if (
|
||||
!interaction.member.roles.cache.has(Admin.id) &&
|
||||
!interaction.member.roles.cache.has(Moderator.id) &&
|
||||
(!supportbot.Roles?.Mod?.AllowSupportStaff || !interaction.member.roles.cache.has(SupportStaff.id))
|
||||
) {
|
||||
return interaction.reply({
|
||||
embeds: [NoPerms],
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
|
||||
if (subcommand === "transfer") {
|
||||
if (!departmentsEnabled()) {
|
||||
return interaction.reply({
|
||||
content: "Departments are disabled, or ticket mode is set to threads.",
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
|
||||
if (interaction.channel.type !== ChannelType.GuildText) {
|
||||
return interaction.reply({
|
||||
content: "Ticket transfer only works for ticket channels.",
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
|
||||
const departmentKey = interaction.options.getString("department");
|
||||
const requestedPriority = interaction.options.getString("priority");
|
||||
|
||||
const departments = getDepartments();
|
||||
const departmentConfig = departments[departmentKey];
|
||||
|
||||
if (!departmentConfig) {
|
||||
return interaction.reply({
|
||||
content: "That department does not exist.",
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
|
||||
const targetCategoryId = departmentConfig.Category;
|
||||
const targetRoleId = departmentConfig.Role || null;
|
||||
|
||||
if (!targetCategoryId) {
|
||||
return interaction.reply({
|
||||
content: "That department does not have a category configured.",
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
|
||||
const targetCategory = interaction.guild.channels.cache.get(targetCategoryId);
|
||||
if (!targetCategory) {
|
||||
return interaction.reply({
|
||||
content: "The target department category could not be found.",
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
|
||||
let finalPriority = ticket.priority || null;
|
||||
|
||||
if (requestedPriority) {
|
||||
finalPriority = requestedPriority;
|
||||
} else if (!finalPriority && departmentConfig.DefaultPriority) {
|
||||
finalPriority = departmentConfig.DefaultPriority;
|
||||
}
|
||||
|
||||
const oldDepartmentKey = ticket.department || null;
|
||||
const oldDepartmentConfig = oldDepartmentKey ? departments[oldDepartmentKey] : null;
|
||||
const oldRoleId = oldDepartmentConfig?.Role || null;
|
||||
|
||||
await interaction.channel.setParent(targetCategory.id);
|
||||
|
||||
if (oldRoleId && oldRoleId !== targetRoleId) {
|
||||
await interaction.channel.permissionOverwrites.delete(oldRoleId).catch(() => { });
|
||||
}
|
||||
|
||||
if (targetRoleId) {
|
||||
await interaction.channel.permissionOverwrites.create(targetRoleId, {
|
||||
ViewChannel: true,
|
||||
SendMessages: true,
|
||||
ReadMessageHistory: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof db.updateTicketDepartment === "function") {
|
||||
db.updateTicketDepartment(interaction.channel.id, departmentKey);
|
||||
}
|
||||
|
||||
if (finalPriority && typeof db.updateTicketPriority === "function") {
|
||||
db.updateTicketPriority(interaction.channel.id, finalPriority);
|
||||
}
|
||||
|
||||
const ticketNumber = ticket.number || interaction.channel.name.replace(/\D/g, "");
|
||||
|
||||
const newChannelName = buildTicketChannelName(
|
||||
ticketNumber,
|
||||
finalPriority,
|
||||
departmentConfig,
|
||||
);
|
||||
|
||||
if (interaction.channel.name !== newChannelName) {
|
||||
await interaction.channel.setName(newChannelName).catch(() => { });
|
||||
}
|
||||
|
||||
const deptEmoji = departmentConfig.Emoji || "🎫";
|
||||
const deptName = departmentConfig.Name || departmentKey;
|
||||
|
||||
let description = `> **Department:** ${deptEmoji} ${deptName}`;
|
||||
|
||||
if (finalPriority && prioritiesEnabled()) {
|
||||
const priorityConfig = getPriorities()[finalPriority];
|
||||
const priorityEmoji = priorityConfig?.Emoji || "🟡";
|
||||
const priorityName = priorityConfig?.Name || finalPriority;
|
||||
description += `\n> **Priority:** ${priorityEmoji} ${priorityName}`;
|
||||
}
|
||||
|
||||
description += `\n> **Transferred By:** <@${interaction.user.id}>`;
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle("Ticket Updated")
|
||||
.setDescription(description)
|
||||
.setColor(supportbot.Embed.Colours.Success);
|
||||
|
||||
return interaction.reply({
|
||||
embeds: [embed],
|
||||
});
|
||||
}
|
||||
|
||||
if (subcommand === "priority") {
|
||||
if (!prioritiesEnabled()) {
|
||||
return interaction.reply({
|
||||
content: "Priority is disabled, or ticket mode is set to threads.",
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
|
||||
const level = interaction.options.getString("level");
|
||||
const priorityConfig = getPriorities()[level];
|
||||
|
||||
if (!priorityConfig) {
|
||||
return interaction.reply({
|
||||
content: "That priority does not exist.",
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof db.updateTicketPriority === "function") {
|
||||
db.updateTicketPriority(interaction.channel.id, level);
|
||||
}
|
||||
|
||||
const departments = getDepartments();
|
||||
const departmentConfig = ticket.department ? departments[ticket.department] : null;
|
||||
const ticketNumber = ticket.number || interaction.channel.name.replace(/\D/g, "");
|
||||
|
||||
const newChannelName = buildTicketChannelName(ticketNumber, level, departmentConfig);
|
||||
|
||||
if (interaction.channel.name !== newChannelName) {
|
||||
await interaction.channel.setName(newChannelName).catch(() => { });
|
||||
}
|
||||
|
||||
const priorityEmoji = priorityConfig.Emoji || "🟡";
|
||||
const priorityName = priorityConfig.Name || level;
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle("Ticket Updated")
|
||||
.setDescription(
|
||||
`> **Priority:** ${priorityEmoji} ${priorityName}\n> **Updated By:** <@${interaction.user.id}>`,
|
||||
)
|
||||
.setColor(supportbot.Embed.Colours.Success);
|
||||
|
||||
return interaction.reply({
|
||||
embeds: [embed],
|
||||
});
|
||||
}
|
||||
|
||||
if (subcommand === (cmdconfig.TicketManage?.ForceAdd?.Command || "forceadd")) {
|
||||
const userToAdd = interaction.options.getUser("user");
|
||||
const ticketChannel = interaction.channel;
|
||||
|
||||
if (
|
||||
(supportbot.Ticket.TicketType === "threads" && !ticketChannel.isThread()) ||
|
||||
(supportbot.Ticket.TicketType === "channels" &&
|
||||
ticketChannel.type !== ChannelType.GuildText)
|
||||
) {
|
||||
const onlyInTicket = new EmbedBuilder()
|
||||
.setTitle(msgconfig.ForceAddUser.NotInTicket_Title)
|
||||
.setDescription(msgconfig.ForceAddUser.NotInTicket_Description)
|
||||
.setColor(supportbot.Embed.Colours.Error);
|
||||
|
||||
return interaction.reply({
|
||||
embeds: [onlyInTicket],
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
|
||||
const ticketRow = db.getTicket(ticketChannel.id);
|
||||
if (!ticketRow) {
|
||||
const notFound = new EmbedBuilder()
|
||||
.setTitle(msgconfig.ForceAddUser.NotInTicket_Title)
|
||||
.setDescription(msgconfig.ForceAddUser.NotInTicket_Description)
|
||||
.setColor(supportbot.Embed.Colours.Error);
|
||||
|
||||
return interaction.reply({
|
||||
embeds: [notFound],
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
if (supportbot.Ticket.TicketType === "threads") {
|
||||
const botMember = await ticketChannel.members
|
||||
.fetch(interaction.client.user.id)
|
||||
.catch(() => null);
|
||||
|
||||
if (!botMember) await ticketChannel.join();
|
||||
|
||||
await new Promise((res) => setTimeout(res, 1000));
|
||||
await ticketChannel.members.add(userToAdd.id);
|
||||
} else {
|
||||
await ticketChannel.permissionOverwrites.create(userToAdd.id, {
|
||||
ViewChannel: true,
|
||||
SendMessages: true,
|
||||
ReadMessageHistory: true,
|
||||
});
|
||||
}
|
||||
|
||||
db.addUserToTicket(ticketChannel.id, userToAdd.id);
|
||||
|
||||
const addedEmbed = new EmbedBuilder()
|
||||
.setTitle(msgconfig.ForceAddUser.Added_Title)
|
||||
.setDescription(
|
||||
msgconfig.ForceAddUser.Added_Description.replace("%username%", userToAdd.username),
|
||||
)
|
||||
.setColor(supportbot.Embed.Colours.Success);
|
||||
|
||||
await interaction.reply({
|
||||
embeds: [addedEmbed],
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
|
||||
if (supportbot.Ticket?.UserManagement?.DMOnAdd !== false) {
|
||||
const addedToTicketEmbed = new EmbedBuilder()
|
||||
.setTitle(msgconfig.ForceAddUser.AddedToTicket_Title)
|
||||
.setDescription(
|
||||
msgconfig.ForceAddUser.AddedToTicket_Description.replace(
|
||||
"%channel_link%",
|
||||
`[${ticketChannel.name}](https://discord.com/channels/${ticketChannel.guild.id}/${ticketChannel.id})`,
|
||||
),
|
||||
)
|
||||
.setColor(supportbot.Embed.Colours.General);
|
||||
|
||||
await userToAdd.send({ embeds: [addedToTicketEmbed] }).catch(() => { });
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error adding user to the ticket:", err);
|
||||
|
||||
const errorEmbed = new EmbedBuilder()
|
||||
.setTitle(msgconfig.ForceAddUser.Error_Title)
|
||||
.setDescription(msgconfig.ForceAddUser.Error_Adding)
|
||||
.setColor(supportbot.Embed.Colours.Error);
|
||||
|
||||
await interaction.reply({
|
||||
embeds: [errorEmbed],
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (subcommand === (cmdconfig.TicketManage?.ForceRemove?.Command || "forceremove")) {
|
||||
const userToRemove = interaction.options.getUser("user");
|
||||
const ticketChannel = interaction.channel;
|
||||
|
||||
if (
|
||||
(supportbot.Ticket.TicketType === "threads" && !ticketChannel.isThread()) ||
|
||||
(supportbot.Ticket.TicketType === "channels" &&
|
||||
ticketChannel.type !== ChannelType.GuildText)
|
||||
) {
|
||||
const onlyInTicket = new EmbedBuilder()
|
||||
.setTitle(msgconfig.RemoveUser.NotInTicket_Title)
|
||||
.setDescription(msgconfig.RemoveUser.NotInTicket_Description)
|
||||
.setColor(supportbot.Embed.Colours.Error);
|
||||
|
||||
return interaction.reply({
|
||||
embeds: [onlyInTicket],
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
if (supportbot.Ticket.TicketType === "threads") {
|
||||
await ticketChannel.members.remove(userToRemove.id);
|
||||
} else {
|
||||
await ticketChannel.permissionOverwrites.delete(userToRemove.id);
|
||||
}
|
||||
|
||||
db.removeUserFromTicket(ticketChannel.id, userToRemove.id);
|
||||
|
||||
const removedEmbed = new EmbedBuilder()
|
||||
.setTitle(msgconfig.RemoveUser.Removed_Title)
|
||||
.setDescription(msgconfig.RemoveUser.Removed_Message)
|
||||
.setColor(supportbot.Embed.Colours.Success);
|
||||
|
||||
await interaction.reply({
|
||||
embeds: [removedEmbed],
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
|
||||
if (supportbot.Ticket?.UserManagement?.DMOnRemove !== false) {
|
||||
const removedFromTicketEmbed = new EmbedBuilder()
|
||||
.setTitle(msgconfig.RemoveUser.RemovedFromTicket_Title)
|
||||
.setDescription(
|
||||
msgconfig.RemoveUser.RemovedFromTicket_Description.replace(
|
||||
"%channel_link%",
|
||||
`[${ticketChannel.name}](https://discord.com/channels/${ticketChannel.guild.id}/${ticketChannel.id})`,
|
||||
),
|
||||
)
|
||||
.setColor(supportbot.Embed.Colours.General);
|
||||
|
||||
await userToRemove.send({ embeds: [removedFromTicketEmbed] }).catch(() => { });
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error removing user from the ticket:", err);
|
||||
|
||||
const errorEmbed = new EmbedBuilder()
|
||||
.setTitle(msgconfig.RemoveUser.Error_Title)
|
||||
.setDescription(msgconfig.RemoveUser.Error_Removing)
|
||||
.setColor(supportbot.Embed.Colours.Error);
|
||||
|
||||
await interaction.reply({
|
||||
embeds: [errorEmbed],
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (subcommand === (cmdconfig.TicketManage?.Rename?.Command || "rename")) {
|
||||
const modal = new ModalBuilder()
|
||||
.setCustomId("renameTicketModal")
|
||||
.setTitle("Rename Channel")
|
||||
.addComponents(
|
||||
new ActionRowBuilder().addComponents(
|
||||
new TextInputBuilder()
|
||||
.setCustomId("renameTicket")
|
||||
.setLabel("New Channel Name")
|
||||
.setStyle(TextInputStyle.Short)
|
||||
.setRequired(true),
|
||||
),
|
||||
);
|
||||
|
||||
return interaction.showModal(modal);
|
||||
}
|
||||
|
||||
if (subcommand === (cmdconfig.TicketManage?.Close?.Command || "close")) {
|
||||
const reason = interaction.options.getString("reason") || "Closed by staff.";
|
||||
|
||||
if (supportbot.Ticket.Close.StaffOnly) {
|
||||
let CloseSupportStaff = await getRole(
|
||||
supportbot.Roles.StaffMember.Staff,
|
||||
interaction.guild,
|
||||
);
|
||||
let CloseAdmin = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild);
|
||||
|
||||
if (!CloseSupportStaff || !CloseAdmin) {
|
||||
return interaction.reply({
|
||||
content:
|
||||
"Some roles seem to be missing! Please check for errors when starting the bot.",
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
|
||||
const noClosePerms = new EmbedBuilder()
|
||||
.setTitle("Invalid Permissions!")
|
||||
.setDescription(
|
||||
`${msgconfig.Error.IncorrectPerms}\n\nRole Required: \`${supportbot.Roles.StaffMember.Staff}\` or \`${supportbot.Roles.StaffMember.Admin}\``,
|
||||
)
|
||||
.setColor(supportbot.Embed.Colours.Warn);
|
||||
|
||||
if (
|
||||
!interaction.member.roles.cache.has(CloseSupportStaff.id) &&
|
||||
!interaction.member.roles.cache.has(CloseAdmin.id)
|
||||
) {
|
||||
return interaction.reply({
|
||||
embeds: [noClosePerms],
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const isThread = interaction.channel.type === ChannelType.PrivateThread;
|
||||
|
||||
if (
|
||||
(supportbot.Ticket.TicketType === "threads" && !isThread) ||
|
||||
(supportbot.Ticket.TicketType === "channels" &&
|
||||
interaction.channel.type !== ChannelType.GuildText)
|
||||
) {
|
||||
const notTicketChannel = new EmbedBuilder()
|
||||
.setTitle("Invalid Channel!")
|
||||
.setDescription(
|
||||
`This command can only be used in a ${supportbot.Ticket.TicketType === "threads" ? "ticket thread" : "ticket channel"}.`,
|
||||
)
|
||||
.setColor(supportbot.Embed.Colours.Warn);
|
||||
|
||||
return interaction.reply({
|
||||
embeds: [notTicketChannel],
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
|
||||
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
||||
|
||||
const ticketData = db.getTicket(interaction.channel.id);
|
||||
|
||||
if (!ticketData) {
|
||||
const exists = new EmbedBuilder()
|
||||
.setTitle("No Ticket Found!")
|
||||
.setDescription(msgconfig.Error.NoValidTicket)
|
||||
.setColor(supportbot.Embed.Colours.Warn);
|
||||
|
||||
return interaction.followUp({
|
||||
embeds: [exists],
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
await TicketManager.createTranscript(interaction, ticketData, reason);
|
||||
|
||||
await interaction.followUp({
|
||||
content: "Ticket closed successfully.",
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
|
||||
await interaction.channel.delete().catch(() => { });
|
||||
} catch (err) {
|
||||
console.error("Error closing ticket:", err);
|
||||
|
||||
await interaction.followUp({
|
||||
content: "An error occurred while closing the ticket.",
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
return interaction.reply({
|
||||
content: "Unknown ticket management action.",
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error in ticket manage command:", error);
|
||||
|
||||
if (!interaction.replied && !interaction.deferred) {
|
||||
return interaction.reply({
|
||||
content: "An error occurred while managing the ticket.",
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,342 @@
|
||||
// SupportBot | Emerald Services
|
||||
// Ticket Panel Command
|
||||
|
||||
const fs = require("fs");
|
||||
const {
|
||||
ApplicationCommandType,
|
||||
ActionRowBuilder,
|
||||
ButtonBuilder,
|
||||
ButtonStyle,
|
||||
MessageFlags,
|
||||
ContainerBuilder,
|
||||
TextDisplayBuilder,
|
||||
MediaGalleryBuilder,
|
||||
SeparatorBuilder,
|
||||
SectionBuilder,
|
||||
StringSelectMenuBuilder,
|
||||
ThumbnailBuilder,
|
||||
EmbedBuilder,
|
||||
} = require("discord.js");
|
||||
const yaml = require("js-yaml");
|
||||
|
||||
const panelconfig = require("../../Structures/ConfigStore").ticketPanel;
|
||||
const supportbot = require("../../Structures/ConfigStore").supportbot;
|
||||
const cmdconfig = require("../../Structures/ConfigStore").commands;
|
||||
|
||||
const Command = require("../../Structures/Command.js");
|
||||
const db = require("../../Structures/Database.js");
|
||||
|
||||
function parseButtonStyle(style) {
|
||||
switch (String(style || "1")) {
|
||||
case "1":
|
||||
return ButtonStyle.Primary;
|
||||
case "2":
|
||||
return ButtonStyle.Secondary;
|
||||
case "3":
|
||||
return ButtonStyle.Danger;
|
||||
case "4":
|
||||
return ButtonStyle.Success;
|
||||
default:
|
||||
return ButtonStyle.Primary;
|
||||
}
|
||||
}
|
||||
|
||||
function getPanelMessage(key, fallback) {
|
||||
return panelconfig.Messages?.[key] || fallback;
|
||||
}
|
||||
|
||||
module.exports = new Command({
|
||||
name: cmdconfig.TicketPanel.Command,
|
||||
description: cmdconfig.TicketPanel.Description,
|
||||
type: ApplicationCommandType.ChatInput,
|
||||
options: [],
|
||||
permissions: cmdconfig.TicketPanel.Permission,
|
||||
|
||||
async run(interaction) {
|
||||
if (!panelconfig.Panel) {
|
||||
console.log(
|
||||
"\u001b[31m",
|
||||
`[TICKET PANEL]`,
|
||||
"\u001b[33m",
|
||||
"Ticket Panel is not setup, You can set this in",
|
||||
`\u001b[31m`,
|
||||
"/Configs/ticket-panel.yml",
|
||||
`\n `,
|
||||
);
|
||||
|
||||
return interaction.reply({
|
||||
content: getPanelMessage(
|
||||
"NotConfigured",
|
||||
"Ticket Panel is not set up. Please configure it in `/Configs/ticket-panel.yml`.",
|
||||
),
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
|
||||
const { getChannel } = interaction.client;
|
||||
|
||||
const channel = await getChannel(supportbot.Ticket.TicketHome, interaction.guild);
|
||||
|
||||
if (!channel) {
|
||||
console.log(`[TICKET PANEL] TicketHome channel not found. Please check your config file.`);
|
||||
return interaction.reply({
|
||||
content: getPanelMessage(
|
||||
"InvalidChannel",
|
||||
"TicketHome channel not found. Please check your config file.",
|
||||
),
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
|
||||
const panelRow = db.getTicketPanel();
|
||||
|
||||
if (panelRow) {
|
||||
try {
|
||||
const oldMessage = await channel.messages.fetch(panelRow.message_id);
|
||||
if (oldMessage) {
|
||||
await oldMessage.delete().catch(() => { });
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(`[TICKET PANEL] Previous panel message not found, creating a new one.`);
|
||||
}
|
||||
}
|
||||
|
||||
const departmentSystem = supportbot.Ticket?.DepartmentSystem || {};
|
||||
const prioritySystem = supportbot.Ticket?.PrioritySystem || {};
|
||||
const usingThreads = supportbot.Ticket?.TicketType === "threads";
|
||||
|
||||
const departmentsEnabled = departmentSystem.Enabled === true && !usingThreads;
|
||||
const prioritiesEnabled = prioritySystem.Enabled === true && !usingThreads;
|
||||
const departments = departmentSystem.Departments || {};
|
||||
|
||||
if (usingThreads && (departmentSystem.Enabled || prioritySystem.Enabled)) {
|
||||
console.log(
|
||||
"[SupportBot] Departments and Priority are disabled because TicketType is set to 'threads'.",
|
||||
);
|
||||
}
|
||||
|
||||
const departmentOptions = Object.entries(departments)
|
||||
.slice(0, 25)
|
||||
.map(([key, dept]) => ({
|
||||
label: dept.Name || key,
|
||||
description:
|
||||
dept.PanelDescription ||
|
||||
dept.Description ||
|
||||
panelconfig.DepartmentMenu?.DefaultDescription ||
|
||||
`Open a ${String(dept.Name || key).toLowerCase()} ticket`,
|
||||
value: key,
|
||||
emoji:
|
||||
dept.Emoji ||
|
||||
panelconfig.DepartmentMenu?.DefaultEmoji ||
|
||||
panelconfig.Button?.Emoji ||
|
||||
undefined,
|
||||
}));
|
||||
|
||||
const shouldUseDepartments = departmentsEnabled && departmentOptions.length > 0;
|
||||
|
||||
let openerComponent;
|
||||
|
||||
if (shouldUseDepartments) {
|
||||
openerComponent = new StringSelectMenuBuilder()
|
||||
.setCustomId(panelconfig.DepartmentMenu?.CustomId || "ticketdepartmentselect")
|
||||
.setPlaceholder(
|
||||
panelconfig.DepartmentMenu?.Placeholder ||
|
||||
departmentSystem.Placeholder ||
|
||||
panelconfig.Button?.Text ||
|
||||
"Choose a ticket department",
|
||||
)
|
||||
.addOptions(departmentOptions);
|
||||
} else {
|
||||
openerComponent = new ButtonBuilder()
|
||||
.setCustomId(panelconfig.Button?.CustomId || "createticket")
|
||||
.setLabel(panelconfig.Button?.Text || "Create Ticket")
|
||||
.setStyle(parseButtonStyle(panelconfig.Button?.Color));
|
||||
|
||||
if (panelconfig.Button?.Emoji) {
|
||||
openerComponent.setEmoji(panelconfig.Button.Emoji);
|
||||
}
|
||||
}
|
||||
|
||||
const panelContainer = new ContainerBuilder();
|
||||
|
||||
if (panelconfig.Style?.Color) {
|
||||
panelContainer.setAccentColor(parseInt(String(panelconfig.Style.Color).replace("#", ""), 16));
|
||||
}
|
||||
|
||||
const layout = String(panelconfig.Style?.Layout || "3");
|
||||
const showImage = panelconfig.Settings?.Image === true && !!panelconfig.Style?.Image;
|
||||
const showTopTitle = panelconfig.Settings?.TopTitle === true;
|
||||
const showTopDivider = panelconfig.Settings?.TopDivider === true;
|
||||
const showBottomTitle = panelconfig.Settings?.BottomTitle === true;
|
||||
const showBottomDivider = panelconfig.Settings?.BottomDivider === true;
|
||||
|
||||
const panelTitle = new TextDisplayBuilder().setContent(
|
||||
panelconfig.Style?.Title || "## SupportBot Ticket Creation",
|
||||
);
|
||||
|
||||
const rawTitle = (panelconfig.Style?.Title || "SupportBot Ticket Creation").replace(/^#+\s*/, "");
|
||||
|
||||
const panelDesc = new TextDisplayBuilder().setContent(
|
||||
panelconfig.Style?.Description || "Click on the button below to create a support ticket.",
|
||||
);
|
||||
|
||||
if (showTopTitle) {
|
||||
panelContainer.addTextDisplayComponents(panelTitle);
|
||||
}
|
||||
|
||||
if (showTopDivider) {
|
||||
panelContainer.addSeparatorComponents(new SeparatorBuilder().setDivider(true));
|
||||
}
|
||||
|
||||
if (layout === "1" || layout === "3") {
|
||||
panelContainer.addTextDisplayComponents(panelDesc);
|
||||
}
|
||||
|
||||
if (showImage && layout === "2") {
|
||||
const panelImage = new MediaGalleryBuilder().addItems([
|
||||
{
|
||||
media: {
|
||||
url: panelconfig.Style.Image,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
panelContainer.addMediaGalleryComponents(panelImage);
|
||||
}
|
||||
|
||||
if (layout === "1") {
|
||||
const middleSection = new SectionBuilder().addTextDisplayComponents(panelDesc);
|
||||
|
||||
if (!shouldUseDepartments) {
|
||||
middleSection.setButtonAccessory(openerComponent);
|
||||
panelContainer.addSectionComponents(middleSection);
|
||||
} else {
|
||||
panelContainer.addSectionComponents(middleSection);
|
||||
panelContainer.addActionRowComponents(
|
||||
new ActionRowBuilder().addComponents(openerComponent),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (showImage && layout === "3") {
|
||||
const panelImage = new MediaGalleryBuilder().addItems([
|
||||
{
|
||||
media: {
|
||||
url: panelconfig.Style.Image,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
panelContainer.addMediaGalleryComponents(panelImage);
|
||||
}
|
||||
|
||||
if (showBottomTitle) {
|
||||
panelContainer.addTextDisplayComponents(panelTitle);
|
||||
}
|
||||
|
||||
if (showBottomDivider) {
|
||||
panelContainer.addSeparatorComponents(new SeparatorBuilder().setDivider(true));
|
||||
}
|
||||
|
||||
if (layout === "2") {
|
||||
const middleSection = new SectionBuilder().addTextDisplayComponents(panelDesc);
|
||||
|
||||
if (!shouldUseDepartments) {
|
||||
middleSection.setButtonAccessory(openerComponent);
|
||||
panelContainer.addSectionComponents(middleSection);
|
||||
} else {
|
||||
panelContainer.addSectionComponents(middleSection);
|
||||
panelContainer.addActionRowComponents(
|
||||
new ActionRowBuilder().addComponents(openerComponent),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (layout === "3") {
|
||||
panelContainer.addActionRowComponents(new ActionRowBuilder().addComponents(openerComponent));
|
||||
}
|
||||
|
||||
if (layout === "4") {
|
||||
// Create a classic Discord Embed for Layout 4
|
||||
const oldEmbed = new EmbedBuilder()
|
||||
.setTitle(rawTitle)
|
||||
.setDescription(panelconfig.Style?.Description || "Click on the button below to create a support ticket.");
|
||||
|
||||
if (panelconfig.Style?.Color) {
|
||||
oldEmbed.setColor(panelconfig.Style.Color);
|
||||
}
|
||||
|
||||
if (showImage && panelconfig.Style?.Image) {
|
||||
oldEmbed.setThumbnail(panelconfig.Style.Image);
|
||||
}
|
||||
|
||||
try {
|
||||
const message = await channel.send({
|
||||
embeds: [oldEmbed],
|
||||
components: [new ActionRowBuilder().addComponents(openerComponent)],
|
||||
});
|
||||
|
||||
db.saveTicketPanel(message.id, channel.id);
|
||||
|
||||
return interaction.reply({
|
||||
content: shouldUseDepartments
|
||||
? getPanelMessage(
|
||||
"PanelSentWithDepartments",
|
||||
"Ticket panel message with department selection has been sent!",
|
||||
)
|
||||
: getPanelMessage("PanelSent", "Ticket panel message has been sent!"),
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
} catch (e) {
|
||||
console.log("Error sending message:", e);
|
||||
return interaction.reply({
|
||||
content: getPanelMessage(
|
||||
"SendError",
|
||||
"An error occurred while sending the ticket panel message.",
|
||||
),
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (showImage && layout === "1") {
|
||||
const panelImage = new MediaGalleryBuilder().addItems([
|
||||
{
|
||||
media: {
|
||||
url: panelconfig.Style.Image,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
panelContainer.addMediaGalleryComponents(panelImage);
|
||||
}
|
||||
|
||||
try {
|
||||
const message = await channel.send({
|
||||
flags: MessageFlags.IsComponentsV2,
|
||||
components: [panelContainer],
|
||||
});
|
||||
|
||||
db.saveTicketPanel(message.id, channel.id);
|
||||
|
||||
return interaction.reply({
|
||||
content: shouldUseDepartments
|
||||
? getPanelMessage(
|
||||
"PanelSentWithDepartments",
|
||||
"Ticket panel message with department selection has been sent!",
|
||||
)
|
||||
: getPanelMessage("PanelSent", "Ticket panel message has been sent!"),
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
} catch (e) {
|
||||
console.log("Error sending message:", e);
|
||||
return interaction.reply({
|
||||
content: getPanelMessage(
|
||||
"SendError",
|
||||
"An error occurred while sending the ticket panel message.",
|
||||
),
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,468 @@
|
||||
const {
|
||||
ApplicationCommandType,
|
||||
ApplicationCommandOptionType,
|
||||
ActionRowBuilder,
|
||||
ButtonBuilder,
|
||||
EmbedBuilder,
|
||||
StringSelectMenuBuilder,
|
||||
ModalBuilder,
|
||||
TextInputBuilder,
|
||||
TextInputStyle,
|
||||
ButtonStyle,
|
||||
ComponentType,
|
||||
MessageFlags,
|
||||
} = require('discord.js');
|
||||
const Command = require('../../Structures/Command.js');
|
||||
const fs = require('fs');
|
||||
const yaml = require('js-yaml');
|
||||
|
||||
const supportbot = require('../../Structures/ConfigStore').supportbot;
|
||||
const cmdconfig = require("../../Structures/ConfigStore").commands;
|
||||
const msgconfig = require("../../Structures/ConfigStore").messages;
|
||||
|
||||
function parseColor(color) {
|
||||
if (!color) return 0x5865F2;
|
||||
if (typeof color === 'number') return color;
|
||||
if (typeof color !== 'string') return 0x5865F2;
|
||||
color = color.replace('#', '');
|
||||
if (/^[0-9A-F]{6}$/i.test(color)) {
|
||||
return parseInt(color, 16);
|
||||
}
|
||||
return 0x5865F2;
|
||||
}
|
||||
|
||||
module.exports = new Command({
|
||||
name: cmdconfig.Embed.Command,
|
||||
description: cmdconfig.Embed.Description,
|
||||
type: ApplicationCommandType.ChatInput,
|
||||
options: [
|
||||
{
|
||||
name: 'channel',
|
||||
description: 'Select channel to send the message',
|
||||
type: ApplicationCommandOptionType.Channel,
|
||||
required: true
|
||||
}
|
||||
],
|
||||
permissions: cmdconfig.Embed.Permission,
|
||||
|
||||
async run(interaction) {
|
||||
const { getRole } = interaction.client;
|
||||
let SupportStaff = await getRole(supportbot.Roles.StaffMember.Staff, interaction.guild);
|
||||
let Admin = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild);
|
||||
|
||||
if (!SupportStaff || !Admin) {
|
||||
return interaction.reply('Some roles seem to be missing!\nPlease check for errors when starting the bot.');
|
||||
}
|
||||
|
||||
if (!interaction.member.roles.cache.has(SupportStaff.id) &&
|
||||
!interaction.member.roles.cache.has(Admin.id)) {
|
||||
const NoPerms = new EmbedBuilder()
|
||||
.setTitle('Invalid Permissions!')
|
||||
.setDescription(`${msgconfig.Error.IncorrectPerms}\n\nRole Required: \`${supportbot.Roles.StaffMember.Staff}\` or \`${supportbot.Roles.StaffMember.Admin}\``)
|
||||
.setColor(0xFF0000);
|
||||
return interaction.reply({ embeds: [NoPerms], flags: MessageFlags.Ephemeral });
|
||||
}
|
||||
|
||||
const channel = interaction.options.getChannel('channel');
|
||||
|
||||
const messageData = {
|
||||
type: 'embed',
|
||||
title: 'New Embed',
|
||||
description: 'Click the buttons below to edit this embed',
|
||||
color: 0x5865F2,
|
||||
fields: [],
|
||||
footer: null,
|
||||
thumbnail: null,
|
||||
image: null,
|
||||
timestamp: false
|
||||
};
|
||||
|
||||
function createEmbed() {
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle(messageData.title)
|
||||
.setDescription(messageData.description)
|
||||
.setColor(messageData.color);
|
||||
|
||||
if (messageData.fields.length > 0) {
|
||||
embed.addFields(messageData.fields);
|
||||
}
|
||||
if (messageData.footer) {
|
||||
embed.setFooter({ text: messageData.footer });
|
||||
}
|
||||
if (messageData.thumbnail) {
|
||||
embed.setThumbnail(messageData.thumbnail);
|
||||
}
|
||||
if (messageData.image) {
|
||||
embed.setImage(messageData.image);
|
||||
}
|
||||
if (messageData.timestamp) {
|
||||
embed.setTimestamp();
|
||||
}
|
||||
|
||||
return embed;
|
||||
}
|
||||
|
||||
const typeRow = new ActionRowBuilder()
|
||||
.addComponents(
|
||||
new StringSelectMenuBuilder()
|
||||
.setCustomId('message_type')
|
||||
.setPlaceholder('Select message type')
|
||||
.addOptions([
|
||||
{
|
||||
label: 'Embed Message',
|
||||
description: 'Send as an embedded message',
|
||||
value: 'embed'
|
||||
},
|
||||
{
|
||||
label: 'Text Message',
|
||||
description: 'Send as a plain text message',
|
||||
value: 'text'
|
||||
}
|
||||
])
|
||||
);
|
||||
|
||||
function createButtons(messageType) {
|
||||
const buttons = [];
|
||||
|
||||
if (messageType === 'embed') {
|
||||
const row1 = new ActionRowBuilder()
|
||||
.addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId('edit_title')
|
||||
.setLabel('Edit Title')
|
||||
.setStyle(ButtonStyle.Secondary),
|
||||
new ButtonBuilder()
|
||||
.setCustomId('edit_description')
|
||||
.setLabel('Edit Description')
|
||||
.setStyle(ButtonStyle.Secondary),
|
||||
new ButtonBuilder()
|
||||
.setCustomId('edit_color')
|
||||
.setLabel('Edit Color')
|
||||
.setStyle(ButtonStyle.Secondary),
|
||||
new ButtonBuilder()
|
||||
.setCustomId('add_field')
|
||||
.setLabel('Add Field')
|
||||
.setStyle(ButtonStyle.Secondary)
|
||||
);
|
||||
|
||||
const row2 = new ActionRowBuilder()
|
||||
.addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId('edit_footer')
|
||||
.setLabel('Edit Footer')
|
||||
.setStyle(ButtonStyle.Secondary),
|
||||
new ButtonBuilder()
|
||||
.setCustomId('edit_image')
|
||||
.setLabel('Add Image')
|
||||
.setStyle(ButtonStyle.Secondary),
|
||||
new ButtonBuilder()
|
||||
.setCustomId('edit_thumbnail')
|
||||
.setLabel('Add Thumbnail')
|
||||
.setStyle(ButtonStyle.Secondary),
|
||||
new ButtonBuilder()
|
||||
.setCustomId('toggle_timestamp')
|
||||
.setLabel('Toggle Timestamp')
|
||||
.setStyle(ButtonStyle.Secondary)
|
||||
);
|
||||
|
||||
buttons.push(row1, row2);
|
||||
} else {
|
||||
const textRow = new ActionRowBuilder()
|
||||
.addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId('edit_description')
|
||||
.setLabel('Edit Text')
|
||||
.setStyle(ButtonStyle.Secondary)
|
||||
);
|
||||
|
||||
buttons.push(textRow);
|
||||
}
|
||||
|
||||
// Send button always shows
|
||||
const sendRow = new ActionRowBuilder()
|
||||
.addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId('send_message')
|
||||
.setLabel('Send')
|
||||
.setStyle(ButtonStyle.Success)
|
||||
);
|
||||
|
||||
buttons.push(sendRow);
|
||||
return buttons;
|
||||
}
|
||||
|
||||
async function updatePreview(i) {
|
||||
if (messageData.type === 'embed') {
|
||||
await i.update({
|
||||
content: 'Customize your message:',
|
||||
embeds: [createEmbed()],
|
||||
components: [typeRow, ...createButtons('embed')]
|
||||
});
|
||||
} else {
|
||||
await i.update({
|
||||
content: messageData.description,
|
||||
embeds: [],
|
||||
components: [typeRow, ...createButtons('text')]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const message = await interaction.reply({
|
||||
content: messageData.type === 'text' ? messageData.description : 'Customize your message:',
|
||||
components: [typeRow, ...createButtons(messageData.type)],
|
||||
embeds: messageData.type === 'embed' ? [createEmbed()] : [],
|
||||
flags: MessageFlags.Ephemeral
|
||||
});
|
||||
|
||||
const collector = message.createMessageComponentCollector({
|
||||
time: 900000
|
||||
});
|
||||
|
||||
collector.on('collect', async (i) => {
|
||||
if (i.user.id !== interaction.user.id) {
|
||||
return i.reply({
|
||||
content: 'You cannot edit this message.',
|
||||
flags: MessageFlags.Ephemeral
|
||||
});
|
||||
}
|
||||
|
||||
switch (i.customId) {
|
||||
case 'message_type':
|
||||
messageData.type = i.values[0];
|
||||
await updatePreview(i);
|
||||
break;
|
||||
|
||||
case 'edit_description':
|
||||
const descModal = new ModalBuilder()
|
||||
.setCustomId('desc_modal')
|
||||
.setTitle(messageData.type === 'embed' ? 'Edit Description' : 'Edit Text');
|
||||
|
||||
descModal.addComponents(
|
||||
new ActionRowBuilder()
|
||||
.addComponents(
|
||||
new TextInputBuilder()
|
||||
.setCustomId('desc_input')
|
||||
.setLabel(messageData.type === 'embed' ? 'Description' : 'Message Text')
|
||||
.setStyle(TextInputStyle.Paragraph)
|
||||
.setValue(messageData.description)
|
||||
.setMaxLength(4000)
|
||||
)
|
||||
);
|
||||
|
||||
await i.showModal(descModal);
|
||||
break;
|
||||
|
||||
case 'edit_title':
|
||||
const titleModal = new ModalBuilder()
|
||||
.setCustomId('title_modal')
|
||||
.setTitle('Edit Title');
|
||||
|
||||
titleModal.addComponents(
|
||||
new ActionRowBuilder()
|
||||
.addComponents(
|
||||
new TextInputBuilder()
|
||||
.setCustomId('title_input')
|
||||
.setLabel('Title')
|
||||
.setStyle(TextInputStyle.Short)
|
||||
.setValue(messageData.title)
|
||||
.setMaxLength(256)
|
||||
)
|
||||
);
|
||||
|
||||
await i.showModal(titleModal);
|
||||
break;
|
||||
|
||||
case 'edit_color':
|
||||
const colorModal = new ModalBuilder()
|
||||
.setCustomId('color_modal')
|
||||
.setTitle('Edit Color');
|
||||
|
||||
colorModal.addComponents(
|
||||
new ActionRowBuilder()
|
||||
.addComponents(
|
||||
new TextInputBuilder()
|
||||
.setCustomId('color_input')
|
||||
.setLabel('Color (HEX)')
|
||||
.setStyle(TextInputStyle.Short)
|
||||
.setPlaceholder('#FF0000')
|
||||
.setMinLength(6)
|
||||
.setMaxLength(7)
|
||||
.setValue(messageData.color.toString(16).padStart(6, '0'))
|
||||
)
|
||||
);
|
||||
|
||||
await i.showModal(colorModal);
|
||||
break;
|
||||
|
||||
case 'add_field':
|
||||
const fieldModal = new ModalBuilder()
|
||||
.setCustomId('field_modal')
|
||||
.setTitle('Add Field');
|
||||
|
||||
fieldModal.addComponents(
|
||||
new ActionRowBuilder()
|
||||
.addComponents(
|
||||
new TextInputBuilder()
|
||||
.setCustomId('field_name')
|
||||
.setLabel('Field Name')
|
||||
.setStyle(TextInputStyle.Short)
|
||||
.setMaxLength(256)
|
||||
),
|
||||
new ActionRowBuilder()
|
||||
.addComponents(
|
||||
new TextInputBuilder()
|
||||
.setCustomId('field_value')
|
||||
.setLabel('Field Value')
|
||||
.setStyle(TextInputStyle.Paragraph)
|
||||
.setMaxLength(1024)
|
||||
),
|
||||
new ActionRowBuilder()
|
||||
.addComponents(
|
||||
new TextInputBuilder()
|
||||
.setCustomId('field_inline')
|
||||
.setLabel('Inline? (true/false)')
|
||||
.setStyle(TextInputStyle.Short)
|
||||
.setValue('false')
|
||||
.setPlaceholder('true or false')
|
||||
)
|
||||
);
|
||||
|
||||
await i.showModal(fieldModal);
|
||||
break;
|
||||
|
||||
case 'edit_footer':
|
||||
const footerModal = new ModalBuilder()
|
||||
.setCustomId('footer_modal')
|
||||
.setTitle('Edit Footer');
|
||||
|
||||
footerModal.addComponents(
|
||||
new ActionRowBuilder()
|
||||
.addComponents(
|
||||
new TextInputBuilder()
|
||||
.setCustomId('footer_input')
|
||||
.setLabel('Footer Text')
|
||||
.setStyle(TextInputStyle.Short)
|
||||
.setValue(messageData.footer || '')
|
||||
.setMaxLength(2048)
|
||||
)
|
||||
);
|
||||
|
||||
await i.showModal(footerModal);
|
||||
break;
|
||||
|
||||
case 'edit_image':
|
||||
const imageModal = new ModalBuilder()
|
||||
.setCustomId('image_modal')
|
||||
.setTitle('Add Image');
|
||||
|
||||
imageModal.addComponents(
|
||||
new ActionRowBuilder()
|
||||
.addComponents(
|
||||
new TextInputBuilder()
|
||||
.setCustomId('image_input')
|
||||
.setLabel('Image URL')
|
||||
.setStyle(TextInputStyle.Short)
|
||||
.setValue(messageData.image || '')
|
||||
.setPlaceholder('https://example.com/image.png')
|
||||
)
|
||||
);
|
||||
|
||||
await i.showModal(imageModal);
|
||||
break;
|
||||
|
||||
case 'edit_thumbnail':
|
||||
const thumbnailModal = new ModalBuilder()
|
||||
.setCustomId('thumbnail_modal')
|
||||
.setTitle('Add Thumbnail');
|
||||
|
||||
thumbnailModal.addComponents(
|
||||
new ActionRowBuilder()
|
||||
.addComponents(
|
||||
new TextInputBuilder()
|
||||
.setCustomId('thumbnail_input')
|
||||
.setLabel('Thumbnail URL')
|
||||
.setStyle(TextInputStyle.Short)
|
||||
.setValue(messageData.thumbnail || '')
|
||||
.setPlaceholder('https://example.com/thumbnail.png')
|
||||
)
|
||||
);
|
||||
|
||||
await i.showModal(thumbnailModal);
|
||||
break;
|
||||
|
||||
case 'toggle_timestamp':
|
||||
messageData.timestamp = !messageData.timestamp;
|
||||
await updatePreview(i);
|
||||
break;
|
||||
|
||||
case 'send_message':
|
||||
try {
|
||||
if (messageData.type === 'embed') {
|
||||
await channel.send({ embeds: [createEmbed()] });
|
||||
} else {
|
||||
await channel.send(messageData.description);
|
||||
}
|
||||
await i.update({
|
||||
content: 'Message sent successfully!',
|
||||
components: [],
|
||||
embeds: []
|
||||
});
|
||||
collector.stop();
|
||||
} catch (error) {
|
||||
await i.reply({
|
||||
content: 'Failed to send message. Please check channel permissions.',
|
||||
flags: MessageFlags.Ephemeral
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// Handle modal submissions
|
||||
interaction.client.on('interactionCreate', async (modal) => {
|
||||
if (!modal.isModalSubmit()) return;
|
||||
if (modal.user.id !== interaction.user.id) return;
|
||||
|
||||
switch (modal.customId) {
|
||||
case 'title_modal':
|
||||
messageData.title = modal.fields.getTextInputValue('title_input');
|
||||
break;
|
||||
case 'desc_modal':
|
||||
messageData.description = modal.fields.getTextInputValue('desc_input');
|
||||
break;
|
||||
case 'color_modal':
|
||||
messageData.color = parseColor(modal.fields.getTextInputValue('color_input'));
|
||||
break;
|
||||
case 'field_modal':
|
||||
const inlineValue = modal.fields.getTextInputValue('field_inline').toLowerCase();
|
||||
messageData.fields.push({
|
||||
name: modal.fields.getTextInputValue('field_name'),
|
||||
value: modal.fields.getTextInputValue('field_value'),
|
||||
inline: inlineValue === 'true'
|
||||
});
|
||||
break;
|
||||
case 'footer_modal':
|
||||
messageData.footer = modal.fields.getTextInputValue('footer_input');
|
||||
break;
|
||||
case 'image_modal':
|
||||
messageData.image = modal.fields.getTextInputValue('image_input');
|
||||
break;
|
||||
case 'thumbnail_modal':
|
||||
messageData.thumbnail = modal.fields.getTextInputValue('thumbnail_input');
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
await updatePreview(modal);
|
||||
});
|
||||
|
||||
collector.on('end', () => {
|
||||
interaction.editReply({
|
||||
content: 'Message editor timed out.',
|
||||
components: [],
|
||||
embeds: []
|
||||
}).catch(() => {});
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,261 @@
|
||||
const fs = require("fs");
|
||||
const { ApplicationCommandOptionType, ApplicationCommandType, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, ComponentType, MessageFlags } = require("discord.js");
|
||||
const yaml = require("js-yaml");
|
||||
const supportbot = require("../../Structures/ConfigStore").supportbot;
|
||||
const cmdconfig = require("../../Structures/ConfigStore").commands;
|
||||
const msgconfig = require("../../Structures/ConfigStore").messages;
|
||||
|
||||
const Command = require("../../Structures/Command.js");
|
||||
|
||||
// Function to chunk array into smaller arrays
|
||||
function chunkArray(arr, size) {
|
||||
const chunks = [];
|
||||
for (let i = 0; i < arr.length; i += size) {
|
||||
chunks.push(arr.slice(i, i + size));
|
||||
}
|
||||
return chunks;
|
||||
}
|
||||
|
||||
module.exports = new Command({
|
||||
name: cmdconfig.Mod.Command,
|
||||
description: cmdconfig.Mod.Command,
|
||||
type: ApplicationCommandType.ChatInput,
|
||||
options: [
|
||||
{
|
||||
name: cmdconfig.Mod.TicketBlacklist.Command,
|
||||
description: cmdconfig.Mod.TicketBlacklist.Description,
|
||||
type: ApplicationCommandOptionType.SubcommandGroup,
|
||||
options: [
|
||||
{
|
||||
name: cmdconfig.Mod.TicketBlacklist.Add.Command,
|
||||
description: msgconfig.Mod.TicketBlacklist.Add.Description || "Add a user to the ticket blacklist",
|
||||
type: ApplicationCommandOptionType.Subcommand,
|
||||
options: [
|
||||
{
|
||||
name: 'user',
|
||||
description: msgconfig.Mod.TicketBlacklist.Add.UserDescription || "User to blacklist",
|
||||
type: ApplicationCommandOptionType.User,
|
||||
required: true
|
||||
},
|
||||
{
|
||||
name: 'reason',
|
||||
description: msgconfig.Mod.TicketBlacklist.Add.ReasonDescription || "Reason for blacklisting",
|
||||
type: ApplicationCommandOptionType.String,
|
||||
required: false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: cmdconfig.Mod.TicketBlacklist.Remove.Command,
|
||||
description: msgconfig.Mod.TicketBlacklist.Remove.Description || "Remove a user from the ticket blacklist",
|
||||
type: ApplicationCommandOptionType.Subcommand,
|
||||
options: [
|
||||
{
|
||||
name: 'user',
|
||||
description: msgconfig.Mod.TicketBlacklist.Remove.UserDescription || "User to remove from blacklist",
|
||||
type: ApplicationCommandOptionType.User,
|
||||
required: true
|
||||
},
|
||||
{
|
||||
name: 'reason',
|
||||
description: msgconfig.Mod.TicketBlacklist.Remove.ReasonDescription || "Reason for removing from blacklist",
|
||||
type: ApplicationCommandOptionType.String,
|
||||
required: false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "view",
|
||||
description: msgconfig.Mod.TicketBlacklist.View.Description || "View all blacklisted users",
|
||||
type: ApplicationCommandOptionType.Subcommand,
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
permissions: cmdconfig.Mod.Permission,
|
||||
|
||||
async run(interaction) {
|
||||
const { getRole } = interaction.client;
|
||||
|
||||
let SupportStaff = await getRole(supportbot.Roles.StaffMember.Staff, interaction.guild);
|
||||
let Admin = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild);
|
||||
let Moderator = await getRole(supportbot.Roles.StaffMember.Moderator, interaction.guild);
|
||||
|
||||
if (!SupportStaff || !Admin || !Moderator) {
|
||||
const missingRolesEmbed = new EmbedBuilder()
|
||||
.setDescription(msgconfig.Error.InvalidChannel || "Required roles are missing!")
|
||||
.setColor(supportbot.Embed.Colours.Warn);
|
||||
return interaction.reply({ embeds: [missingRolesEmbed] });
|
||||
}
|
||||
|
||||
const NoPerms = new EmbedBuilder()
|
||||
.setDescription(msgconfig.Error.IncorrectPerms || "You do not have the correct permissions!")
|
||||
.setColor(supportbot.Embed.Colours.Warn);
|
||||
|
||||
if (!interaction.member.roles.cache.has(Admin.id) && !interaction.member.roles.cache.has(Moderator.id) && (!supportbot.Mod.AllowSupportStaff || !interaction.member.roles.cache.has(SupportStaff.id))) {
|
||||
return interaction.reply({ embeds: [NoPerms] });
|
||||
}
|
||||
|
||||
const subcommand = interaction.options.getSubcommand(false);
|
||||
const user = interaction.options.getUser('user');
|
||||
const reason = interaction.options.getString('reason') || "No reason provided";
|
||||
|
||||
try {
|
||||
let blacklistedUsers;
|
||||
try {
|
||||
blacklistedUsers = JSON.parse(fs.readFileSync("./Data/BlacklistedUsers.json", "utf8")).blacklistedUsers;
|
||||
if (!Array.isArray(blacklistedUsers)) {
|
||||
blacklistedUsers = [];
|
||||
}
|
||||
} catch (error) {
|
||||
blacklistedUsers = [];
|
||||
}
|
||||
|
||||
if (subcommand === cmdconfig.Mod.TicketBlacklist.Add.Command) {
|
||||
if (blacklistedUsers.includes(user.id)) {
|
||||
const alreadyBlacklistedEmbed = new EmbedBuilder()
|
||||
.setDescription(msgconfig.Mod.TicketBlacklist.Add.AlreadyBlacklisted.replace("{userTag}", user.tag) || ":x: User is already blacklisted.")
|
||||
.setColor(supportbot.Embed.Colours.Warn);
|
||||
return interaction.reply({ embeds: [alreadyBlacklistedEmbed], flags: MessageFlags.Ephemeral });
|
||||
}
|
||||
|
||||
blacklistedUsers.push(user.id);
|
||||
fs.writeFileSync("./Data/BlacklistedUsers.json", JSON.stringify({ "blacklistedUsers": blacklistedUsers }, null, 4));
|
||||
|
||||
const successEmbed = new EmbedBuilder()
|
||||
.setDescription(msgconfig.Mod.TicketBlacklist.Add.Success.replace("{userTag}", user.tag) || ":white_check_mark: User has been blacklisted.")
|
||||
.setColor(supportbot.Embed.Colours.Success);
|
||||
|
||||
const blacklistChannel = interaction.guild.channels.cache.get(supportbot.Ticket.Log.TicketBlacklistLog);
|
||||
if (blacklistChannel) {
|
||||
const blacklistLogEmbed = new EmbedBuilder()
|
||||
.setTitle(msgconfig.TicketBlacklistLog.Title || "Ticket Blacklist Log")
|
||||
.setColor(msgconfig.TicketBlacklistLog.Colour || supportbot.Embed.Colours.Success)
|
||||
.setFooter({ text: supportbot.Embed.Footer, iconURL: interaction.user.displayAvatarURL() })
|
||||
.setDescription(`> **User:** ${user.tag} (\`${user.id}\`)\n> **Actioned by:** <@${interaction.user.id}>`)
|
||||
.addFields({ name: "Action", value: "\`\`\`Removed from blacklist\`\`\`", inline: false })
|
||||
.addFields({ name: "Reason", value: `\`\`\`${reason}\`\`\``, inline: false });
|
||||
|
||||
blacklistChannel.send({ embeds: [blacklistLogEmbed] });
|
||||
}
|
||||
|
||||
return interaction.reply({
|
||||
embeds: [successEmbed],
|
||||
flags: MessageFlags.Ephemeral
|
||||
});
|
||||
|
||||
} else if (subcommand === cmdconfig.Mod.TicketBlacklist.Remove.Command) {
|
||||
if (!blacklistedUsers.includes(user.id)) {
|
||||
const notBlacklistedEmbed = new EmbedBuilder()
|
||||
.setDescription(msgconfig.Mod.TicketBlacklist.Remove.NotBlacklisted.replace("{userTag}", user.tag) || ":x: User is not blacklisted.")
|
||||
.setColor(supportbot.Embed.Colours.Warn);
|
||||
return interaction.reply({ embeds: [notBlacklistedEmbed], flags: MessageFlags.Ephemeral });
|
||||
}
|
||||
|
||||
blacklistedUsers = blacklistedUsers.filter(id => id !== user.id);
|
||||
fs.writeFileSync("./Data/BlacklistedUsers.json", JSON.stringify({ "blacklistedUsers": blacklistedUsers }, null, 4));
|
||||
|
||||
const removedEmbed = new EmbedBuilder()
|
||||
.setDescription(msgconfig.Mod.TicketBlacklist.Remove.Success.replace("{userTag}", user.tag) || ":white_check_mark: User has been removed from the blacklist.")
|
||||
.setColor(supportbot.Embed.Colours.Success);
|
||||
|
||||
const blacklistChannel = interaction.guild.channels.cache.get(supportbot.Ticket.Log.TicketBlacklistLog);
|
||||
if (blacklistChannel) {
|
||||
const blacklistLogEmbed = new EmbedBuilder()
|
||||
.setTitle(msgconfig.TicketBlacklistLog.Title || "Ticket Blacklist Log")
|
||||
.setColor(msgconfig.TicketBlacklistLog.Colour || supportbot.Embed.Colours.Success)
|
||||
.setFooter({ text: supportbot.Embed.Footer, iconURL: interaction.user.displayAvatarURL() })
|
||||
.setDescription(`> **User:** ${user.tag} (\`${user.id}\`)\n> **Actioned by:** <@${interaction.user.id}>`)
|
||||
.addFields({ name: "Action", value: "\`\`\`Removed from blacklist\`\`\`", inline: false })
|
||||
.addFields({ name: "Reason", value: `\`\`\`${reason}\`\`\``, inline: false });
|
||||
|
||||
blacklistChannel.send({ embeds: [blacklistLogEmbed] });
|
||||
}
|
||||
|
||||
return interaction.reply({ embeds: [removedEmbed], flags: MessageFlags.Ephemeral });
|
||||
|
||||
} else if (subcommand === "view") {
|
||||
if (blacklistedUsers.length === 0) {
|
||||
const noBlacklistedUsersEmbed = new EmbedBuilder()
|
||||
.setDescription(msgconfig.Mod.TicketBlacklist.View.NoBlacklistedUsers || "No users are blacklisted.")
|
||||
.setColor(supportbot.Embed.Colours.Success);
|
||||
return interaction.reply({ embeds: [noBlacklistedUsersEmbed], flags: MessageFlags.Ephemeral });
|
||||
}
|
||||
|
||||
const chunkedUsers = chunkArray(blacklistedUsers, 5);
|
||||
let currentPage = 0;
|
||||
|
||||
const createEmbed = (page) => {
|
||||
return new EmbedBuilder()
|
||||
.setTitle(msgconfig.Mod.TicketBlacklist.View.EmbedTitle || "Blacklisted Users")
|
||||
.setDescription(chunkedUsers[page].map(id => `<@${id}>`).join("\n"))
|
||||
.setColor(supportbot.Embed.Colours.Success)
|
||||
.setFooter({ text: `Page ${page + 1} of ${chunkedUsers.length}` });
|
||||
};
|
||||
|
||||
const buttons = new ActionRowBuilder()
|
||||
.addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId('previous')
|
||||
.setLabel('Previous')
|
||||
.setStyle(ButtonStyle.Secondary)
|
||||
.setDisabled(currentPage === 0),
|
||||
new ButtonBuilder()
|
||||
.setCustomId('next')
|
||||
.setLabel('Next')
|
||||
.setStyle(ButtonStyle.Secondary)
|
||||
.setDisabled(currentPage === chunkedUsers.length - 1)
|
||||
);
|
||||
|
||||
const response = await interaction.reply({
|
||||
embeds: [createEmbed(currentPage)],
|
||||
components: chunkedUsers.length > 1 ? [buttons] : [],
|
||||
flags: MessageFlags.Ephemeral
|
||||
});
|
||||
|
||||
if (chunkedUsers.length <= 1) return;
|
||||
|
||||
const collector = response.createMessageComponentCollector({
|
||||
componentType: ComponentType.Button,
|
||||
time: 60000
|
||||
});
|
||||
|
||||
collector.on('collect', async (i) => {
|
||||
if (i.user.id !== interaction.user.id) {
|
||||
return i.reply({
|
||||
content: 'You cannot use these buttons.',
|
||||
flags: MessageFlags.Ephemeral
|
||||
});
|
||||
}
|
||||
|
||||
if (i.customId === 'previous' && currentPage > 0) {
|
||||
currentPage--;
|
||||
} else if (i.customId === 'next' && currentPage < chunkedUsers.length - 1) {
|
||||
currentPage++;
|
||||
}
|
||||
|
||||
buttons.components[0].setDisabled(currentPage === 0);
|
||||
buttons.components[1].setDisabled(currentPage === chunkedUsers.length - 1);
|
||||
|
||||
await i.update({
|
||||
embeds: [createEmbed(currentPage)],
|
||||
components: [buttons]
|
||||
});
|
||||
});
|
||||
|
||||
collector.on('end', () => {
|
||||
buttons.components.forEach(button => button.setDisabled(true));
|
||||
interaction.editReply({
|
||||
components: [buttons]
|
||||
}).catch(() => {});
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error in mod command:", error);
|
||||
const errorEmbed = new EmbedBuilder()
|
||||
.setDescription(msgconfig.Error.ActionFailed || "An error occurred while processing your request.")
|
||||
.setColor(supportbot.Embed.Colours.Warn);
|
||||
interaction.reply({ embeds: [errorEmbed], flags: MessageFlags.Ephemeral });
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,237 @@
|
||||
const { Command } = require('../../Structures/Addon.js');
|
||||
const Discord = require("discord.js");
|
||||
const fs = require("fs");
|
||||
const yaml = require("js-yaml");
|
||||
const db = require("../../Structures/Database.js");
|
||||
|
||||
const supportbot = require("../../Structures/ConfigStore").supportbot;
|
||||
const cmdconfig = require("../../Structures/ConfigStore").commands;
|
||||
|
||||
async function loadSettings() {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.get("SELECT data FROM settings WHERE id = 1", (err, row) => {
|
||||
if (err) return reject(err);
|
||||
if (!row) return resolve({});
|
||||
try {
|
||||
resolve(JSON.parse(row.data));
|
||||
} catch (e) {
|
||||
resolve({});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function saveSettings(settings) {
|
||||
const data = JSON.stringify(settings);
|
||||
return new Promise((resolve, reject) => {
|
||||
db.run(
|
||||
"INSERT INTO settings (id, data) VALUES (1, ?) ON CONFLICT(id) DO UPDATE SET data = excluded.data",
|
||||
[data],
|
||||
(err) => (err ? reject(err) : resolve())
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = new Command({
|
||||
name: cmdconfig.Settings.Command,
|
||||
description: cmdconfig.Settings.Description,
|
||||
permissions: cmdconfig.Settings.Permission,
|
||||
options: [
|
||||
{
|
||||
name: 'setstatus',
|
||||
type: Discord.ApplicationCommandOptionType.Subcommand,
|
||||
description: 'Set the bot status',
|
||||
options: [
|
||||
{
|
||||
name: 'status',
|
||||
type: Discord.ApplicationCommandOptionType.String,
|
||||
description: 'Choose the bot status',
|
||||
required: true,
|
||||
choices: [
|
||||
{ name: 'Online 🟢', value: 'online' },
|
||||
{ name: 'Idle 🌙', value: 'idle' },
|
||||
{ name: 'Do Not Disturb ⛔', value: 'dnd' },
|
||||
{ name: 'Invisible 👻', value: 'invisible' }
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'setactivity',
|
||||
type: Discord.ApplicationCommandOptionType.Subcommand,
|
||||
description: 'Set the bot activity',
|
||||
options: [
|
||||
{
|
||||
name: 'type',
|
||||
type: Discord.ApplicationCommandOptionType.String,
|
||||
description: 'Choose the activity type',
|
||||
required: true,
|
||||
choices: [
|
||||
{ name: 'Playing 🎮', value: 'PLAYING' },
|
||||
{ name: 'Watching 👀', value: 'WATCHING' },
|
||||
{ name: 'Listening 🎧', value: 'LISTENING' },
|
||||
{ name: 'Competing 🏆', value: 'COMPETING' }
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'message',
|
||||
type: Discord.ApplicationCommandOptionType.String,
|
||||
description: 'The activity message',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'streammode',
|
||||
type: Discord.ApplicationCommandOptionType.Subcommand,
|
||||
description: 'Set the bot streaming status',
|
||||
options: [
|
||||
{
|
||||
name: 'toggle',
|
||||
type: Discord.ApplicationCommandOptionType.String,
|
||||
description: 'Turn streaming mode on or off',
|
||||
required: true,
|
||||
choices: [
|
||||
{ name: 'On', value: 'on' },
|
||||
{ name: 'Off', value: 'off' }
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'twitchurl',
|
||||
type: Discord.ApplicationCommandOptionType.String,
|
||||
description: 'Twitch stream URL (Required if turning on)',
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'setnickname',
|
||||
type: Discord.ApplicationCommandOptionType.Subcommand,
|
||||
description: 'Set the bot nickname',
|
||||
options: [
|
||||
{
|
||||
name: 'nickname',
|
||||
type: Discord.ApplicationCommandOptionType.String,
|
||||
description: 'The new nickname for the bot',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
async run(interaction) {
|
||||
const subcommand = interaction.options.getSubcommand();
|
||||
const settings = await loadSettings();
|
||||
|
||||
if (subcommand === 'setstatus') {
|
||||
const status = interaction.options.getString('status');
|
||||
|
||||
try {
|
||||
interaction.client.user.setPresence({
|
||||
status: status,
|
||||
activities: interaction.client.user.presence.activities || [],
|
||||
});
|
||||
|
||||
settings.status = status;
|
||||
await saveSettings(settings);
|
||||
|
||||
const embed = new Discord.EmbedBuilder()
|
||||
.setDescription(`Status set to **${status.charAt(0).toUpperCase() + status.slice(1)}**`)
|
||||
.setColor(supportbot.Embed.Colours.General);
|
||||
|
||||
return interaction.reply({ embeds: [embed], flags: Discord.MessageFlags.Ephemeral });
|
||||
} catch (error) {
|
||||
return interaction.reply({ content: `Failed to set status: ${error.message}`, flags: Discord.MessageFlags.Ephemeral });
|
||||
}
|
||||
}
|
||||
|
||||
if (subcommand === 'setactivity') {
|
||||
const activityType = interaction.options.getString('type');
|
||||
const activityMessage = interaction.options.getString('message');
|
||||
|
||||
try {
|
||||
interaction.client.user.setPresence({
|
||||
activities: [{ name: activityMessage, type: Discord.ActivityType[activityType] }],
|
||||
status: interaction.client.user.presence.status || 'online',
|
||||
});
|
||||
|
||||
settings.activity = { type: activityType, message: activityMessage };
|
||||
await saveSettings(settings);
|
||||
|
||||
const embed = new Discord.EmbedBuilder()
|
||||
.setDescription(`Activity set to **${activityType.toLowerCase()} ${activityMessage}**`)
|
||||
.setColor(supportbot.Embed.Colours.General);
|
||||
|
||||
return interaction.reply({ embeds: [embed], flags: Discord.MessageFlags.Ephemeral });
|
||||
} catch (error) {
|
||||
return interaction.reply({ content: `Failed to set activity: ${error.message}`, flags: Discord.MessageFlags.Ephemeral });
|
||||
}
|
||||
} else if (subcommand === 'streammode') {
|
||||
const toggle = interaction.options.getString('toggle');
|
||||
const twitchUrl = interaction.options.getString('twitchurl');
|
||||
|
||||
if (toggle === 'on') {
|
||||
if (!twitchUrl) {
|
||||
return interaction.reply({ content: 'Please provide a Twitch URL to stream.', flags: Discord.MessageFlags.Ephemeral });
|
||||
}
|
||||
|
||||
try {
|
||||
interaction.client.user.setPresence({
|
||||
activities: [{ name: 'Streaming', type: Discord.ActivityType.Streaming, url: twitchUrl }],
|
||||
status: 'online',
|
||||
});
|
||||
|
||||
settings.streaming = { active: true, url: twitchUrl };
|
||||
await saveSettings(settings);
|
||||
|
||||
const embed = new Discord.EmbedBuilder()
|
||||
.setDescription(`**Streaming mode enabled!**\nStreaming: [Twitch](${twitchUrl})`)
|
||||
.setColor(supportbot.Embed.Colours.General);
|
||||
|
||||
return interaction.reply({ embeds: [embed], flags: Discord.MessageFlags.Ephemeral });
|
||||
} catch (error) {
|
||||
return interaction.reply({ content: `Failed to enable streaming mode: ${error.message}`, flags: Discord.MessageFlags.Ephemeral });
|
||||
}
|
||||
} else if (toggle === 'off') {
|
||||
try {
|
||||
interaction.client.user.setPresence({
|
||||
activities: interaction.client.user.presence.activities.filter(a => a.type !== Discord.ActivityType.Streaming),
|
||||
status: settings.status || 'online',
|
||||
});
|
||||
|
||||
settings.streaming = { active: false };
|
||||
await saveSettings(settings);
|
||||
|
||||
const embed = new Discord.EmbedBuilder()
|
||||
.setDescription(`**Streaming mode disabled!**`)
|
||||
.setColor(supportbot.Embed.Colours.General);
|
||||
|
||||
return interaction.reply({ embeds: [embed], flags: Discord.MessageFlags.Ephemeral });
|
||||
} catch (error) {
|
||||
return interaction.reply({ content: `Failed to disable streaming mode: ${error.message}`, flags: Discord.MessageFlags.Ephemeral });
|
||||
}
|
||||
}
|
||||
} else if (subcommand === 'setnickname') {
|
||||
const nickname = interaction.options.getString('nickname');
|
||||
|
||||
try {
|
||||
const guild = interaction.guild;
|
||||
const botMember = guild.members.cache.get(interaction.client.user.id);
|
||||
await botMember.setNickname(nickname);
|
||||
|
||||
settings.nickname = nickname;
|
||||
await saveSettings(settings);
|
||||
|
||||
const embed = new Discord.EmbedBuilder()
|
||||
.setDescription(`Bot nickname set to **${nickname}**`)
|
||||
.setColor(supportbot.Embed.Colours.General);
|
||||
|
||||
return interaction.reply({ embeds: [embed], flags: Discord.MessageFlags.Ephemeral });
|
||||
} catch (error) {
|
||||
return interaction.reply({ content: `Failed to set nickname: ${error.message}`, flags: Discord.MessageFlags.Ephemeral });
|
||||
}
|
||||
} else {
|
||||
return interaction.reply({ content: 'Invalid command usage.', flags: Discord.MessageFlags.Ephemeral });
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -1,335 +0,0 @@
|
||||
const fs = require("fs");
|
||||
const Discord = require("discord.js");
|
||||
const yaml = require("js-yaml");
|
||||
|
||||
const supportbot = yaml.load(
|
||||
fs.readFileSync("./Configs/supportbot.yml", "utf8")
|
||||
);
|
||||
const cmdconfig = yaml.load(
|
||||
fs.readFileSync("./Configs/commands.yml", "utf8")
|
||||
);
|
||||
const msgconfig = yaml.load(
|
||||
fs.readFileSync("./Configs/messages.yml", "utf8")
|
||||
);
|
||||
|
||||
const Command = require("../Structures/Command.js");
|
||||
|
||||
module.exports = new Command({
|
||||
name: cmdconfig.CloseTicket.Command,
|
||||
description: cmdconfig.CloseTicket.Description,
|
||||
type: Discord.ApplicationCommandType.ChatInput,
|
||||
options: [
|
||||
{
|
||||
name: "reason",
|
||||
description: "Ticket Close Reason",
|
||||
type: Discord.ApplicationCommandOptionType.String,
|
||||
},
|
||||
],
|
||||
permissions: cmdconfig.CloseTicket.Permission,
|
||||
|
||||
async run(interaction) {
|
||||
const { getRole, getChannel } = interaction.client;
|
||||
|
||||
if (supportbot.Ticket.Close.StaffOnly) {
|
||||
let SupportStaff = await getRole(supportbot.Roles.StaffMember.Staff, interaction.guild);
|
||||
let Admin = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild);
|
||||
|
||||
if (!SupportStaff || !Admin)
|
||||
return interaction.reply(
|
||||
"Some roles seem to be missing!\nPlease check for errors when starting the bot."
|
||||
);
|
||||
|
||||
const NoPerms = new Discord.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(SupportStaff.id) &&
|
||||
!interaction.member.roles.cache.has(Admin.id)
|
||||
)
|
||||
return interaction.reply({ embeds: [NoPerms] });
|
||||
}
|
||||
|
||||
if (interaction.channel.type !== Discord.ChannelType.PrivateThread) {
|
||||
const NotTicketChannel = new Discord.EmbedBuilder()
|
||||
.setTitle("Invalid Channel!")
|
||||
.setDescription("This command can only be used in a ticket thread.")
|
||||
.setColor(supportbot.Embed.Colours.Warn);
|
||||
|
||||
return interaction.reply({ embeds: [NotTicketChannel], ephemeral: true });
|
||||
}
|
||||
|
||||
await interaction.deferReply();
|
||||
|
||||
let tickets = JSON.parse(fs.readFileSync("./Data/TicketData.json", "utf8"));
|
||||
let TicketData = tickets.tickets.findIndex((t) => t.id === interaction.channel.id);
|
||||
let ticket = tickets.tickets[TicketData];
|
||||
|
||||
if (TicketData === -1) {
|
||||
const Exists = new Discord.EmbedBuilder()
|
||||
.setTitle("No Ticket Found!")
|
||||
.setDescription(msgconfig.Error.NoValidTicket)
|
||||
.setColor(supportbot.Embed.Colours.Warn);
|
||||
return interaction.followUp({ embeds: [Exists] });
|
||||
}
|
||||
|
||||
let reason = interaction.options?.getString("reason") || "No Reason Provided.";
|
||||
|
||||
const CloseButton = new Discord.ButtonBuilder()
|
||||
.setCustomId("confirmCloseTicket")
|
||||
.setLabel(supportbot.Ticket.Close.Confirmation_Button)
|
||||
.setStyle(Discord.ButtonStyle.Danger);
|
||||
|
||||
const row = new Discord.ActionRowBuilder().addComponents(CloseButton);
|
||||
|
||||
const CloseTicketRequest = new Discord.EmbedBuilder()
|
||||
.setTitle(`**${supportbot.Ticket.Close.Title}**`)
|
||||
.setDescription(
|
||||
`${msgconfig.Ticket.ConfirmClose}`
|
||||
)
|
||||
.setColor(supportbot.Embed.Colours.General);
|
||||
|
||||
await interaction.followUp({
|
||||
embeds: [CloseTicketRequest],
|
||||
components: [row],
|
||||
});
|
||||
|
||||
const filter = (btnInteraction) => btnInteraction.customId === "confirmCloseTicket" && btnInteraction.user.id === interaction.user.id;
|
||||
|
||||
interaction.channel.awaitMessageComponent({ filter, time: 20000 })
|
||||
.then(async (btnInteraction) => {
|
||||
await handleCloseTicket(interaction, reason, ticket, TicketData);
|
||||
await btnInteraction.reply({ content: "Ticket successfully closed.", ephemeral: true });
|
||||
})
|
||||
.catch((err) => {
|
||||
interaction.followUp(`${msgconfig.Ticket.CloseTimeout}`);
|
||||
console.error(err);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
async function handleCloseTicket(interaction, reason, ticket, TicketData) {
|
||||
const { getChannel } = interaction.client;
|
||||
|
||||
let tickets = JSON.parse(fs.readFileSync("./Data/TicketData.json", "utf8"));
|
||||
let tUser = interaction.client.users.cache.get(ticket.user);
|
||||
let transcriptChannel = await getChannel(supportbot.Ticket.Log.TranscriptLog, interaction.guild);
|
||||
let logChannel = await getChannel(supportbot.Ticket.Log.TicketLog, interaction.guild);
|
||||
|
||||
if (!transcriptChannel || !logChannel)
|
||||
return interaction.followUp("Some Channels seem to be missing!");
|
||||
|
||||
try {
|
||||
tickets.tickets[TicketData].open = false;
|
||||
fs.writeFileSync(
|
||||
"./Data/TicketData.json",
|
||||
JSON.stringify(tickets, null, 4),
|
||||
(err) => {
|
||||
if (err) console.error(err);
|
||||
}
|
||||
);
|
||||
|
||||
const transcriptEmbed = new Discord.EmbedBuilder()
|
||||
.setTitle(supportbot.Ticket.Log.TranscriptTitle)
|
||||
.setColor(supportbot.Embed.Colours.General)
|
||||
.setFooter({
|
||||
text: supportbot.Embed.Footer,
|
||||
iconURL: interaction.user.displayAvatarURL(),
|
||||
})
|
||||
.addFields(
|
||||
{ name: "Ticket", value: `${interaction.channel.name} (${interaction.channel.id})` },
|
||||
{ name: "User", value: `${tUser?.username || "N/A"}#${tUser?.discriminator || "N/A"} (${tUser?.id || ticket.user})` },
|
||||
{ name: "Closed By", value: interaction.user.tag },
|
||||
{ name: "Reason", value: reason }
|
||||
);
|
||||
|
||||
const logEmbed = new Discord.EmbedBuilder()
|
||||
.setTitle(supportbot.Ticket.Log.TicketLog_Title)
|
||||
.setColor(supportbot.Embed.Colours.General)
|
||||
.setFooter({
|
||||
text: supportbot.Embed.Footer,
|
||||
iconURL: interaction.user.displayAvatarURL(),
|
||||
})
|
||||
.addFields(
|
||||
{ name: "Ticket", value: `${interaction.channel.name} (${interaction.channel.id})` },
|
||||
{ name: "User", value: `${tUser?.username || "N/A"}#${tUser?.discriminator || "N/A"} (${tUser?.id || ticket.user})` },
|
||||
{ name: "Closed By", value: interaction.user.tag },
|
||||
{ name: "Reason", value: reason }
|
||||
);
|
||||
|
||||
let msgs = await interaction.channel.messages.fetch({ limit: 100 });
|
||||
let html = `
|
||||
<html>
|
||||
<head>
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.css" rel="stylesheet">
|
||||
<style>
|
||||
body {
|
||||
background-color: #1a1a1a;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
color: white;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.container {
|
||||
backdrop-filter: blur(10px);
|
||||
background-color: rgba(0, 128, 0, 0.3);
|
||||
border-radius: 15px;
|
||||
padding: 20px;
|
||||
margin: 20px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 1000px;
|
||||
width: 100%;
|
||||
margin: auto;
|
||||
}
|
||||
.message {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
padding: 10px;
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 10px;
|
||||
}
|
||||
.avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.content {
|
||||
flex: 1;
|
||||
}
|
||||
.username {
|
||||
font-weight: bold;
|
||||
}
|
||||
.timestamp {
|
||||
font-size: 0.8em;
|
||||
color: #bbb;
|
||||
}
|
||||
.download-button {
|
||||
background-color: #4CAF50;
|
||||
color: white;
|
||||
padding: 10px 20px;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
font-size: 16px;
|
||||
margin-top: 20px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1 class="text-2xl font-bold mb-4">Ticket Transcript</h1>
|
||||
<p><strong>Server Name:</strong> ${interaction.guild.name}</p>
|
||||
<p><strong>Ticket:</strong> ${interaction.channel.name}</p>
|
||||
<p><strong>Messages:</strong> ${msgs.size} Messages</p>
|
||||
<div class="mt-4">
|
||||
`;
|
||||
|
||||
msgs = msgs.sort((a, b) => a.createdTimestamp - b.createdTimestamp);
|
||||
msgs.forEach((msg) => {
|
||||
if (msg.content) {
|
||||
html += `
|
||||
<div class="message">
|
||||
<img class="avatar" src="${msg.author.displayAvatarURL()}" alt="Avatar">
|
||||
<div class="content">
|
||||
<p class="username">${msg.author.tag}</p>
|
||||
<p class="timestamp">${msg.createdAt.toLocaleString()}</p>
|
||||
<p>${convertMarkdownToHTML(msg.content)}</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
});
|
||||
|
||||
const fileName = `${interaction.channel.name}.html`;
|
||||
|
||||
html += `
|
||||
</div>
|
||||
<a href="${fileName}" download class="download-button">Download Transcript</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
if (!supportbot.Ticket.Log.DisableTicketLogChannel) {
|
||||
await logChannel.send({ embeds: [logEmbed] }).catch((err) => {
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
|
||||
let file = new Discord.AttachmentBuilder(
|
||||
Buffer.from(html),
|
||||
{ name: fileName }
|
||||
);
|
||||
|
||||
await transcriptChannel
|
||||
.send({ embeds: [transcriptEmbed], files: [file] })
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
});
|
||||
|
||||
if (supportbot.Ticket.DMTranscripts) {
|
||||
await tUser
|
||||
?.send({ embeds: [transcriptEmbed], files: [file] })
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
});
|
||||
|
||||
if (ticket.subUsers) {
|
||||
ticket.subUsers.forEach(async (subUser) => {
|
||||
await interaction.client.users.cache
|
||||
.get(subUser)
|
||||
?.send({ embeds: [transcriptEmbed], files: [file] });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await interaction.channel.delete().catch((error) => {
|
||||
console.error(error);
|
||||
if (error.code !== Discord.Constants.APIErrors.UNKNOWN_CHANNEL) {
|
||||
console.error("Failed to delete the interaction:", error);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
function convertMarkdownToHTML(text) {
|
||||
// Convert **bold** to <strong>bold</strong>
|
||||
text = text.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
|
||||
|
||||
// Convert *italic* to <em>italic</em>
|
||||
text = text.replace(/\*(.*?)\*/g, '<em>$1</em>');
|
||||
|
||||
// Convert __underline__ to <u>underline</u>
|
||||
text = text.replace(/__(.*?)__/g, '<u>$1</u>');
|
||||
|
||||
// Convert ~~strikethrough~~ to <s>strikethrough</s>
|
||||
text = text.replace(/~~(.*?)~~/g, '<s>$1</s>');
|
||||
|
||||
// Convert inline code `code` to <code>code</code>
|
||||
text = text.replace(/`(.*?)`/g, '<code>$1</code>');
|
||||
|
||||
// Convert blockquotes > quote to <blockquote>quote</blockquote>
|
||||
text = text.replace(/^> (.*?$)/gm, '<blockquote>$1</blockquote>');
|
||||
|
||||
// Convert multiline code blocks ```code``` to <pre><code>code</code></pre>
|
||||
text = text.replace(/```(.*?)```/gs, '<pre><code>$1</code></pre>');
|
||||
|
||||
// Convert line breaks to <br>
|
||||
text = text.replace(/\n/g, '<br>');
|
||||
|
||||
return text;
|
||||
}
|
||||
@@ -1,167 +0,0 @@
|
||||
// File path: ./commands/embed.js
|
||||
|
||||
const fs = require("fs");
|
||||
const Discord = require("discord.js");
|
||||
const yaml = require("js-yaml");
|
||||
|
||||
const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8"));
|
||||
const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8"));
|
||||
const msgconfig = yaml.load(fs.readFileSync("./Configs/messages.yml", "utf8"));
|
||||
|
||||
const Command = require("../Structures/Command.js");
|
||||
|
||||
module.exports = new Command({
|
||||
name: cmdconfig.Embed.Command,
|
||||
description: cmdconfig.Embed.Description,
|
||||
type: Discord.ApplicationCommandType.ChatInput,
|
||||
options: [
|
||||
{
|
||||
name: "title",
|
||||
description: "Embed Title",
|
||||
type: Discord.ApplicationCommandOptionType.String,
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: "message",
|
||||
description: "Embed Message",
|
||||
type: Discord.ApplicationCommandOptionType.String,
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: "color",
|
||||
description: "Embed HEX Color",
|
||||
type: Discord.ApplicationCommandOptionType.String,
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: "fields",
|
||||
description: "Embed Fields (title|content;title|content;...)",
|
||||
type: Discord.ApplicationCommandOptionType.String,
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: "thumbnail",
|
||||
description: "Embed Thumbnail URL",
|
||||
type: Discord.ApplicationCommandOptionType.String,
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: "image",
|
||||
description: "Embed Image URL",
|
||||
type: Discord.ApplicationCommandOptionType.String,
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: "footer",
|
||||
description: "Embed Footer Text",
|
||||
type: Discord.ApplicationCommandOptionType.String,
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: "footericon",
|
||||
description: "Embed Footer Icon URL",
|
||||
type: Discord.ApplicationCommandOptionType.String,
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: "author",
|
||||
description: "Embed Author Name",
|
||||
type: Discord.ApplicationCommandOptionType.String,
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: "authoricon",
|
||||
description: "Embed Author Icon URL",
|
||||
type: Discord.ApplicationCommandOptionType.String,
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: "authorurl",
|
||||
description: "Embed Author URL",
|
||||
type: Discord.ApplicationCommandOptionType.String,
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: "timestamp",
|
||||
description: "Include Timestamp",
|
||||
type: Discord.ApplicationCommandOptionType.Boolean,
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: "inline",
|
||||
description: "Make Fields Inline",
|
||||
type: Discord.ApplicationCommandOptionType.Boolean,
|
||||
required: false,
|
||||
}
|
||||
],
|
||||
permissions: cmdconfig.Embed.Permission,
|
||||
|
||||
async run(interaction) {
|
||||
const { getRole } = interaction.client;
|
||||
let SupportStaff = await getRole(supportbot.Roles.StaffMember.Staff, interaction.guild);
|
||||
let Admin = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild);
|
||||
|
||||
if (!SupportStaff || !Admin) {
|
||||
return interaction.reply("Some roles seem to be missing!\nPlease check for errors when starting the bot.");
|
||||
}
|
||||
|
||||
const NoPerms = new Discord.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(SupportStaff.id) ||
|
||||
interaction.member.roles.cache.has(Admin.id)
|
||||
) {
|
||||
const EmbedTitle = interaction.options.getString("title").replace(/\\n/g, '\n');
|
||||
const EmbedSubject = interaction.options.getString("message").replace(/\\n/g, '\n');
|
||||
const GeneralColour = interaction.options.getString("color") || supportbot.Embed.Colours.Default;
|
||||
const EmbedThumbnail = interaction.options.getString("thumbnail");
|
||||
const EmbedImage = interaction.options.getString("image");
|
||||
const EmbedFields = interaction.options.getString("fields");
|
||||
const EmbedFooter = interaction.options.getString("footer")?.replace(/\\n/g, '\n');
|
||||
const EmbedFooterIcon = interaction.options.getString("footericon");
|
||||
const EmbedAuthor = interaction.options.getString("author")?.replace(/\\n/g, '\n');
|
||||
const EmbedAuthorIcon = interaction.options.getString("authoricon");
|
||||
const EmbedAuthorURL = interaction.options.getString("authorurl");
|
||||
const EmbedTimestamp = interaction.options.getBoolean("timestamp");
|
||||
const InlineFields = interaction.options.getBoolean("inline");
|
||||
|
||||
const EmbedMsg = new Discord.EmbedBuilder()
|
||||
.setTitle(EmbedTitle)
|
||||
.setDescription(EmbedSubject)
|
||||
.setColor(GeneralColour)
|
||||
.setThumbnail(EmbedThumbnail)
|
||||
.setImage(EmbedImage);
|
||||
|
||||
if (EmbedFields) {
|
||||
const fieldsArray = EmbedFields.split(';');
|
||||
fieldsArray.forEach(field => {
|
||||
const [title, value] = field.split('|');
|
||||
if (title && value) {
|
||||
EmbedMsg.addFields({ name: title.replace(/\\n/g, '\n'), value: value.replace(/\\n/g, '\n'), inline: InlineFields || false });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (EmbedFooter) {
|
||||
EmbedMsg.setFooter({ text: EmbedFooter, iconURL: EmbedFooterIcon });
|
||||
}
|
||||
|
||||
if (EmbedAuthor) {
|
||||
EmbedMsg.setAuthor({ name: EmbedAuthor, iconURL: EmbedAuthorIcon, url: EmbedAuthorURL });
|
||||
}
|
||||
|
||||
if (EmbedTimestamp) {
|
||||
EmbedMsg.setTimestamp();
|
||||
}
|
||||
|
||||
interaction.reply({
|
||||
embeds: [EmbedMsg],
|
||||
});
|
||||
} else {
|
||||
return interaction.reply({ embeds: [NoPerms] });
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -1,79 +0,0 @@
|
||||
const fs = require("fs");
|
||||
const Discord = require("discord.js");
|
||||
const yaml = require("js-yaml");
|
||||
const generateImage = require('../Structures/generateImage.js');
|
||||
|
||||
const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8"));
|
||||
const Command = require("../Structures/Command.js");
|
||||
|
||||
module.exports = new Command({
|
||||
name: "image",
|
||||
description: "Create an image through SupportBot AI",
|
||||
type: Discord.ApplicationCommandType.ChatInput,
|
||||
options: [
|
||||
{
|
||||
name: "prompt",
|
||||
description: "The prompt to generate an image from",
|
||||
type: Discord.ApplicationCommandOptionType.String,
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: "negativeprompt",
|
||||
description: "The negative prompt to exclude from the image",
|
||||
type: Discord.ApplicationCommandOptionType.String,
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: "cfgscale",
|
||||
description: "The cfg scale for the image generation",
|
||||
type: Discord.ApplicationCommandOptionType.Number,
|
||||
required: false,
|
||||
}
|
||||
],
|
||||
permissions: "SendMessages",
|
||||
|
||||
async run(interaction) {
|
||||
const prompt = interaction.options.getString("prompt");
|
||||
const negativePrompt = interaction.options.getString("negativeprompt") || "";
|
||||
const cfgScale = interaction.options.getNumber("cfgscale") || 7.5; // Default value
|
||||
|
||||
try {
|
||||
const imageUrl = await generateImage(prompt, negativePrompt, cfgScale);
|
||||
|
||||
const ImageEmbed = new Discord.EmbedBuilder()
|
||||
.setTitle("Generated Image")
|
||||
.setDescription(`Here is the image generated for your prompt: "${prompt}"`)
|
||||
.setImage(imageUrl)
|
||||
.setColor(supportbot.Embed.Colours.General)
|
||||
.setFooter({
|
||||
text: supportbot.Embed.Footer,
|
||||
iconURL: interaction.user.displayAvatarURL(),
|
||||
});
|
||||
|
||||
if (!interaction.replied) {
|
||||
await interaction.reply({
|
||||
ephemeral: true,
|
||||
embeds: [ImageEmbed],
|
||||
});
|
||||
} else {
|
||||
await interaction.followUp({
|
||||
ephemeral: true,
|
||||
embeds: [ImageEmbed],
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error generating image:', error);
|
||||
if (!interaction.replied) {
|
||||
await interaction.reply({
|
||||
ephemeral: true,
|
||||
content: `An error occurred while generating the image: ${error.message}. Please try again later.`,
|
||||
});
|
||||
} else {
|
||||
await interaction.followUp({
|
||||
ephemeral: true,
|
||||
content: `An error occurred while generating the image: ${error.message}. Please try again later.`,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -1,323 +0,0 @@
|
||||
const { EmbedBuilder, ModalBuilder, TextInputBuilder, TextInputStyle, ApplicationCommandType, ActionRowBuilder, ButtonBuilder, ButtonStyle, ApplicationCommandOptionType } = require("discord.js");
|
||||
const fs = require("fs");
|
||||
const yaml = require("js-yaml");
|
||||
|
||||
const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8"));
|
||||
const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8"));
|
||||
const msgconfig = yaml.load(fs.readFileSync("./Configs/messages.yml", "utf8"));
|
||||
|
||||
const Command = require("../Structures/Command.js");
|
||||
|
||||
const clockedInUsers = new Set();
|
||||
|
||||
async function getRole(roleName, guild) {
|
||||
return guild.roles.cache.find(role => role.name === roleName);
|
||||
}
|
||||
|
||||
function updateClockedInStatus(profileEmbed, clockedIn) {
|
||||
profileEmbed.data.fields = profileEmbed.data.fields.map(field => {
|
||||
if (field.name === 'Clocked In Status') {
|
||||
field.value = clockedIn ? '✅ Clocked In' : '❌ Clocked Out';
|
||||
}
|
||||
return field;
|
||||
});
|
||||
return profileEmbed;
|
||||
}
|
||||
|
||||
module.exports = new Command({
|
||||
name: 'profile',
|
||||
description: 'Show user profile',
|
||||
type: ApplicationCommandType.ChatInput,
|
||||
permissions: ["SendMessages"],
|
||||
options: [
|
||||
{
|
||||
name: 'user',
|
||||
description: 'The user to view the profile of',
|
||||
type: ApplicationCommandOptionType.User,
|
||||
required: false
|
||||
}
|
||||
],
|
||||
|
||||
async run(interaction) {
|
||||
await interaction.deferReply({ ephemeral: true }); // Acknowledge the interaction immediately
|
||||
|
||||
const userOption = interaction.options.getUser('user');
|
||||
const viewingUser = userOption || interaction.user;
|
||||
const viewingUserId = viewingUser.id;
|
||||
const interactionUserId = interaction.user.id;
|
||||
|
||||
const user = interaction.guild.members.cache.get(viewingUserId);
|
||||
|
||||
const Staff = await getRole(supportbot.Roles.StaffMember.Staff, interaction.guild);
|
||||
const Admin = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild);
|
||||
|
||||
if (!Staff || !Admin) {
|
||||
return interaction.editReply({
|
||||
content: "Some roles seem to be missing!\nPlease check for errors when starting the bot.",
|
||||
ephemeral: true,
|
||||
});
|
||||
}
|
||||
|
||||
const isStaff = user.roles.cache.has(Staff.id) || user.roles.cache.has(Admin.id);
|
||||
const isOwnProfile = interactionUserId === viewingUserId;
|
||||
|
||||
const profilePath = `./Data/Profiles/${viewingUserId}.json`;
|
||||
let profileData;
|
||||
|
||||
if (fs.existsSync(profilePath)) {
|
||||
profileData = JSON.parse(fs.readFileSync(profilePath, "utf8"));
|
||||
} else {
|
||||
profileData = {
|
||||
bio: "",
|
||||
timezone: "",
|
||||
clockedIn: false
|
||||
};
|
||||
fs.writeFileSync(profilePath, JSON.stringify(profileData, null, 2));
|
||||
}
|
||||
|
||||
const { bio, timezone, clockedIn } = profileData;
|
||||
|
||||
let profileEmbed = new EmbedBuilder()
|
||||
.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) {
|
||||
profileEmbed.addFields({ name: 'Clocked In Status', value: clockedIn ? '✅ Clocked In' : '❌ Clocked Out', inline: true });
|
||||
}
|
||||
|
||||
const buttonRow = new ActionRowBuilder();
|
||||
if (isOwnProfile) {
|
||||
buttonRow.addComponents(
|
||||
new ButtonBuilder()
|
||||
.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({ embeds: [profileEmbed], components: buttonRow.components.length > 0 ? [buttonRow] : [] });
|
||||
|
||||
const filter = i => (
|
||||
['editProfile', 'clockInOut', 'viewTicketStats', 'showOpenTickets', 'backToStats', 'backToProfile'].includes(i.customId)
|
||||
&& i.user.id === interactionUserId
|
||||
);
|
||||
|
||||
const collector = interaction.channel.createMessageComponentCollector({ filter, time: 60000 });
|
||||
|
||||
collector.on('collect', async i => {
|
||||
if (i.customId === 'editProfile') {
|
||||
const modal = new ModalBuilder()
|
||||
.setCustomId('editProfileModal')
|
||||
.setTitle('Edit Profile')
|
||||
.addComponents(
|
||||
new ActionRowBuilder().addComponents(
|
||||
new TextInputBuilder()
|
||||
.setCustomId('bio')
|
||||
.setLabel('Bio')
|
||||
.setStyle(TextInputStyle.Paragraph)
|
||||
.setPlaceholder('Enter your bio')
|
||||
.setValue(String(bio))
|
||||
.setRequired(false)
|
||||
),
|
||||
new ActionRowBuilder().addComponents(
|
||||
new TextInputBuilder()
|
||||
.setCustomId('timezone')
|
||||
.setLabel('Timezone')
|
||||
.setStyle(TextInputStyle.Short)
|
||||
.setPlaceholder('Enter your timezone')
|
||||
.setValue(String(timezone))
|
||||
.setRequired(false)
|
||||
)
|
||||
);
|
||||
|
||||
await i.showModal(modal);
|
||||
}
|
||||
|
||||
if (i.customId === 'clockInOut') {
|
||||
profileData.clockedIn = !profileData.clockedIn;
|
||||
fs.writeFileSync(profilePath, JSON.stringify(profileData, null, 2));
|
||||
profileEmbed = updateClockedInStatus(profileEmbed, profileData.clockedIn);
|
||||
|
||||
if (profileData.clockedIn) {
|
||||
clockedInUsers.add(viewingUserId);
|
||||
} else {
|
||||
clockedInUsers.delete(viewingUserId);
|
||||
}
|
||||
|
||||
const updatedButtonRow = new ActionRowBuilder()
|
||||
.addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId('editProfile')
|
||||
.setLabel('Edit Profile')
|
||||
.setStyle(ButtonStyle.Secondary)
|
||||
);
|
||||
|
||||
if (isStaff) {
|
||||
updatedButtonRow.addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId('clockInOut')
|
||||
.setLabel(profileData.clockedIn ? 'Clock Out' : 'Clock In')
|
||||
.setStyle(ButtonStyle.Secondary)
|
||||
);
|
||||
|
||||
if (supportbot.Ticket.ClaimTickets.Enabled) {
|
||||
updatedButtonRow.addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId('viewTicketStats')
|
||||
.setLabel('View Ticket Stats')
|
||||
.setStyle(ButtonStyle.Secondary)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await i.update({ embeds: [profileEmbed], components: [updatedButtonRow] });
|
||||
|
||||
let clockedinout = new EmbedBuilder()
|
||||
.setTitle(profileData.clockedIn ? 'Clocked In!' : 'Clocked Out!')
|
||||
.setDescription(profileData.clockedIn ? 'You have successfully clocked in.' : 'You have successfully clocked out.')
|
||||
.setColor(supportbot.Embed.Colours.General)
|
||||
|
||||
await interaction.followUp({ embeds: [clockedinout], ephemeral: true });
|
||||
}
|
||||
|
||||
if (i.customId === 'viewTicketStats') {
|
||||
const TicketData = JSON.parse(fs.readFileSync("./Data/TicketData.json", "utf8"));
|
||||
const ticketsClaimed = TicketData.tickets.filter(t => t.claimedBy === viewingUserId);
|
||||
const ticketsOpen = ticketsClaimed.filter(t => t.open);
|
||||
const totalTickets = ticketsClaimed.length;
|
||||
|
||||
let totalResponseTime = 0;
|
||||
let responseCount = 0;
|
||||
|
||||
ticketsClaimed.forEach(ticket => {
|
||||
if (ticket.claimedAt && ticket.createdAt) {
|
||||
const responseTime = new Date(ticket.claimedAt) - new Date(ticket.createdAt);
|
||||
totalResponseTime += responseTime;
|
||||
responseCount++;
|
||||
}
|
||||
});
|
||||
|
||||
const averageResponseTime = responseCount ? totalResponseTime / responseCount : 0;
|
||||
const averageResponseTimeMinutes = Math.round(averageResponseTime / 60000);
|
||||
|
||||
const statsEmbed = new EmbedBuilder()
|
||||
.setTitle(msgconfig.TicketStats.Title)
|
||||
.setDescription(`> Ticket stats for <@${viewingUserId}>`)
|
||||
.setColor(supportbot.Embed.Colours.General)
|
||||
.addFields(
|
||||
{ name: msgconfig.TicketStats.OpenTickets, value: `${ticketsOpen.length}`, inline: true },
|
||||
{ name: msgconfig.TicketStats.TotalTickets, value: `${totalTickets}`, inline: true },
|
||||
{ name: msgconfig.TicketStats.ResponseTime, value: `${averageResponseTimeMinutes} minutes`, inline: false }
|
||||
);
|
||||
|
||||
const statsButtonRow = new ActionRowBuilder()
|
||||
.addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId('showOpenTickets')
|
||||
.setLabel('Show Open Tickets')
|
||||
.setStyle(ButtonStyle.Secondary),
|
||||
new ButtonBuilder()
|
||||
.setCustomId('backToProfile')
|
||||
.setLabel('Back to Profile')
|
||||
.setStyle(ButtonStyle.Secondary)
|
||||
);
|
||||
|
||||
await i.update({ embeds: [statsEmbed], components: [statsButtonRow] });
|
||||
}
|
||||
|
||||
if (i.customId === 'showOpenTickets') {
|
||||
const TicketData = JSON.parse(fs.readFileSync("./Data/TicketData.json", "utf8"));
|
||||
const ticketsOpen = TicketData.tickets.filter(t => t.claimedBy === viewingUserId && t.open);
|
||||
|
||||
const openTicketsEmbed = new EmbedBuilder()
|
||||
.setTitle('Active Tickets')
|
||||
.setDescription(ticketsOpen.map(t => `<#${t.id}>`).join('\n') || 'No active tickets.')
|
||||
.setColor(supportbot.Embed.Colours.General);
|
||||
|
||||
const openTicketsButtonRow = new ActionRowBuilder()
|
||||
.addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId('backToStats')
|
||||
.setLabel('Back to Stats')
|
||||
.setStyle(ButtonStyle.Secondary)
|
||||
);
|
||||
|
||||
await i.update({ embeds: [openTicketsEmbed], components: [openTicketsButtonRow] });
|
||||
}
|
||||
|
||||
if (i.customId === 'backToStats') {
|
||||
const TicketData = JSON.parse(fs.readFileSync("./Data/TicketData.json", "utf8"));
|
||||
const ticketsClaimed = TicketData.tickets.filter(t => t.claimedBy === viewingUserId);
|
||||
const ticketsOpen = ticketsClaimed.filter(t => t.open);
|
||||
const totalTickets = ticketsClaimed.length;
|
||||
|
||||
let totalResponseTime = 0;
|
||||
let responseCount = 0;
|
||||
|
||||
ticketsClaimed.forEach(ticket => {
|
||||
if (ticket.claimedAt && ticket.createdAt) {
|
||||
const responseTime = new Date(ticket.claimedAt) - new Date(ticket.createdAt);
|
||||
totalResponseTime += responseTime;
|
||||
responseCount++;
|
||||
}
|
||||
});
|
||||
|
||||
const averageResponseTime = responseCount ? totalResponseTime / responseCount : 0;
|
||||
const averageResponseTimeMinutes = Math.round(averageResponseTime / 60000);
|
||||
|
||||
const statsEmbed = new EmbedBuilder()
|
||||
.setTitle(msgconfig.TicketStats.Title)
|
||||
.setDescription(`> Ticket stats for <@${viewingUserId}>`)
|
||||
.setColor(supportbot.Embed.Colours.General)
|
||||
.addFields(
|
||||
{ name: msgconfig.TicketStats.OpenTickets, value: `${ticketsOpen.length}`, inline: true },
|
||||
{ name: msgconfig.TicketStats.TotalTickets, value: `${totalTickets}`, inline: true },
|
||||
{ name: msgconfig.TicketStats.ResponseTime, value: `${averageResponseTimeMinutes} minutes`, inline: false }
|
||||
);
|
||||
|
||||
const statsButtonRow = new ActionRowBuilder()
|
||||
.addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId('showOpenTickets')
|
||||
.setLabel('Show Open Tickets')
|
||||
.setStyle(ButtonStyle.Secondary),
|
||||
new ButtonBuilder()
|
||||
.setCustomId('backToProfile')
|
||||
.setLabel('Back to Profile')
|
||||
.setStyle(ButtonStyle.Secondary)
|
||||
);
|
||||
|
||||
await i.update({ embeds: [statsEmbed], components: [statsButtonRow] });
|
||||
}
|
||||
|
||||
if (i.customId === 'backToProfile') {
|
||||
await i.update({ embeds: [profileEmbed], components: buttonRow.components.length > 0 ? [buttonRow] : [] });
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports.clockedInUsers = clockedInUsers;
|
||||
@@ -1,115 +0,0 @@
|
||||
const fs = require("fs");
|
||||
const Discord = require("discord.js");
|
||||
const yaml = require("js-yaml");
|
||||
|
||||
// Load configurations
|
||||
const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8"));
|
||||
const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8"));
|
||||
const msgconfig = yaml.load(fs.readFileSync("./Configs/messages.yml", "utf8"));
|
||||
|
||||
const Command = require("../Structures/Command.js");
|
||||
|
||||
// Ensure suggestions file exists and is correctly loaded
|
||||
let suggestions;
|
||||
try {
|
||||
suggestions = require("../Data/SuggestionData.json");
|
||||
} catch (error) {
|
||||
suggestions = {};
|
||||
}
|
||||
|
||||
module.exports = new Command({
|
||||
name: cmdconfig.Suggestion.Command,
|
||||
description: cmdconfig.Suggestion.Description,
|
||||
type: Discord.ApplicationCommandType.ChatInput,
|
||||
options: [
|
||||
{
|
||||
name: "suggestion",
|
||||
description: "Create a Suggestion",
|
||||
type: Discord.ApplicationCommandOptionType.String,
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
permissions: cmdconfig.Suggestion.Permission,
|
||||
|
||||
async run(interaction) {
|
||||
const { getChannel } = interaction.client;
|
||||
|
||||
const suggestChannel = await getChannel(
|
||||
supportbot.Suggestions.Channel,
|
||||
interaction.guild
|
||||
);
|
||||
|
||||
const NoChannel = new Discord.EmbedBuilder()
|
||||
.setTitle("Missing Channel!")
|
||||
.setDescription(msgconfig.Error.InvalidChannel)
|
||||
.setColor(supportbot.Embed.Colours.Error);
|
||||
|
||||
if (!suggestChannel) return interaction.reply({ embeds: [NoChannel] });
|
||||
|
||||
let suggestion = interaction.options.getString("suggestion");
|
||||
|
||||
const SuggestEmbed = new Discord.EmbedBuilder()
|
||||
.addFields(
|
||||
{ name: "Suggestion", value: suggestion, inline: true },
|
||||
{ name: "From", value: `<@${interaction.user.id}>` },
|
||||
{ name: `${supportbot.Suggestions.UpvoteEmoji} ${supportbot.Suggestions.UpvoteTitle}`, value: "0", inline: true },
|
||||
{ name: `${supportbot.Suggestions.DownvoteEmoji} ${supportbot.Suggestions.DownvoteTitle}`, value: "0", inline: true }
|
||||
)
|
||||
.setThumbnail(interaction.user.displayAvatarURL())
|
||||
.setFooter({
|
||||
text: supportbot.Embed.Footer,
|
||||
iconURL: interaction.user.displayAvatarURL(),
|
||||
})
|
||||
.setColor(supportbot.Embed.Colours.General);
|
||||
|
||||
const UpvoteButton = new Discord.ButtonBuilder()
|
||||
.setCustomId("upvote")
|
||||
.setEmoji(supportbot.Suggestions.UpvoteEmoji)
|
||||
.setStyle(supportbot.Suggestions.Buttons.Upvote);
|
||||
|
||||
const DownvoteButton = new Discord.ButtonBuilder()
|
||||
.setCustomId("downvote")
|
||||
.setEmoji(supportbot.Suggestions.DownvoteEmoji)
|
||||
.setStyle(supportbot.Suggestions.Buttons.Downvote);
|
||||
|
||||
const RemoveVoteButton = new Discord.ButtonBuilder()
|
||||
.setCustomId("removevote")
|
||||
.setLabel(supportbot.Suggestions.Buttons.RemoveVote_Title)
|
||||
.setStyle(supportbot.Suggestions.Buttons.RemoveVote); // Red button
|
||||
|
||||
const row = new Discord.ActionRowBuilder().addComponents(UpvoteButton, DownvoteButton, RemoveVoteButton);
|
||||
|
||||
const suggestionMsg = await suggestChannel.send({ embeds: [SuggestEmbed], components: [row] });
|
||||
|
||||
suggestions[suggestionMsg.id] = {
|
||||
suggestion: suggestion,
|
||||
author: interaction.user.id,
|
||||
upvotes: [],
|
||||
downvotes: []
|
||||
};
|
||||
|
||||
fs.writeFileSync("./Data/SuggestionData.json", JSON.stringify(suggestions, null, 2));
|
||||
|
||||
if (supportbot.Suggestions.Threads.Enabled) {
|
||||
await suggestionMsg.startThread({
|
||||
name: supportbot.Suggestions.Threads.Title,
|
||||
autoArchiveDuration: 60,
|
||||
type: Discord.ChannelType.PublicThread,
|
||||
reason: supportbot.Suggestions.Threads.Reason,
|
||||
});
|
||||
}
|
||||
|
||||
const Submitted = new Discord.EmbedBuilder()
|
||||
.setTitle(`${msgconfig.Suggestions.Sent_Title}`)
|
||||
.setDescription(`${msgconfig.Suggestions.Sent}`)
|
||||
.addFields(
|
||||
{ name: "Sent to:", value: `<#${suggestChannel.id}>` },
|
||||
)
|
||||
.setColor(supportbot.Embed.Colours.Success);
|
||||
|
||||
await interaction.reply({
|
||||
ephemeral: true,
|
||||
embeds: [Submitted]
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -1,451 +0,0 @@
|
||||
const fs = require("fs");
|
||||
const {
|
||||
EmbedBuilder,
|
||||
ApplicationCommandOptionType,
|
||||
ApplicationCommandType,
|
||||
ChannelType,
|
||||
ActionRowBuilder,
|
||||
ButtonBuilder,
|
||||
StringSelectMenuBuilder,
|
||||
StringSelectMenuOptionBuilder,
|
||||
ButtonStyle
|
||||
} = require("discord.js");
|
||||
const yaml = require("js-yaml");
|
||||
|
||||
const panelconfig = yaml.load(fs.readFileSync("./Configs/ticket-panel.yml", "utf8"));
|
||||
const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8"));
|
||||
const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8"));
|
||||
const msgconfig = yaml.load(fs.readFileSync("./Configs/messages.yml", "utf8"));
|
||||
|
||||
const Command = require("../Structures/Command.js");
|
||||
const TicketNumberID = require("../Structures/TicketID.js");
|
||||
|
||||
async function getClockedInUsers(guild) {
|
||||
const clockedInUsers = new Set();
|
||||
|
||||
const profilesDir = './Data/Profiles';
|
||||
const profileFiles = fs.readdirSync(profilesDir);
|
||||
|
||||
for (const file of profileFiles) {
|
||||
const profileData = JSON.parse(fs.readFileSync(`${profilesDir}/${file}`, "utf8"));
|
||||
if (profileData.clockedIn) {
|
||||
const userId = file.replace('.json', '');
|
||||
const member = await guild.members.fetch(userId);
|
||||
if (member) clockedInUsers.add(member.id);
|
||||
}
|
||||
}
|
||||
return clockedInUsers;
|
||||
}
|
||||
|
||||
async function assignTicketToUser(ticketChannel, Staff, Admin, interaction, attempts = 0) {
|
||||
console.log(`Assigning ticket attempt ${attempts}`);
|
||||
|
||||
try {
|
||||
const clockedInUsers = await getClockedInUsers(interaction.guild);
|
||||
console.log(`Clocked in users: ${Array.from(clockedInUsers).join(', ')}`);
|
||||
|
||||
if (clockedInUsers.size === 0 || attempts >= 2) {
|
||||
console.log("No clocked in users or max attempts reached");
|
||||
const claimChannel = await interaction.guild.channels.cache.get(supportbot.Ticket.ClaimTickets.Channel);
|
||||
if (claimChannel) {
|
||||
const claimEmbed = new EmbedBuilder()
|
||||
.setTitle(msgconfig.Ticket.ClaimTickets.ClaimTitle || 'Default Claim Title')
|
||||
.setDescription(msgconfig.Ticket.ClaimTickets.ClaimMessage || 'Default Claim Message')
|
||||
.setColor(supportbot.Embed.Colours.General);
|
||||
|
||||
const claimButton = new ButtonBuilder()
|
||||
.setCustomId(`claimticket-${ticketChannel.id}`)
|
||||
.setLabel(supportbot.Ticket.ClaimTickets.ButtonTitle || 'Claim Ticket')
|
||||
.setStyle(ButtonStyle.Primary);
|
||||
|
||||
const row = new ActionRowBuilder().addComponents(claimButton);
|
||||
|
||||
const claimMessage = await claimChannel.send({
|
||||
content: `<@&${Staff.id}> <@&${Admin.id}>`,
|
||||
embeds: [claimEmbed],
|
||||
components: [row],
|
||||
});
|
||||
|
||||
const filter = i => i.customId === `claimticket-${ticketChannel.id}` && i.member.roles.cache.has(Staff.id);
|
||||
const collector = claimMessage.createMessageComponentCollector({ filter, time: 120000 });
|
||||
|
||||
collector.on('collect', async i => {
|
||||
console.log(`Ticket claimed by user ${i.user.id}`);
|
||||
await ticketChannel.send({ content: `<@${i.user.id}> has claimed the ticket.` });
|
||||
});
|
||||
|
||||
collector.on('end', async collected => {
|
||||
if (collected.size === 0) {
|
||||
console.log("No one claimed the ticket, notifying admins");
|
||||
await claimMessage.delete();
|
||||
|
||||
await claimChannel.send({
|
||||
content: `No one claimed the ticket. Tagging roles: <@&${Staff.id}> <@&${Admin.id}>`,
|
||||
embeds: [new EmbedBuilder().setDescription(`Ticket is still unclaimed: <#${ticketChannel.id}>`).setColor(supportbot.Embed.Colours.Warn)],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return null;
|
||||
} else {
|
||||
console.error("Claim channel not found");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const clockedInArray = Array.from(clockedInUsers);
|
||||
const assignedUserId = clockedInArray[Math.floor(Math.random() * clockedInArray.length)];
|
||||
const assignedUser = await interaction.guild.members.fetch(assignedUserId);
|
||||
console.log(`Assigned user: ${assignedUserId}`);
|
||||
|
||||
const claimEmbed = new EmbedBuilder()
|
||||
.setTitle(msgconfig.Ticket.ClaimTickets.ClaimTitle || 'Default Claim Title')
|
||||
.setDescription(msgconfig.Ticket.ClaimTickets.ClaimMessage.replace('%user%', interaction.user.id) || 'Default Claim Message')
|
||||
.setColor(supportbot.Embed.Colours.General);
|
||||
|
||||
const claimButton = new ButtonBuilder()
|
||||
.setCustomId(`claimticket-${ticketChannel.id}`)
|
||||
.setLabel(supportbot.Ticket.ClaimTickets.ButtonTitle || 'Claim Ticket')
|
||||
.setStyle(ButtonStyle.Primary);
|
||||
|
||||
const row = new ActionRowBuilder().addComponents(claimButton);
|
||||
|
||||
const claimChannel = await interaction.guild.channels.cache.get(supportbot.Ticket.ClaimTickets.Channel);
|
||||
if (!claimChannel) {
|
||||
console.error("Claim channel not found");
|
||||
return null;
|
||||
}
|
||||
|
||||
const claimMessage = await claimChannel.send({
|
||||
content: `${assignedUser}`,
|
||||
embeds: [claimEmbed],
|
||||
components: [row],
|
||||
});
|
||||
|
||||
const filter = i => i.customId === `claimticket-${ticketChannel.id}` && i.user.id === assignedUserId;
|
||||
const collector = claimMessage.createMessageComponentCollector({ filter, time: 120000 });
|
||||
|
||||
collector.on('collect', async i => {
|
||||
console.log(`Ticket claimed by user ${i.user.id}`);
|
||||
await ticketChannel.send({ content: `<@${assignedUserId}>` });
|
||||
});
|
||||
|
||||
collector.on('end', async collected => {
|
||||
if (collected.size === 0) {
|
||||
console.log("No one claimed the ticket, attempting reassignment");
|
||||
await claimMessage.delete();
|
||||
|
||||
if (attempts >= 1) {
|
||||
await claimChannel.send({
|
||||
content: `No one claimed the ticket. Tagging roles: <@&${Staff.id}> <@&${Admin.id}>`,
|
||||
embeds: [new EmbedBuilder().setDescription(`Ticket is still unclaimed: <#${ticketChannel.id}>`).setColor(supportbot.Embed.Colours.Warn)],
|
||||
});
|
||||
|
||||
// Add a claim button for any staff member to click
|
||||
const claimEmbed = new EmbedBuilder()
|
||||
.setTitle(msgconfig.Ticket.ClaimTickets.ClaimTitle)
|
||||
.setDescription(msgconfig.Ticket.ClaimTickets.ClaimMessage)
|
||||
.setColor(supportbot.Embed.Colours.General);
|
||||
|
||||
const claimButton = new ButtonBuilder()
|
||||
.setCustomId(`claimticket-${ticketChannel.id}`)
|
||||
.setLabel(supportbot.Ticket.ClaimTickets.ButtonTitle)
|
||||
.setEmoji(supportbot.Ticket.ClaimTickets.ButtonEmoji)
|
||||
.setStyle(ButtonStyle.Primary);
|
||||
|
||||
const row = new ActionRowBuilder().addComponents(claimButton);
|
||||
|
||||
const claimMessage = await claimChannel.send({
|
||||
content: `<@&${Staff.id}> <@&${Admin.id}>`,
|
||||
embeds: [claimEmbed],
|
||||
components: [row],
|
||||
});
|
||||
|
||||
const filter = i => i.customId === `claimticket-${ticketChannel.id}` && i.member.roles.cache.has(Staff.id);
|
||||
const collector = claimMessage.createMessageComponentCollector({ filter, time: 120000 });
|
||||
|
||||
collector.on('collect', async i => {
|
||||
console.log(`Ticket ${ticketChannel.id} claimed by user ${i.user.id}`);
|
||||
await ticketChannel.member.add(i.user.id);
|
||||
});
|
||||
|
||||
collector.on('end', async collected => {
|
||||
if (collected.size === 0) {
|
||||
console.log(`No one claimed the ticket (${ticketChannel.id}), notifying all staff`);
|
||||
await claimMessage.delete();
|
||||
|
||||
await claimChannel.send({
|
||||
content: `No one claimed the ticket. Tagging roles: <@&${Staff.id}> <@&${Admin.id}>`,
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const ReassigningEmbed = new EmbedBuilder()
|
||||
.setTitle(msgconfig.Ticket.ClaimTickets.ReassigningTitle)
|
||||
.setDescription(msgconfig.Ticket.ClaimTickets.ReassigningMessage.replace('%user%', assignedUserId))
|
||||
.setColor(supportbot.Embed.Colours.General);
|
||||
|
||||
await claimChannel.send({ embeds: [ReassigningEmbed], components: [] });
|
||||
await assignTicketToUser(ticketChannel, Staff, Admin, interaction, attempts + 1);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return assignedUserId;
|
||||
} catch (error) {
|
||||
console.error("Error in assignTicketToUser function:", error);
|
||||
if (error instanceof CombinedError) {
|
||||
console.error("CombinedError details:", error.errors);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
module.exports = new Command({
|
||||
name: cmdconfig.OpenTicket.Command,
|
||||
description: cmdconfig.OpenTicket.Description,
|
||||
type: ApplicationCommandType.ChatInput,
|
||||
options: [
|
||||
{
|
||||
name: "reason",
|
||||
description: "Ticket Reason",
|
||||
type: ApplicationCommandOptionType.String,
|
||||
},
|
||||
],
|
||||
permissions: cmdconfig.OpenTicket.Permission,
|
||||
|
||||
async run(interaction) {
|
||||
try {
|
||||
let department = interaction.customId?.split("-")[1] || null;
|
||||
let TicketData = JSON.parse(fs.readFileSync("./Data/TicketData.json", "utf8"));
|
||||
const { getRole, getChannel } = interaction.client;
|
||||
let User = interaction.guild.members.cache.get(interaction.user.id);
|
||||
|
||||
if (
|
||||
supportbot.Ticket.TicketsPerUser &&
|
||||
TicketData.tickets.filter((t) => t.user === interaction.user.id && t.open).length >= supportbot.Ticket.TicketsPerUser
|
||||
) {
|
||||
return interaction.reply({
|
||||
embeds: [
|
||||
{
|
||||
title: "Too Many Tickets!",
|
||||
description: `You can't have more than ${supportbot.Ticket.TicketsPerUser} open tickets!`,
|
||||
color: supportbot.Embed.Colours.Warn,
|
||||
},
|
||||
],
|
||||
ephemeral: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (User.roles.cache.has(supportbot.Roles.ModRoles.Blacklisted)) {
|
||||
return interaction.reply({
|
||||
content: msgconfig.Ticket.Blacklisted,
|
||||
ephemeral: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (User.roles.cache.has(supportbot.Roles.ModRoles.Muted)) {
|
||||
return interaction.reply({
|
||||
content: msgconfig.Ticket.Muted,
|
||||
ephemeral: true,
|
||||
});
|
||||
}
|
||||
|
||||
let ticketNumberID = await TicketNumberID.pad();
|
||||
const TicketSubject = interaction.options?.getString("reason") || msgconfig.Ticket.InvalidSubject;
|
||||
|
||||
const TicketExists = new EmbedBuilder()
|
||||
.setTitle("Ticket Exists!")
|
||||
.setDescription(msgconfig.Ticket.TicketExists);
|
||||
|
||||
if (
|
||||
await interaction.guild.channels.cache.find(
|
||||
(ticketChannel) => ticketChannel.name === `${supportbot.Ticket.Channel}${ticketNumberID}`
|
||||
)
|
||||
) {
|
||||
return await interaction.reply({
|
||||
embeds: [TicketExists],
|
||||
ephemeral: true,
|
||||
});
|
||||
}
|
||||
|
||||
const Staff = await getRole(supportbot.Roles.StaffMember.Staff, interaction.guild);
|
||||
const Admin = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild);
|
||||
if (!Staff || !Admin)
|
||||
return interaction.reply({
|
||||
content: "Some roles seem to be missing!\nPlease check for errors when starting the bot.",
|
||||
ephemeral: true,
|
||||
});
|
||||
|
||||
const TicketCategory = interaction.guild.channels.cache.find(
|
||||
(c) => c.name === supportbot.Ticket.TicketCategory || c.id === supportbot.Ticket.TicketCategory
|
||||
);
|
||||
|
||||
const channel = await getChannel(supportbot.Ticket.TicketCategory, interaction.guild);
|
||||
const ticketChannel = await channel.threads.create({
|
||||
name: `${supportbot.Ticket.Channel}${ticketNumberID}`,
|
||||
type: ChannelType.PrivateThread,
|
||||
parent: TicketCategory,
|
||||
autoArchiveDuration: 60,
|
||||
reason: 'support ticket',
|
||||
});
|
||||
|
||||
// Assign ticket to a user if claiming tickets is enabled
|
||||
if (supportbot.Ticket.ClaimTickets.Enabled) {
|
||||
const assignedUserId = await assignTicketToUser(ticketChannel, Staff, Admin, interaction);
|
||||
if (!assignedUserId) {
|
||||
const claimChannel = await interaction.guild.channels.cache.get(supportbot.Ticket.ClaimTickets.Channel);
|
||||
await claimChannel.send(`<@&${Staff.id}> <@&${Admin.id}>`);
|
||||
}
|
||||
}
|
||||
|
||||
// Add user to the ticket channel
|
||||
if (!department) {
|
||||
await ticketChannel.members.add(interaction.user.id);
|
||||
}
|
||||
|
||||
// Save ticket data
|
||||
TicketData.tickets.push({
|
||||
id: ticketChannel.id,
|
||||
name: ticketChannel.name,
|
||||
user: interaction.user.id,
|
||||
number: ticketNumberID,
|
||||
reason: TicketSubject,
|
||||
open: true,
|
||||
subUsers: [],
|
||||
createdAt: new Date().toISOString(),
|
||||
claimedAt: null,
|
||||
});
|
||||
|
||||
fs.writeFileSync(
|
||||
"./Data/TicketData.json",
|
||||
JSON.stringify(TicketData, null, 4),
|
||||
(err) => {
|
||||
if (err) console.error(err);
|
||||
}
|
||||
);
|
||||
|
||||
// Notify the user that the ticket was created
|
||||
const CreatedTicket = new EmbedBuilder()
|
||||
.setDescription(
|
||||
msgconfig.Ticket.TicketCreatedAlert.replace(/%ticketauthor%/g, interaction.user.id)
|
||||
.replace(/%ticketid%/g, ticketChannel.id)
|
||||
.replace(/%ticketusername%/g, interaction.user.username)
|
||||
)
|
||||
.setColor(supportbot.Embed.Colours.General);
|
||||
await interaction.reply({ embeds: [CreatedTicket], ephemeral: true });
|
||||
|
||||
// Create the ticket message
|
||||
const TicketMessage = new EmbedBuilder()
|
||||
.setAuthor({
|
||||
name: msgconfig.Ticket.TicketAuthorTitle.replace(/%ticketauthor%/g, interaction.user.id)
|
||||
.replace(/%ticketid%/g, ticketChannel.id)
|
||||
.replace(/%ticketusername%/g, interaction.user.username),
|
||||
iconURL: interaction.user.displayAvatarURL(),
|
||||
})
|
||||
.setTitle(
|
||||
msgconfig.Ticket.TicketTitle.replace(/%ticketauthor%/g, interaction.user.id)
|
||||
.replace(/%ticketid%/g, ticketChannel.id)
|
||||
.replace(/%ticketusername%/g, interaction.user.username)
|
||||
)
|
||||
.setDescription(
|
||||
msgconfig.Ticket.TicketMessage.replace(/%ticketauthor%/g, interaction.user.id)
|
||||
.replace(/%ticketid%/g, ticketChannel.id)
|
||||
.replace(/%ticketusername%/g, interaction.user.username)
|
||||
)
|
||||
.setColor(supportbot.Embed.Colours.General);
|
||||
|
||||
// Create the control panel
|
||||
const SelectMenus = new StringSelectMenuBuilder()
|
||||
.setCustomId("ticketcontrolpanel")
|
||||
.setPlaceholder("Ticket Control Panel")
|
||||
.addOptions(
|
||||
new StringSelectMenuOptionBuilder()
|
||||
.setLabel("Add User")
|
||||
.setDescription("Add a user to the ticket.")
|
||||
.setEmoji(supportbot.SelectMenus.Tickets.AddUserEmoji)
|
||||
.setValue("ticketadduser"),
|
||||
new StringSelectMenuOptionBuilder()
|
||||
.setLabel("Remove User")
|
||||
.setDescription("Remove a user from the ticket.")
|
||||
.setEmoji(supportbot.SelectMenus.Tickets.RemoveUserEmoji)
|
||||
.setValue("ticketremoveuser"),
|
||||
new StringSelectMenuOptionBuilder()
|
||||
.setLabel("Close")
|
||||
.setDescription("Close the ticket.")
|
||||
.setEmoji(supportbot.SelectMenus.Tickets.CloseEmoji)
|
||||
.setValue("ticketclose"),
|
||||
new StringSelectMenuOptionBuilder()
|
||||
.setLabel("Enable Invites")
|
||||
.setDescription("Enable invites.")
|
||||
.setEmoji(supportbot.SelectMenus.Tickets.EnableInvitesEmoji)
|
||||
.setValue("enableinvites"),
|
||||
new StringSelectMenuOptionBuilder()
|
||||
.setLabel("Disable Invites")
|
||||
.setDescription("Disable invites.")
|
||||
.setEmoji(supportbot.SelectMenus.Tickets.DisableInvitesEmoji)
|
||||
.setValue("disableinvites"),
|
||||
);
|
||||
|
||||
const row2 = new ActionRowBuilder().addComponents(SelectMenus);
|
||||
|
||||
// Send the ticket message and control panel
|
||||
await ticketChannel.send({
|
||||
embeds: [TicketMessage],
|
||||
components: [row2],
|
||||
});
|
||||
|
||||
if (!department) {
|
||||
try {
|
||||
let buttons = supportbot.Departments.map((x) =>
|
||||
new ButtonBuilder()
|
||||
.setCustomId("Department" + supportbot.Departments.indexOf(x))
|
||||
.setLabel(x.title)
|
||||
.setStyle(x.color)
|
||||
.setEmoji(x.emoji)
|
||||
);
|
||||
|
||||
const row = new ActionRowBuilder().addComponents(buttons);
|
||||
|
||||
try {
|
||||
const filter = (i) => i.user.id === interaction.user.id;
|
||||
await m.awaitMessageComponent({
|
||||
filter,
|
||||
max: 1,
|
||||
componentType: "BUTTON",
|
||||
time: supportbot.Ticket.Timeout * 60000,
|
||||
});
|
||||
} catch (e) {
|
||||
if (e.code === "COLLECTOR_ERROR") {
|
||||
try {
|
||||
let ticket = TicketData.tickets.findIndex((t) => t.id === ticketChannel.id);
|
||||
TicketData.tickets[ticket].open = false;
|
||||
fs.writeFileSync(
|
||||
"./Data/TicketData.json",
|
||||
JSON.stringify(TicketData, null, 4),
|
||||
(err) => {
|
||||
if (err) console.error(err);
|
||||
}
|
||||
);
|
||||
interaction.user.send(
|
||||
"Your ticket has timed out. Please open a new one, and select a department."
|
||||
);
|
||||
} catch (error) {return assignedUserId;
|
||||
console.error("An error occurred while handling collector:", error);
|
||||
}
|
||||
return await ticketChannel.delete();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error sending message:", error);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error in run method:", error);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -1,115 +0,0 @@
|
||||
// ticketpanel.js
|
||||
const fs = require("fs");
|
||||
const {
|
||||
EmbedBuilder,
|
||||
ApplicationCommandType,
|
||||
ActionRowBuilder,
|
||||
ButtonBuilder,
|
||||
} = require("discord.js");
|
||||
const yaml = require("js-yaml");
|
||||
|
||||
const panelconfig = yaml.load(fs.readFileSync("./Configs/ticket-panel.yml", "utf8"));
|
||||
const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8"));
|
||||
const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8"));
|
||||
|
||||
const Command = require("../Structures/Command.js");
|
||||
|
||||
module.exports = new Command({
|
||||
name: cmdconfig.TicketPanel.Command,
|
||||
description: cmdconfig.TicketPanel.Description,
|
||||
type: ApplicationCommandType.ChatInput,
|
||||
options: [],
|
||||
permissions: cmdconfig.TicketPanel.Permission,
|
||||
|
||||
async run(interaction) {
|
||||
if (!panelconfig.Panel) {
|
||||
console.log(
|
||||
"\u001b[31m",
|
||||
`[TICKET PANEL]`,
|
||||
"\u001b[33m",
|
||||
"Ticket Panel is not setup, You can set this in", `\u001b[31m`, '/Configs/ticket-panel.yml', `\n `,
|
||||
);
|
||||
return interaction.reply({
|
||||
content: "Ticket Panel is not set up. Please configure it in `/Configs/ticket-panel.yml`.",
|
||||
ephemeral: true,
|
||||
});
|
||||
}
|
||||
|
||||
const channelName = panelconfig.Channel;
|
||||
const channel = interaction.guild.channels.cache.find(ch => ch.name === channelName);
|
||||
if (!channel) {
|
||||
console.log(`[TICKET PANEL] ${channelName} channel not found. Please check your config file.`);
|
||||
return interaction.reply({
|
||||
content: `${channelName} channel not found. Please check your config file.`,
|
||||
ephemeral: true,
|
||||
});
|
||||
}
|
||||
|
||||
const panelid = JSON.parse(fs.readFileSync("./Data/ticket-panel-id.json", "utf8"));
|
||||
|
||||
try {
|
||||
await channel.messages.fetch(panelid.TicketPanelID);
|
||||
return interaction.reply({
|
||||
content: "Ticket panel message already exists.",
|
||||
ephemeral: true,
|
||||
});
|
||||
} catch (error) {
|
||||
let embed = new EmbedBuilder()
|
||||
.setTitle(panelconfig.PanelTitle)
|
||||
.setColor(panelconfig.PanelColour)
|
||||
.setFooter({
|
||||
text: supportbot.Embed.Footer,
|
||||
iconURL: interaction.user.displayAvatarURL(),
|
||||
});
|
||||
|
||||
if (panelconfig.TicketPanel_Description) {
|
||||
embed.setDescription(panelconfig.PanelMessage);
|
||||
}
|
||||
|
||||
if (panelconfig.TicketPanel_Thumbnail) {
|
||||
embed.setThumbnail(panelconfig.PanelThumbnail);
|
||||
}
|
||||
|
||||
if (panelconfig.TicketPanel_Image) {
|
||||
embed.setImage(panelconfig.PanelImage);
|
||||
}
|
||||
|
||||
const createTicketButton = new ButtonBuilder()
|
||||
.setCustomId("createticket")
|
||||
.setLabel(panelconfig.Button.Text)
|
||||
.setEmoji(panelconfig.Button.Emoji)
|
||||
.setStyle(panelconfig.Button.Color);
|
||||
|
||||
let row = new ActionRowBuilder().addComponents(createTicketButton);
|
||||
|
||||
try {
|
||||
const message = await channel.send({
|
||||
embeds: [embed],
|
||||
components: [row],
|
||||
});
|
||||
|
||||
let data = {
|
||||
id: panelid.id,
|
||||
TicketPanelID: message.id,
|
||||
};
|
||||
|
||||
fs.writeFileSync(
|
||||
"./Data/ticket-panel-id.json",
|
||||
JSON.stringify(data),
|
||||
"utf8"
|
||||
);
|
||||
|
||||
return interaction.reply({
|
||||
content: "Ticket panel message has been sent!",
|
||||
ephemeral: true,
|
||||
});
|
||||
} catch (e) {
|
||||
console.log("Error sending message:", e);
|
||||
return interaction.reply({
|
||||
content: "An error occurred while sending the ticket panel message.",
|
||||
ephemeral: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -1,117 +0,0 @@
|
||||
// SupportBot | Emerald Services
|
||||
// Translate Command
|
||||
|
||||
const fs = require("fs");
|
||||
|
||||
const Discord = require("discord.js");
|
||||
const yaml = require("js-yaml");
|
||||
|
||||
const supportbot = yaml.load(
|
||||
fs.readFileSync("./Configs/supportbot.yml", "utf8")
|
||||
);
|
||||
|
||||
const cmdconfig = yaml.load(
|
||||
fs.readFileSync("./Configs/commands.yml", "utf8")
|
||||
);
|
||||
|
||||
const msgconfig = yaml.load(
|
||||
fs.readFileSync("./Configs/messages.yml", "utf8")
|
||||
)
|
||||
|
||||
const Command = require("../Structures/Command.js");
|
||||
const translate = require("@vitalets/google-translate-api");
|
||||
|
||||
module.exports = new Command({
|
||||
name: cmdconfig.Translate.Command,
|
||||
description: cmdconfig.Translate.Description,
|
||||
type: Discord.ApplicationCommandType.ChatInput,
|
||||
options: [
|
||||
{
|
||||
type: Discord.ApplicationCommandOptionType.String,
|
||||
name: "language",
|
||||
description: "A 2 letter language code",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
type: Discord.ApplicationCommandOptionType.String,
|
||||
name: "text",
|
||||
description: "The text to translate",
|
||||
required: true,
|
||||
}, // The input text for the translation
|
||||
],
|
||||
permissions: cmdconfig.Translate.Permission, // The permission the user/role at least requires
|
||||
|
||||
async run(interaction) {
|
||||
let disableCommand = true;
|
||||
|
||||
const { getRole } = interaction.client;
|
||||
let SupportStaff = await getRole(supportbot.Roles.StaffMember.Staff, interaction.guild);
|
||||
let Admin = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild);
|
||||
if (!SupportStaff || !Admin)
|
||||
|
||||
return interaction.reply(
|
||||
"Some roles seem to be missing!\nPlease check for errors when starting the bot."
|
||||
);
|
||||
|
||||
const NoPerms = new Discord.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(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.Log,
|
||||
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."
|
||||
),
|
||||
],
|
||||
ephemeral: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
if (!result) return;
|
||||
|
||||
// Embed containing language code, original text and translated text
|
||||
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
|
||||
await interaction.reply({ embeds: [transembed] });
|
||||
|
||||
// Send embed [transembed] to logging channel
|
||||
await translatelog.send({ embeds: [transembed] });
|
||||
},
|
||||
|
||||
})
|
||||
@@ -1,137 +0,0 @@
|
||||
// SupportBot | Emerald Services
|
||||
// User Info Command
|
||||
|
||||
const fs = require("fs");
|
||||
const moment = require("moment");
|
||||
|
||||
const Discord = require("discord.js");
|
||||
const yaml = require("js-yaml");
|
||||
|
||||
const supportbot = yaml.load(
|
||||
fs.readFileSync("./Configs/supportbot.yml", "utf8")
|
||||
);
|
||||
|
||||
const cmdconfig = yaml.load(
|
||||
fs.readFileSync("./Configs/commands.yml", "utf8")
|
||||
);
|
||||
|
||||
const msgconfig = yaml.load(
|
||||
fs.readFileSync("./Configs/messages.yml", "utf8")
|
||||
)
|
||||
|
||||
const Command = require("../Structures/Command.js");
|
||||
|
||||
module.exports = new Command({
|
||||
name: cmdconfig.UserInfo.Command,
|
||||
description: cmdconfig.UserInfo.Description,
|
||||
type: Discord.ApplicationCommandType.ChatInput,
|
||||
options: [
|
||||
{
|
||||
type: Discord.ApplicationCommandOptionType.User,
|
||||
name: "user",
|
||||
description: "The user you want to get info about.",
|
||||
required: true,
|
||||
}, // The user input
|
||||
],
|
||||
permissions: cmdconfig.UserInfo.Permission,
|
||||
|
||||
async run(interaction) {
|
||||
let disableCommand = true;
|
||||
|
||||
const { getRole } = interaction.client;
|
||||
let SupportStaff = await getRole(supportbot.Roles.StaffMember.Staff, interaction.guild);
|
||||
let Admin = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild);
|
||||
if (!SupportStaff || !Admin)
|
||||
|
||||
return interaction.reply(
|
||||
"Some roles seem to be missing!\nPlease check for errors when starting the bot."
|
||||
);
|
||||
|
||||
const NoPerms = new Discord.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(SupportStaff.id) &&
|
||||
!interaction.member.roles.cache.has(Admin.id)
|
||||
)
|
||||
return interaction.reply({ embeds: [NoPerms] });
|
||||
|
||||
let user = await interaction.options.getUser("user") || interaction.author; // Grab the mentioned user
|
||||
let member = await interaction.guild.members.cache.get(user.id) // Check for user and his ID
|
||||
|
||||
const UserNotExist = new Discord.EmbedBuilder()
|
||||
.setTitle("User Not Found!")
|
||||
.setDescription(
|
||||
`${msgconfig.Error.UserNotFound}\n\nTry Again:\`/${cmdconfig.UserInfoCommand} <@!User_ID> or @User\``
|
||||
)
|
||||
.setColor(supportbot.ErrorColour);
|
||||
|
||||
if (!user) return interaction.reply({ embeds: [UserNotExist] });
|
||||
|
||||
// Flags a user/bot can have
|
||||
const flags = {
|
||||
DISCORD_EMPLOYEE: 'Discord Employee',
|
||||
DISCORD_PARTNER: 'Discord Partner',
|
||||
BUGHUNTER_LEVEL_1: 'Bug Hunter (Level 1)',
|
||||
BUGHUNTER_LEVEL_2: 'Bug Hunter (Level 2)',
|
||||
HYPESQUAD_EVENTS: 'HypeSquad Events',
|
||||
HOUSE_BRAVERY: 'House of Bravery',
|
||||
HOUSE_BRILLIANCE: 'House of Brilliance',
|
||||
HOUSE_BALANCE: 'House of Balance',
|
||||
EARLY_SUPPORTER: 'Early Supporter',
|
||||
TEAM_USER: 'Team User',
|
||||
SYSTEM: 'System',
|
||||
VERIFIED_BOT: 'Verified Bot',
|
||||
VERIFIED_DEVELOPER: 'Verified Bot Developer'
|
||||
};
|
||||
|
||||
// Status message with icon
|
||||
let status = {
|
||||
online: "🟢Online",
|
||||
idle: "🟡Idle",
|
||||
dnd: "🔴Do Not Disturb",
|
||||
offline: "⚫Offline"
|
||||
};
|
||||
|
||||
const userFlags = user.flags.toArray();
|
||||
|
||||
// Embed containing the userinfo
|
||||
let userembed = new Discord.EmbedBuilder()
|
||||
.setTitle(`User Info for User: ${user.tag}`)
|
||||
.setThumbnail(member.displayAvatarURL({ dynamic: true, size: 512 }))
|
||||
.setColor(member.roles.highest.hexColor)
|
||||
.addFields(
|
||||
{ name: "Name:", value: `${user.tag}\n<@${member.id}>`, inline: true },
|
||||
{ name: "ID:", value: `${member.id}`, inline: true },
|
||||
{ name: "Is Bot:", value: user.bot ? "Yes" : "No", inline: true },
|
||||
{ name: "House/Badge:", value: `${userFlags.length ? userFlags.map(flag => flags[flag]).join("\n") : "None"}`, inline: true },
|
||||
{ name: "Current Status:", value: `${status[member.presence ? member.presence.status : "offline"]}`, inline: true },
|
||||
{ name: "Avatar URL:", value: `[Click Here](${member.displayAvatarURL()})`, inline: true },
|
||||
{ name: "\u200b", value: "\u200b", inline: false}
|
||||
)
|
||||
.addFields(
|
||||
{ name: "Creation Date:", value: `${moment(user.createdAt).format("MMMM Do YYYY, h:mm:ss a")}\n> ${moment(user.createdAt).startOf("day").fromNow()}`, inline: true },
|
||||
{ name: "Joined Date:", value: `${moment(member.joinedAt).format("MMMM Do YYYY, h:mm:ss a")}\n> ${moment(member.joinedAt).startOf("day").fromNow()}`, inline: true },
|
||||
{ name: "Roles Total:", value: `${member.roles.cache.size -1}`, inline: false },
|
||||
{ name: "Highest Role:", value: `${member.roles.highest.id === interaction.guild.id ? "None" : member.roles.highest.toString()}`, inline: true },
|
||||
{ name: "Roles:", value: `${member.roles.cache.map(r => r).join(" ").replace("@everyone", " ") || "None"}`, inline: false },
|
||||
)
|
||||
.setFooter({ text: supportbot.Footer })
|
||||
.setTimestamp();
|
||||
|
||||
// Send embed [userembed]
|
||||
await interaction.reply({ embeds: [userembed] }).catch((err) => console.error("An error has occured: ", err));
|
||||
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
* @INFO
|
||||
* UserInfo command by Sypher#3415 | 321332200668790786
|
||||
* @INFO
|
||||
* Solely created for SupportBot | https://github.com/Emerald-Services/SupportBot/
|
||||
**/
|
||||
@@ -0,0 +1,34 @@
|
||||
# -------------------------------
|
||||
# API Server Configuration
|
||||
# -------------------------------
|
||||
# Copy to api.yml and fill in your values (api.yml is not committed with real secrets).
|
||||
|
||||
API:
|
||||
Enabled: true
|
||||
Port: 3000
|
||||
|
||||
SecretKey: "SECRET_KEY"
|
||||
# Set to 1 when the dashboard is behind nginx, Cloudflare, Pterodactyl proxy, etc.
|
||||
# so http/https is detected from X-Forwarded-Proto.
|
||||
|
||||
TrustProxy: false
|
||||
|
||||
EmeraldAPIKey: "EMERALDSRV_API_KEY"
|
||||
|
||||
OAuth:
|
||||
Enabled: true
|
||||
ClientId: "1530910674551574580"
|
||||
ClientSecret: "GIWdonM3bdFILhkg_1XWWeVQRlGf_npu"
|
||||
# Canonical callback URL (host + path). Protocol (http/https) can follow the
|
||||
# browser when UseRequestOrigin is true (default).
|
||||
|
||||
RedirectUri: "http://localhost:3000/api/auth/discord/callback"
|
||||
# Optional: register extra callback URLs in Discord and list them here.
|
||||
# RedirectUris:
|
||||
# - "https://bot.example.com/api/auth/discord/callback"
|
||||
# UseRequestOrigin: true
|
||||
# CookieSecure: false
|
||||
|
||||
OwnerUserIds:
|
||||
- "OWNER_ID"
|
||||
AllowedUserIds: []
|
||||
+103
-16
@@ -7,11 +7,11 @@
|
||||
# SupportBot created by Emerald Services
|
||||
# Installed with MIT License
|
||||
#
|
||||
# Discord Support: https://dsc.gg/emerald-dev
|
||||
# Official Documentation: https://emeraldsrv.dev/wiki
|
||||
# Third-Party Documentation: https://emeraldsrv.dev/third-party
|
||||
# Discord Support: https://discord.gg/emeraldsrv
|
||||
# Official Documentation: https://docs.emeraldsrv.com
|
||||
# Addons Marketplace: https://community.emeraldsrv.com
|
||||
#
|
||||
# Command Configuration
|
||||
# Commands Configuration (commands.yml)
|
||||
|
||||
Help:
|
||||
Command: "help"
|
||||
@@ -19,12 +19,6 @@ Help:
|
||||
Permission: ["SendMessages"]
|
||||
Enabled: true
|
||||
|
||||
EnableAI:
|
||||
Command: "enableai"
|
||||
Description: "Talk with SupportBot"
|
||||
Permission: ["SendMessages"]
|
||||
Enabled: true
|
||||
|
||||
Info:
|
||||
Command: "info"
|
||||
Description: "Get information about the **ServerName**."
|
||||
@@ -37,9 +31,9 @@ TicketPanel:
|
||||
Permission: ["Administrator"]
|
||||
Enabled: true
|
||||
|
||||
TicketStats:
|
||||
Command: "ticketstats"
|
||||
Description: "Shows staff members their ticket statistics"
|
||||
Profile:
|
||||
Command: "profile"
|
||||
Description: "Shows the profile for a discord member."
|
||||
Permission: ["SendMessages"]
|
||||
# Command is automatically disabled when the TicketClaims is disabled.
|
||||
|
||||
@@ -49,9 +43,9 @@ OpenTicket:
|
||||
Permission: ["SendMessages"]
|
||||
Enabled: true
|
||||
|
||||
Add:
|
||||
AddUser:
|
||||
Command: "add"
|
||||
Description: "Add a user to the ticket."
|
||||
Description: "Add a user to a support ticket."
|
||||
Permission: ["SendMessages"]
|
||||
Enabled: true
|
||||
|
||||
@@ -61,12 +55,40 @@ CloseTicket:
|
||||
Permission: ["SendMessages"]
|
||||
Enabled: true
|
||||
|
||||
TicketManage:
|
||||
Command: "ticketmanage"
|
||||
Description: "Manage an existing support ticket."
|
||||
Permission: ["SendMessages"]
|
||||
Enabled: true
|
||||
|
||||
Close:
|
||||
Command: "close"
|
||||
Description: "Close a support ticket."
|
||||
|
||||
ForceAdd:
|
||||
Command: "forceadd"
|
||||
Description: "Add a user to a support ticket by bypassing the invite system."
|
||||
|
||||
ForceRemove:
|
||||
Command: "forceremove"
|
||||
Description: "Remove a user from a support ticket by bypassing the normal restrictions."
|
||||
|
||||
Rename:
|
||||
Command: "rename"
|
||||
Description: "Rename a support ticket by bypassing the normal restrictions."
|
||||
|
||||
Suggestion:
|
||||
Command: "suggest"
|
||||
Description: "Create a suggestion."
|
||||
Permission: ["SendMessages"]
|
||||
Enabled: true
|
||||
|
||||
SuggestionAdmin:
|
||||
Command: "suggestadmin"
|
||||
Description: "Update a suggestion’s status."
|
||||
Permission: ['SendMessages']
|
||||
# Disable "Suggestion" to disable this command.
|
||||
|
||||
Embed:
|
||||
Command: "embed"
|
||||
Description: "Build a discord embed."
|
||||
@@ -77,7 +99,7 @@ Translate:
|
||||
Command: "translate"
|
||||
Description: "Translate something to english"
|
||||
Permission: ["SendMessages"]
|
||||
Enabled: false
|
||||
Enabled: true
|
||||
|
||||
UserInfo:
|
||||
Command: "userinfo"
|
||||
@@ -91,6 +113,66 @@ Ping:
|
||||
Permission: ["SendMessages"]
|
||||
Enabled: true
|
||||
|
||||
# ---------------------------------------
|
||||
# Bot Moderator Commands
|
||||
# ---------------------------------------
|
||||
|
||||
Mod:
|
||||
Command: "mod"
|
||||
Description: "Execute a moderation action."
|
||||
Permission: ["BanMembers", "KickMembers"]
|
||||
Enabled: true
|
||||
|
||||
TicketBlacklist:
|
||||
Command: "ticket-blacklist"
|
||||
Description: "Blacklist a user from creating Tickets."
|
||||
|
||||
Add:
|
||||
Command: "add"
|
||||
Description: "Add a user to the blacklist."
|
||||
|
||||
Remove:
|
||||
Command: "remove"
|
||||
Description: "Remove a user from the blacklist."
|
||||
|
||||
View:
|
||||
Command: "view"
|
||||
Description: "View all blacklisted users."
|
||||
|
||||
# ---------------------------------------
|
||||
# Bot Administrator Commands
|
||||
# ---------------------------------------
|
||||
|
||||
Settings:
|
||||
Command: "settings"
|
||||
Description: "Update bot settings."
|
||||
Permission: ["Administrator"]
|
||||
Enabled: true
|
||||
|
||||
AI:
|
||||
Command: "ai"
|
||||
Description: "Manage SupportBot AI settings and memory."
|
||||
Permission: ["Administrator"]
|
||||
Enabled: true
|
||||
|
||||
Train:
|
||||
Command: "train"
|
||||
Description: "Train the AI by adding knowledge to its database."
|
||||
|
||||
Knowledge:
|
||||
Command: "knowledge"
|
||||
Description: "Manage the AI knowledge base."
|
||||
Clear:
|
||||
Command: "clear"
|
||||
Description: "Clear all trained knowledge from the AI."
|
||||
|
||||
Memory:
|
||||
Command: "memory"
|
||||
Description: "Manage the AI conversational memory."
|
||||
Clear:
|
||||
Command: "clear"
|
||||
Description: "Clear recent conversation memory for this channel."
|
||||
|
||||
# ---------------------------------------
|
||||
# Command Description Configuration
|
||||
# ---------------------------------------
|
||||
@@ -107,3 +189,8 @@ HelpCommandDesc: "Get a list of all the commands."
|
||||
EmbedCommandDesc: "Send a message as an embed."
|
||||
SuggestCommandDesc: "Suggest a feature for the server."
|
||||
UserInfoCommandDesc: "Get info about a user."
|
||||
TicketManageDesc: "Manage an existing support ticket."
|
||||
TicketManageCloseDesc: "Close a support ticket."
|
||||
TicketManageForceAddDesc: "Add a user to a support ticket by bypassing the invite system."
|
||||
TicketManageForceRemoveDesc: "Remove a user from a support ticket by bypassing the normal restrictions."
|
||||
TicketManageForceRenameDesc: "Rename a support ticket by bypassing the normal restrictions."
|
||||
+273
-61
@@ -7,45 +7,108 @@
|
||||
# SupportBot created by Emerald Services
|
||||
# Installed with MIT License
|
||||
#
|
||||
# Discord Support: https://dsc.gg/emerald-dev
|
||||
# Official Documentation: https://emeraldsrv.com/wiki
|
||||
# Third-Party Documentation: https://emeraldsrv.com/third-party
|
||||
# Discord Support: https://discord.gg/emeraldsrv
|
||||
# Official Documentation: https://docs.emeraldsrv.com
|
||||
# Addons Marketplace: https://community.emeraldsrv.com
|
||||
#
|
||||
# Messages Configuration
|
||||
# Messages Configuration (messages.yml)
|
||||
|
||||
# ----------------------------
|
||||
# Info Command
|
||||
# ----------------------------
|
||||
|
||||
Info:
|
||||
Title: "About Us"
|
||||
Description: "SupportBot is an open-sourced support system for discord. It's completely free to use with high amounts of customization to match your own brand."
|
||||
Button: "Visit Site"
|
||||
URL: "https://emeraldsrv.com"
|
||||
Colour: "#f74343"
|
||||
URL: "https://docs.emeraldsrv.com"
|
||||
Colour: "#45b463"
|
||||
|
||||
# ----------------------------
|
||||
# Welcome/Leave Messages
|
||||
# ----------------------------
|
||||
|
||||
Welcome:
|
||||
Embed:
|
||||
Colour: "#f74343"
|
||||
Title: "Welcome to ServerName"
|
||||
Message: "> :smile: %joined_user% has just landed!"
|
||||
Thumbnail: "BOT"
|
||||
Enabled: true
|
||||
LayoutStyle: "modern"
|
||||
# Available Layout Styles
|
||||
# "modern" - For the new Discord v2 components
|
||||
# "classic" - Use the classic Discord look.
|
||||
|
||||
ImageEnabled: false
|
||||
ImageURL: "https://animagelink.png/"
|
||||
Embed:
|
||||
Colour: "#45b463"
|
||||
Title: "Welcome to ServerName"
|
||||
Message: ":smile: %joined_user% has just landed!\n\n🔨 **Server Rules:** <#CHANNEL_ID>"
|
||||
Icon:
|
||||
Mode: "USER" # USER | BOT | CUSTOM
|
||||
CustomURL: "https://example.com/custom.png"
|
||||
Image:
|
||||
Enabled: false
|
||||
URL: "https://example.com/banner.png"
|
||||
Buttons:
|
||||
Enabled: true
|
||||
List:
|
||||
- label: "BuiltByBit"
|
||||
style: "Link"
|
||||
url: "https://your-website.com/how-to-play-java"
|
||||
emoji: "⌨️"
|
||||
- label: "GitHub"
|
||||
style: "Link"
|
||||
url: "https://your-website.com/how-to-play-bedrock"
|
||||
emoji: "👾"
|
||||
- label: "Tebex"
|
||||
style: "Link"
|
||||
url: "https://your-store.tebex.io/"
|
||||
emoji: "🛒"
|
||||
|
||||
Modern:
|
||||
Colour: "#45b463"
|
||||
Title: "**Welcome to ServerName**"
|
||||
Body: ":smile: %joined_user% has just landed!\n\n🔨 **Server Rules:** <#CHANNEL_ID>"
|
||||
Icon:
|
||||
Mode: "BOT" # USER | BOT | CUSTOM
|
||||
CustomURL: "https://example.com/custom-icon.png"
|
||||
Image:
|
||||
Enabled: false
|
||||
URL: "https://example.com/banner.png"
|
||||
Buttons:
|
||||
- label: "BuiltByBit"
|
||||
style: "Link"
|
||||
url: "https://your-website.com/how-to-play-java"
|
||||
emoji: "⌨️"
|
||||
- label: "GitHub"
|
||||
style: "Link"
|
||||
url: "https://your-website.com/how-to-play-bedrock"
|
||||
emoji: "👾"
|
||||
- label: "Tebex"
|
||||
style: "Link"
|
||||
url: "https://your-store.tebex.io/"
|
||||
emoji: "🛒"
|
||||
|
||||
Leave:
|
||||
Embed:
|
||||
Colour: "#f74343"
|
||||
Title: "Goodbye!"
|
||||
Message: "> :slight_frown: %joined_user% has recently dropped out!"
|
||||
Message: ":slight_frown: %left_user% has recently dropped out!"
|
||||
Thumbnail: "BOT"
|
||||
|
||||
ImageEnabled: false
|
||||
ImageURL: "https://animagelink.png/"
|
||||
|
||||
# ----------------------------
|
||||
# Suggestion Messages
|
||||
# ----------------------------
|
||||
|
||||
Suggestions:
|
||||
Sent_Title: "Suggestion Sent!"
|
||||
Sent: "> :white_check_mark: **You have successfully sent a suggestion.**"
|
||||
NoSelfVote: "> :x: **You cannot vote on your own suggestion.**"
|
||||
Sent: ":white_check_mark: **You have successfully sent a suggestion.**"
|
||||
NoSelfVote: ":x: **You cannot vote on your own suggestion.**"
|
||||
RemoveVoteButton: "Remove Vote"
|
||||
|
||||
# ----------------------------
|
||||
# Ticket Messages
|
||||
# ----------------------------
|
||||
|
||||
Ticket:
|
||||
|
||||
# Available Placeholders:
|
||||
@@ -54,68 +117,217 @@ Ticket:
|
||||
# <@%ticketauthor%> - Tags the user who created the ticket.
|
||||
# <#%ticketid%> - Mentions the ticket channel.
|
||||
|
||||
TicketAuthorTitle: "%ticketusername%'s Ticket"
|
||||
TicketTitle: "Support Ticket"
|
||||
TicketMessage: "> Hello <@%ticketauthor%>, Thank you for creating a support ticket\nPlease wait patiently whilst a member of our team reaches out to you."
|
||||
TicketThumbnail: "https://images.emojiterra.com/google/android-pie/512px/1f39f.png"
|
||||
TicketCreatedAlert: "> :ticket: Ticked Created: <#%ticketid%>"
|
||||
TicketExists: "> ❌ Ticket already exists"
|
||||
TicketClaim: "> :white_check_mark: Ticket claimed by" #The user who claimed the ticket will be added to the end of this message
|
||||
TicketUnclaim: "> :white_check_mark: Ticket unclaimed by" #The user who claimed the ticket will be added to the end of this message
|
||||
# You can use Markdown formatting in this message:
|
||||
# # Heading 1
|
||||
# ## Heading 2
|
||||
# ### Heading 3
|
||||
# **Bold** text
|
||||
# *Italic* text
|
||||
# -# Small text
|
||||
# - Bullet points
|
||||
# Links: [text](https://example.com)
|
||||
|
||||
DisableInvites: "> 🔒 Invites have been disabled for this ticket."
|
||||
EnableInvites: "> 🔓 Invites have been enabled for this ticket."
|
||||
TicketAuthorTitle: "-# %ticketusername%'s Ticket"
|
||||
TicketTitle: "## Support Ticket"
|
||||
TicketMessage: "Hello <@%ticketauthor%>, Thank you for creating a support ticket\nPlease wait patiently whilst a member of our team reaches out to you."
|
||||
TicketThumbnail: "https://images.emojiterra.com/google/android-pie/512px/1f39f.png"
|
||||
TicketCreatedAlert: ":ticket: Ticked Created: <#%ticketid%>"
|
||||
TicketExists: "❌ Ticket already exists"
|
||||
TicketClaim: ":white_check_mark: Ticket claimed by" #The user who claimed the ticket will be added to the end of this message
|
||||
TicketUnclaim: ":white_check_mark: Ticket unclaimed by" #The user who claimed the ticket will be added to the end of this message
|
||||
|
||||
TicketArchived: ":white_check_mark: Ticket archived."
|
||||
TicketUnarchived: ":white_check_mark: Ticket unarchived."
|
||||
TicketLocked: ":lock: Ticket is locked."
|
||||
DisableInvites: "🔒 Invites have been disabled for this ticket."
|
||||
EnableInvites: "🔓 Invites have been enabled for this ticket."
|
||||
|
||||
# Available Placeholders:
|
||||
|
||||
# <@%user%> - Tags the user who was added to the ticket.
|
||||
|
||||
AddUserToTicket_Title: "➕ Add User!"
|
||||
AddUserToTicket: "> To add a user to this ticket, Tag them."
|
||||
Blacklisted: ":x: You are not allowed to open a ticket"
|
||||
Muted: ":x: You are `Muted` and therefore not allowed to open any tickets"
|
||||
InvalidSubject: "No Reason Provided!"
|
||||
|
||||
RemovedUserFromTicket_Title: "➖ Removed User!"
|
||||
RemovedUserFromTicket: "> <@%user%> has been `Removed` from this ticket"
|
||||
ConfirmClose: "Please confirm closing the ticket by clicking the button below."
|
||||
CloseTimeout: "Ticket close confirmation timed out."
|
||||
|
||||
Blacklisted: "> :x: You are not allowed to open a ticket"
|
||||
Muted: "> :x: You are `Muted` and therefore not allowed to open any tickets"
|
||||
InvalidSubject: "> No Reason Provided!"
|
||||
|
||||
TranscriptSavedMessage: "📝 **Transcript Saved!**"
|
||||
TicketLockMessage: "❌ **Ticket Locked!**"
|
||||
TicketDeleteMessage: "⚠ **Ticket Deleting!**"
|
||||
|
||||
ClosingTicket: "> :warning: You have requested to close your support ticket."
|
||||
ConfirmClose: "> Please confirm closing the ticket by clicking the button below."
|
||||
CloseTimeout: "> Ticket close confirmation timed out."
|
||||
RenamedSuccess: "🏷️ Ticket renamed successfully!"
|
||||
|
||||
ClaimTickets:
|
||||
ClaimTitle: "Incoming Support Request!"
|
||||
ClaimMessage: "> A new ticket has been created by <@!%user%>. Click the button below to claim this ticket."
|
||||
ClaimMessage_Edit: "> <#%channel%> was claimed by <@!%user%>."
|
||||
ClaimMessage: "A new ticket has been created by <@!%user%>. Click the button below to claim this ticket."
|
||||
ClaimMessage_Edit: "<#%channel%> was claimed by <@!%user%>."
|
||||
|
||||
ClaimedTitle: "Ticket Claimed!"
|
||||
Claimed: "> You have successfully claimed this ticket. <#%channel%>"
|
||||
Claimed: "You have successfully claimed this ticket. <#%channel%>"
|
||||
NoPermsToClaimTitle: "Error, no permissions!"
|
||||
NoPermsToClaim: "> :x: **Err!** You do not permissions to claim this ticket."
|
||||
NoPermsToClaim: ":x: **Err!** You do not permissions to claim this ticket."
|
||||
|
||||
ReassigningTitle: "Reassigning!..."
|
||||
ReassigningMessage: "> <@%user%> did not claim the ticket. **Reassigning...**"
|
||||
ReassigningMessage: "<@%user%> did not claim the ticket. **Reassigning...**"
|
||||
|
||||
TicketStats:
|
||||
OpenTickets: "✅ Open Tickets"
|
||||
TotalTickets: "🎫 Total Tickets"
|
||||
ResponseTime: "⏱️ Average Response Time"
|
||||
TicketQuestions:
|
||||
Details:
|
||||
Title: "Ticket Details"
|
||||
Description: "Questions by <@%user%>" # Use <@%user%> to display the username.
|
||||
|
||||
Title: "Your Ticket Stats"
|
||||
AIMode:
|
||||
Title: "SupportBot AI"
|
||||
Description: "Use the buttons below to enable or disable AI chat in this ticket."
|
||||
EnabledMessage: "AI Mode has been enabled for this ticket."
|
||||
DisabledMessage: "AI Mode has been disabled for this ticket."
|
||||
|
||||
ThreadWelcomeTitle: "SupportBot AI"
|
||||
ThreadWelcomeMessage: "Hi! I’m SupportBot AI. You can chat with me here about your issue."
|
||||
|
||||
EnableButton: "Enable AI Mode"
|
||||
DisableButton: "Disable AI Mode"
|
||||
|
||||
TicketNotFound: "Ticket not found."
|
||||
EnableError: "There was an error enabling SupportBot AI."
|
||||
DisableError: "There was an error disabling SupportBot AI."
|
||||
|
||||
# ----------------------------
|
||||
# Review Messages
|
||||
# ----------------------------
|
||||
|
||||
ReviewSystem:
|
||||
Rate:
|
||||
Title: "Review us!"
|
||||
Description: "Please rate our support for this ticket (1-5 stars)."
|
||||
|
||||
Stars:
|
||||
One: "⭐ 1 Star"
|
||||
Two: "⭐⭐ 2 Stars"
|
||||
Three: "⭐⭐⭐ 3 Stars"
|
||||
Four: "⭐⭐⭐⭐ 4 Stars"
|
||||
Five: "⭐⭐⭐⭐⭐ 5 Stars"
|
||||
|
||||
Comment:
|
||||
Title: "Write a comment."
|
||||
Description: "Write a comment about your experience."
|
||||
|
||||
ReviewEmbed:
|
||||
Title: "Ticket Review"
|
||||
CommentTitle: "Comment"
|
||||
RatingTitle: "Rating"
|
||||
ReviewedStaffTitle: "Reviewed Staff:"
|
||||
ReviewedByTitle: "Reviewed By:"
|
||||
ReviewEmoji: "⭐"
|
||||
Color: "#f7a943"
|
||||
|
||||
# ----------------------------
|
||||
# Mod Command Messages
|
||||
# ----------------------------
|
||||
|
||||
Mod:
|
||||
|
||||
TicketBlacklist:
|
||||
Add:
|
||||
Description: "Add a user to the ticket blacklist"
|
||||
UserDescription: "User to blacklist"
|
||||
AlreadyBlacklisted: ":x: {userTag} is already blacklisted."
|
||||
Success: ":white_check_mark: {userTag} has been added to the blacklist. They can no longer use the ticket system."
|
||||
|
||||
Remove:
|
||||
Description: "Remove a user from the ticket blacklist"
|
||||
UserDescription: "User to remove from blacklist"
|
||||
NotBlacklisted: ":x: {userTag} is not blacklisted."
|
||||
Success: ":white_check_mark: {userTag} has been removed from the blacklist. They can now use the ticket system again."
|
||||
|
||||
View:
|
||||
Description: "View the list of all blacklisted users"
|
||||
NoBlacklistedUsers: ":information_source: There are currently no users on the blacklist."
|
||||
|
||||
|
||||
# ----------------------------
|
||||
# Logging Messages
|
||||
# ----------------------------
|
||||
|
||||
TicketLog:
|
||||
Title: "Ticket Log"
|
||||
Colour: "#0cc559"
|
||||
|
||||
TicketBlacklistLog:
|
||||
Title: "Ticket Blacklist Log"
|
||||
Colour: "#0cc559"
|
||||
|
||||
# ----------------------------
|
||||
# Misc Messages
|
||||
# ----------------------------
|
||||
|
||||
AddUser:
|
||||
NotInTicket_Title: "Ticket Invitation"
|
||||
NotInTicket_Description: "This command can only be used within a ticket thread."
|
||||
|
||||
Invite_Title: "User Invited"
|
||||
Invite_Description: "%username% has been invited to join the ticket."
|
||||
|
||||
DM_Title: "Ticket Invitation"
|
||||
DM_Description: "You have been invited to join the ticket %channelname%. Do you accept?"
|
||||
|
||||
Accept_Label: "Accept"
|
||||
Reject_Label: "Reject"
|
||||
ViewTicket_Label: "Go to Ticket"
|
||||
|
||||
Accepted_Title: "Invitation Accepted"
|
||||
Accepted_Message: "You have been added to the ticket."
|
||||
AddedToTicket_Title: "Added to Ticket"
|
||||
AddedToTicket_Description: "You have been added to the ticket %channel_link%."
|
||||
|
||||
# Added Message for inside the ticket, this informs everyone inside the ticket that the user has been added.
|
||||
Added_Title: "User Added"
|
||||
Added_Description: "Successfully added **%username%** to the ticket."
|
||||
|
||||
Declined_Title: "Invitation Declined"
|
||||
Declined_Message: "You have declined the invitation."
|
||||
|
||||
NoResponse_Title: "No Response"
|
||||
NoResponse_Message: "You did not respond in time. Please contact staff if you wish to join the ticket."
|
||||
|
||||
Error_Title: "Error"
|
||||
Error_Adding: "There was an error adding you to the ticket. Please try again."
|
||||
|
||||
DM_Error_Title: "Error"
|
||||
DM_Error_Description: "Unable to send a DM to %username%. Please ensure your DMs are enabled."
|
||||
|
||||
Unexpected_Error_Title: "Error"
|
||||
Unexpected_Error_Description: "An unexpected error occurred: %error%"
|
||||
|
||||
RemoveUser:
|
||||
NotInTicket_Title: "Error"
|
||||
NotInTicket_Description: "This command can only be used in ticket threads."
|
||||
|
||||
Removed_Title: "User Removed"
|
||||
Removed_Message: "The user has been successfully removed from the ticket."
|
||||
|
||||
RemovedFromTicket_Title: "Removed from Ticket"
|
||||
RemovedFromTicket_Description: "You have been removed from the ticket channel %channel_link%."
|
||||
|
||||
Error_Title: "Error"
|
||||
Error_Removing: "There was an error removing the user from the ticket."
|
||||
|
||||
ForceAddUser:
|
||||
NotInTicket_Title: "Not a Ticket"
|
||||
NotInTicket_Description: "This command can only be used within a ticket thread."
|
||||
|
||||
Added_Title: "User Added"
|
||||
Added_Description: "Successfully added %username% to the ticket."
|
||||
|
||||
AddedToTicket_Title: "You have been added to a Ticket"
|
||||
AddedToTicket_Description: "You have been added to the ticket channel [%channel_link%]."
|
||||
|
||||
Error_Title: "Error"
|
||||
Error_Adding: "There was an error adding the user to the ticket."
|
||||
|
||||
Error:
|
||||
IncorrectPerms: "> :x: **Err!** You do not have the correct permissions to use this."
|
||||
InvalidChannel: "> :x: **Err!** Channel not found. Please create it!"
|
||||
UserNotFound: "> :x: **Err!** This user doesn't exist, Are they in this server?"
|
||||
NoValidTicket: "> :warning: You cannot execute this command here."
|
||||
TicketExists: "> :warning: This ticket cannot be opened, It already exists!"
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
IncorrectPermissions: ":x: **Error!** You do not have the correct permissions to use this command."
|
||||
MissingChannel: ":x: **Error!** Required channel not found. Please ensure it is set up correctly!"
|
||||
UserNotFound: ":x: **Error!** Specified user not found. Please check if they are in this server."
|
||||
CommandNotAllowedHere: ":warning: This command cannot be used in this channel."
|
||||
EntryAlreadyExists: ":warning: Cannot complete the action, as the entry already exists."
|
||||
EntryDoesNotExist: ":warning: Cannot complete the action, as the entry does not exist."
|
||||
ActionFailed: ":x: **Error!** An error occurred while processing your request. Please try again later."
|
||||
InvalidChannel: ":x: **Error!** The suggestions channel has not been set up correctly."
|
||||
+58
-22
@@ -1,40 +1,76 @@
|
||||
# -------------------------
|
||||
# SupportBot AI
|
||||
#
|
||||
# In order to use the SupportBot AI, You need to setup an OpenAI Account.
|
||||
# this can be done at https://platform.openai.com/
|
||||
# In order to use the SupportBot AI, You need to setup an OpenAI or Groq Developer Account.
|
||||
# this can be done at https://platform.openai.com/ or https://groq.com/
|
||||
# -------------------------
|
||||
|
||||
Enabled: true
|
||||
|
||||
General:
|
||||
Provider: "claude"
|
||||
Model: "claude-sonnet-4-6"
|
||||
Model_API_Key: "MODEL_API_KEY"
|
||||
# Providers Available:
|
||||
# "openai" - Use OpenAI's GPT models. Example: "gpt-5.4" "gpt-5-mini"
|
||||
# "groq" - Use Groq's GroqAI models. Example: "meta-llama/llama-4-scout-17b-16e-instruct"
|
||||
# "claude" - Use Anthropic's Claude models. Example: "claude-sonnet-4-6"
|
||||
|
||||
Tokens: 1200
|
||||
|
||||
Temperature: 0.4
|
||||
Name: "SupportBot AI"
|
||||
# What would you like to call your AI
|
||||
Model: "gpt-4o"
|
||||
# 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/
|
||||
PastebinAPI_Key: "PASTEBIN_API_KEY"
|
||||
PastebinAPI_URL: "https://pastebin.com/api/api_post.php"
|
||||
|
||||
OpenAI_Key: "OPEN AI KEY"
|
||||
# To create an OpenAI API key, go to https://platform.openai.com/api-keys
|
||||
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.
|
||||
|
||||
|
||||
PastebinAPI_Key: "PASTEBIN KEY"
|
||||
# You can get your Pastebin API Key from https://pastebin.com
|
||||
PastebinAPI_URL: "https://pastebin.com/api/api_post.php"
|
||||
Features:
|
||||
SuggestTickets: true # Should SupportBot AI suggest creating a ticket if the user needs more help?
|
||||
|
||||
Channels:
|
||||
AIChannel: "1250190338954498048"
|
||||
# This is the channel where AI will respond in without needing to mention the bot.
|
||||
AIChannel: "CHANNEL_ID"
|
||||
|
||||
Embed:
|
||||
Color: 0x0cc559 # 0x<hexcode>
|
||||
Footer: "SupportBot | Developed by Emerald Development"
|
||||
Color: "#45b463"
|
||||
Footer: "SupportBot AI"
|
||||
|
||||
Memory:
|
||||
Enabled: true
|
||||
StoreFacts: true
|
||||
# StoreFacts - Enables saving important facts the AI learns.
|
||||
|
||||
StoreSummaries: true
|
||||
# StoreSummaries - Enables saving conversation summaries.
|
||||
|
||||
MaxRecentMessages: 15
|
||||
# MaxRecentMessages - Number of recent messages used for context.
|
||||
|
||||
Scope: "guild" # channel | guild
|
||||
# Available Scopes for Memory:
|
||||
# "channel" - Memory is per channel only.
|
||||
# "guild" - Memory is shared across the server.
|
||||
|
||||
CustomInstructions:
|
||||
- "You are SupportBot AI made by Emerald Services"
|
||||
- "You are built with DiscordJS/NodeJS and currently running on dev build 26.0 build 2"
|
||||
- "You're current build can be found here https://github.com/Emerald-Services/SupportBot/tree/26.0"
|
||||
- "You're main build can be found here https://builtbybit.com/resources/supportbot-free-advanced-ticket-bot.8940/ and Polymart at https://polymart.org/product/518/supportbot-1-discord-ticket-bot which is version 8.4"
|
||||
- "dev build 26.0 build 2 includes all-new AI features for SupportBot such as memory system that allows you to remember facts and summaries of conversations, which can be used to provide better responses."
|
||||
- "SupportBot AI also have a new tickets AI Mode which is enabled in supportbot.ymll, which allows you to assist users in support tickets and provide better responses based on the context of the ticket."
|
||||
- "Your main purpose is to assist users in the support server and provide helpful responses to their questions and issues. Always be accurate and if you do not have answer then simply ask the person to create a ticket if they need more help."
|
||||
#- "Add custom instructions here"
|
||||
|
||||
Messages:
|
||||
MessageTooLong: "The response was too long, so I've posted it here: {pastebin}"
|
||||
ErrorResponse: "I didn't receive a proper response from OpenAI. Please try again later."
|
||||
OpenAIError: "I'm having some trouble talking to SupportBot AI's Model, OpenAI. Please try again later."
|
||||
AIDisabled: "I am disabled, You have enable me in `supportbot-ai.yml`"
|
||||
ErrorResponse: "I couldn't generate a response."
|
||||
AIError: "There was an error contacting the AI provider."
|
||||
AIDisabled: "AI is currently disabled."
|
||||
MessageTooLong: "My response was too long, so I uploaded it here: {pastebin}"
|
||||
SuggestTicketText: "-# Need more help? React with 🎫 to open a ticket."
|
||||
SuggestTicketReaction : "🎫"
|
||||
+230
-81
@@ -4,15 +4,15 @@
|
||||
# <___/`___|| _/| _/\___/|_| |_| |___/\___/ |_|
|
||||
# |_| |_|
|
||||
#
|
||||
# SupportBot created by Emerald Development
|
||||
# SupportBot created by Emerald Services
|
||||
# Installed with MIT License
|
||||
#
|
||||
# Discord Support: https://dsc.gg/emerald-dev
|
||||
# Official Documentation: https://github.com/Emerald-Services/SupportBot/wiki
|
||||
# Third-Party Documentation: COMING SOON
|
||||
# Discord Support: https://discord.gg/emeraldsrv
|
||||
# Website: https://emeraldsrv.dev
|
||||
#
|
||||
# SupportBot Configuration (supportbot.yml)
|
||||
|
||||
SupportBot_Version: "v8.0.1"
|
||||
# Do not change the version number
|
||||
SupportBot_Version: "v26.2-pre1"
|
||||
|
||||
# -------------------------
|
||||
# General
|
||||
@@ -20,71 +20,81 @@ SupportBot_Version: "v8.0.1"
|
||||
|
||||
General:
|
||||
Name: "SupportBot"
|
||||
Token: "BOT TOKEN HERE"
|
||||
# To get your Discord Bot Token, Go to https://discord.com/developers/applications
|
||||
|
||||
Token: "BOT_TOKEN"
|
||||
Addons:
|
||||
Enabled: true
|
||||
|
||||
GuildId: "GUILD_ID"
|
||||
|
||||
# -------------------------
|
||||
# Activity
|
||||
# -------------------------
|
||||
|
||||
Activity:
|
||||
Status: "SupportBot v8"
|
||||
Type: "Competing"
|
||||
# The Type can either be Playing, Watching, Listening or Competing
|
||||
Status: "SupportBot"
|
||||
Type: "Watching"
|
||||
StreamingURL: "https://www.twitch.tv/YOUR_TWITCH_NAME"
|
||||
# Set The Type to "Streaming" for a streaming status!
|
||||
# Available Types:
|
||||
# Playing
|
||||
# Watching
|
||||
# Listening
|
||||
# Competing
|
||||
# Streaming (URL required)
|
||||
|
||||
# -------------------------
|
||||
# Embeds
|
||||
# -------------------------
|
||||
# You can also update the activity via Discord using: /settings
|
||||
|
||||
# -------------------------
|
||||
# Embeds
|
||||
# -------------------------
|
||||
|
||||
Embed:
|
||||
Colours:
|
||||
General: 0x0cc559 # 0x<hexcode>
|
||||
Success: 0x04b545 # 0x<hexcode>
|
||||
Error: 0xd13030 # 0x<hexcode>
|
||||
Warn: 0xfca117 # 0x<hexcode>
|
||||
General: "#0cc559"
|
||||
Success: "#04b545"
|
||||
Error: "#d13030"
|
||||
Warn: "#fca117"
|
||||
|
||||
Footer: "SupportBot | Developed by Emerald Development"
|
||||
# Use 0x<hexcode> to change the colour of the embeds.
|
||||
|
||||
# -------------------------
|
||||
# Roles
|
||||
# -------------------------
|
||||
# -------------------------
|
||||
# Roles
|
||||
# -------------------------
|
||||
|
||||
Roles:
|
||||
AutoRole:
|
||||
Enabled: true
|
||||
Role: "Member"
|
||||
Role: "ROLE_ID"
|
||||
|
||||
StaffMember:
|
||||
Staff: "Staff"
|
||||
Admin: "Admin"
|
||||
Staff: "ROLE_ID"
|
||||
Admin: "ROLE_ID"
|
||||
Moderator: "ROLE_ID"
|
||||
|
||||
ModRoles:
|
||||
Muted: "Muted"
|
||||
Blacklisted: "Blacklisted"
|
||||
Mod:
|
||||
AllowSupportStaff: false
|
||||
# Allow Support Staff to use the /mod command. (More in detail customisation in commands.yml)
|
||||
|
||||
# ----------------------------
|
||||
# System Messages
|
||||
# ----------------------------
|
||||
# ----------------------------
|
||||
# System Messages
|
||||
# ----------------------------
|
||||
|
||||
Welcome:
|
||||
Enabled: true
|
||||
Channel: "🚨┃join-leave-logs"
|
||||
Welcome:
|
||||
Enabled: false
|
||||
Channel: "CHANNEL_ID"
|
||||
|
||||
Leave:
|
||||
Enabled: true
|
||||
Channel: "🚨┃join-leave-logs"
|
||||
Leave:
|
||||
Enabled: false
|
||||
Channel: "CHANNEL_ID"
|
||||
|
||||
# ----------------------------
|
||||
# Suggestions
|
||||
# ----------------------------
|
||||
|
||||
Suggestions:
|
||||
Channel: "📗┃suggestions"
|
||||
Mode: "Channel" # Channel or Forum
|
||||
ForumChannel: "FORUM_CHANNEL_ID" # A forum channel, disabled if mode is set to Channel.
|
||||
Channel: "CHANNEL_ID" # A text channel, disabled if mode is set to Forum.
|
||||
|
||||
UpvoteEmoji: "✅"
|
||||
DownvoteEmoji: "❌"
|
||||
|
||||
@@ -92,11 +102,12 @@ Suggestions:
|
||||
DownvoteTitle: "No"
|
||||
|
||||
OwnSuggestion: true
|
||||
|
||||
# If false this will not allow the user to vote for a suggestion they created
|
||||
# If true this will allow the user to vote for a suggestion they created
|
||||
|
||||
ShowUsers: true
|
||||
|
||||
|
||||
Threads:
|
||||
Enabled: true
|
||||
Title: "Suggestion-releated Thread"
|
||||
@@ -107,114 +118,252 @@ Suggestions:
|
||||
Downvote: "2"
|
||||
RemoveVote: "4"
|
||||
RemoveVote_Title: "Remove Vote"
|
||||
|
||||
# Available Button Styles
|
||||
# 1 - Blurple
|
||||
# 2 - Grey
|
||||
# 3 - Green
|
||||
# 4 - Red
|
||||
|
||||
Tags:
|
||||
Created: "TAG_ID"
|
||||
Considering: "TAG_ID"
|
||||
Solved: "TAG_ID"
|
||||
Denied: "TAG_ID"
|
||||
|
||||
# -------------------------
|
||||
# Tickets
|
||||
# -------------------------
|
||||
|
||||
VoiceTickets:
|
||||
Name: "%username%'s VC"
|
||||
Category: "CATEGORY_ID"
|
||||
|
||||
Ticket:
|
||||
TicketCategory: "📗┃tickets" #Where tickets normally go with the /ticket command
|
||||
# This is the channel name for the ticket threads to be created, NOT A DISCORD CATEGORY!
|
||||
TicketType: "channels"
|
||||
|
||||
# Available TicketTypes:
|
||||
# channels - Tickets will be created as channels in the TicketChannelsCategory
|
||||
# threads - Tickets will be created as threads through the "TicketHome" channel.
|
||||
|
||||
UserManagement:
|
||||
DMOnAdd: true # DM the user when they are added to the ticket
|
||||
DMOnRemove: true # DM the user when they are removed from the ticket
|
||||
DMTranscript: true # DM the user a transcript of the ticket when it is closed
|
||||
|
||||
TicketHome: "CHANNEL_ID"
|
||||
|
||||
# What is TicketHome?
|
||||
# - Channel where the Ticket creation panel is sent.
|
||||
# - Channel where ticket threads are made. (only if ticket threads is enabled above.)
|
||||
|
||||
# For TicketType: channels
|
||||
TicketChannelsCategory: "CATEGORY_ID"
|
||||
TicketChannelsCategory2: "CATEGORY_ID" # This will be used if the first category has reached the max channel limit.
|
||||
TicketArchiveCategory: "CATEGORY_ID"
|
||||
# ---
|
||||
|
||||
Channel: "ticket-" # The prefix for ticket channel names
|
||||
TicketsPerUser: 100
|
||||
DMTranscripts: true
|
||||
TicketReason: true
|
||||
# If ticket reason is set to false, It will not prompt to create a reason
|
||||
|
||||
TicketSubject: "description"
|
||||
|
||||
# Available TicketSubjects:
|
||||
# "description" will display the ticket reason within the channel description
|
||||
# "embed" will display the ticket reason within the ticket embed
|
||||
|
||||
Timeout: 5
|
||||
# Set to true to allow all staff to access the ticket
|
||||
|
||||
Log:
|
||||
TicketLog: "🚨┃ticket-data"
|
||||
TicketLog_Title: "Ticket Closed"
|
||||
DisableTicketLogChannel: false
|
||||
|
||||
TranscriptLog: "🚨┃ticket-data"
|
||||
TranscriptTitle: "Ticket Transcript"
|
||||
|
||||
Close:
|
||||
StaffOnly: true
|
||||
# Set to false is anyone is able to close a ticket.
|
||||
Confirmation_Button: "Confirm Close"
|
||||
Title: "Are you sure you want to close this ticket?"
|
||||
|
||||
Invites:
|
||||
# Invites is for ticket threads.
|
||||
StaffOnly: true
|
||||
|
||||
ClaimTickets:
|
||||
Enabled: true
|
||||
Channel: "1249723473757077624"
|
||||
# Channel name or Channel ID
|
||||
|
||||
Enabled: false
|
||||
Channel: "CHANNEL_ID"
|
||||
# Channel where the ticket assignments are sent for staff to claim.
|
||||
|
||||
ClaimAcceptTime: 120000
|
||||
# How long the assigned user has to accept the ticket before it gets re-assigned to someone else.
|
||||
# 120000 = 2 Minutes
|
||||
# 180000 = 3 Minutes
|
||||
# 240000 = 4 Minutes
|
||||
# 300000 = 5 Minutes
|
||||
# Time in Mliseconds
|
||||
|
||||
ButtonTitle: "Claim Ticket"
|
||||
ButtonEmoji: "🎫"
|
||||
Button: "1"
|
||||
|
||||
# Available Button Styles
|
||||
# 1 - Blurple
|
||||
# 2 - Grey
|
||||
# 3 - Green
|
||||
# 4 - Red
|
||||
|
||||
ReviewSystem:
|
||||
Enabled: false
|
||||
Channel: "CHANNEL_ID"
|
||||
UseModal: true
|
||||
|
||||
# -------------------------
|
||||
# Departments - NOT IN THIS UPDATE
|
||||
# -------------------------
|
||||
Questions:
|
||||
# Default questions asked when a ticket is created, these are used if departments are disabled.
|
||||
Enabled: false
|
||||
List:
|
||||
- "Please provide your account email?"
|
||||
- "What is the nature of your issue?"
|
||||
- "Have you tried resolving the issue yourself?"
|
||||
|
||||
Departments:
|
||||
- title: "Create Ticket"
|
||||
role: "Staff"
|
||||
color: "2"
|
||||
emoji: "🎫"
|
||||
category: "TICKETS"
|
||||
Log:
|
||||
TicketDataLog: "CHANNEL_ID"
|
||||
TickeDataTitle: "Ticket Data"
|
||||
TicketBlacklistLog: "CHANNEL_ID"
|
||||
|
||||
AIMode:
|
||||
Enabled: false
|
||||
ThreadName: "Chat with SupportBot AI"
|
||||
WelcomeMessage: "Hi! I’m SupportBot AI. You can chat with me here about your issue."
|
||||
|
||||
# -------------------------
|
||||
# Ticket Departments
|
||||
# -------------------------
|
||||
|
||||
DepartmentSystem:
|
||||
Enabled: false
|
||||
Placeholder: "Choose a ticket department"
|
||||
DefaultDepartment: "general"
|
||||
|
||||
Departments:
|
||||
general:
|
||||
Name: "General Support"
|
||||
Description: "Open a general support ticket"
|
||||
Emoji: "🎫"
|
||||
Category: "CATEGORY_ID"
|
||||
Role: "ROLE_ID"
|
||||
ChannelPrefix: "support-"
|
||||
DefaultPriority: "normal"
|
||||
Questions:
|
||||
- "What do you need help with?"
|
||||
- "When did the issue start?"
|
||||
|
||||
purchase:
|
||||
Name: "Purchase Support"
|
||||
Description: "Get help with store purchases"
|
||||
Emoji: "💳"
|
||||
Category: "CATEGORY_ID"
|
||||
Role: "ROLE_ID"
|
||||
ChannelPrefix: "purchase-"
|
||||
DefaultPriority: "high"
|
||||
Questions:
|
||||
- "What is your order ID?"
|
||||
- "What product did you purchase?"
|
||||
- "What payment issue are you having?"
|
||||
|
||||
reports:
|
||||
Name: "Player Report"
|
||||
Description: "Report a player for breaking the rules"
|
||||
Emoji: "🚨"
|
||||
Category: "CATEGORY_ID"
|
||||
Role: "ROLE_ID"
|
||||
ChannelPrefix: "report-"
|
||||
DefaultPriority: "normal"
|
||||
Questions:
|
||||
- "Who are you reporting?"
|
||||
- "What happened?"
|
||||
- "Do you have evidence?"
|
||||
|
||||
appeals:
|
||||
Name: "Ban Appeals"
|
||||
Description: "Appeal a server punishment"
|
||||
Emoji: "⚖️"
|
||||
Category: "CATEGORY_ID"
|
||||
Role: "ROLE_ID"
|
||||
ChannelPrefix: "appeals-"
|
||||
DefaultPriority: "low"
|
||||
Questions:
|
||||
- "What punishment are you appealing?"
|
||||
- "Why should it be reviewed?"
|
||||
- "Anything else staff should know?"
|
||||
|
||||
PrioritySystem:
|
||||
Enabled: false
|
||||
DefaultPriority: "Normal"
|
||||
AllowUsersToChooseOnOpen: true
|
||||
Priorities:
|
||||
low:
|
||||
Name: "Low"
|
||||
Emoji: "🟢"
|
||||
normal:
|
||||
Name: "Normal"
|
||||
Emoji: "🟡"
|
||||
high:
|
||||
Name: "High"
|
||||
Emoji: "🟠"
|
||||
urgent:
|
||||
Name: "Urgent"
|
||||
Emoji: "🔴"
|
||||
|
||||
# -------------------------
|
||||
# Buttons
|
||||
# -------------------------
|
||||
Buttons:
|
||||
General:
|
||||
Delete: "🗑️"
|
||||
DeleteStyle: "4"
|
||||
|
||||
Voice:
|
||||
TicketDeleteText: "Delete Voice Channel"
|
||||
|
||||
Tickets:
|
||||
ClaimEmoji: "🧑💻"
|
||||
ClaimStyle: "1"
|
||||
|
||||
Unarchive: "Unarchive Ticket"
|
||||
Unarchive_Emoji: "♻️"
|
||||
Unarchive_Style: "2"
|
||||
|
||||
Unlock: "Unlock Ticket"
|
||||
Unlock_Emoji: "🔓"
|
||||
Unlock_Style: "2"
|
||||
# Available Button Styles
|
||||
# 1 - Blurple
|
||||
# 2 - Grey
|
||||
# 3 - Green
|
||||
# 4 - Red
|
||||
|
||||
|
||||
# -------------------------
|
||||
# Select Menus
|
||||
# -------------------------
|
||||
# -------------------------
|
||||
# Select Menus
|
||||
# -------------------------
|
||||
|
||||
SelectMenus:
|
||||
Tickets:
|
||||
PanelEmoji: "⚙️"
|
||||
|
||||
CloseEmoji: "🗑️"
|
||||
ArchiveEmoji: "♻️"
|
||||
LockTicketEmoji: "🔒"
|
||||
UnLockTicketEmoji: "🔓"
|
||||
EnableInvitesEmoji: "🔒"
|
||||
DisableInvitesEmoji: "🔓"
|
||||
AddUserEmoji: "➕"
|
||||
RemoveUserEmoji: "➖"
|
||||
LockEmoji: "🔐"
|
||||
RenameEmoji: "🏷️"
|
||||
VCEmoji: "🔊"
|
||||
|
||||
EnableInvitesEmoji: "📤"
|
||||
DisableInvitesEmoji: "📥"
|
||||
|
||||
# -------------------------
|
||||
# Misc
|
||||
# Logging
|
||||
# -------------------------
|
||||
|
||||
MessageDelete:
|
||||
Channel: "CHANNEL_ID"
|
||||
Colour: "#d92525"
|
||||
|
||||
MessageUpdate:
|
||||
Channel: "CHANNEL_ID"
|
||||
Colour: "#f2a024"
|
||||
|
||||
Translate:
|
||||
Log: "🚨┃logs"
|
||||
TranslateLog: "CHANNEL_ID"
|
||||
+37
-16
@@ -7,8 +7,11 @@
|
||||
# SupportBot created by Emerald Services
|
||||
# Installed with MIT License
|
||||
#
|
||||
# Discord Support: https://emeraldsrv.dev/discord
|
||||
# Community Resources: https://emeraldsrv.dev/resources
|
||||
# Discord Support: https://discord.gg/emeraldsrv
|
||||
# Official Documentation: https://docs.emeraldsrv.com
|
||||
# Addons Marketplace: https://community.emeraldsrv.com
|
||||
#
|
||||
# Ticket Creation Panel Configuration (ticket-panel.yml)
|
||||
|
||||
# -------------------------------
|
||||
# Ticket Panel Configuration
|
||||
@@ -16,20 +19,25 @@
|
||||
|
||||
Panel: true
|
||||
|
||||
# ----------------------------------------
|
||||
# Ticket Panel Message Configuration #2b2d31
|
||||
# ----------------------------------------
|
||||
Style:
|
||||
Title: "## SupportBot Ticket Creation"
|
||||
Description: "Click on the button to file a `Ticket` and our team will try their
|
||||
best to assist you in the best way we can!"
|
||||
Image: "https://i.imgur.com/dejQvkg.png"
|
||||
Color: "#45b463"
|
||||
Layout: "4"
|
||||
|
||||
Channel: "📗┃tickets"
|
||||
# The channel used to create the ticket panel, Use command /ticketpanel to generate it.
|
||||
|
||||
PanelTitle: "SupportBot"
|
||||
PanelColour: "#0cc559"
|
||||
# Available Layout Styles
|
||||
# "1" - Style 1 - Title & Text, Button at the top -- Image at the bottom
|
||||
# "2" - Style 2 - Title & Text, Button at the bottom -- Image at the top
|
||||
# "3" - Style 3 - The classic look - Title & Text at the top -- Image in the middle -- Button at the bottom
|
||||
# "4" - Style 4 - Classic Embed (Title & Text, Image on right, Button at bottom)
|
||||
|
||||
Button:
|
||||
Text: "Create Ticket"
|
||||
Emoji: "🎫"
|
||||
Color: "2"
|
||||
CustomId: "createticket"
|
||||
|
||||
# Available Button Styles
|
||||
# 1 - Blurple
|
||||
@@ -37,10 +45,23 @@ Button:
|
||||
# 3 - Red
|
||||
# 4 - Green
|
||||
|
||||
TicketPanel_Description: true
|
||||
TicketPanel_Thumbnail: false
|
||||
TicketPanel_Image: true
|
||||
DepartmentMenu:
|
||||
CustomId: "ticketdepartmentselect"
|
||||
Placeholder: "Choose a ticket department"
|
||||
DefaultDescription: "Open a support ticket"
|
||||
DefaultEmoji: "🎫"
|
||||
|
||||
PanelMessage: "Click on the button to file a `Ticket` and our team will try their best to assist you in the best way we can!"
|
||||
PanelThumbnail: "https://i.imgur.com/ieeZejl.png"
|
||||
PanelImage: "https://i.imgur.com/dejQvkg.png"
|
||||
Settings:
|
||||
Image: true
|
||||
TopTitle: true
|
||||
TopDivider: true
|
||||
BottomTitle: false
|
||||
BottomDivider: false
|
||||
|
||||
Messages:
|
||||
NotConfigured: "Ticket Panel is not set up. Please configure it in
|
||||
`/Configs/ticket-panel.yml`."
|
||||
InvalidChannel: "TicketHome channel not found. Please check your config file."
|
||||
PanelSent: "Ticket panel message has been sent!"
|
||||
PanelSentWithDepartments: "Ticket panel message with department selection has been sent!"
|
||||
SendError: "An error occurred while sending the ticket panel message."
|
||||
@@ -0,0 +1,33 @@
|
||||
Theme:
|
||||
Background: '#0e0e0e'
|
||||
Panel: '#1d1d1d'
|
||||
PanelSecondary: '#222222'
|
||||
PanelTertiary: '#1d2938'
|
||||
Border: '#2e2e2e'
|
||||
Text: '#e6edf3'
|
||||
Muted: '#606060'
|
||||
Accent: '#396ff8'
|
||||
AccentSoft: rgba(34, 197, 94, 0.12)
|
||||
AccentBorder: rgba(34, 197, 94, 0.25)
|
||||
AccentText: '#8df0ad'
|
||||
Link: '#396ff8'
|
||||
AttachmentLink: '#396ff8'
|
||||
CodeBackground: '#0e0e0e'
|
||||
Typography:
|
||||
FontFamily: Poppins
|
||||
Header:
|
||||
Title: Ticket Transcript
|
||||
Subtitle: Exported conversation log for support ticket **{{ticketName}}**
|
||||
Badge: Closed Transcript
|
||||
ShowMetaCards: true
|
||||
Meta:
|
||||
ChannelLabel: Ticket Name
|
||||
TicketIdLabel: Ticket ID
|
||||
MessageCountLabel: Message Count
|
||||
CloseReasonLabel: Close Reason
|
||||
DefaultCloseReason: No Reason Provided.
|
||||
Messages:
|
||||
SectionTitle: Messages
|
||||
EmptyState: No messages were found in this ticket.
|
||||
Advanced:
|
||||
CustomCss: ''
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"419821884431925260": {
|
||||
"bio": "Hi",
|
||||
"timezone": "",
|
||||
"isClockedIn": false
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"bio": "",
|
||||
"timezone": "",
|
||||
"clockedIn": false
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"clockedIn": true,
|
||||
"bio": "Hi",
|
||||
"timezone": "GMT"
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"bio": "This is the most W bio",
|
||||
"timezone": "cst",
|
||||
"clockedIn": false
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
[
|
||||
"419821884431925260"
|
||||
]
|
||||
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"1250864227460186240": {
|
||||
"suggestion": "hi",
|
||||
"author": "419821884431925260",
|
||||
"upvotes": [
|
||||
"419821884431925260"
|
||||
],
|
||||
"downvotes": []
|
||||
},
|
||||
"1250900161958248533": {
|
||||
"suggestion": "blob",
|
||||
"author": "419821884431925260",
|
||||
"upvotes": [],
|
||||
"downvotes": []
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
|
||||
}
|
||||
@@ -1,184 +0,0 @@
|
||||
{
|
||||
"tickets": [
|
||||
{},
|
||||
{
|
||||
"id": "1253862843053510687",
|
||||
"name": "ticket-0021",
|
||||
"user": "419821884431925260",
|
||||
"number": "0021",
|
||||
"reason": "> No Reason Provided!",
|
||||
"open": true,
|
||||
"subUsers": [],
|
||||
"createdAt": "2024-06-22T00:03:18.227Z",
|
||||
"claimedAt": null
|
||||
},
|
||||
{
|
||||
"id": "1253863588012232727",
|
||||
"name": "ticket-0022",
|
||||
"user": "419821884431925260",
|
||||
"number": "0022",
|
||||
"reason": "> No Reason Provided!",
|
||||
"open": true,
|
||||
"subUsers": [],
|
||||
"createdAt": "2024-06-22T00:06:16.126Z",
|
||||
"claimedAt": "2024-06-22T00:09:12.693Z",
|
||||
"claimedBy": "419821884431925260"
|
||||
},
|
||||
{
|
||||
"id": "1255499438483574847",
|
||||
"name": "ticket-0023",
|
||||
"user": "419821884431925260",
|
||||
"number": "0023",
|
||||
"reason": "> No Reason Provided!",
|
||||
"open": true,
|
||||
"subUsers": [],
|
||||
"createdAt": "2024-06-26T12:26:35.729Z",
|
||||
"claimedAt": null
|
||||
},
|
||||
{
|
||||
"id": "1255500796355678229",
|
||||
"name": "ticket-0024",
|
||||
"user": "419821884431925260",
|
||||
"number": "0024",
|
||||
"reason": "> No Reason Provided!",
|
||||
"open": true,
|
||||
"subUsers": [],
|
||||
"createdAt": "2024-06-26T12:31:56.888Z",
|
||||
"claimedAt": "2024-06-26T12:32:41.240Z",
|
||||
"claimedBy": "419821884431925260"
|
||||
},
|
||||
{
|
||||
"id": "1255501873386094603",
|
||||
"name": "ticket-0025",
|
||||
"user": "419821884431925260",
|
||||
"number": "0025",
|
||||
"reason": "> No Reason Provided!",
|
||||
"open": false,
|
||||
"subUsers": [],
|
||||
"createdAt": "2024-06-26T12:36:13.810Z",
|
||||
"claimedAt": null
|
||||
},
|
||||
{
|
||||
"id": "1255508653906198709",
|
||||
"name": "ticket-0026",
|
||||
"user": "419821884431925260",
|
||||
"number": "0026",
|
||||
"reason": "> No Reason Provided!",
|
||||
"open": true,
|
||||
"subUsers": [],
|
||||
"createdAt": "2024-06-26T13:03:10.321Z",
|
||||
"claimedAt": null
|
||||
},
|
||||
{
|
||||
"id": "1256299016023572572",
|
||||
"name": "ticket-0027",
|
||||
"user": "419821884431925260",
|
||||
"number": "0027",
|
||||
"reason": "> No Reason Provided!",
|
||||
"open": false,
|
||||
"subUsers": [],
|
||||
"createdAt": "2024-06-28T17:23:50.113Z",
|
||||
"claimedAt": null
|
||||
},
|
||||
{
|
||||
"id": "1256300956824633426",
|
||||
"name": "ticket-0028",
|
||||
"user": "419821884431925260",
|
||||
"number": "0028",
|
||||
"reason": "> No Reason Provided!",
|
||||
"open": false,
|
||||
"subUsers": [],
|
||||
"createdAt": "2024-06-28T17:31:31.454Z",
|
||||
"claimedAt": null
|
||||
},
|
||||
{
|
||||
"id": "1256302271461986507",
|
||||
"name": "ticket-0029",
|
||||
"user": "419821884431925260",
|
||||
"number": "0029",
|
||||
"reason": "> No Reason Provided!",
|
||||
"open": false,
|
||||
"subUsers": [],
|
||||
"createdAt": "2024-06-28T17:36:44.673Z",
|
||||
"claimedAt": null
|
||||
},
|
||||
{
|
||||
"id": "1256303116505317457",
|
||||
"name": "ticket-0030",
|
||||
"user": "419821884431925260",
|
||||
"number": "0030",
|
||||
"reason": "> No Reason Provided!",
|
||||
"open": true,
|
||||
"subUsers": [],
|
||||
"createdAt": "2024-06-28T17:40:06.292Z",
|
||||
"claimedAt": null
|
||||
},
|
||||
{
|
||||
"id": "1257775687562166304",
|
||||
"name": "ticket-0031",
|
||||
"user": "419821884431925260",
|
||||
"number": "0031",
|
||||
"reason": "> No Reason Provided!",
|
||||
"open": false,
|
||||
"subUsers": [],
|
||||
"createdAt": "2024-07-02T19:11:33.760Z",
|
||||
"claimedAt": null
|
||||
},
|
||||
{
|
||||
"id": "1257776136843427841",
|
||||
"name": "ticket-0032",
|
||||
"user": "419821884431925260",
|
||||
"number": "0032",
|
||||
"reason": "> No Reason Provided!",
|
||||
"open": false,
|
||||
"subUsers": [],
|
||||
"createdAt": "2024-07-02T19:13:20.641Z",
|
||||
"claimedAt": null
|
||||
},
|
||||
{
|
||||
"id": "1257777774483996795",
|
||||
"name": "ticket-0033",
|
||||
"user": "419821884431925260",
|
||||
"number": "0033",
|
||||
"reason": "> No Reason Provided!",
|
||||
"open": false,
|
||||
"subUsers": [],
|
||||
"createdAt": "2024-07-02T19:19:51.116Z",
|
||||
"claimedAt": null
|
||||
},
|
||||
{
|
||||
"id": "1257778630600167535",
|
||||
"name": "ticket-0034",
|
||||
"user": "419821884431925260",
|
||||
"number": "0034",
|
||||
"reason": "> No Reason Provided!",
|
||||
"open": false,
|
||||
"subUsers": [],
|
||||
"createdAt": "2024-07-02T19:23:15.328Z",
|
||||
"claimedAt": null
|
||||
},
|
||||
{
|
||||
"id": "1257780259374366841",
|
||||
"name": "ticket-0035",
|
||||
"user": "419821884431925260",
|
||||
"number": "0035",
|
||||
"reason": "> No Reason Provided!",
|
||||
"open": false,
|
||||
"subUsers": [],
|
||||
"createdAt": "2024-07-02T19:29:43.607Z",
|
||||
"claimedAt": null
|
||||
},
|
||||
{
|
||||
"id": "1257782171389657180",
|
||||
"name": "ticket-0036",
|
||||
"user": "419821884431925260",
|
||||
"number": "0036",
|
||||
"reason": "> No Reason Provided!",
|
||||
"open": true,
|
||||
"subUsers": [],
|
||||
"createdAt": "2024-07-02T19:37:19.467Z",
|
||||
"claimedAt": "2024-07-02T19:43:12.873Z",
|
||||
"claimedBy": "419821884431925260"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"id": 37,
|
||||
"TicketPanelID": "1250863985335472312"
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
// Emerald Services | SupportBot
|
||||
// Claude integration for SupportBot AI
|
||||
|
||||
const fs = require("fs");
|
||||
const yaml = require("js-yaml");
|
||||
const axios = require("axios");
|
||||
|
||||
const supportbotai = require("../../Structures/ConfigStore").supportbotAi;
|
||||
|
||||
function extractTextFromMessage(message) {
|
||||
if (!message) return "";
|
||||
|
||||
if (typeof message === "string") {
|
||||
return message.trim();
|
||||
}
|
||||
|
||||
if (message.text) {
|
||||
return message.text.trim();
|
||||
}
|
||||
|
||||
if (Array.isArray(message)) {
|
||||
return message
|
||||
.map((part) => {
|
||||
if (typeof part === "string") return part;
|
||||
if (part?.type === "text" && typeof part.text === "string") {
|
||||
return part.text;
|
||||
}
|
||||
return "";
|
||||
})
|
||||
.join("")
|
||||
.trim();
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
async function chat({ messages, temperature, maxTokens }) {
|
||||
const model = supportbotai.General.Model;
|
||||
const apiKey = supportbotai.General.Model_API_Key;
|
||||
|
||||
const systemMessages = messages.filter((m) => m.role === "system");
|
||||
const systemPrompt = systemMessages.map((m) => m.content).join("\n\n");
|
||||
|
||||
const userAssistantMessages = messages.filter((m) => m.role !== "system");
|
||||
|
||||
const response = await axios.post(
|
||||
"https://api.anthropic.com/v1/messages",
|
||||
{
|
||||
model,
|
||||
system: systemPrompt || undefined,
|
||||
messages: userAssistantMessages,
|
||||
temperature: temperature || supportbotai.General.Temperature || 0.4,
|
||||
max_tokens: maxTokens || supportbotai.General.Tokens || 800,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"x-api-key": apiKey,
|
||||
"anthropic-version": "2023-06-01",
|
||||
"content-type": "application/json",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const data = response.data;
|
||||
const content = extractTextFromMessage(data.content);
|
||||
|
||||
if (!content) {
|
||||
console.error("Claude raw response:", JSON.stringify(data, null, 2));
|
||||
throw new Error(
|
||||
`Claude returned empty response. stop_reason=${data?.stop_reason || "unknown"}`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
content,
|
||||
raw: data,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
chat,
|
||||
};
|
||||
@@ -0,0 +1,70 @@
|
||||
// Emerald Services | SupportBot
|
||||
// Groq integration for SupportBot AI
|
||||
|
||||
const fs = require("fs");
|
||||
const yaml = require("js-yaml");
|
||||
const OpenAI = require("openai");
|
||||
|
||||
const supportbotai = require("../../Structures/ConfigStore").supportbotAi;
|
||||
|
||||
const client = new OpenAI({
|
||||
apiKey: supportbotai.General.Model_API_Key,
|
||||
baseURL: "https://api.groq.com/openai/v1",
|
||||
});
|
||||
|
||||
function extractTextFromMessage(message) {
|
||||
if (!message) return "";
|
||||
|
||||
if (typeof message.content === "string") {
|
||||
return message.content.trim();
|
||||
}
|
||||
|
||||
if (Array.isArray(message.content)) {
|
||||
return message.content
|
||||
.map((part) => {
|
||||
if (typeof part === "string") return part;
|
||||
if (part?.type === "text" && typeof part.text === "string") {
|
||||
return part.text;
|
||||
}
|
||||
if (typeof part?.text?.value === "string") {
|
||||
return part.text.value;
|
||||
}
|
||||
return "";
|
||||
})
|
||||
.join("")
|
||||
.trim();
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
async function chat({ messages, temperature, maxTokens }) {
|
||||
const model = supportbotai.General.Model;
|
||||
|
||||
const response = await client.chat.completions.create({
|
||||
model,
|
||||
messages,
|
||||
temperature: temperature || supportbotai.General.Temperature || 0.4,
|
||||
max_tokens: maxTokens || supportbotai.General.Tokens || 800,
|
||||
});
|
||||
|
||||
const choice = response?.choices?.[0];
|
||||
const message = choice?.message;
|
||||
const content = extractTextFromMessage(message);
|
||||
|
||||
if (!content) {
|
||||
console.error("Groq raw response:", JSON.stringify(response, null, 2));
|
||||
throw new Error(
|
||||
`Groq returned empty response. finish_reason=${choice?.finish_reason || "unknown"}`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
content,
|
||||
raw: response,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
chat,
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
// Emerald Services | SupportBot
|
||||
// Open AI integration for SupportBot AI
|
||||
|
||||
const fs = require("fs");
|
||||
const yaml = require("js-yaml");
|
||||
const OpenAI = require("openai");
|
||||
|
||||
const supportbotai = require("../../Structures/ConfigStore").supportbotAi;
|
||||
|
||||
const client = new OpenAI({
|
||||
apiKey: supportbotai.General.Model_API_Key,
|
||||
});
|
||||
|
||||
function extractTextFromMessage(message) {
|
||||
if (!message) return "";
|
||||
|
||||
if (typeof message.content === "string") {
|
||||
return message.content.trim();
|
||||
}
|
||||
|
||||
if (Array.isArray(message.content)) {
|
||||
return message.content
|
||||
.map((part) => {
|
||||
if (typeof part === "string") return part;
|
||||
if (part?.type === "text" && typeof part.text === "string")
|
||||
return part.text;
|
||||
if (typeof part?.text?.value === "string") return part.text.value;
|
||||
return "";
|
||||
})
|
||||
.join("")
|
||||
.trim();
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
async function chat({ messages, temperature, maxTokens }) {
|
||||
const model = supportbotai.General.Model;
|
||||
|
||||
const response = await client.chat.completions.create({
|
||||
model,
|
||||
messages,
|
||||
temperature: temperature || supportbotai.General.Temperature || 0.4,
|
||||
max_completion_tokens: maxTokens || supportbotai.General.Tokens || 800,
|
||||
});
|
||||
|
||||
const choice = response?.choices?.[0];
|
||||
const message = choice?.message;
|
||||
const content = extractTextFromMessage(message);
|
||||
|
||||
if (!content) {
|
||||
console.error("OpenAI raw response:", JSON.stringify(response, null, 2));
|
||||
throw new Error(
|
||||
`OpenAI returned empty response. finish_reason=${choice?.finish_reason || "unknown"}`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
content,
|
||||
raw: response,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
chat,
|
||||
};
|
||||
+28
-6
@@ -1,9 +1,31 @@
|
||||
const Event = require("../Structures/Event.js");
|
||||
module.exports = new Event("guildCreate", async (client) => {
|
||||
console.log(
|
||||
`\u001b[31m`,
|
||||
`${client.user.username} is not in the correct server set in your config. Please leave all other servers and restart the bot.`
|
||||
const {
|
||||
getConfiguredGuildId,
|
||||
syncGuildHealth,
|
||||
logGuildWarning,
|
||||
} = require("../Structures/GuildManager.js");
|
||||
|
||||
module.exports = new Event("guildCreate", async (client, guild) => {
|
||||
const configuredId = getConfiguredGuildId();
|
||||
|
||||
if (!configuredId) {
|
||||
logGuildWarning(
|
||||
client,
|
||||
`Joined "${guild.name}" (${guild.id}). Set General.GuildId in config to lock the bot to one server.`,
|
||||
);
|
||||
syncGuildHealth(client);
|
||||
return;
|
||||
}
|
||||
|
||||
if (guild.id === configuredId) {
|
||||
syncGuildHealth(client);
|
||||
return;
|
||||
}
|
||||
|
||||
logGuildWarning(
|
||||
client,
|
||||
`Joined unauthorized server "${guild.name}" (${guild.id}). Leave it from the dashboard or remove the invite.`,
|
||||
);
|
||||
console.log(`\u001b[31m`, `${client.user.username} will now exit.`);
|
||||
return process.exit(1);
|
||||
|
||||
syncGuildHealth(client);
|
||||
});
|
||||
+20
-7
@@ -1,10 +1,23 @@
|
||||
const Event = require("../Structures/Event.js");
|
||||
const {
|
||||
getConfiguredGuildId,
|
||||
syncGuildHealth,
|
||||
logGuildWarning,
|
||||
} = require("../Structures/GuildManager.js");
|
||||
|
||||
module.exports = new Event("guildDelete", async (client) => {
|
||||
console.log(
|
||||
`\u001b[31m`,
|
||||
`${client.user.username} is not in the correct server set in your config. Please join the server and restart the bot.`
|
||||
);
|
||||
console.log(`\u001b[31m`, `${client.user.username} will now exit.`);
|
||||
return process.exit(1);
|
||||
module.exports = new Event("guildDelete", async (client, guild) => {
|
||||
const configuredId = getConfiguredGuildId();
|
||||
|
||||
if (configuredId && guild.id === configuredId) {
|
||||
logGuildWarning(
|
||||
client,
|
||||
`Left the configured server "${guild.name}" (${guild.id}). Re-invite the bot or update General.GuildId.`,
|
||||
);
|
||||
} else if (!configuredId) {
|
||||
logGuildWarning(client, `Left server "${guild.name}" (${guild.id}).`);
|
||||
} else {
|
||||
logGuildWarning(client, `Left server "${guild.name}" (${guild.id}).`);
|
||||
}
|
||||
|
||||
syncGuildHealth(client);
|
||||
});
|
||||
+144
-48
@@ -1,55 +1,151 @@
|
||||
|
||||
const Discord = require('discord.js');
|
||||
const fs = require("fs");
|
||||
const {
|
||||
EmbedBuilder,
|
||||
ActionRowBuilder,
|
||||
ButtonBuilder,
|
||||
ButtonStyle,
|
||||
MessageFlags,
|
||||
ContainerBuilder,
|
||||
TextDisplayBuilder,
|
||||
SectionBuilder,
|
||||
ThumbnailBuilder,
|
||||
MediaGalleryBuilder
|
||||
} = require("discord.js");
|
||||
const Event = require("../Structures/Event.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 supportbot = require("../Structures/ConfigStore").supportbot;
|
||||
const msgconfig = require("../Structures/ConfigStore").messages;
|
||||
|
||||
module.exports = new Event("guildMemberAdd", async (client, member, interaction, guild) => {
|
||||
function getWelcomeThumbnail(client, member, iconCfg) {
|
||||
switch ((iconCfg?.Mode || "").toUpperCase()) {
|
||||
case "USER":
|
||||
return member.user.displayAvatarURL();
|
||||
case "BOT":
|
||||
return client.user.displayAvatarURL();
|
||||
case "CUSTOM":
|
||||
return iconCfg.CustomURL;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (supportbot.Welcome.Enabled) {
|
||||
const WelcomeChannel = member.guild.channels.cache.find(channel => channel.name === supportbot.Welcome.Channel)
|
||||
function buildModernComponents(client, member) {
|
||||
const container = new ContainerBuilder();
|
||||
|
||||
const WelcomeEmbed = new Discord.EmbedBuilder()
|
||||
.setColor(msgconfig.Welcome.Embed.Colour)
|
||||
.setTitle(msgconfig.Welcome.Embed.Title)
|
||||
.setDescription(msgconfig.Welcome.Embed.Message.replace(/%joined_user%/g, member.user))
|
||||
.setTimestamp();
|
||||
|
||||
if (msgconfig.Welcome.Embed.Thumbnail === "BOT") {
|
||||
WelcomeEmbed.setThumbnail(client.user.displayAvatarURL())
|
||||
}
|
||||
|
||||
if (msgconfig.Welcome.Embed.Thumbnail === "USER") {
|
||||
WelcomeEmbed.setThumbnail(member.user.displayAvatarURL())
|
||||
}
|
||||
|
||||
if (msgconfig.Welcome.Embed.ImageEnabled) {
|
||||
WelcomeEmbed.setImage(msgconfig.Welcome.Embed.ImageURL)
|
||||
}
|
||||
|
||||
WelcomeChannel.send({
|
||||
embeds: [WelcomeEmbed]
|
||||
})
|
||||
|
||||
if (supportbot.Roles.AutoRole.Enabled) {
|
||||
var role = member.guild.roles.cache.find(role => role.name === supportbot.Roles.AutoRole.Role);
|
||||
member.roles.add(role);
|
||||
}
|
||||
|
||||
console.log(`\u001b[32m`, `[+]`, `\u001b[33m`, `${member.user.username} joined the server!`)
|
||||
|
||||
if (msgconfig.Welcome.Modern.Colour) {
|
||||
container.setAccentColor(parseInt(msgconfig.Welcome.Modern.Colour.replace("#", ""), 16));
|
||||
}
|
||||
|
||||
});
|
||||
const section = new SectionBuilder().addTextDisplayComponents(
|
||||
new TextDisplayBuilder().setContent(
|
||||
msgconfig.Welcome.Modern.Title.replace(/%joined_user%/g, member.user)
|
||||
),
|
||||
new TextDisplayBuilder().setContent(
|
||||
msgconfig.Welcome.Modern.Body.replace(/%joined_user%/g, member.user)
|
||||
)
|
||||
);
|
||||
|
||||
const thumbUrl = getWelcomeThumbnail(client, member, msgconfig.Welcome.Modern.Icon);
|
||||
if (thumbUrl) {
|
||||
section.setThumbnailAccessory(
|
||||
new ThumbnailBuilder()
|
||||
.setURL(thumbUrl)
|
||||
);
|
||||
}
|
||||
|
||||
container.addSectionComponents(section);
|
||||
|
||||
// FIXED: Using MediaGalleryBuilder instead of TextDisplayBuilder for images
|
||||
if (msgconfig.Welcome.Modern.Image?.Enabled && msgconfig.Welcome.Modern.Image.URL) {
|
||||
container.addMediaGalleryComponents(
|
||||
new MediaGalleryBuilder().addItems([
|
||||
{
|
||||
media: {
|
||||
url: msgconfig.Welcome.Modern.Image.URL
|
||||
}
|
||||
}
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
if (Array.isArray(msgconfig.Welcome.Modern.Buttons)) {
|
||||
const row = new ActionRowBuilder();
|
||||
msgconfig.Welcome.Modern.Buttons.slice(0, 5).forEach(b => {
|
||||
const btn = new ButtonBuilder()
|
||||
.setLabel(b.label)
|
||||
.setStyle(ButtonStyle[b.style])
|
||||
.setEmoji(b.emoji || null);
|
||||
if (b.style === "Link") btn.setURL(b.url);
|
||||
else if (b.customId) btn.setCustomId(b.customId);
|
||||
row.addComponents(btn);
|
||||
});
|
||||
|
||||
container.addActionRowComponents(row);
|
||||
}
|
||||
|
||||
return [container];
|
||||
}
|
||||
|
||||
module.exports = new Event("guildMemberAdd", async (client, member) => {
|
||||
if (!supportbot.Welcome.Enabled) return;
|
||||
|
||||
const WelcomeChannel =
|
||||
member.guild.channels.cache.get(supportbot.Welcome.Channel) ||
|
||||
member.guild.channels.cache.find(c => c.name === supportbot.Welcome.Channel);
|
||||
if (!WelcomeChannel) return;
|
||||
|
||||
const style = (msgconfig.Welcome.LayoutStyle || "classic").toLowerCase();
|
||||
|
||||
if (style === "classic") {
|
||||
const WelcomeEmbed = new EmbedBuilder()
|
||||
.setColor(msgconfig.Welcome.Embed.Colour)
|
||||
.setTitle(msgconfig.Welcome.Embed.Title || null)
|
||||
.setDescription(
|
||||
msgconfig.Welcome.Embed.Message.replace(/%joined_user%/g, member.user)
|
||||
)
|
||||
.setTimestamp();
|
||||
|
||||
const thumb = getWelcomeThumbnail(client, member, msgconfig.Welcome.Embed.Icon);
|
||||
if (thumb) WelcomeEmbed.setThumbnail(thumb);
|
||||
|
||||
if (msgconfig.Welcome.Embed.Image?.Enabled && msgconfig.Welcome.Embed.Image.URL) {
|
||||
WelcomeEmbed.setImage(msgconfig.Welcome.Embed.Image.URL);
|
||||
}
|
||||
|
||||
const components = [];
|
||||
if (
|
||||
msgconfig.Welcome.Embed.Buttons?.Enabled &&
|
||||
Array.isArray(msgconfig.Welcome.Embed.Buttons.List)
|
||||
) {
|
||||
const row = new ActionRowBuilder();
|
||||
msgconfig.Welcome.Embed.Buttons.List.slice(0, 5).forEach(b => {
|
||||
const btn = new ButtonBuilder()
|
||||
.setLabel(b.label)
|
||||
.setStyle(ButtonStyle[b.style])
|
||||
.setEmoji(b.emoji || null);
|
||||
if (b.style === "Link") btn.setURL(b.url);
|
||||
else if (b.customId) btn.setCustomId(b.customId);
|
||||
row.addComponents(btn);
|
||||
});
|
||||
components.push(row);
|
||||
}
|
||||
|
||||
await WelcomeChannel.send({ embeds: [WelcomeEmbed], components });
|
||||
} else if (style === "modern") {
|
||||
const components = buildModernComponents(client, member);
|
||||
if (components.length > 0) {
|
||||
await WelcomeChannel.send({
|
||||
components,
|
||||
flags: MessageFlags.IsComponentsV2
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (supportbot.Roles.AutoRole.Enabled) {
|
||||
const role =
|
||||
member.guild.roles.cache.get(supportbot.Roles.AutoRole.Role) ||
|
||||
member.guild.roles.cache.find(r => r.name === supportbot.Roles.AutoRole.Role);
|
||||
if (role) await member.roles.add(role);
|
||||
}
|
||||
|
||||
console.log("\u001b[32m", "[+]", "\u001b[33m", `${member.user.username} joined the server!`);
|
||||
});
|
||||
+31
-41
@@ -1,50 +1,40 @@
|
||||
|
||||
const Discord = require('discord.js');
|
||||
const { EmbedBuilder } = require("discord.js");
|
||||
const fs = require("fs");
|
||||
const Event = require("../Structures/Event.js");
|
||||
const yaml = require("js-yaml");
|
||||
const Event = require("../Structures/Event.js");
|
||||
|
||||
const supportbot = yaml.load(
|
||||
fs.readFileSync("./Configs/supportbot.yml", "utf8")
|
||||
);
|
||||
|
||||
const cmdconfig = yaml.load(
|
||||
fs.readFileSync("./Configs/commands.yml", "utf8")
|
||||
);
|
||||
|
||||
const msgconfig = yaml.load(
|
||||
fs.readFileSync("./Configs/messages.yml", "utf8")
|
||||
);
|
||||
const supportbot = require("../Structures/ConfigStore").supportbot;
|
||||
const msgconfig = require("../Structures/ConfigStore").messages;
|
||||
|
||||
module.exports = new Event("guildMemberRemove", async (client, member, interaction, guild) => {
|
||||
module.exports = new Event("guildMemberRemove", async (client, member) => {
|
||||
|
||||
if (supportbot.Leave.Enabled) {
|
||||
const LeaveChannel = member.guild.channels.cache.find(channel => channel.name === supportbot.Leave.Channel)
|
||||
if (!supportbot.Leave || !supportbot.Leave.Enabled) return;
|
||||
|
||||
const LeaveEmbed = new Discord.EmbedBuilder()
|
||||
.setColor(msgconfig.Leave.Embed.Colour)
|
||||
.setTitle(msgconfig.Leave.Embed.Title)
|
||||
.setDescription(msgconfig.Leave.Embed.Message.replace(/%joined_user%/g, member.user))
|
||||
.setTimestamp();
|
||||
|
||||
if (msgconfig.Leave.Embed.Thumbnail === "BOT") {
|
||||
LeaveEmbed.setThumbnail(client.user.displayAvatarURL())
|
||||
}
|
||||
|
||||
if (msgconfig.Leave.Embed.Thumbnail === "USER") {
|
||||
LeaveEmbed.setThumbnail(member.user.displayAvatarURL())
|
||||
}
|
||||
|
||||
if (msgconfig.Leave.Embed.ImageEnabled) {
|
||||
LeaveEmbed.setImage(msgconfig.Leave.Embed.ImageURL)
|
||||
}
|
||||
|
||||
LeaveChannel.send({
|
||||
embeds: [LeaveEmbed]
|
||||
})
|
||||
|
||||
console.log(`\u001b[31m`, `[-]`, `\u001b[33m`, `${member.user.username} left the server!`)
|
||||
const leaveChannel =
|
||||
member.guild.channels.cache.get(supportbot.Leave.Channel) ||
|
||||
member.guild.channels.cache.find(c => c.name === supportbot.Leave.Channel);
|
||||
|
||||
if (!leaveChannel) return;
|
||||
|
||||
const leaveEmbed = new EmbedBuilder()
|
||||
.setColor(msgconfig.Leave.Embed.Colour)
|
||||
.setTitle(msgconfig.Leave.Embed.Title)
|
||||
.setDescription(
|
||||
msgconfig.Leave.Embed.Message.replace(/%left_user%/g, member.user.toString())
|
||||
)
|
||||
.setTimestamp();
|
||||
|
||||
if (msgconfig.Leave.Embed.Thumbnail === "BOT") {
|
||||
leaveEmbed.setThumbnail(client.user.displayAvatarURL());
|
||||
} else if (msgconfig.Leave.Embed.Thumbnail === "USER") {
|
||||
leaveEmbed.setThumbnail(member.user.displayAvatarURL());
|
||||
}
|
||||
|
||||
});
|
||||
if (msgconfig.Leave.Embed.ImageEnabled) {
|
||||
leaveEmbed.setImage(msgconfig.Leave.Embed.ImageURL);
|
||||
}
|
||||
|
||||
await leaveChannel.send({ embeds: [leaveEmbed] });
|
||||
|
||||
console.log(`[-] ${member.user.tag} left the server`);
|
||||
});
|
||||
+826
-116
File diff suppressed because it is too large.
Load diff
+558
-198
@@ -1,218 +1,578 @@
|
||||
const fs = require("fs");
|
||||
const Discord = require("discord.js");
|
||||
const yaml = require("js-yaml");
|
||||
const axios = require("axios");
|
||||
const { OpenAI } = require('openai');
|
||||
|
||||
// Load configuration files with validation
|
||||
function loadConfig(path) {
|
||||
try {
|
||||
return yaml.load(fs.readFileSync(path, "utf8"));
|
||||
} catch (error) {
|
||||
console.error(`Error loading config file at ${path}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
const supportbotai = require("../Structures/ConfigStore").supportbotAi;
|
||||
const supportbot = require("../Structures/ConfigStore").supportbot;
|
||||
|
||||
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 AIDatabase = require("../Structures/AIDatabase.js");
|
||||
const db = require("../Structures/Database.js");
|
||||
|
||||
// Initialize OpenAI client
|
||||
const openai = new OpenAI({
|
||||
apiKey: supportbotai.General.OpenAI_Key,
|
||||
});
|
||||
const openai = require("./Models/openai.js");
|
||||
const groq = require("./Models/groq.js");
|
||||
const claude = require("./Models/claude.js");
|
||||
|
||||
const PASTEBIN_API_KEY = supportbotai.General.PastebinAPI_Key;
|
||||
const PASTEBIN_API_URL = supportbotai.General.PastebinAPI_URL;
|
||||
|
||||
// Function to create a paste on Pastebin
|
||||
const CHANNELS = [supportbotai.Channels.AIChannel].filter(Boolean);
|
||||
|
||||
async function createPaste(content) {
|
||||
try {
|
||||
const response = await axios.post(PASTEBIN_API_URL, new URLSearchParams({
|
||||
api_dev_key: PASTEBIN_API_KEY,
|
||||
api_option: 'paste',
|
||||
api_paste_code: content,
|
||||
api_paste_private: 1, // 0 = public, 1 = unlisted, 2 = private
|
||||
api_paste_expire_date: '1D', // Expire in 1 day
|
||||
api_paste_format: 'text'
|
||||
}));
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Pastebin Error:', error);
|
||||
throw error;
|
||||
}
|
||||
try {
|
||||
if (!PASTEBIN_API_KEY || !PASTEBIN_API_URL) return null;
|
||||
|
||||
const response = await axios.post(
|
||||
PASTEBIN_API_URL,
|
||||
new URLSearchParams({
|
||||
api_dev_key: PASTEBIN_API_KEY,
|
||||
api_option: "paste",
|
||||
api_paste_code: content,
|
||||
api_paste_private: 1,
|
||||
api_paste_expire_date: "1D",
|
||||
api_paste_format: "text",
|
||||
}),
|
||||
);
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error("Pastebin Error:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getProvider() {
|
||||
const provider = String(
|
||||
supportbotai.General.Provider || "openai",
|
||||
).toLowerCase();
|
||||
|
||||
switch (provider) {
|
||||
case "openai":
|
||||
return openai;
|
||||
|
||||
case "groq":
|
||||
return groq;
|
||||
|
||||
case "claude":
|
||||
return claude;
|
||||
|
||||
default:
|
||||
throw new Error(`Unsupported AI provider: ${provider}`);
|
||||
}
|
||||
}
|
||||
|
||||
function formatTicketQuestionAnswers(questionAnswers) {
|
||||
if (!Array.isArray(questionAnswers) || questionAnswers.length === 0) {
|
||||
return "No ticket question answers were saved.";
|
||||
}
|
||||
|
||||
return questionAnswers
|
||||
.map((item, index) => {
|
||||
const question = item?.question || `Question ${index + 1}`;
|
||||
const answer = item?.answer || "No answer provided";
|
||||
return `${index + 1}. ${question}\nAnswer: ${answer}`;
|
||||
})
|
||||
.join("\n\n");
|
||||
}
|
||||
|
||||
function buildTicketContext(ticket, message) {
|
||||
if (!ticket) return "";
|
||||
|
||||
return `
|
||||
Ticket Information:
|
||||
Ticket ID: ${ticket.ticket_id || ticket.id || "Unknown"}
|
||||
Subject: ${ticket.subject || ticket.reason || "No subject provided"}
|
||||
Department: ${ticket.department || "Not set"}
|
||||
Priority: ${ticket.priority || "Not set"}
|
||||
Created By: ${ticket.user_id || ticket.user || "Unknown"}
|
||||
Current User: ${message?.author?.tag || "Unknown"} (${message?.author?.id || "Unknown"})
|
||||
|
||||
Ticket Question Answers:
|
||||
${formatTicketQuestionAnswers(ticket.questionAnswers)}
|
||||
|
||||
Use this information when helping the user with their ticket.
|
||||
Do not invent missing ticket details.
|
||||
`;
|
||||
}
|
||||
|
||||
function buildSystemPrompt(message) {
|
||||
const sections = [];
|
||||
|
||||
if (Array.isArray(supportbotai.CustomInstructions)) {
|
||||
sections.push(...supportbotai.CustomInstructions);
|
||||
}
|
||||
|
||||
sections.push(
|
||||
`I am currently powered by the ${supportbotai.General.Provider || "Unknown"} provider, running the ${supportbotai.General.Model || "Unknown"} model.`,
|
||||
);
|
||||
sections.push(
|
||||
`The current guild is: ${message.guild?.name || "Unknown Guild"}`,
|
||||
);
|
||||
sections.push(
|
||||
`The current channel is: #${message.channel?.name || "unknown-channel"}`,
|
||||
);
|
||||
|
||||
return sections.join("\n- ").replace(/^/, "- ");
|
||||
}
|
||||
|
||||
function buildConversationFromMemory(currentMessage) {
|
||||
const memoryConfig = supportbotai.Memory || {};
|
||||
const memoryEnabled = memoryConfig.Enabled !== false;
|
||||
const maxRecentMessages = memoryConfig.MaxRecentMessages || 15;
|
||||
const memoryScope = String(memoryConfig.Scope || "guild").toLowerCase();
|
||||
|
||||
const conversation = [
|
||||
{
|
||||
role: "system",
|
||||
content: buildSystemPrompt(currentMessage),
|
||||
},
|
||||
];
|
||||
|
||||
if (!memoryEnabled) return conversation;
|
||||
|
||||
const recentMessages =
|
||||
typeof AIDatabase.getRecentMessagesByScope === "function"
|
||||
? AIDatabase.getRecentMessagesByScope({
|
||||
guildId: currentMessage.guild?.id || null,
|
||||
channelId: currentMessage.channel?.id || null,
|
||||
limit: maxRecentMessages,
|
||||
scope: memoryScope,
|
||||
})
|
||||
: AIDatabase.getRecentMessages(
|
||||
currentMessage.channel?.id,
|
||||
maxRecentMessages,
|
||||
);
|
||||
|
||||
const latestSummary =
|
||||
memoryConfig.StoreSummaries !== false
|
||||
? typeof AIDatabase.getLatestSummaryByScope === "function"
|
||||
? AIDatabase.getLatestSummaryByScope({
|
||||
guildId: currentMessage.guild?.id || null,
|
||||
channelId: currentMessage.channel?.id || null,
|
||||
scope: memoryScope,
|
||||
})
|
||||
: AIDatabase.getLatestSummary(currentMessage.channel?.id)
|
||||
: null;
|
||||
|
||||
const facts =
|
||||
memoryConfig.StoreFacts !== false
|
||||
? typeof AIDatabase.getFactsByScope === "function"
|
||||
? AIDatabase.getFactsByScope({
|
||||
guildId: currentMessage.guild?.id || null,
|
||||
channelId: currentMessage.channel?.id || null,
|
||||
scope: memoryScope,
|
||||
})
|
||||
: AIDatabase.getFacts(currentMessage.channel?.id)
|
||||
: [];
|
||||
|
||||
const userFacts =
|
||||
memoryConfig.StoreFacts !== false &&
|
||||
typeof AIDatabase.getUserFactsByScope === "function"
|
||||
? AIDatabase.getUserFactsByScope({
|
||||
guildId: currentMessage.guild?.id || null,
|
||||
userId: currentMessage.author?.id || null,
|
||||
scope: memoryScope,
|
||||
})
|
||||
: memoryConfig.StoreFacts !== false &&
|
||||
typeof AIDatabase.getUserFacts === "function"
|
||||
? AIDatabase.getUserFacts(
|
||||
currentMessage.guild?.id,
|
||||
currentMessage.author?.id,
|
||||
)
|
||||
: [];
|
||||
|
||||
const knowledgeBase = typeof AIDatabase.getKnowledge === "function"
|
||||
? AIDatabase.getKnowledge(25)
|
||||
: [];
|
||||
|
||||
if (latestSummary?.summary) {
|
||||
conversation.push({
|
||||
role: "system",
|
||||
content: `Shared memory summary (${memoryScope} scope):\n${latestSummary.summary}`,
|
||||
});
|
||||
}
|
||||
|
||||
if (facts.length > 0) {
|
||||
const factText = facts
|
||||
.map(
|
||||
(fact) => `- [${fact.fact_type}] ${fact.fact_key}: ${fact.fact_value}`,
|
||||
)
|
||||
.join("\n");
|
||||
|
||||
conversation.push({
|
||||
role: "system",
|
||||
content: `Known shared facts (${memoryScope} scope):\n${factText}`,
|
||||
});
|
||||
}
|
||||
|
||||
if (userFacts.length > 0) {
|
||||
const userFactText = userFacts
|
||||
.map(
|
||||
(fact) => `- [${fact.fact_type}] ${fact.fact_key}: ${fact.fact_value}`,
|
||||
)
|
||||
.join("\n");
|
||||
|
||||
conversation.push({
|
||||
role: "system",
|
||||
content: `Known facts about this user across chats:\n${userFactText}`,
|
||||
});
|
||||
}
|
||||
|
||||
if (knowledgeBase.length > 0) {
|
||||
const knowledgeText = knowledgeBase
|
||||
.map((k) => `- ${k.content}`)
|
||||
.join("\n");
|
||||
|
||||
conversation.push({
|
||||
role: "system",
|
||||
content: `Server Knowledge Base (use this to answer questions):\n${knowledgeText}`,
|
||||
});
|
||||
}
|
||||
|
||||
for (const msg of recentMessages) {
|
||||
conversation.push({
|
||||
role: msg.role === "assistant" ? "assistant" : "user",
|
||||
content: msg.content,
|
||||
});
|
||||
}
|
||||
|
||||
return conversation;
|
||||
}
|
||||
|
||||
function getTicketContextForMessage(message) {
|
||||
if (!message?.channel) return null;
|
||||
|
||||
if (typeof db.getTicketByAIChannel === "function") {
|
||||
const aiTicket = db.getTicketByAIChannel(message.channel.id);
|
||||
if (aiTicket) return aiTicket;
|
||||
}
|
||||
|
||||
if (typeof db.getTicket === "function") {
|
||||
const directTicket = db.getTicket(message.channel.id);
|
||||
if (directTicket) return directTicket;
|
||||
}
|
||||
|
||||
if (message.channel.isThread() && typeof db.getTicket === "function") {
|
||||
const parentTicket = db.getTicket(message.channel.parentId);
|
||||
if (parentTicket) return parentTicket;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function getAIMessageType() {
|
||||
return String(supportbotai.Style?.MessageType || "embed").toLowerCase();
|
||||
}
|
||||
|
||||
function shouldSuggestTicket(message, ticket) {
|
||||
if (supportbotai.Features?.SuggestTickets !== true) return false;
|
||||
if (ticket) return false;
|
||||
if (message.channel.isThread()) return false;
|
||||
return CHANNELS.includes(message.channel.id);
|
||||
}
|
||||
|
||||
function getSuggestTicketText() {
|
||||
return (
|
||||
supportbotai.Messages?.SuggestTicketText ||
|
||||
"-# Need more help? React with 🎫 to open a ticket."
|
||||
);
|
||||
}
|
||||
|
||||
async function sendAIResponse({
|
||||
client,
|
||||
message,
|
||||
replyContent,
|
||||
suggestTicket = false,
|
||||
}) {
|
||||
const messageType = getAIMessageType();
|
||||
const ticketHint = suggestTicket ? `\n\n${getSuggestTicketText()}` : "";
|
||||
|
||||
if (messageType === "text") {
|
||||
return message.reply({
|
||||
content: `${replyContent}${ticketHint}`,
|
||||
});
|
||||
}
|
||||
|
||||
if (messageType === "components_v2") {
|
||||
const {
|
||||
ContainerBuilder,
|
||||
TextDisplayBuilder,
|
||||
SeparatorBuilder,
|
||||
MessageFlags,
|
||||
} = Discord;
|
||||
|
||||
const container = new ContainerBuilder();
|
||||
|
||||
if (supportbot.Embed?.Colours?.General) {
|
||||
container.setAccentColor(
|
||||
parseInt(String(supportbot.Embed.Colours.General).replace("#", ""), 16),
|
||||
);
|
||||
}
|
||||
|
||||
const title = new TextDisplayBuilder().setContent(
|
||||
`**${supportbotai.General?.Name || "SupportBot AI"}**`,
|
||||
);
|
||||
|
||||
const body = new TextDisplayBuilder().setContent(replyContent);
|
||||
|
||||
container.addTextDisplayComponents(title);
|
||||
container.addSeparatorComponents(new SeparatorBuilder().setDivider(true));
|
||||
container.addTextDisplayComponents(body);
|
||||
|
||||
if (suggestTicket) {
|
||||
container.addSeparatorComponents(new SeparatorBuilder().setDivider(true));
|
||||
container.addTextDisplayComponents(
|
||||
new TextDisplayBuilder().setContent(getSuggestTicketText()),
|
||||
);
|
||||
}
|
||||
|
||||
return message.reply({
|
||||
flags: MessageFlags.IsComponentsV2,
|
||||
components: [container],
|
||||
});
|
||||
}
|
||||
|
||||
const embed = new Discord.EmbedBuilder()
|
||||
.setAuthor({
|
||||
name: supportbotai.General.Name,
|
||||
iconURL: client.user.displayAvatarURL(),
|
||||
})
|
||||
.setDescription(`${replyContent}${ticketHint}`)
|
||||
.setColor(supportbotai.Embed.Color)
|
||||
.setFooter({
|
||||
text: supportbotai.Embed.Footer,
|
||||
iconURL: message.author.displayAvatarURL(),
|
||||
})
|
||||
.setTimestamp();
|
||||
|
||||
return message.reply({ embeds: [embed] });
|
||||
}
|
||||
|
||||
function rememberUserFacts(message) {
|
||||
if (!message?.guild || !message?.author || !message?.content) return;
|
||||
|
||||
const content = message.content.trim();
|
||||
|
||||
const nameMatch = content.match(/^my name is\s+(.+)$/i);
|
||||
if (nameMatch && typeof AIDatabase.saveUserFact === "function") {
|
||||
AIDatabase.saveUserFact({
|
||||
guildId: message.guild.id,
|
||||
userId: message.author.id,
|
||||
factType: "profile",
|
||||
factKey: "name",
|
||||
factValue: nameMatch[1].trim(),
|
||||
});
|
||||
}
|
||||
|
||||
const nicknameMatch = content.match(/^i(?:'| a)?m\s+(.+)$/i);
|
||||
if (
|
||||
nicknameMatch &&
|
||||
nicknameMatch[1] &&
|
||||
nicknameMatch[1].length <= 32 &&
|
||||
typeof AIDatabase.saveUserFact === "function"
|
||||
) {
|
||||
AIDatabase.saveUserFact({
|
||||
guildId: message.guild.id,
|
||||
userId: message.author.id,
|
||||
factType: "profile",
|
||||
factKey: "display_name",
|
||||
factValue: nicknameMatch[1].trim(),
|
||||
});
|
||||
}
|
||||
|
||||
const usesMatch = content.match(/^i use\s+(.+)$/i);
|
||||
if (usesMatch && typeof AIDatabase.saveUserFact === "function") {
|
||||
AIDatabase.saveUserFact({
|
||||
guildId: message.guild.id,
|
||||
userId: message.author.id,
|
||||
factType: "preference",
|
||||
factKey: "uses",
|
||||
factValue: usesMatch[1].trim(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Event handler for message creation
|
||||
module.exports = new Event("messageCreate", async (client, message) => {
|
||||
if (message.author.bot || !message.guild) return;
|
||||
if (!CHANNELS.includes(message.channelId) && !message.mentions.users.has(client.user.id)) return;
|
||||
if (message.author.bot || !message.guild) return;
|
||||
|
||||
if (supportbotai.Enabled) {
|
||||
const sendTypingInterval = setInterval(() => {
|
||||
message.channel.sendTyping();
|
||||
}, 5000);
|
||||
let shouldRespond =
|
||||
CHANNELS.includes(message.channelId) ||
|
||||
message.mentions.users.has(client.user.id);
|
||||
|
||||
let conversation = [{
|
||||
role: "system",
|
||||
content: `SupportBot is your friendly neighbourhood support bot. Current date and time: ${new Date().toLocaleString()}.
|
||||
The bot uses a modular command handler where commands are loaded dynamically from the './Commands' directory. Each command has a name, description, and execute method.
|
||||
|
||||
The main Client class structure:
|
||||
- Commands are loaded from the './Commands/' directory.
|
||||
- Configs are loaded from the './Configs/' directory if enabled.
|
||||
- Events are loaded from the './Events/' directory.
|
||||
- Addons are loaded from the './Addons/' directory if enabled.
|
||||
|
||||
Example of a command structure:
|
||||
{
|
||||
module.exports = new Command({
|
||||
name: cmdconfig.Ping.Command,
|
||||
description: cmdconfig.Ping.Description,
|
||||
options: [],
|
||||
permissions: cmdconfig.Ping.Permission,
|
||||
)}
|
||||
}
|
||||
if (!shouldRespond && message.channel.isThread()) {
|
||||
let ticket = null;
|
||||
|
||||
You can find more information about the commands and configuration files in the 'Configs' and 'Commands' directories.
|
||||
|
||||
All commands are loaded into the bot at startup from ./Events/ready.js and ./Client.js.
|
||||
|
||||
If someone asks you to do something, you can use the 'assist' command to have the bot respond, For example: "Can I speak to a support agent?", You would respond by opening a ticket which is the /Commands/ticket.js file or you can tell them to create a ticket in the channel defined as supportbot.Ticket.
|
||||
|
||||
You can find the source code for the bot at https://github.com/EmeraldServices/SupportBot.
|
||||
|
||||
You then need to understand how the bot works. Interactions are handled by the ./Events/interactionCreate.js file.
|
||||
|
||||
Commands are filtered based on configuration settings and loaded into the bot at startup.
|
||||
|
||||
Xiled Gaming Network (XGN) is a gaming community that typically offers various services and activities for gamers.
|
||||
|
||||
Xiled Gaming Network (XGN) has 4 divisions, Team Xiled International (TXI), The XiledOnes (TXO), Dealers of Death (DOD), Psychotic Solutions (PS)
|
||||
Xiled Gaming Network (XGN) can be found at https://officialxgn.gg/handbook/, This is where you can the code of conduct, policies and other important information for members.
|
||||
You can join XGN with the discord - discord.gg/xgn
|
||||
You can apply to join the XGN Clan - join.officialxgn.gg
|
||||
|
||||
You can find the founders of XGN under the history section of the handbook. https://officialxgn.gg/handbook/.
|
||||
`
|
||||
}];
|
||||
|
||||
let prevMessages = await message.channel.messages.fetch({ limit: 10 });
|
||||
prevMessages = prevMessages.reverse().filter(msg => !msg.author.bot || msg.author.id === client.user.id);
|
||||
|
||||
prevMessages.forEach((msg) => {
|
||||
const username = msg.author.username.replace(/\s+/g, '_').replace(/[^\w\s]/gi, '');
|
||||
|
||||
conversation.push({
|
||||
role: msg.author.id === client.user.id ? "assistant" : "user",
|
||||
name: username,
|
||||
content: msg.content,
|
||||
});
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await openai.chat.completions.create({
|
||||
model: supportbotai.General.Model,
|
||||
messages: conversation,
|
||||
max_tokens: supportbotai.General.Tokens,
|
||||
});
|
||||
|
||||
clearInterval(sendTypingInterval);
|
||||
|
||||
if (response.choices && response.choices.length > 0) {
|
||||
const replyContent = response.choices[0].message.content;
|
||||
|
||||
if (replyContent.toLowerCase().startsWith("generate image:")) {
|
||||
const prompt = replyContent.substring(15).trim();
|
||||
const imageUrl = await generateImage(prompt);
|
||||
|
||||
const SupportBotAIImage = new Discord.EmbedBuilder()
|
||||
.setAuthor({ name: supportbotai.General.Name, iconURL: client.user.displayAvatarURL() })
|
||||
.setDescription(`Here is the image generated for your prompt: "${prompt}"`)
|
||||
.setImage(imageUrl)
|
||||
.setColor(supportbotai.Embed.Color)
|
||||
.setFooter({ text: supportbotai.Embed.Footer, iconURL: message.author.displayAvatarURL() })
|
||||
.setTimestamp();
|
||||
|
||||
message.reply({ embeds: [SupportBotAIImage] });
|
||||
} else {
|
||||
const SupportBotAI = new Discord.EmbedBuilder()
|
||||
.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) {
|
||||
const pasteLink = await createPaste(replyContent);
|
||||
|
||||
const SupportBotAIPastebin = new Discord.EmbedBuilder()
|
||||
.setAuthor({ name: supportbotai.General.Name, iconURL: client.user.displayAvatarURL() })
|
||||
.setDescription(supportbotai.Messages.MessageTooLong.replace("{pastebin}", pasteLink))
|
||||
.setColor(supportbotai.Embed.Color)
|
||||
.setFooter({ text: supportbotai.Embed.Footer, iconURL: message.author.displayAvatarURL() })
|
||||
.setTimestamp();
|
||||
|
||||
message.reply({ embeds: [SupportBotAIPastebin] });
|
||||
} 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 });
|
||||
}
|
||||
} catch (error) {
|
||||
clearInterval(sendTypingInterval);
|
||||
|
||||
const SupportBotAIErrorResponse = new Discord.EmbedBuilder()
|
||||
.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);
|
||||
message.reply({ embeds: [SupportBotAIErrorResponse], ephemeral: true });
|
||||
}
|
||||
if (typeof db.getTicketByAIChannel === "function") {
|
||||
ticket = db.getTicketByAIChannel(message.channel.id);
|
||||
}
|
||||
|
||||
if (!supportbotai.Enabled) {
|
||||
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;
|
||||
if (!ticket && typeof db.getTicket === "function") {
|
||||
ticket = db.getTicket(message.channel.id);
|
||||
}
|
||||
});
|
||||
|
||||
if (
|
||||
!ticket &&
|
||||
message.channel.parentId &&
|
||||
typeof db.getTicket === "function"
|
||||
) {
|
||||
ticket = db.getTicket(message.channel.parentId);
|
||||
}
|
||||
|
||||
if (
|
||||
ticket &&
|
||||
ticket.aiModeEnabled &&
|
||||
ticket.aiChannelId === message.channel.id
|
||||
) {
|
||||
shouldRespond = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!shouldRespond) return;
|
||||
|
||||
if (!supportbotai.Enabled) {
|
||||
const embed = new Discord.EmbedBuilder()
|
||||
.setAuthor({
|
||||
name: supportbotai.General.Name,
|
||||
iconURL: client.user.displayAvatarURL(),
|
||||
})
|
||||
.setDescription(
|
||||
supportbotai.Messages?.AIDisabled || "AI is currently disabled.",
|
||||
)
|
||||
.setColor(supportbot.Embed.Colours.Error)
|
||||
.setFooter({
|
||||
text: supportbotai.Embed.Footer,
|
||||
iconURL: message.author.displayAvatarURL(),
|
||||
});
|
||||
|
||||
return message.reply({ embeds: [embed] });
|
||||
}
|
||||
|
||||
const typingInterval = setInterval(() => {
|
||||
message.channel.sendTyping().catch(() => { });
|
||||
}, 5000);
|
||||
|
||||
try {
|
||||
const memoryConfig = supportbotai.Memory || {};
|
||||
const memoryEnabled = memoryConfig.Enabled !== false;
|
||||
|
||||
if (memoryEnabled) {
|
||||
AIDatabase.addMessage({
|
||||
guildId: message.guild.id,
|
||||
channelId: message.channel.id,
|
||||
userId: message.author.id,
|
||||
role: "user",
|
||||
content: message.content,
|
||||
});
|
||||
|
||||
rememberUserFacts(message);
|
||||
}
|
||||
|
||||
const ticket = getTicketContextForMessage(message);
|
||||
|
||||
const conversation = buildConversationFromMemory(message);
|
||||
|
||||
if (ticket) {
|
||||
conversation.push({
|
||||
role: "system",
|
||||
content: buildTicketContext(ticket, message),
|
||||
});
|
||||
}
|
||||
|
||||
const provider = getProvider();
|
||||
|
||||
const response = await provider.chat({
|
||||
messages: conversation,
|
||||
temperature: supportbotai.General.Temperature || 0.4,
|
||||
maxTokens: supportbotai.General.Tokens || 800,
|
||||
});
|
||||
|
||||
clearInterval(typingInterval);
|
||||
|
||||
if (!response?.content) {
|
||||
const embed = new Discord.EmbedBuilder()
|
||||
.setDescription(
|
||||
supportbotai.Messages?.ErrorResponse ||
|
||||
"I couldn't generate a response.",
|
||||
)
|
||||
.setColor(supportbotai.Embed.Color);
|
||||
|
||||
return message.reply({ embeds: [embed] });
|
||||
}
|
||||
|
||||
const replyContent = response.content;
|
||||
|
||||
if (memoryEnabled) {
|
||||
AIDatabase.addMessage({
|
||||
guildId: message.guild.id,
|
||||
channelId: message.channel.id,
|
||||
userId: client.user.id,
|
||||
role: "assistant",
|
||||
content: replyContent,
|
||||
});
|
||||
}
|
||||
|
||||
if (replyContent.length > 4000) {
|
||||
const pasteLink = await createPaste(replyContent);
|
||||
|
||||
if (pasteLink) {
|
||||
const embed = new Discord.EmbedBuilder()
|
||||
.setDescription(
|
||||
(
|
||||
supportbotai.Messages?.MessageTooLong ||
|
||||
"My response was too long, so I uploaded it here: {pastebin}"
|
||||
).replace("{pastebin}", pasteLink),
|
||||
)
|
||||
.setColor(supportbotai.Embed.Color);
|
||||
|
||||
return message.reply({ embeds: [embed] });
|
||||
}
|
||||
|
||||
return message.reply({
|
||||
content: replyContent.slice(0, 1900),
|
||||
});
|
||||
}
|
||||
|
||||
const suggestTicket = shouldSuggestTicket(message, ticket);
|
||||
|
||||
const sentMessage = await sendAIResponse({
|
||||
client,
|
||||
message,
|
||||
replyContent,
|
||||
suggestTicket: false,
|
||||
});
|
||||
|
||||
if (
|
||||
shouldSuggestTicket(message, ticket) &&
|
||||
supportbotai.Features?.SuggestTickets === true
|
||||
) {
|
||||
const hintMessage = await message.channel.send(
|
||||
supportbotai.Messages?.SuggestTicketText ||
|
||||
"Need more help? React with 🎫 to open a ticket.",
|
||||
);
|
||||
|
||||
await hintMessage
|
||||
.react(supportbotai.Messages?.SuggestTicketReaction || "🎫")
|
||||
.catch(() => { });
|
||||
|
||||
setTimeout(
|
||||
() => {
|
||||
hintMessage.delete().catch(() => { });
|
||||
},
|
||||
5 * 60 * 1000,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
clearInterval(typingInterval);
|
||||
|
||||
console.error("AI Error:", error);
|
||||
|
||||
const embed = new Discord.EmbedBuilder()
|
||||
.setDescription(
|
||||
supportbotai.Messages?.AIError ||
|
||||
supportbotai.Messages?.ErrorResponse ||
|
||||
"There was an error contacting the AI provider.",
|
||||
)
|
||||
.setColor(supportbot.Embed.Colours.Error);
|
||||
|
||||
message.reply({ embeds: [embed] }).catch(() => { });
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
const { EmbedBuilder } = require('discord.js');
|
||||
const fs = require("fs");
|
||||
const yaml = require("js-yaml");
|
||||
const Event = require("../Structures/Event.js");
|
||||
|
||||
const supportbot = require("../Structures/ConfigStore").supportbot;
|
||||
const msgconfig = require("../Structures/ConfigStore").messages;
|
||||
|
||||
module.exports = new Event("messageDelete", async (client, message) => {
|
||||
if (!message.guild) return;
|
||||
|
||||
const author = message.author || (message.partial ? await message.fetch().then(m => m.author).catch(() => null) : null);
|
||||
if (!author || author.bot) return;
|
||||
|
||||
const content = message.content || (message.attachments.size > 0 ? "Contains attachment(s)" : "*No content*");
|
||||
|
||||
const deleteLogChannel = message.guild.channels.cache.get(supportbot.MessageDelete.Channel) ||
|
||||
message.guild.channels.cache.find(c => c.name === supportbot.MessageDelete.Channel);
|
||||
|
||||
const deletedMessageEmbed = new EmbedBuilder()
|
||||
.setColor(supportbot.MessageDelete.Colour)
|
||||
.setTitle("Message Deleted")
|
||||
.setDescription(`> **Channel:** <#${message.channel.id}>\n> **Message ID:** ${message.id}\n> **Message author:** ${author.tag} (${author.id})\n> **Message Created:** <t:${Math.floor(message.createdTimestamp / 1000)}:F>`)
|
||||
.addFields({ name: "Deleted Content", value: content, inline: false })
|
||||
.setThumbnail(author.displayAvatarURL())
|
||||
.setTimestamp();
|
||||
|
||||
if (message.attachments.size > 0) {
|
||||
const attachment = message.attachments.first();
|
||||
deletedMessageEmbed.addFields({ name: "Attachment", value: `[${attachment.name}](${attachment.proxyURL})` });
|
||||
}
|
||||
|
||||
if (deleteLogChannel) {
|
||||
deleteLogChannel.send({ embeds: [deletedMessageEmbed] });
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
const supportbot = require("../Structures/ConfigStore").supportbot;
|
||||
const supportbotai = require("../Structures/ConfigStore").supportbotAi;
|
||||
const cmdconfig = require("../Structures/ConfigStore").commands;
|
||||
|
||||
const Event = require("../Structures/Event.js");
|
||||
|
||||
module.exports = new Event(
|
||||
"messageReactionAdd",
|
||||
async (client, reaction, user) => {
|
||||
try {
|
||||
if (user.bot) return;
|
||||
|
||||
if (reaction.partial) {
|
||||
await reaction.fetch().catch(() => null);
|
||||
}
|
||||
|
||||
const message = reaction.message;
|
||||
if (!message || !message.guild) return;
|
||||
|
||||
const reactionEmoji = reaction.emoji?.name || reaction.emoji?.id || null;
|
||||
const expectedReaction =
|
||||
supportbotai.Messages?.SuggestTicketReaction || "🎫";
|
||||
|
||||
if (reactionEmoji !== expectedReaction) return;
|
||||
if (supportbotai.Features?.SuggestTickets !== true) return;
|
||||
if (message.author?.id !== client.user.id) return;
|
||||
|
||||
const expectedText =
|
||||
supportbotai.Messages?.SuggestTicketText ||
|
||||
"Need more help? React with 🎫 to open a ticket.";
|
||||
|
||||
if ((message.content || "").trim() !== expectedText.trim()) return;
|
||||
|
||||
await message.delete().catch(() => {});
|
||||
|
||||
const cmd = client.commands.get(cmdconfig.OpenTicket.Command);
|
||||
if (!cmd) {
|
||||
console.error("Open ticket command not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
const member = await message.guild.members
|
||||
.fetch(user.id)
|
||||
.catch(() => null);
|
||||
|
||||
if (!member) return;
|
||||
|
||||
const fakeInteraction = {
|
||||
user,
|
||||
member,
|
||||
guild: message.guild,
|
||||
channel: message.channel,
|
||||
client,
|
||||
replied: true,
|
||||
deferred: false,
|
||||
reason: `Opened from AI suggestion in #${message.channel.name}`,
|
||||
options: {
|
||||
getString: () => null,
|
||||
},
|
||||
reply: async () => null,
|
||||
};
|
||||
|
||||
await cmd.run(fakeInteraction);
|
||||
} catch (error) {
|
||||
console.error("Error handling AI ticket reaction:", error);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,57 @@
|
||||
const { EmbedBuilder } = require('discord.js');
|
||||
const fs = require("fs");
|
||||
const yaml = require("js-yaml");
|
||||
const Event = require("../Structures/Event.js");
|
||||
|
||||
// Load the configuration files
|
||||
const supportbot = require("../Structures/ConfigStore").supportbot;
|
||||
const msgconfig = require("../Structures/ConfigStore").messages;
|
||||
|
||||
module.exports = new Event("messageUpdate", async (client, oldMessage, newMessage) => {
|
||||
|
||||
// Check if the message is from a guild and not from a bot
|
||||
if (!newMessage.guild || newMessage.author.bot) return;
|
||||
|
||||
// Fetch old message if it's not cached (important for message content)
|
||||
if (!oldMessage.content && oldMessage.partial) {
|
||||
try {
|
||||
oldMessage = await oldMessage.fetch();
|
||||
} catch (err) {
|
||||
console.error('Error fetching old message:', err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback if content is missing
|
||||
const oldMessageContent = oldMessage.content || (oldMessage.attachments.size > 0 ? 'Contains attachment(s)' : '*No content*');
|
||||
const newMessageContent = newMessage.content || (newMessage.attachments.size > 0 ? 'Contains attachment(s)' : '*No content*');
|
||||
|
||||
// Get the update log channel either by ID or by name
|
||||
const updateLogChannel = newMessage.guild.channels.cache.get(supportbot.MessageUpdate.Channel) ||
|
||||
newMessage.guild.channels.cache.find(channel => channel.name === supportbot.MessageUpdate.Channel);
|
||||
|
||||
// Create the embed for the updated message
|
||||
const updatedMessageEmbed = new EmbedBuilder()
|
||||
.setColor(supportbot.MessageUpdate.Colour)
|
||||
.setTitle("Message Updated")
|
||||
.setDescription(`> **Channel:** <#${newMessage.channel.id}>\n> **Message ID:** [${newMessage.id}](https://discord.com/channels/${newMessage.guild.id}/${newMessage.channel.id}/${newMessage.id})\n> **Message author:** ${newMessage.author.tag} (${newMessage.author.id})\n> **Message Created:** <t:${Math.floor(oldMessage.createdTimestamp / 1000)}:F>`)
|
||||
.addFields(
|
||||
{ name: 'Old Message', value: oldMessageContent, inline: false },
|
||||
{ name: 'New Message', value: newMessageContent, inline: false }
|
||||
)
|
||||
.setThumbnail(newMessage.author.displayAvatarURL())
|
||||
.setTimestamp();
|
||||
|
||||
// If the new message contains an attachment, add it to the embed
|
||||
if (newMessage.attachments.size > 0) {
|
||||
const attachment = newMessage.attachments.first();
|
||||
updatedMessageEmbed.addFields({ name: 'Attachment', value: `[${attachment.name}](${attachment.proxyURL})` });
|
||||
}
|
||||
|
||||
// Send the embed to the update log channel if the channel is found
|
||||
if (updateLogChannel) {
|
||||
updateLogChannel.send({
|
||||
embeds: [updatedMessageEmbed]
|
||||
});
|
||||
}
|
||||
});
|
||||
+6
-200
@@ -1,202 +1,8 @@
|
||||
// SupportBot | Emerald Services
|
||||
// Ready Event
|
||||
|
||||
const fs = require("fs");
|
||||
|
||||
const Discord = require("discord.js");
|
||||
const { Client, GatewayIntentBits, ActivityType } = require('discord.js');
|
||||
const client = new Discord.Client({intents: 32767})
|
||||
|
||||
const yaml = require("js-yaml");
|
||||
|
||||
const supportbot = yaml.load(
|
||||
fs.readFileSync("./Configs/supportbot.yml", "utf8")
|
||||
);
|
||||
const panelconfig = yaml.load(
|
||||
fs.readFileSync("./Configs/ticket-panel.yml", "utf8")
|
||||
);
|
||||
|
||||
const cmdconfig = yaml.load(
|
||||
fs.readFileSync("./Configs/commands.yml", "utf8")
|
||||
);
|
||||
|
||||
const msgconfig = yaml.load(
|
||||
fs.readFileSync("./Configs/messages.yml", "utf8")
|
||||
);
|
||||
|
||||
const Event = require("../Structures/Event.js");
|
||||
const { runBotReady } = require("../Structures/BotStartup.js");
|
||||
|
||||
let chan1 = client.channels.cache.get(panelconfig.Channel);
|
||||
|
||||
module.exports = new Event("ready", async (client, interaction) => {
|
||||
const { getRole, getChannel, getCategory } = client;
|
||||
|
||||
if (supportbot.Activity.Type === "Playing", "playing") {
|
||||
client.user.setPresence({
|
||||
activities: [{ name: supportbot.Activity.Status, type: Discord.ActivityType.Playing }],
|
||||
status: supportbot.Activity.Type,
|
||||
});
|
||||
}
|
||||
|
||||
if (supportbot.Activity.Type === "Watching", "watching") {
|
||||
client.user.setPresence({
|
||||
activities: [{ name: supportbot.Activity.Status, type: Discord.ActivityType.Watching }],
|
||||
status: supportbot.Activity.Type,
|
||||
});
|
||||
}
|
||||
|
||||
if (supportbot.Activity.Type === "Listening", "listening") {
|
||||
client.user.setPresence({
|
||||
activities: [{ name: supportbot.Activity.Status, type: Discord.ActivityType.Listening }],
|
||||
status: supportbot.Activity.Type,
|
||||
});
|
||||
}
|
||||
|
||||
if (supportbot.Activity.Type === "Competing", "competing") {
|
||||
client.user.setPresence({
|
||||
activities: [{ name: supportbot.Activity.Status, type: Discord.ActivityType.Competing }],
|
||||
status: supportbot.Activity.Type,
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`\u001b[32m`, `――――――――――――――――――――――――――――――――――――――――――――`);
|
||||
console.log(` `);
|
||||
console.log(`\u001b[31m`, `┏━━━┓╋╋╋╋╋╋╋╋╋╋╋╋╋┏┓┏━━┓╋╋╋┏┓`);
|
||||
console.log(`\u001b[31m`, `┃┏━┓┃╋╋╋╋╋╋╋╋╋╋╋╋┏┛┗┫┏┓┃╋╋┏┛┗┓`);
|
||||
console.log(`\u001b[31m`, `┃┗━━┳┓┏┳━━┳━━┳━━┳┻┓┏┫┗┛┗┳━┻┓┏┛`);
|
||||
console.log(`\u001b[31m`, `┗━━┓┃┃┃┃┏┓┃┏┓┃┏┓┃┏┫┃┃┏━┓┃┏┓┃┃`);
|
||||
console.log(`\u001b[31m`, `┃┗━┛┃┗┛┃┗┛┃┗┛┃┗┛┃┃┃┗┫┗━┛┃┗┛┃┗┓`);
|
||||
console.log(`\u001b[31m`, `┗━━━┻━━┫┏━┫┏━┻━━┻┛┗━┻━━━┻━━┻━┛`);
|
||||
console.log(`\u001b[31m`, `┗╋╋╋╋╋╋╋┃┃╋┃┃`);
|
||||
console.log(`\u001b[31m`, `╋╋╋╋╋╋╋┗┛╋┗┛`);
|
||||
console.log(` `);
|
||||
console.log(`\u001b[32m`, `――――――――――――――――――――――――――――――――――――――――――――`);
|
||||
console.log(` `);
|
||||
console.log(
|
||||
`\u001b[31m`,
|
||||
`${supportbot.General.Name} | [${supportbot.SupportBot_Version}]`,
|
||||
`\u001b[32m`,
|
||||
`Connected to Discord`,
|
||||
);
|
||||
|
||||
console.log(`\u001b[31m`, `SupportBot proudly created by Emerald Development`);
|
||||
|
||||
console.log(` `);
|
||||
|
||||
console.log(
|
||||
`\u001b[31m`,
|
||||
`Discord`,
|
||||
`\u001b[32m`,
|
||||
`https://dsc.gg/emerald-dev`,
|
||||
);
|
||||
|
||||
console.log(
|
||||
`\u001b[31m`,
|
||||
`Website`,
|
||||
`\u001b[32m`,
|
||||
`https://emeraldsrv.com`,
|
||||
);
|
||||
|
||||
console.log(
|
||||
`\u001b[31m`,
|
||||
`Marketplace`,
|
||||
`\u001b[32m`,
|
||||
`https://market.emeraldsrv.com`,
|
||||
);
|
||||
|
||||
console.log(
|
||||
`\u001b[31m`,
|
||||
`Documentation`,
|
||||
`\u001b[32m`,
|
||||
`https://emeraldsrv.com/third-party`,
|
||||
);
|
||||
|
||||
console.log(` `);
|
||||
|
||||
console.log(
|
||||
`\u001b[31m`,
|
||||
`Invite URL:`,
|
||||
`\u001b[36m`,
|
||||
`https://discord.com/api/oauth2/authorize?client_id=` +
|
||||
client.user.id +
|
||||
`&permissions=8&scope=bot%20applications.commands`
|
||||
);
|
||||
|
||||
console.log(` `);
|
||||
console.log(`\u001b[32m`, `――――――――――――――――――――――――――――――――――――――――――――`);
|
||||
|
||||
const roles = [
|
||||
supportbot.Roles.StaffMember.Admin,
|
||||
supportbot.Roles.StaffMember.Staff,
|
||||
supportbot.Roles.ModRoles.Blacklisted,
|
||||
supportbot.Roles.ModRoles.Muted
|
||||
];
|
||||
|
||||
supportbot.Departments.forEach((department) => roles.push(department.role));
|
||||
|
||||
if (supportbot.Roles.AutoRole.Role) roles.push(supportbot.Roles.AutoRole.Role);
|
||||
|
||||
const channels = [
|
||||
supportbot.Suggestions.Channel,
|
||||
supportbot.Ticket.Log.TicketLog,
|
||||
supportbot.Ticket.Log.TranscriptLog,
|
||||
panelconfig.Channel,
|
||||
supportbot.Welcome.Channel,
|
||||
supportbot.Leave.Channel,
|
||||
supportbot.Translate.Log
|
||||
];
|
||||
//const categories = [supportbot.TicketCategory];
|
||||
|
||||
if (!channels) {
|
||||
console.log("\u001b[31m", `[MISSING CHANNEL]`, `\u001b[37;1m`, `${channels}`, "\u001b[31m", `channel not found. Please check your config file.`);
|
||||
return;
|
||||
}
|
||||
// else {
|
||||
// console.log(`\u001b[32m`, `[CHANNEL LOCATED]`, `\u001b[37;1m`, `${channels}`, `\u001b[32;1m`, `channel has been found.`);
|
||||
// }
|
||||
|
||||
const missingC = [];
|
||||
const missingR = [];
|
||||
const missingCat = [];
|
||||
|
||||
for (let r of roles) {
|
||||
const find = await getRole(r, client.guilds.cache.first());
|
||||
if (!find) missingR.push(r);
|
||||
}
|
||||
// for (let cat of categories) {
|
||||
// const find = await getCategory(cat, client.guilds.cache.first());
|
||||
// if (!find) missingCat.push(cat);
|
||||
// }
|
||||
|
||||
if (missingR.length > 0) {
|
||||
console.log(
|
||||
"\u001b[31m",
|
||||
`[MISSING CONFIGURATION]`,
|
||||
"\u001b[33m",
|
||||
`The following Roles could not be found in your server.\n`, `\u001b[31m`, `[${missingR.join(
|
||||
", "
|
||||
)}]`, `\n `,
|
||||
);
|
||||
}
|
||||
|
||||
if (missingC.length > 0) {
|
||||
console.log(
|
||||
"\u001b[31m",
|
||||
`[MISSING CONFIGURATION]`,
|
||||
"\u001b[33m",
|
||||
`The following Channels could not be found in your server.\n`, `\u001b[31m`, `[${missingC.join(
|
||||
", "
|
||||
)}]`, `\n `
|
||||
);
|
||||
}
|
||||
if (missingCat.length > 0) {
|
||||
console.log(
|
||||
"\u001b[31m",
|
||||
`[MISSING CONFIGURATION]`,
|
||||
"\u001b[33m",
|
||||
`The following Categories could not be found in your server.\n`, `\u001b[31m`, `[${missingCat.join(
|
||||
", "
|
||||
)}]`, `\n `,
|
||||
);
|
||||
}
|
||||
});
|
||||
module.exports = new Event("clientReady", async (client) => {
|
||||
// restartDiscordBot runs startup manually — clientReady often does not re-fire anyway
|
||||
if (client.__explicitStartup) return;
|
||||
await runBotReady(client, { clearConsole: true, logCommands: false });
|
||||
});
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Emerald Services
|
||||
Copyright (c) 2026 Emerald Services
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -13,11 +13,11 @@
|
||||
</a>
|
||||
|
||||
<a href="https://github.com/discordjs">
|
||||
<img src="https://img.shields.io/badge/discord.js-v13.6.x-blue.svg?style=for-the-badge&logo=javascript.svg" alt="shield.png">
|
||||
<img src="https://img.shields.io/badge/DISCORD.JS%20-%20v14-blue?style=for-the-badge" alt="shield.png">
|
||||
</a>
|
||||
|
||||
<a href="https://github.com/nodejs/node">
|
||||
<img src="https://img.shields.io/badge/node.js-v16.14.x-green.svg?style=for-the-badge&logo=node" alt="shield.png">
|
||||
<img src="https://img.shields.io/badge/NODE.JS%20-%20v20.x-green?style=for-the-badge" alt="shield.png">
|
||||
</a>
|
||||
|
||||
<a href="https://github.com/Emerald-Services/SupportBot/releases">
|
||||
@@ -28,8 +28,9 @@
|
||||
|
||||
| SupportBot | Node.JS | Discord.JS | Supported |
|
||||
|------------|---------|------------|------------|
|
||||
| 8.x | 20.x | 14.x. | ✅ Yes
|
||||
| 7.7.3 | 16.x | 13.x. | ✅ Yes
|
||||
| 26.x | 22.x | 14.x. | ✅ Yes
|
||||
| 8.x | 22.x | 14.x. | ✅ Yes
|
||||
| 7.7.3 | 16.x | 13.x. | ❌ No
|
||||
| 7.x | 16.x | 13.x. | ❌ No
|
||||
| 6.x | 14.x | 13.x. | ❌ No
|
||||
| 5.x | 12.x | 13.x. | ❌ No
|
||||
@@ -39,50 +40,183 @@
|
||||
</div>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://emeraldsrv.com/">Website</a>
|
||||
•
|
||||
<a href="https://github.com/Emerald-Services/SupportBot/wiki">Wiki</a>
|
||||
<br>
|
||||
<a href="#about">About</a>
|
||||
•
|
||||
<a href="#support">Support</a>
|
||||
•
|
||||
<a href="#license">License</a>
|
||||
•
|
||||
<a href="https://github.com/Emerald-Services/SupportBot/wiki/Third-Party-Developer">Third-Party</a>
|
||||
•
|
||||
<a href="#contributors">Contributors</a>
|
||||
<a href="https://opensource.org/licenses/MIT">
|
||||
<img src="https://img.shields.io/badge/license-MIT-green?style=for-the-badge" alt="MIT License">
|
||||
</a>
|
||||
<a href="https://github.com/discordjs/discord.js">
|
||||
<img src="https://img.shields.io/badge/discord.js-v14-blue?style=for-the-badge" alt="discord.js v14">
|
||||
</a>
|
||||
<a href="https://nodejs.org/">
|
||||
<img src="https://img.shields.io/badge/node.js-22.x-green?style=for-the-badge" alt="Node.js 22.x">
|
||||
</a>
|
||||
<a href="https://github.com/Emerald-Services/SupportBot">
|
||||
<img src="https://img.shields.io/badge/upstream-SupportBot-purple?style=for-the-badge" alt="Upstream SupportBot">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="#about">About</a> ·
|
||||
<a href="#features">Features</a> ·
|
||||
<a href="#requirements">Requirements</a> ·
|
||||
<a href="#getting-started">Getting started</a> ·
|
||||
<a href="#dashboard-access">Dashboard access</a> ·
|
||||
<a href="#configuration">Configuration</a> ·
|
||||
<a href="#addons">Addons</a> ·
|
||||
<a href="#development">Development</a> ·
|
||||
<a href="#upstream--credits">Upstream & credits</a> ·
|
||||
<a href="#license">License</a>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## About
|
||||
|
||||
SupportBot is an Open-Source discord bot built with [discord.js](https://github.com/discordjs/discord.js). It's primarily focused to help servers organise and manage their support system in a unique way with full customisation to match your brand.
|
||||
This repository is a **community fork** of [SupportBot](https://github.com/Emerald-Services/SupportBot) — the open-source Discord support/ticket bot by [Emerald Services](https://emeraldsrv.dev). It is **not** affiliated with or endorsed by Emerald Services.
|
||||
|
||||
## Support
|
||||
What this fork adds is a **self-hosted dashboard**: a modern web UI and REST API that run in the **same Node process** as the bot. You configure tickets, messages, commands, AI settings, addons, and more from the browser instead of editing YAML by hand. The API starts immediately on launch, so the dashboard stays reachable even while the bot is connecting or restarting.
|
||||
|
||||
<a href="https://emeraldsrv.com/discord">
|
||||
<img alt="Discord" src="https://img.shields.io/discord/597779408295821323?logo=discord&logoColor=white&style=for-the-badge">
|
||||
</a>
|
||||
If you only need the original bot without the dashboard, use the [official SupportBot repository](https://github.com/Emerald-Services/SupportBot).
|
||||
|
||||
## Features
|
||||
|
||||
### Self-hosted dashboard
|
||||
|
||||
| Area | What you get |
|
||||
|------|----------------|
|
||||
| **Overview** | Live stats — servers, members, open/closed tickets, module setup status, RAM/CPU/uptime |
|
||||
| **Visual config editors** | Edit `supportbot`, ticket panel, commands, messages, and AI config with form fields instead of raw YAML |
|
||||
| **Discord pickers** | Choose channels, roles, and categories from dropdowns when configuring the bot (no copy-pasting IDs) |
|
||||
| **Guild health** | Warnings if the bot is in the wrong server or extra guilds; quick link to fix `GuildId` or leave stray servers |
|
||||
| **Logs** | Browse Output, Warn, and Error logs from the dashboard |
|
||||
| **Transcripts** | View saved ticket HTML transcripts |
|
||||
| **Notifications** | In-app alerts for bot restarts, update checks, and installs |
|
||||
| **Settings** | Check for dashboard updates and apply them from the UI |
|
||||
| **Users & roles** | Invite staff via Discord OAuth; assign owner, admin, editor, viewer, or custom per-page permissions |
|
||||
| **Addons** | Browse and install addons from the [official Addons repo](https://github.com/Emerald-Services/Addons); edit addon configs in the sidebar like other config files |
|
||||
|
||||
### Bot & API (unchanged upstream behaviour, plus improvements)
|
||||
|
||||
- Full **SupportBot v26** ticket system, slash commands, AI assistant, and addon support
|
||||
- **Discord OAuth** login for the dashboard (no shared API secret in the browser)
|
||||
- **API key** (`SecretKey` in `api.yml`) still works for automation and scripts
|
||||
- Config saves can **restart the Discord bot** while keeping the dashboard and API online
|
||||
- Timeouts and caching so the UI does not hang when Discord or GitHub is slow
|
||||
|
||||
## Requirements
|
||||
|
||||
| Component | Version |
|
||||
|-----------|---------|
|
||||
| **Node.js** | 22.x (matches [SupportBot 8.x](https://github.com/Emerald-Services/SupportBot)) |
|
||||
| **discord.js** | 14.x (installed via `package.json`) |
|
||||
| **Discord bot** | Application with bot token and required intents |
|
||||
| **Discord OAuth app** | For dashboard login (optional but recommended) |
|
||||
|
||||
You need a machine or VPS that can run Node and reach Discord’s API (and GitHub, if you use in-dashboard updates or the addon marketplace).
|
||||
|
||||
## Getting started
|
||||
|
||||
### 1. Clone and install
|
||||
|
||||
```bash
|
||||
git clone https://github.com/C-h-a-r/SupportBot-Dashboard.git
|
||||
cd SupportBot-Dashboard
|
||||
npm install
|
||||
```
|
||||
|
||||
### 2. Configure the bot
|
||||
|
||||
Edit `Configs/supportbot.yml`:
|
||||
|
||||
- Set `General.Token` to your bot token
|
||||
- Set `General.GuildId` to your Discord server ID
|
||||
- Enable addons if you use them: `General.Addons.Enabled: true`
|
||||
|
||||
Other YAML files in `Configs/` can stay at defaults until you open the dashboard.
|
||||
|
||||
### 3. Configure the API & dashboard
|
||||
|
||||
Edit `Configs/api.yml`:
|
||||
|
||||
```yaml
|
||||
API:
|
||||
Enabled: true
|
||||
Port: 3000
|
||||
SecretKey: "generate-a-long-random-string"
|
||||
OAuth:
|
||||
Enabled: true
|
||||
ClientId: "YOUR_DISCORD_APP_CLIENT_ID"
|
||||
ClientSecret: "YOUR_DISCORD_APP_CLIENT_SECRET"
|
||||
RedirectUri: "http://localhost:3000/api/auth/discord/callback"
|
||||
OwnerUserIds:
|
||||
- "YOUR_DISCORD_USER_ID"
|
||||
```
|
||||
|
||||
**Discord Developer Portal**
|
||||
|
||||
1. Create an application at [discord.com/developers/applications](https://discord.com/developers/applications)
|
||||
2. Under **OAuth2 → Redirects**, add your callback URL(s). If users may sign in over **both** HTTP and HTTPS, register **both** (same host/path, different protocol), e.g. `http://bot.example.com/api/auth/discord/callback` and `https://bot.example.com/api/auth/discord/callback`.
|
||||
3. Use the same app’s **Client ID** and **Client Secret** in `api.yml`
|
||||
4. Put your Discord user ID in `OwnerUserIds` for full dashboard access
|
||||
|
||||
Set `RedirectUri` to your usual URL (host and path). By default, `UseRequestOrigin` keeps the **same protocol** as the page you opened (http vs https). Behind a reverse proxy, set `TrustProxy: 1` so forwarded `X-Forwarded-Proto` is respected.
|
||||
|
||||
### 4. Start the bot
|
||||
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
|
||||
When startup succeeds you should see:
|
||||
|
||||
- `[Dashboard] API listening on http://localhost:3000`
|
||||
- Discord bot login and slash command registration
|
||||
|
||||
Open **http://localhost:3000** in your browser and sign in with Discord.
|
||||
|
||||
The web UI is shipped as static files in `public/dashboard/` (included in this repository). You do not need a separate frontend build step.
|
||||
|
||||
> **Tip:** On first run, invite the bot to your server with the permissions SupportBot needs. Set `General.GuildId` to that server’s ID so channel/role pickers and guild health checks work.
|
||||
|
||||
## Dashboard access
|
||||
|
||||
| Method | Use case |
|
||||
|--------|----------|
|
||||
| **Discord OAuth** | Normal use — open the site, click **Continue with Discord**, sign in as an owner or invited user |
|
||||
| **Bearer token** | Scripts and automation — `Authorization: Bearer <SecretKey>` on `/api/*` routes |
|
||||
|
||||
Owners listed in `api.yml` (`OwnerUserIds`) always have full access. Additional users can be added under **Users** in the dashboard with granular permissions per config file and page.
|
||||
|
||||
## Configuration
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `Configs/supportbot.yml` | Bot token, guild, staff roles, ticket channels, embed colours, etc. |
|
||||
| `Configs/ticket-panel.yml` | Ticket panel layout and buttons |
|
||||
| `Configs/commands.yml` | Enable/disable slash commands |
|
||||
| `Configs/messages.yml` | User-facing message strings |
|
||||
| `Configs/supportbot-ai.yml` | AI assistant settings |
|
||||
| `Configs/api.yml` | Dashboard port, session secret, OAuth |
|
||||
| `Addons/Configs/*` | Per-addon config (after installing from **Addons** page) |
|
||||
|
||||
Most of these are editable from the sidebar under **Tickets & support**, **Community**, **Server control**, and **Addons**.
|
||||
|
||||
## Addons
|
||||
|
||||
1. Open **Addons** in the dashboard (or install manually into `Addons/` as in the [upstream docs](https://github.com/Emerald-Services/Addons))
|
||||
2. Click **Install** on an addon from the marketplace — files are copied into `Addons/` and `Addons/Configs/`, then the bot restarts
|
||||
3. Edit the addon’s config from the **Addons** section in the left sidebar (same editor as core configs)
|
||||
4. Ensure `General.Addons.Enabled` is `true` in bot config
|
||||
|
||||
Official addon list: [github.com/Emerald-Services/Addons](https://github.com/Emerald-Services/Addons)
|
||||
|
||||
## Upstream & credits
|
||||
|
||||
- **Original project:** [Emerald-Services/SupportBot](https://github.com/Emerald-Services/SupportBot) — MIT license, ticket bot for Discord
|
||||
- **Official addons:** [Emerald-Services/Addons](https://github.com/Emerald-Services/Addons)
|
||||
- **Community:** [Emerald Discord](https://emeraldsrv.dev/discord) · [Wiki](https://github.com/Emerald-Services/SupportBot/wiki)
|
||||
|
||||
This fork is maintained independently. For bugs in **core ticket/command behaviour** that also exist upstream, consider reporting to the [official repo](https://github.com/Emerald-Services/SupportBot/issues). For **dashboard-specific** issues, use this repository’s issues.
|
||||
|
||||
## License
|
||||
|
||||
Released under the [MIT](https://opensource.org/licenses/MIT) license.
|
||||
|
||||
|
||||
## Third-Party
|
||||
|
||||
- [Official addons repository](https://github.com/Emerald-Services/Addons/)
|
||||
- [Syphers Addons](https://github.com/SypherRed/SB_Addons_Unofficial)
|
||||
- [BuiltByBit](https://builtbybit.com/supportbot)
|
||||
- [Polymart](https://polymart.org/resource/supportbot-1-discord-ticket-bot.518)
|
||||
|
||||
|
||||
## Contributors
|
||||
|
||||
<a href="https://github.com/Emerald-Services/SupportBot/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=Emerald-Services/SupportBot">
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
Released under the [MIT License](https://opensource.org/licenses/MIT), same as upstream SupportBot.
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
Use this section to tell people about which versions of your project are
|
||||
currently being supported with security updates.
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 7.7.2 | :white_check_mark: |
|
||||
| 7.7.1 | :white_check_mark: |
|
||||
| < 4.0 | :x: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Use this section to tell people how to report a vulnerability.
|
||||
|
||||
Tell them where to go, how often they can expect to get an update on a
|
||||
reported vulnerability, what to expect if the vulnerability is accepted or
|
||||
declined, etc.
|
||||
@@ -0,0 +1,438 @@
|
||||
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);
|
||||
},
|
||||
|
||||
clearKnowledge() {
|
||||
db.prepare(`DELETE FROM ai_knowledge`).run();
|
||||
},
|
||||
|
||||
clearConversations(channelId) {
|
||||
if (!channelId) return;
|
||||
db.prepare(`DELETE FROM ai_conversations WHERE channel_id = ?`).run(channelId);
|
||||
},
|
||||
};
|
||||
+7
-1
@@ -1,6 +1,6 @@
|
||||
|
||||
// SupportBot | Emerald Services
|
||||
// Addon Structure
|
||||
|
||||
class Command {
|
||||
constructor(options) {
|
||||
this.name = options.name;
|
||||
@@ -10,7 +10,13 @@ class Command {
|
||||
this.run = options.run;
|
||||
this.defaultPermission = true;
|
||||
}
|
||||
|
||||
// Method to dynamically update options
|
||||
setOptions(options) {
|
||||
this.options = options;
|
||||
}
|
||||
}
|
||||
|
||||
class Event {
|
||||
constructor(event, run) {
|
||||
this.event = event;
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const axios = require("axios");
|
||||
const { withTimeout } = require("./asyncTimeout.js");
|
||||
const AdmZip = require("adm-zip");
|
||||
const yaml = require("js-yaml");
|
||||
|
||||
const EMERALD_API = "https://emeraldsrv.dev/api/v1";
|
||||
|
||||
function getApiKey() {
|
||||
try {
|
||||
if (fs.existsSync("./Configs/api.yml")) {
|
||||
const config = yaml.load(fs.readFileSync("./Configs/api.yml", "utf8"));
|
||||
return config?.API?.EmeraldAPIKey || "";
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to read api.yml:", e);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function ensureAddonDirs() {
|
||||
if (!fs.existsSync("./Addons")) {
|
||||
fs.mkdirSync("./Addons", { recursive: true });
|
||||
}
|
||||
if (!fs.existsSync("./Addons/Configs")) {
|
||||
fs.mkdirSync("./Addons/Configs", { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
async function listCatalogAddons() {
|
||||
const key = getApiKey();
|
||||
if (!key) throw new Error("EmeraldAPIKey not configured in api.yml");
|
||||
|
||||
const res = await withTimeout(
|
||||
axios.get(`${EMERALD_API}/resources`, {
|
||||
headers: { Authorization: `Bearer ${key}` },
|
||||
timeout: 15_000,
|
||||
}),
|
||||
18_000,
|
||||
"Emerald API request timed out",
|
||||
);
|
||||
|
||||
if (res.data.status !== "success") {
|
||||
throw new Error(res.data.message || "Failed to fetch catalog");
|
||||
}
|
||||
|
||||
return res.data.data.map(item => ({
|
||||
id: item.id.toString(),
|
||||
name: item.title,
|
||||
slug: item.slug,
|
||||
description: `Emerald Services Resource - ${item.slug}`,
|
||||
price: item.price,
|
||||
is_external: item.is_external,
|
||||
external_store: item.external_store,
|
||||
repositoryUrl: item.external_url || `https://emeraldsrv.dev`,
|
||||
jsFiles: [],
|
||||
configFiles: []
|
||||
})).sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
|
||||
async function downloadFile(id) {
|
||||
const key = getApiKey();
|
||||
const res = await withTimeout(
|
||||
axios.get(`${EMERALD_API}/download?id=${id}`, {
|
||||
headers: { Authorization: `Bearer ${key}` },
|
||||
responseType: "arraybuffer",
|
||||
timeout: 30_000
|
||||
}),
|
||||
35_000,
|
||||
"Addon file download timed out",
|
||||
);
|
||||
return Buffer.from(res.data);
|
||||
}
|
||||
|
||||
function listLocalAddonJs() {
|
||||
ensureAddonDirs();
|
||||
return fs
|
||||
.readdirSync("./Addons")
|
||||
.filter((f) => f.endsWith(".js") && f !== "addon.js");
|
||||
}
|
||||
|
||||
function listLocalAddonConfigs() {
|
||||
ensureAddonDirs();
|
||||
const dir = "./Addons/Configs";
|
||||
if (!fs.existsSync(dir)) return [];
|
||||
return fs
|
||||
.readdirSync(dir)
|
||||
.filter((f) => f.endsWith(".yml") || f.endsWith(".yaml") || f.endsWith(".json"));
|
||||
}
|
||||
|
||||
function isAddonInstalled(catalogEntry) {
|
||||
const localJs = listLocalAddonJs();
|
||||
return localJs.some(f => f.includes(catalogEntry.slug) || f.includes(catalogEntry.id.toString()) || f.includes(catalogEntry.name.replace(/ /g, '')));
|
||||
}
|
||||
|
||||
async function listInstalledAddons(catalog) {
|
||||
const localJs = listLocalAddonJs();
|
||||
const installed = [];
|
||||
|
||||
for (const file of localJs) {
|
||||
const name = file.replace(/\.js$/, "");
|
||||
const match = catalog.find(c => c.slug === name || c.id.toString() === name || c.name.replace(/ /g, '') === name);
|
||||
|
||||
installed.push({
|
||||
id: match ? match.id : name,
|
||||
name: match ? match.name : name,
|
||||
jsFiles: [file],
|
||||
configFiles: listLocalAddonConfigs().filter(f => f.startsWith(name)),
|
||||
custom: !match
|
||||
});
|
||||
}
|
||||
return installed;
|
||||
}
|
||||
|
||||
async function installAddonFromCatalog(addonId) {
|
||||
const catalog = await listCatalogAddons();
|
||||
const addon = catalog.find(c => c.id === addonId.toString());
|
||||
|
||||
if (!addon) {
|
||||
throw new Error(`Addon not found in catalog`);
|
||||
}
|
||||
|
||||
ensureAddonDirs();
|
||||
const content = await downloadFile(addon.id);
|
||||
|
||||
const zip = new AdmZip(content);
|
||||
zip.extractAllTo("./Addons", true);
|
||||
|
||||
return {
|
||||
addon: addon,
|
||||
filesWritten: [],
|
||||
};
|
||||
}
|
||||
|
||||
function readAddonConfig(filename) {
|
||||
const safe = path.basename(filename);
|
||||
if (!/^[a-zA-Z0-9_.-]+\.(yml|yaml|json)$/.test(safe)) {
|
||||
throw new Error("Invalid config filename");
|
||||
}
|
||||
|
||||
const filePath = path.join("./Addons/Configs", safe);
|
||||
if (!fs.existsSync(filePath)) {
|
||||
throw new Error("Addon config not found");
|
||||
}
|
||||
|
||||
const raw = fs.readFileSync(filePath, "utf8");
|
||||
if (safe.endsWith(".json")) {
|
||||
return { filename: safe, format: "json", data: JSON.parse(raw), raw };
|
||||
}
|
||||
|
||||
const yaml = require("js-yaml");
|
||||
return { filename: safe, format: "yaml", data: yaml.load(raw) || {}, raw };
|
||||
}
|
||||
|
||||
function writeAddonConfig(filename, data) {
|
||||
const safe = path.basename(filename);
|
||||
if (!/^[a-zA-Z0-9_.-]+\.(yml|yaml|json)$/.test(safe)) {
|
||||
throw new Error("Invalid config filename");
|
||||
}
|
||||
|
||||
ensureAddonDirs();
|
||||
const filePath = path.join("./Addons/Configs", safe);
|
||||
let content;
|
||||
|
||||
if (safe.endsWith(".json")) {
|
||||
content = `${JSON.stringify(data, null, 2)}\n`;
|
||||
} else {
|
||||
const yaml = require("js-yaml");
|
||||
content = yaml.dump(data, { lineWidth: 120, noRefs: true });
|
||||
}
|
||||
|
||||
fs.writeFileSync(filePath, content);
|
||||
return { filename: safe, path: filePath.replace(/\\/g, "/") };
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
listCatalogAddons,
|
||||
listInstalledAddons,
|
||||
isAddonInstalled,
|
||||
installAddonFromCatalog,
|
||||
listLocalAddonConfigs,
|
||||
readAddonConfig,
|
||||
writeAddonConfig,
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
function getBotInviteUrl(clientId) {
|
||||
if (!clientId) return null;
|
||||
return `https://discord.com/api/oauth2/authorize?client_id=${clientId}&permissions=8&scope=bot%20applications.commands`;
|
||||
}
|
||||
|
||||
module.exports = { getBotInviteUrl };
|
||||
@@ -0,0 +1,150 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const configStore = require("./ConfigStore.js");
|
||||
const { getPrimaryGuild } = require("./GuildManager.js");
|
||||
const {
|
||||
applyPresence,
|
||||
runBotReady,
|
||||
logSlashCommandsRegistered,
|
||||
} = require("./BotStartup.js");
|
||||
|
||||
function walk(dir, ext = ".js", fileList = []) {
|
||||
if (!fs.existsSync(dir)) return fileList;
|
||||
for (const file of fs.readdirSync(dir)) {
|
||||
const full = path.join(dir, file);
|
||||
if (fs.statSync(full).isDirectory()) {
|
||||
walk(full, ext, fileList);
|
||||
} else if (file.endsWith(ext)) {
|
||||
fileList.push(full.replace(/\\/g, "/"));
|
||||
}
|
||||
}
|
||||
return fileList;
|
||||
}
|
||||
|
||||
async function refreshSlashCommands(client) {
|
||||
if (!client?.application) return { count: 0 };
|
||||
|
||||
const cmdconfig = configStore.commands;
|
||||
const commandFiles = walk("./Commands");
|
||||
client.commands.clear();
|
||||
|
||||
for (const file of commandFiles) {
|
||||
const resolved = path.resolve(file);
|
||||
if (require.cache[resolved]) {
|
||||
delete require.cache[resolved];
|
||||
}
|
||||
}
|
||||
|
||||
let count = 0;
|
||||
for (const file of commandFiles) {
|
||||
const command = require(path.resolve(file));
|
||||
const matchingConfig = Object.values(cmdconfig).find(
|
||||
(entry) =>
|
||||
entry &&
|
||||
typeof entry === "object" &&
|
||||
entry.Command &&
|
||||
String(entry.Command).toLowerCase() ===
|
||||
String(command.name).toLowerCase(),
|
||||
);
|
||||
|
||||
if (matchingConfig && matchingConfig.Enabled === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
client.commands.set(command.name, command);
|
||||
count++;
|
||||
}
|
||||
|
||||
const payload = client.commands.map((cmd) => cmd.data ?? cmd);
|
||||
for (const guild of client.guilds.cache.values()) {
|
||||
await guild.commands.set(payload);
|
||||
}
|
||||
|
||||
return { count };
|
||||
}
|
||||
|
||||
async function reloadBot(client, options = {}) {
|
||||
const { fileKey, refreshCommands = true } = options;
|
||||
|
||||
let reloaded;
|
||||
if (fileKey) {
|
||||
configStore.reloadFile(fileKey);
|
||||
reloaded = [fileKey];
|
||||
} else {
|
||||
reloaded = configStore.reloadBotConfigs();
|
||||
}
|
||||
|
||||
applyPresence(client);
|
||||
|
||||
let commands = null;
|
||||
if (refreshCommands && (!fileKey || fileKey === "commands")) {
|
||||
commands = await refreshSlashCommands(client);
|
||||
}
|
||||
|
||||
return {
|
||||
reloaded,
|
||||
commands,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Reload configs from disk, reconnect the Discord session, and refresh presence/commands.
|
||||
* API and dashboard keep running on the same process.
|
||||
*/
|
||||
async function restartDiscordBot(client) {
|
||||
if (!client) {
|
||||
const err = new Error("Bot client not available");
|
||||
err.code = "NO_CLIENT";
|
||||
throw err;
|
||||
}
|
||||
|
||||
const reloaded = configStore.reloadBotConfigs();
|
||||
const token = configStore.supportbot?.General?.Token;
|
||||
|
||||
if (!token || String(token).trim() === "" || token === "BOT_TOKEN") {
|
||||
const err = new Error("Invalid or missing bot token in supportbot.yml");
|
||||
err.code = "INVALID_TOKEN";
|
||||
throw err;
|
||||
}
|
||||
|
||||
console.log("[BotReload] Restarting Discord bot after config change…");
|
||||
client.__restarting = true;
|
||||
|
||||
if (client.user) {
|
||||
try {
|
||||
await client.destroy();
|
||||
} catch (destroyErr) {
|
||||
console.warn("[BotReload] destroy:", destroyErr.message);
|
||||
}
|
||||
}
|
||||
|
||||
client.__explicitStartup = true;
|
||||
try {
|
||||
try {
|
||||
await client.login(token);
|
||||
} catch (loginErr) {
|
||||
console.error("[BotReload] login failed:", loginErr.message);
|
||||
const err = new Error(`Could not start bot: ${loginErr.message}`);
|
||||
err.code = "LOGIN_FAILED";
|
||||
throw err;
|
||||
}
|
||||
|
||||
// clientReady does not fire again after destroy()+login() — run startup explicitly
|
||||
await runBotReady(client, { clearConsole: true, logCommands: false });
|
||||
const commands = await refreshSlashCommands(client);
|
||||
logSlashCommandsRegistered(client);
|
||||
|
||||
console.log("[BotReload] Bot restarted successfully");
|
||||
return { restarted: true, reloaded, commands };
|
||||
} finally {
|
||||
client.__explicitStartup = false;
|
||||
client.__restarting = false;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
applyPresence,
|
||||
refreshSlashCommands,
|
||||
reloadBot,
|
||||
restartDiscordBot,
|
||||
};
|
||||
@@ -0,0 +1,180 @@
|
||||
const Discord = require("discord.js");
|
||||
const configStore = require("./ConfigStore.js");
|
||||
const {
|
||||
getPrimaryGuild,
|
||||
syncGuildHealth,
|
||||
} = require("./GuildManager.js");
|
||||
|
||||
const ACTIVITY_TYPES = {
|
||||
PLAYING: Discord.ActivityType.Playing,
|
||||
WATCHING: Discord.ActivityType.Watching,
|
||||
LISTENING: Discord.ActivityType.Listening,
|
||||
COMPETING: Discord.ActivityType.Competing,
|
||||
STREAMING: Discord.ActivityType.Streaming,
|
||||
};
|
||||
|
||||
function applyPresence(client) {
|
||||
if (!client?.user) return;
|
||||
|
||||
const activity = configStore.supportbot?.Activity;
|
||||
const typeKey = activity?.Type?.toUpperCase();
|
||||
const type = ACTIVITY_TYPES[typeKey] ?? Discord.ActivityType.Playing;
|
||||
const name = activity?.Status || "Online";
|
||||
|
||||
const activityPayload = { name, type };
|
||||
|
||||
if (typeKey === "STREAMING" && activity?.StreamingURL) {
|
||||
activityPayload.url = activity.StreamingURL;
|
||||
}
|
||||
|
||||
client.user.setPresence({
|
||||
activities: [activityPayload],
|
||||
status: "online",
|
||||
});
|
||||
}
|
||||
|
||||
const c = {
|
||||
reset: "\x1b[0m",
|
||||
gray: "\x1b[90m",
|
||||
green: "\x1b[32m",
|
||||
yellow: "\x1b[33m",
|
||||
bold: "\x1b[1m",
|
||||
white: "\x1b[37m",
|
||||
};
|
||||
|
||||
function line() {
|
||||
console.log(
|
||||
`${c.gray}────────────────────────────────────────────────────────${c.reset}`,
|
||||
);
|
||||
}
|
||||
|
||||
function section(title) {
|
||||
console.log(`\n${c.green}${title}${c.reset}`);
|
||||
console.log(`${c.gray}${"-".repeat(title.length)}${c.reset}`);
|
||||
}
|
||||
|
||||
function info(label, value) {
|
||||
console.log(`${c.gray}${label.padEnd(14)}${c.reset} ${value}`);
|
||||
}
|
||||
|
||||
function status(label, ok, details = "") {
|
||||
const icon = ok ? `${c.green}✓${c.reset}` : `${c.yellow}!${c.reset}`;
|
||||
console.log(` ${icon} ${label}${details ? ` (${details})` : ""}`);
|
||||
}
|
||||
|
||||
async function logStartup(client, options = {}) {
|
||||
const { clearConsole = true } = options;
|
||||
const supportbot = configStore.supportbot;
|
||||
|
||||
if (clearConsole) {
|
||||
console.clear();
|
||||
}
|
||||
|
||||
const botName = supportbot?.General?.Name || "SupportBot";
|
||||
const botVersion = supportbot?.SupportBot_Version || "Unknown";
|
||||
const { getBotInviteUrl } = require("./BotInvite.js");
|
||||
const inviteUrl = getBotInviteUrl(client.user.id);
|
||||
|
||||
line();
|
||||
console.log(`${c.bold}${c.green}SupportBot${c.reset}`);
|
||||
console.log(`${c.gray}Emerald Services${c.reset}`);
|
||||
line();
|
||||
|
||||
section("System");
|
||||
info("Bot Name", botName);
|
||||
info("Version", botVersion);
|
||||
info("Node.js", process.version);
|
||||
info("Discord.js", `v${Discord.version}`);
|
||||
info("Logged in as", client.user.tag);
|
||||
info("Guilds", `${client.guilds.cache.size}`);
|
||||
|
||||
section("Links");
|
||||
info("Docs", "https://emerald-services.gitbook.io/");
|
||||
info("Discord", "https://dsc.gg/emerald-dev");
|
||||
|
||||
if (configStore.api?.API?.Enabled) {
|
||||
const port = configStore.api?.API?.Port || 3000;
|
||||
info("Dashboard", `http://localhost:${port}`);
|
||||
}
|
||||
|
||||
info("Invite", inviteUrl);
|
||||
|
||||
console.log("");
|
||||
}
|
||||
|
||||
async function checkConfigs(client) {
|
||||
const supportbot = configStore.supportbot;
|
||||
const health = syncGuildHealth(client);
|
||||
|
||||
if (health.status !== "ok") {
|
||||
section("Guild");
|
||||
status("Guild setup", false, health.message || health.status);
|
||||
console.log("");
|
||||
}
|
||||
|
||||
const guild = getPrimaryGuild(client);
|
||||
if (!guild) return;
|
||||
|
||||
section("Config Check");
|
||||
|
||||
const roles = [
|
||||
supportbot?.Roles?.StaffMember?.Admin,
|
||||
supportbot?.Roles?.StaffMember?.Staff,
|
||||
supportbot?.Roles?.AutoRole?.Role,
|
||||
].filter(Boolean);
|
||||
|
||||
const channels = [supportbot?.Ticket?.TicketHome].filter(Boolean);
|
||||
|
||||
const [roleResults, channelResults] = await Promise.all([
|
||||
Promise.all(roles.map((role) => client.getRole(role, guild))),
|
||||
Promise.all(channels.map((channel) => client.getChannel(channel, guild))),
|
||||
]);
|
||||
|
||||
const missingRoles = roles.filter((_, i) => !roleResults[i]);
|
||||
const missingChannels = channels.filter((_, i) => !channelResults[i]);
|
||||
|
||||
if (!missingRoles.length && !missingChannels.length) {
|
||||
status("All configurations validated successfully", true);
|
||||
} else {
|
||||
if (missingRoles.length) {
|
||||
status("Missing roles", false, missingRoles.join(", "));
|
||||
}
|
||||
if (missingChannels.length) {
|
||||
status("Missing channels", false, missingChannels.join(", "));
|
||||
}
|
||||
}
|
||||
|
||||
console.log("");
|
||||
}
|
||||
|
||||
function logSlashCommandsRegistered(client) {
|
||||
const guild = getPrimaryGuild(client);
|
||||
if (!guild) return;
|
||||
console.log(
|
||||
`\n${c.green}✓${c.reset} ${c.white}Slash Commands Registered${c.reset} ${c.gray}for ${guild.name}${c.reset}`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Same work as Events/ready.js — safe to call after config reload reconnects.
|
||||
*/
|
||||
async function runBotReady(client, options = {}) {
|
||||
const { clearConsole = true, logCommands = false } = options;
|
||||
|
||||
syncGuildHealth(client);
|
||||
applyPresence(client);
|
||||
await logStartup(client, { clearConsole });
|
||||
await checkConfigs(client);
|
||||
|
||||
if (logCommands) {
|
||||
logSlashCommandsRegistered(client);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
applyPresence,
|
||||
logStartup,
|
||||
checkConfigs,
|
||||
logSlashCommandsRegistered,
|
||||
runBotReady,
|
||||
};
|
||||
+132
-117
@@ -1,11 +1,47 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const Discord = require("discord.js");
|
||||
const { GatewayIntentBits, Partials } = require('discord.js');
|
||||
const yaml = require("js-yaml");
|
||||
const { Command, Event } = require('./Addon.js'); // Adjust the path as needed
|
||||
const { GatewayIntentBits, Partials } = require("discord.js");
|
||||
const { Command, Event } = require("./Addon.js");
|
||||
const configStore = require("./ConfigStore.js");
|
||||
const { getPrimaryGuild } = require("./GuildManager.js");
|
||||
|
||||
const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8"));
|
||||
const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8"));
|
||||
const supportbot = configStore.supportbot;
|
||||
const cmdconfig = configStore.commands;
|
||||
|
||||
const c = {
|
||||
reset: "\x1b[0m",
|
||||
gray: "\x1b[90m",
|
||||
green: "\x1b[32m",
|
||||
yellow: "\x1b[33m",
|
||||
cyan: "\x1b[36m",
|
||||
red: "\x1b[31m",
|
||||
white: "\x1b[37m",
|
||||
bold: "\x1b[1m",
|
||||
};
|
||||
|
||||
function line() {
|
||||
console.log(`${c.gray}────────────────────────────────────────────────────────${c.reset}`);
|
||||
}
|
||||
|
||||
function section(title) {
|
||||
console.log(`\n${c.green}${title}${c.reset}`);
|
||||
console.log(`${c.gray}${"-".repeat(title.length)}${c.reset}`);
|
||||
}
|
||||
|
||||
function walk(dir, ext = ".js", fileList = []) {
|
||||
const files = fs.readdirSync(dir);
|
||||
for (const file of files) {
|
||||
const full = `${dir}/${file}`;
|
||||
const stat = fs.statSync(full);
|
||||
if (stat.isDirectory()) {
|
||||
walk(full, ext, fileList);
|
||||
} else if (file.endsWith(ext)) {
|
||||
fileList.push(full);
|
||||
}
|
||||
}
|
||||
return fileList;
|
||||
}
|
||||
|
||||
class Client extends Discord.Client {
|
||||
constructor() {
|
||||
@@ -13,164 +49,143 @@ class Client extends Discord.Client {
|
||||
intents: [
|
||||
GatewayIntentBits.Guilds,
|
||||
GatewayIntentBits.GuildMembers,
|
||||
GatewayIntentBits.GuildMessages
|
||||
GatewayIntentBits.GuildMessages,
|
||||
GatewayIntentBits.GuildMessageReactions,
|
||||
GatewayIntentBits.GuildEmojisAndStickers,
|
||||
GatewayIntentBits.MessageContent,
|
||||
],
|
||||
partials: [Partials.Channel]
|
||||
partials: [Partials.Message, Partials.Channel, Partials.Reaction],
|
||||
});
|
||||
|
||||
this.commands = new Discord.Collection();
|
||||
}
|
||||
|
||||
async getChannel(channel, guild) {
|
||||
if (!channel) return null; // Add check for undefined channel
|
||||
if (!channel) return null;
|
||||
const channelStr = String(channel).toLowerCase();
|
||||
return guild.channels.cache.find(
|
||||
(c) =>
|
||||
(c.type === Discord.ChannelType.GuildText || c.type === Discord.ChannelType.GuildNews) &&
|
||||
(c.id === channel || (c.name && c.name.toLowerCase() === channel.toLowerCase()))
|
||||
(c.id === channel || (c.name && c.name.toLowerCase() === channelStr)),
|
||||
);
|
||||
}
|
||||
|
||||
async getRole(role, guild) {
|
||||
if (!role) return null; // Add check for undefined role
|
||||
if (!role) return null;
|
||||
const roleStr = String(role).toLowerCase();
|
||||
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() === roleStr),
|
||||
);
|
||||
}
|
||||
|
||||
async getCategory(category, guild) {
|
||||
if (!category) return null; // Add check for undefined category
|
||||
if (!category) return null;
|
||||
const catStr = String(category).toLowerCase();
|
||||
return guild.channels.cache.find(
|
||||
(c) =>
|
||||
c.type === Discord.ChannelType.GuildCategory &&
|
||||
(c.id === category || (c.name && c.name.toLowerCase() === category.toLowerCase()))
|
||||
(c.id === category || (c.name && c.name.toLowerCase() === catStr)),
|
||||
);
|
||||
}
|
||||
|
||||
async start(token) {
|
||||
let tempCommandFiles = fs.readdirSync("./Commands").filter((file) => file.endsWith(".js"));
|
||||
const commandFiles = walk("./Commands");
|
||||
|
||||
if (cmdconfig.Suggestion.Enabled === false) {
|
||||
tempCommandFiles = tempCommandFiles.filter(item => item !== "suggest.js");
|
||||
}
|
||||
const allCommands = commandFiles.map((file) => ({
|
||||
file,
|
||||
command: require(`../${file}`),
|
||||
}));
|
||||
|
||||
if (cmdconfig.Help.Enabled === false) {
|
||||
tempCommandFiles = tempCommandFiles.filter(item => item !== "help.js");
|
||||
}
|
||||
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 (cmdconfig.Info.Enabled === false) {
|
||||
tempCommandFiles = tempCommandFiles.filter(item => item !== "info.js");
|
||||
}
|
||||
if (!matchingConfig) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (cmdconfig.OpenTicket.Enabled === false) {
|
||||
tempCommandFiles = tempCommandFiles.filter(item => item !== "ticket.js");
|
||||
}
|
||||
|
||||
if (cmdconfig.CloseTicket.Enabled === false) {
|
||||
tempCommandFiles = tempCommandFiles.filter(item => item !== "close.js");
|
||||
}
|
||||
|
||||
if (cmdconfig.Embed.Enabled === false) {
|
||||
tempCommandFiles = tempCommandFiles.filter(item => item !== "embed.js");
|
||||
}
|
||||
|
||||
if (cmdconfig.Translate.Enabled === false) {
|
||||
tempCommandFiles = tempCommandFiles.filter(item => item !== "translate.js");
|
||||
}
|
||||
|
||||
if (cmdconfig.UserInfo.Enabled === false) {
|
||||
tempCommandFiles = tempCommandFiles.filter(item => item !== "userinfo.js");
|
||||
}
|
||||
|
||||
if (cmdconfig.Ping.Enabled === false) {
|
||||
tempCommandFiles = tempCommandFiles.filter(item => item !== "Ping.js");
|
||||
}
|
||||
|
||||
if (supportbot.Ticket.ClaimTickets.Enabled === false) {
|
||||
tempCommandFiles = tempCommandFiles.filter(item => item !== "ticketstats.js");
|
||||
}
|
||||
|
||||
const commandFiles = tempCommandFiles;
|
||||
const commands = commandFiles.map((file) => require(`../Commands/${file}`));
|
||||
|
||||
// Load main commands
|
||||
console.log(`\u001b[34;1m`, "▬▬▬▬▬▬▬ Commands ▬▬▬▬▬▬▬");
|
||||
|
||||
commands.forEach((cmd) => {
|
||||
console.log(`\u001b[32m`, `[CMD]`, `\u001b[37;1m`, `${cmd.name}`, `\u001b[32;1m`, "Loaded");
|
||||
this.commands.set(cmd.name, cmd);
|
||||
return matchingConfig.Enabled !== false;
|
||||
});
|
||||
|
||||
// Load addon commands and events
|
||||
console.log(`\u001b[34;1m`, "▬▬▬▬▬▬▬ 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) {
|
||||
const addonFiles = fs.readdirSync("./Addons").filter((file) => file.endsWith(".js"));
|
||||
|
||||
const addons = addonFiles.map((file) => {
|
||||
const addon = require(`../Addons/${file}`);
|
||||
addon.name = file.split(".")[0];
|
||||
return addon;
|
||||
section("Addons");
|
||||
|
||||
addonFiles.forEach((file) => {
|
||||
let addon = require(`../Addons/${file}`);
|
||||
|
||||
// Handle single or multiple addons per file
|
||||
const addonArray = Array.isArray(addon) ? addon : [addon];
|
||||
|
||||
addonArray.forEach((item, index) => {
|
||||
// If name is missing, use filename (handling index for arrays)
|
||||
if (!item.name) {
|
||||
item.name = index === 0 ? file.split(".")[0] : `${file.split(".")[0]}_${index}`;
|
||||
}
|
||||
|
||||
console.log(`${c.green}✓${c.reset} ${c.white}${item.name}${c.reset}`);
|
||||
|
||||
if (item instanceof Command) {
|
||||
this.commands.set(item.name, item);
|
||||
}
|
||||
|
||||
if (item.events && Array.isArray(item.events)) {
|
||||
item.events.forEach((event) => {
|
||||
if (event instanceof Event) {
|
||||
this.on(event.event, (...args) => event.run(this, ...args));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// If it's a direct Event instance
|
||||
if (item instanceof Event) {
|
||||
this.on(item.event, (...args) => item.run(this, ...args));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Addons
|
||||
console.log(" ");
|
||||
console.log(`\u001b[34;1m`, "▬▬▬▬▬▬▬ Addons ▬▬▬▬▬▬▬");
|
||||
|
||||
addons.forEach((addon) => {
|
||||
console.log(
|
||||
`\u001b[32m`,
|
||||
`[ADDON]`,
|
||||
`\u001b[37;1m`,
|
||||
`${addon.name}`,
|
||||
`\u001b[32;1m`,
|
||||
"Loaded"
|
||||
);
|
||||
|
||||
if (addon instanceof Command) {
|
||||
this.commands.set(addon.name, addon);
|
||||
}
|
||||
|
||||
if (addon.events && Array.isArray(addon.events)) {
|
||||
addon.events.forEach((event) => {
|
||||
if (event instanceof Event) {
|
||||
this.on(event.event, (...args) => event.run(this, ...args));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`\u001b[34;1m`, "▬▬▬▬▬▬▬ Addons ▬▬▬▬▬▬▬");
|
||||
}
|
||||
|
||||
// Register Slash Commands
|
||||
|
||||
this.once("ready", async () => {
|
||||
await this.guilds.cache.first()?.commands.set(this.commands);
|
||||
console.log(
|
||||
"\u001b[32m",
|
||||
`[SUCCESS]`,
|
||||
`\u001b[37;1m`,
|
||||
`Slash Commands Registered for ${this.guilds.cache.first().name}`
|
||||
);
|
||||
this.on("clientReady", async () => {
|
||||
const guild = getPrimaryGuild(this);
|
||||
if (!guild) return;
|
||||
await guild.commands.set(this.commands);
|
||||
const { logSlashCommandsRegistered } = require("./BotStartup.js");
|
||||
logSlashCommandsRegistered(this);
|
||||
});
|
||||
|
||||
console.log(" ");
|
||||
console.log(`\u001b[34;1m`, "▬▬▬▬▬▬▬ Events ▬▬▬▬▬▬▬");
|
||||
section("Events");
|
||||
|
||||
fs.readdirSync("./Events").filter((file) => file.endsWith(".js")).forEach((file) => {
|
||||
const event = require(`../Events/${file}`);
|
||||
console.log(`\u001b[32m`, `[EVENT]`, `\u001b[37;1m`, `${event.event}`, `\u001b[32;1m`, "Loaded");
|
||||
this.on(event.event, (...args) => event.run(this, ...args));
|
||||
});
|
||||
fs.readdirSync("./Events")
|
||||
.filter((file) => file.endsWith(".js"))
|
||||
.forEach((file) => {
|
||||
const event = require(`../Events/${file}`);
|
||||
console.log(
|
||||
`${c.green}✓${c.reset} ${c.white}${event.event}${c.reset} ${c.gray}(${file})${c.reset}`,
|
||||
);
|
||||
this.on(event.event, (...args) => event.run(this, ...args));
|
||||
});
|
||||
|
||||
console.log("");
|
||||
|
||||
console.log(`\u001b[34;1m`, "▬▬▬▬▬▬▬ Events ▬▬▬▬▬▬▬");
|
||||
if (process.argv[2] !== "test") {
|
||||
await this.login(token);
|
||||
} else {
|
||||
console.log(
|
||||
`\u001b[31;1m`,
|
||||
"RUNNING IN TEST MODE, IF NOT INTENDED, PLEASE USE npm start"
|
||||
);
|
||||
console.log(`${c.red}RUNNING IN TEST MODE, IF NOT INTENDED, PLEASE USE npm start${c.reset}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
// Live config objects — reload mutates in place so existing references stay valid.
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const yaml = require("js-yaml");
|
||||
|
||||
const CONFIG_DIR = path.join(__dirname, "../Configs");
|
||||
|
||||
const FILE_KEYS = [
|
||||
"supportbot",
|
||||
"commands",
|
||||
"messages",
|
||||
"ticket-panel",
|
||||
"supportbot-ai",
|
||||
"api",
|
||||
];
|
||||
|
||||
function readConfig(name) {
|
||||
return yaml.load(fs.readFileSync(path.join(CONFIG_DIR, `${name}.yml`), "utf8"));
|
||||
}
|
||||
|
||||
function deepAssign(target, source) {
|
||||
if (!target || typeof target !== "object" || Array.isArray(target)) {
|
||||
return source;
|
||||
}
|
||||
if (!source || typeof source !== "object" || Array.isArray(source)) {
|
||||
return source;
|
||||
}
|
||||
|
||||
for (const key of Object.keys(source)) {
|
||||
const next = source[key];
|
||||
if (
|
||||
next &&
|
||||
typeof next === "object" &&
|
||||
!Array.isArray(next) &&
|
||||
target[key] &&
|
||||
typeof target[key] === "object" &&
|
||||
!Array.isArray(target[key])
|
||||
) {
|
||||
deepAssign(target[key], next);
|
||||
} else {
|
||||
target[key] = next;
|
||||
}
|
||||
}
|
||||
|
||||
for (const key of Object.keys(target)) {
|
||||
if (!(key in source)) {
|
||||
delete target[key];
|
||||
}
|
||||
}
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
const store = {};
|
||||
for (const name of FILE_KEYS) {
|
||||
store[name] = readConfig(name);
|
||||
}
|
||||
|
||||
function reloadFile(fileKey) {
|
||||
if (!FILE_KEYS.includes(fileKey)) {
|
||||
throw new Error(`Unknown config: ${fileKey}`);
|
||||
}
|
||||
const fresh = readConfig(fileKey);
|
||||
deepAssign(store[fileKey], fresh);
|
||||
return store[fileKey];
|
||||
}
|
||||
|
||||
function reloadBotConfigs() {
|
||||
const reloaded = [];
|
||||
for (const name of FILE_KEYS) {
|
||||
if (name === "api") continue;
|
||||
reloadFile(name);
|
||||
reloaded.push(name);
|
||||
}
|
||||
return reloaded;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
supportbot: store.supportbot,
|
||||
commands: store.commands,
|
||||
messages: store.messages,
|
||||
ticketPanel: store["ticket-panel"],
|
||||
supportbotAi: store["supportbot-ai"],
|
||||
api: store.api,
|
||||
store,
|
||||
reloadFile,
|
||||
reloadBotConfigs,
|
||||
readConfig,
|
||||
};
|
||||
@@ -0,0 +1,229 @@
|
||||
/**
|
||||
* Dashboard alert registry — server-side providers for actionable banners.
|
||||
*
|
||||
* Register a provider to surface new alerts anywhere in the dashboard:
|
||||
*
|
||||
* const { dashboardAlerts } = require("./DashboardAlerts.js");
|
||||
* dashboardAlerts.register("my-addon", (ctx) => {
|
||||
* if (!ctx.configStore.myAddon?.Configured) {
|
||||
* return [{
|
||||
* id: "my-addon:setup",
|
||||
* severity: "warning",
|
||||
* category: "my-addon",
|
||||
* title: "My addon needs setup",
|
||||
* message: "Finish configuration to enable this feature.",
|
||||
* action: { label: "Configure", href: "/configs?file=my-addon" },
|
||||
* }];
|
||||
* }
|
||||
* return [];
|
||||
* });
|
||||
*/
|
||||
const { evaluateGuildHealth } = require("./GuildManager.js");
|
||||
const { evaluateAllModules } = require("./ModuleStatus.js");
|
||||
|
||||
const SEVERITY_ORDER = { error: 0, warning: 1, info: 2 };
|
||||
|
||||
const MODULE_LINKS = {
|
||||
tickets: {
|
||||
title: "Ticket system",
|
||||
href: "/configs?file=ticket-panel",
|
||||
},
|
||||
supportbot: {
|
||||
title: "Core bot settings",
|
||||
href: "/configs?file=supportbot",
|
||||
},
|
||||
transcripts: {
|
||||
title: "Ticket transcripts",
|
||||
href: "/transcripts",
|
||||
},
|
||||
suggestions: {
|
||||
title: "Suggestions",
|
||||
href: "/configs?file=messages",
|
||||
},
|
||||
ai: {
|
||||
title: "AI assistant",
|
||||
href: "/configs?file=supportbot-ai",
|
||||
},
|
||||
commands: {
|
||||
title: "Commands",
|
||||
href: "/configs?file=commands",
|
||||
},
|
||||
};
|
||||
|
||||
class DashboardAlertRegistry {
|
||||
constructor() {
|
||||
/** @type {Map<string, (ctx: DashboardAlertContext) => DashboardAlert[] | Promise<DashboardAlert[]>>} */
|
||||
this.providers = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} id Unique provider id (used for debugging / overrides)
|
||||
* @param {(ctx: DashboardAlertContext) => DashboardAlert[] | Promise<DashboardAlert[]>} fn
|
||||
*/
|
||||
register(id, fn) {
|
||||
this.providers.set(id, fn);
|
||||
return this;
|
||||
}
|
||||
|
||||
unregister(id) {
|
||||
this.providers.delete(id);
|
||||
}
|
||||
|
||||
/** @param {DashboardAlertContext} ctx */
|
||||
async evaluate(ctx) {
|
||||
const alerts = [];
|
||||
for (const fn of this.providers.values()) {
|
||||
const result = await fn(ctx);
|
||||
if (Array.isArray(result) && result.length) {
|
||||
alerts.push(...result);
|
||||
}
|
||||
}
|
||||
return sortAlerts(alerts);
|
||||
}
|
||||
}
|
||||
|
||||
function sortAlerts(alerts) {
|
||||
return [...alerts].sort((a, b) => {
|
||||
const sa = SEVERITY_ORDER[a.severity] ?? 9;
|
||||
const sb = SEVERITY_ORDER[b.severity] ?? 9;
|
||||
if (sa !== sb) return sa - sb;
|
||||
return a.title.localeCompare(b.title);
|
||||
});
|
||||
}
|
||||
|
||||
function guildSeverity(status) {
|
||||
if (status === "offline") return "error";
|
||||
return "warning";
|
||||
}
|
||||
|
||||
function shouldSkipModuleAlert(moduleId, info) {
|
||||
if (info.status === "done") return true;
|
||||
if (moduleId === "ai" && info.subtitle === "AI is disabled in config") return true;
|
||||
// Transcripts are automatic — empty archive is normal, not a setup step.
|
||||
if (moduleId === "transcripts") return true;
|
||||
// Partially configured modules are tracked on Overview — only banner when nothing is done yet.
|
||||
if (info.status === "progress" && (info.passed ?? 0) > 0) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function moduleSeverity(moduleId, info) {
|
||||
if (info.status === "todo") {
|
||||
return moduleId === "tickets" || moduleId === "supportbot" ? "warning" : "info";
|
||||
}
|
||||
return "info";
|
||||
}
|
||||
|
||||
function evaluateGuildAlerts(ctx) {
|
||||
const { client } = ctx;
|
||||
if (!client?.user) {
|
||||
return [
|
||||
{
|
||||
id: "guild:offline",
|
||||
severity: "error",
|
||||
category: "guild",
|
||||
title: "Bot offline",
|
||||
message: "The bot is not connected to Discord. Check your token and restart the server.",
|
||||
action: { label: "Bot config", href: "/configs?file=supportbot" },
|
||||
meta: { status: "offline" },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const health = evaluateGuildHealth(client);
|
||||
if (health.status === "ok") return [];
|
||||
|
||||
return [
|
||||
{
|
||||
id: "guild:health",
|
||||
severity: guildSeverity(health.status),
|
||||
category: "guild",
|
||||
title: "Server setup required",
|
||||
message: health.message,
|
||||
action: { label: "Bot config", href: "/configs?file=supportbot" },
|
||||
meta: {
|
||||
status: health.status,
|
||||
configuredGuildId: health.configuredGuildId,
|
||||
configuredGuild: health.configuredGuild,
|
||||
extraGuilds: health.extraGuilds,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function evaluateModuleAlerts(ctx) {
|
||||
const { configStore, transcriptCount = 0 } = ctx;
|
||||
const modules = evaluateAllModules(configStore, { transcriptCount });
|
||||
const alerts = [];
|
||||
|
||||
for (const [moduleId, info] of Object.entries(modules)) {
|
||||
if (shouldSkipModuleAlert(moduleId, info)) continue;
|
||||
|
||||
const link = MODULE_LINKS[moduleId] || {
|
||||
title: moduleId,
|
||||
href: "/configs",
|
||||
};
|
||||
|
||||
const progress =
|
||||
info.total > 0 ? `${info.passed ?? 0} of ${info.total} checks complete` : null;
|
||||
|
||||
alerts.push({
|
||||
id: `module:${moduleId}`,
|
||||
severity: moduleSeverity(moduleId, info),
|
||||
category: "modules",
|
||||
title:
|
||||
info.status === "todo"
|
||||
? `${link.title} — setup required`
|
||||
: `${link.title} — in progress`,
|
||||
message: info.subtitle,
|
||||
action: { label: "Open module", href: link.href },
|
||||
meta: {
|
||||
moduleId,
|
||||
status: info.status,
|
||||
passed: info.passed,
|
||||
total: info.total,
|
||||
progress,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return alerts;
|
||||
}
|
||||
|
||||
const dashboardAlerts = new DashboardAlertRegistry();
|
||||
dashboardAlerts.register("guild", evaluateGuildAlerts);
|
||||
dashboardAlerts.register("modules", evaluateModuleAlerts);
|
||||
|
||||
/**
|
||||
* @typedef {Object} DashboardAlertAction
|
||||
* @property {string} label
|
||||
* @property {string} href
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} DashboardAlert
|
||||
* @property {string} id
|
||||
* @property {"error"|"warning"|"info"} severity
|
||||
* @property {string} category
|
||||
* @property {string} title
|
||||
* @property {string} message
|
||||
* @property {DashboardAlertAction} [action]
|
||||
* @property {Record<string, unknown>} [meta]
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} DashboardAlertContext
|
||||
* @property {import("discord.js").Client} [client]
|
||||
* @property {import("./ConfigStore.js")} configStore
|
||||
* @property {number} [transcriptCount]
|
||||
*/
|
||||
|
||||
async function evaluateDashboardAlerts(ctx) {
|
||||
return dashboardAlerts.evaluate(ctx);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
dashboardAlerts,
|
||||
evaluateDashboardAlerts,
|
||||
sortAlerts,
|
||||
MODULE_LINKS,
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
const { fullPermissions, hasPermission, canAccessConfig } = require("./DashboardPermissions.js");
|
||||
|
||||
function grantServiceAccess(req) {
|
||||
req.dashboardService = true;
|
||||
req.dashboardPermissions = fullPermissions();
|
||||
req.dashboardRole = "service";
|
||||
req.dashboardIsOwner = true;
|
||||
}
|
||||
|
||||
function forbid(res, message = "Forbidden") {
|
||||
return res.status(403).json({ success: false, error: message });
|
||||
}
|
||||
|
||||
function createPermissionMiddleware(getPermissions) {
|
||||
return function requirePermission(permission) {
|
||||
return (req, res, next) => {
|
||||
if (req.dashboardService) return next();
|
||||
const perms = getPermissions(req) || req.dashboardPermissions;
|
||||
if (hasPermission(perms, permission)) return next();
|
||||
return forbid(res);
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
function createConfigMiddleware(getPermissions) {
|
||||
return function requireConfigAccess(mode = "view") {
|
||||
return (req, res, next) => {
|
||||
if (req.dashboardService) return next();
|
||||
const perms = getPermissions(req) || req.dashboardPermissions;
|
||||
const file = req.params?.file || req.body?.filename;
|
||||
if (!file) return next();
|
||||
if (!canAccessConfig(perms, file, mode)) return forbid(res);
|
||||
return next();
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
function assertConfigFilesEditable(permissions, files, res) {
|
||||
if (!permissions) return forbid(res);
|
||||
for (const file of files) {
|
||||
if (!canAccessConfig(permissions, file, "edit")) {
|
||||
return forbid(res, `No permission to edit ${file}.yml`);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
grantServiceAccess,
|
||||
forbid,
|
||||
createPermissionMiddleware,
|
||||
createConfigMiddleware,
|
||||
assertConfigFilesEditable,
|
||||
fullPermissions,
|
||||
hasPermission,
|
||||
canAccessConfig,
|
||||
};
|
||||
@@ -0,0 +1,168 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const BRANDING_DIR = path.join(__dirname, '../Data/branding');
|
||||
const BRANDING_FILE = path.join(BRANDING_DIR, 'branding.json');
|
||||
const FAVICON_BASENAME = 'favicon';
|
||||
|
||||
const DEFAULTS = {
|
||||
title: 'SupportBot',
|
||||
pageTitle: 'SupportBot Dashboard',
|
||||
faviconFile: null,
|
||||
faviconMime: null,
|
||||
updatedAt: null,
|
||||
};
|
||||
|
||||
const ALLOWED_MIME = new Set([
|
||||
'image/png',
|
||||
'image/jpeg',
|
||||
'image/x-icon',
|
||||
'image/vnd.microsoft.icon',
|
||||
'image/svg+xml',
|
||||
'image/webp',
|
||||
]);
|
||||
|
||||
const MIME_EXT = {
|
||||
'image/png': '.png',
|
||||
'image/jpeg': '.jpg',
|
||||
'image/x-icon': '.ico',
|
||||
'image/vnd.microsoft.icon': '.ico',
|
||||
'image/svg+xml': '.svg',
|
||||
'image/webp': '.webp',
|
||||
};
|
||||
|
||||
function ensureDir() {
|
||||
if (!fs.existsSync(BRANDING_DIR)) {
|
||||
fs.mkdirSync(BRANDING_DIR, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
function readBranding() {
|
||||
ensureDir();
|
||||
if (!fs.existsSync(BRANDING_FILE)) {
|
||||
return { ...DEFAULTS };
|
||||
}
|
||||
try {
|
||||
const raw = JSON.parse(fs.readFileSync(BRANDING_FILE, 'utf8'));
|
||||
return {
|
||||
title: String(raw.title || DEFAULTS.title).slice(0, 64),
|
||||
pageTitle: String(raw.pageTitle || DEFAULTS.pageTitle).slice(0, 80),
|
||||
faviconFile: raw.faviconFile || null,
|
||||
faviconMime: raw.faviconMime || null,
|
||||
updatedAt: raw.updatedAt || null,
|
||||
};
|
||||
} catch {
|
||||
return { ...DEFAULTS };
|
||||
}
|
||||
}
|
||||
|
||||
function writeBranding(data) {
|
||||
ensureDir();
|
||||
const next = {
|
||||
title: String(data.title || DEFAULTS.title).slice(0, 64),
|
||||
pageTitle: String(data.pageTitle || DEFAULTS.pageTitle).slice(0, 80),
|
||||
faviconFile: data.faviconFile ?? null,
|
||||
faviconMime: data.faviconMime ?? null,
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
fs.writeFileSync(BRANDING_FILE, JSON.stringify(next, null, 2), 'utf8');
|
||||
return next;
|
||||
}
|
||||
|
||||
function publicPayload(branding) {
|
||||
const hasFavicon = Boolean(branding.faviconFile);
|
||||
return {
|
||||
title: branding.title,
|
||||
pageTitle: branding.pageTitle,
|
||||
hasFavicon,
|
||||
faviconUrl: hasFavicon ? '/api/branding/favicon' : null,
|
||||
updatedAt: branding.updatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
function updateBranding({ title, pageTitle }) {
|
||||
const current = readBranding();
|
||||
return writeBranding({
|
||||
...current,
|
||||
title: title !== undefined ? title : current.title,
|
||||
pageTitle: pageTitle !== undefined ? pageTitle : current.pageTitle,
|
||||
});
|
||||
}
|
||||
|
||||
function removeExistingFavicons() {
|
||||
if (!fs.existsSync(BRANDING_DIR)) return;
|
||||
for (const file of fs.readdirSync(BRANDING_DIR)) {
|
||||
if (file.startsWith(`${FAVICON_BASENAME}.`)) {
|
||||
fs.unlinkSync(path.join(BRANDING_DIR, file));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function saveFaviconFromBase64(dataUrl) {
|
||||
const match = String(dataUrl || '').match(
|
||||
/^data:([\w/+.-]+);base64,([A-Za-z0-9+/=]+)$/,
|
||||
);
|
||||
if (!match) {
|
||||
throw new Error('Invalid image data. Upload a PNG, ICO, SVG, or WebP file.');
|
||||
}
|
||||
|
||||
const mime = match[1];
|
||||
if (!ALLOWED_MIME.has(mime)) {
|
||||
throw new Error('Unsupported image type. Use PNG, ICO, SVG, JPEG, or WebP.');
|
||||
}
|
||||
|
||||
const buffer = Buffer.from(match[2], 'base64');
|
||||
if (buffer.length > 512 * 1024) {
|
||||
throw new Error('Favicon must be 512 KB or smaller.');
|
||||
}
|
||||
|
||||
ensureDir();
|
||||
removeExistingFavicons();
|
||||
|
||||
const ext = MIME_EXT[mime] || '.png';
|
||||
const filename = `${FAVICON_BASENAME}${ext}`;
|
||||
const filePath = path.join(BRANDING_DIR, filename);
|
||||
fs.writeFileSync(filePath, buffer);
|
||||
|
||||
const current = readBranding();
|
||||
return writeBranding({
|
||||
...current,
|
||||
faviconFile: filename,
|
||||
faviconMime: mime,
|
||||
});
|
||||
}
|
||||
|
||||
function clearFavicon() {
|
||||
removeExistingFavicons();
|
||||
const current = readBranding();
|
||||
return writeBranding({
|
||||
...current,
|
||||
faviconFile: null,
|
||||
faviconMime: null,
|
||||
});
|
||||
}
|
||||
|
||||
function getFaviconPath() {
|
||||
const branding = readBranding();
|
||||
if (!branding.faviconFile) return null;
|
||||
const filePath = path.join(BRANDING_DIR, branding.faviconFile);
|
||||
if (!fs.existsSync(filePath)) return null;
|
||||
return { filePath, mime: branding.faviconMime || 'image/png' };
|
||||
}
|
||||
|
||||
function resetBranding() {
|
||||
clearFavicon();
|
||||
return writeBranding({ ...DEFAULTS });
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
readBranding,
|
||||
writeBranding,
|
||||
publicPayload,
|
||||
updateBranding,
|
||||
saveFaviconFromBase64,
|
||||
clearFavicon,
|
||||
getFaviconPath,
|
||||
resetBranding,
|
||||
DEFAULTS,
|
||||
};
|
||||
@@ -0,0 +1,135 @@
|
||||
const CONFIG_FILES = [
|
||||
"supportbot",
|
||||
"ticket-panel",
|
||||
"commands",
|
||||
"messages",
|
||||
"supportbot-ai",
|
||||
"api",
|
||||
];
|
||||
|
||||
const ROLES = ["owner", "admin", "moderator", "editor", "viewer", "custom"];
|
||||
|
||||
function configMap(view, edit) {
|
||||
const configs = {};
|
||||
for (const file of CONFIG_FILES) {
|
||||
configs[file] = { view: Boolean(view), edit: Boolean(edit) };
|
||||
}
|
||||
return configs;
|
||||
}
|
||||
|
||||
function fullPermissions() {
|
||||
return {
|
||||
overview: true,
|
||||
logs: true,
|
||||
transcripts: true,
|
||||
settings: { view: true, update: true },
|
||||
configs: configMap(true, true),
|
||||
users: { view: true, manage: true },
|
||||
};
|
||||
}
|
||||
|
||||
function permissionsFromRole(role, customPermissions) {
|
||||
switch (role) {
|
||||
case "owner":
|
||||
case "admin":
|
||||
return fullPermissions();
|
||||
case "moderator":
|
||||
return {
|
||||
overview: true,
|
||||
logs: true,
|
||||
transcripts: true,
|
||||
settings: { view: true, update: false },
|
||||
configs: configMap(true, false),
|
||||
users: { view: false, manage: false },
|
||||
};
|
||||
case "editor":
|
||||
return {
|
||||
overview: true,
|
||||
logs: true,
|
||||
transcripts: false,
|
||||
settings: { view: true, update: false },
|
||||
configs: configMap(true, true),
|
||||
users: { view: false, manage: false },
|
||||
};
|
||||
case "viewer":
|
||||
return {
|
||||
overview: true,
|
||||
logs: true,
|
||||
transcripts: true,
|
||||
settings: { view: true, update: false },
|
||||
configs: configMap(true, false),
|
||||
users: { view: false, manage: false },
|
||||
};
|
||||
case "custom":
|
||||
return normalizePermissions(customPermissions);
|
||||
default:
|
||||
return permissionsFromRole("viewer");
|
||||
}
|
||||
}
|
||||
|
||||
function normalizePermissions(input) {
|
||||
const base = permissionsFromRole("viewer");
|
||||
if (!input || typeof input !== "object") return base;
|
||||
|
||||
return {
|
||||
overview: input.overview ?? base.overview,
|
||||
logs: input.logs ?? base.logs,
|
||||
transcripts: input.transcripts ?? base.transcripts,
|
||||
settings: {
|
||||
view: input.settings?.view ?? base.settings.view,
|
||||
update: input.settings?.update ?? base.settings.update,
|
||||
},
|
||||
configs: CONFIG_FILES.reduce((acc, file) => {
|
||||
acc[file] = {
|
||||
view: input.configs?.[file]?.view ?? base.configs[file].view,
|
||||
edit: input.configs?.[file]?.edit ?? base.configs[file].edit,
|
||||
};
|
||||
return acc;
|
||||
}, {}),
|
||||
users: {
|
||||
view: input.users?.view ?? base.users.view,
|
||||
manage: input.users?.manage ?? base.users.manage,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function hasPermission(permissions, check) {
|
||||
if (!permissions) return false;
|
||||
|
||||
if (check === "overview") return Boolean(permissions.overview);
|
||||
if (check === "logs") return Boolean(permissions.logs);
|
||||
if (check === "transcripts") return Boolean(permissions.transcripts);
|
||||
if (check === "settings.view") return Boolean(permissions.settings?.view);
|
||||
if (check === "settings.update") return Boolean(permissions.settings?.update);
|
||||
if (check === "users.view") return Boolean(permissions.users?.view);
|
||||
if (check === "users.manage") return Boolean(permissions.users?.manage);
|
||||
|
||||
const configView = check.match(/^configs\.([a-z0-9-]+)\.view$/);
|
||||
if (configView) {
|
||||
const file = configView[1];
|
||||
return Boolean(permissions.configs?.[file]?.view);
|
||||
}
|
||||
|
||||
const configEdit = check.match(/^configs\.([a-z0-9-]+)\.edit$/);
|
||||
if (configEdit) {
|
||||
const file = configEdit[1];
|
||||
return Boolean(permissions.configs?.[file]?.edit);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function canAccessConfig(permissions, file, mode = "view") {
|
||||
if (!CONFIG_FILES.includes(file)) return false;
|
||||
return hasPermission(permissions, `configs.${file}.${mode}`);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
CONFIG_FILES,
|
||||
ROLES,
|
||||
fullPermissions,
|
||||
permissionsFromRole,
|
||||
normalizePermissions,
|
||||
hasPermission,
|
||||
canAccessConfig,
|
||||
};
|
||||
@@ -0,0 +1,75 @@
|
||||
const crypto = require("crypto");
|
||||
|
||||
const SESSION_COOKIE = "sb_dashboard";
|
||||
const SESSION_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
||||
|
||||
function signSession(payload, secret) {
|
||||
const body = Buffer.from(JSON.stringify(payload)).toString("base64url");
|
||||
const sig = crypto
|
||||
.createHmac("sha256", secret)
|
||||
.update(body)
|
||||
.digest("base64url");
|
||||
return `${body}.${sig}`;
|
||||
}
|
||||
|
||||
function verifySession(token, secret) {
|
||||
if (!token || typeof token !== "string") return null;
|
||||
const dot = token.lastIndexOf(".");
|
||||
if (dot <= 0) return null;
|
||||
|
||||
const body = token.slice(0, dot);
|
||||
const sig = token.slice(dot + 1);
|
||||
const expected = crypto
|
||||
.createHmac("sha256", secret)
|
||||
.update(body)
|
||||
.digest("base64url");
|
||||
|
||||
if (sig.length !== expected.length) return null;
|
||||
if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = JSON.parse(
|
||||
Buffer.from(body, "base64url").toString("utf8"),
|
||||
);
|
||||
if (!payload?.userId || !payload?.exp) return null;
|
||||
if (Date.now() > payload.exp) return null;
|
||||
return payload;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function createSessionPayload(user) {
|
||||
return {
|
||||
userId: user.id,
|
||||
username: user.username,
|
||||
globalName: user.global_name || null,
|
||||
avatar: user.avatar || null,
|
||||
exp: Date.now() + SESSION_MAX_AGE_MS,
|
||||
};
|
||||
}
|
||||
|
||||
function sessionCookieOptions(maxAgeMs = SESSION_MAX_AGE_MS, opts = {}) {
|
||||
const secure =
|
||||
opts.secure !== undefined
|
||||
? Boolean(opts.secure)
|
||||
: process.env.NODE_ENV === "production";
|
||||
return {
|
||||
httpOnly: true,
|
||||
secure,
|
||||
sameSite: "lax",
|
||||
maxAge: maxAgeMs,
|
||||
path: "/",
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
SESSION_COOKIE,
|
||||
SESSION_MAX_AGE_MS,
|
||||
signSession,
|
||||
verifySession,
|
||||
createSessionPayload,
|
||||
sessionCookieOptions,
|
||||
};
|
||||
@@ -0,0 +1,440 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const axios = require("axios");
|
||||
const YAML = require("yaml");
|
||||
const configStore = require("./ConfigStore.js");
|
||||
const { CALLBACK_PATH } = require("./DiscordOAuth.js");
|
||||
|
||||
const DISCORD_API = "https://discord.com/api/v10";
|
||||
const CONFIG_DIR = path.join(__dirname, "../Configs");
|
||||
const API_FILE = path.join(CONFIG_DIR, "api.yml");
|
||||
const SUPPORTBOT_FILE = path.join(CONFIG_DIR, "supportbot.yml");
|
||||
|
||||
const PLACEHOLDER_PATTERNS = [
|
||||
/DISCORD_CLIENT_ID/i,
|
||||
/DISCORD_CLIENT_SECRET/i,
|
||||
/YOUR_DISCORD_USER_ID/i,
|
||||
/CHANGE_ME/i,
|
||||
/BOT_TOKEN/i,
|
||||
/PUT_RANDOM_STRING/i,
|
||||
];
|
||||
|
||||
function isEmpty(value) {
|
||||
if (value == null) return true;
|
||||
if (typeof value === "boolean") return false;
|
||||
if (typeof value === "number") return false;
|
||||
return String(value).trim() === "";
|
||||
}
|
||||
|
||||
function isPlaceholder(value) {
|
||||
if (isEmpty(value)) return true;
|
||||
const s = String(value).trim();
|
||||
return PLACEHOLDER_PATTERNS.some((p) => p.test(s));
|
||||
}
|
||||
|
||||
function isDiscordSnowflake(value) {
|
||||
return /^\d{17,20}$/.test(String(value).trim());
|
||||
}
|
||||
|
||||
function isValidBotToken(value) {
|
||||
if (isPlaceholder(value)) return false;
|
||||
const s = String(value).trim();
|
||||
return s.length >= 50 && s.includes(".");
|
||||
}
|
||||
|
||||
function isValidSecretKey(value) {
|
||||
if (isPlaceholder(value)) return false;
|
||||
return String(value).trim().length >= 16;
|
||||
}
|
||||
|
||||
function readApiRoot() {
|
||||
if (!fs.existsSync(API_FILE)) return null;
|
||||
try {
|
||||
const raw = YAML.parse(fs.readFileSync(API_FILE, "utf8"));
|
||||
return raw?.API ?? raw ?? null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function readSupportbotRoot() {
|
||||
if (!fs.existsSync(SUPPORTBOT_FILE)) return {};
|
||||
try {
|
||||
return YAML.parse(fs.readFileSync(SUPPORTBOT_FILE, "utf8")) || {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function getOwnerIds(oauth) {
|
||||
const ids = [];
|
||||
for (const id of oauth?.OwnerUserIds || []) {
|
||||
const s = String(id).trim();
|
||||
if (s) ids.push(s);
|
||||
}
|
||||
if (ids.length === 0) {
|
||||
for (const id of oauth?.AllowedUserIds || []) {
|
||||
const s = String(id).trim();
|
||||
if (s) ids.push(s);
|
||||
}
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
function buildSteps(apiRoot, supportbot) {
|
||||
const oauth = apiRoot?.OAuth || {};
|
||||
const token = supportbot?.General?.Token;
|
||||
|
||||
return [
|
||||
{
|
||||
id: "botToken",
|
||||
title: "Bot token",
|
||||
description: "From the Discord Developer Portal → your application → Bot → Reset Token.",
|
||||
complete: isValidBotToken(token),
|
||||
},
|
||||
{
|
||||
id: "secretKey",
|
||||
title: "Dashboard session secret",
|
||||
description: "Random string used to sign login cookies. Keep it private.",
|
||||
complete: isValidSecretKey(apiRoot?.SecretKey),
|
||||
},
|
||||
{
|
||||
id: "oauth",
|
||||
title: "Discord OAuth app",
|
||||
description: "Same application as your bot — Client ID and Client Secret from OAuth2.",
|
||||
complete:
|
||||
!isPlaceholder(oauth.ClientId) &&
|
||||
!isPlaceholder(oauth.ClientSecret) &&
|
||||
isDiscordSnowflake(oauth.ClientId),
|
||||
},
|
||||
{
|
||||
id: "redirectUri",
|
||||
title: "OAuth redirect URL",
|
||||
description: `Add this exact URL under OAuth2 → Redirects in the Discord portal.`,
|
||||
complete: isValidRedirectUriConfigured(oauth.RedirectUri),
|
||||
},
|
||||
{
|
||||
id: "owners",
|
||||
title: "Dashboard owners",
|
||||
description: "Discord user IDs that can sign in and manage the dashboard (at least one).",
|
||||
complete: getOwnerIds(oauth).length > 0 && getOwnerIds(oauth).every(isDiscordSnowflake),
|
||||
},
|
||||
{
|
||||
id: "emeraldApi",
|
||||
title: "Emerald API",
|
||||
description: "Optional. Used for the one-click addon installer.",
|
||||
complete: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function isValidRedirectUriConfigured(uri) {
|
||||
if (isPlaceholder(uri)) return false;
|
||||
try {
|
||||
const u = new URL(String(uri).trim());
|
||||
if (!["http:", "https:"].includes(u.protocol)) return false;
|
||||
return u.pathname.replace(/\/$/, "") === CALLBACK_PATH.replace(/\/$/, "");
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isSetupComplete() {
|
||||
const apiRoot = readApiRoot();
|
||||
if (!apiRoot?.Enabled) return false;
|
||||
const oauth = apiRoot.OAuth || {};
|
||||
if (!oauth.Enabled) return false;
|
||||
const steps = buildSteps(apiRoot, readSupportbotRoot());
|
||||
return steps.every((s) => s.complete);
|
||||
}
|
||||
|
||||
function getSetupStatus(req) {
|
||||
const apiRoot = readApiRoot() || {};
|
||||
const steps = buildSteps(apiRoot, readSupportbotRoot());
|
||||
const complete = steps.every((s) => s.complete);
|
||||
|
||||
let suggestedRedirectUri = null;
|
||||
if (req) {
|
||||
const proto =
|
||||
req.get("x-forwarded-proto")?.split(",")[0]?.trim() ||
|
||||
req.protocol ||
|
||||
"http";
|
||||
const host = req.get("x-forwarded-host")?.split(",")[0]?.trim() || req.get("host");
|
||||
if (host) {
|
||||
suggestedRedirectUri = `${proto}://${host}${CALLBACK_PATH}`;
|
||||
}
|
||||
}
|
||||
if (!suggestedRedirectUri) {
|
||||
suggestedRedirectUri = `http://localhost:${apiRoot.Port || 3000}${CALLBACK_PATH}`;
|
||||
}
|
||||
|
||||
return {
|
||||
complete,
|
||||
steps,
|
||||
suggestedRedirectUri,
|
||||
oauthCallbackPath: CALLBACK_PATH,
|
||||
};
|
||||
}
|
||||
|
||||
async function validateBotToken(token) {
|
||||
const trimmed = String(token || "").trim();
|
||||
if (!isValidBotToken(trimmed)) {
|
||||
return { ok: false, error: "Enter a valid bot token from the Discord Developer Portal." };
|
||||
}
|
||||
|
||||
try {
|
||||
const [userRes, appRes] = await Promise.all([
|
||||
axios.get(`${DISCORD_API}/users/@me`, {
|
||||
headers: { Authorization: `Bot ${trimmed}` },
|
||||
timeout: 12_000,
|
||||
}),
|
||||
axios.get(`${DISCORD_API}/oauth2/applications/@me`, {
|
||||
headers: { Authorization: `Bot ${trimmed}` },
|
||||
timeout: 12_000,
|
||||
}),
|
||||
]);
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
bot: {
|
||||
id: userRes.data.id,
|
||||
username: userRes.data.username,
|
||||
discriminator: userRes.data.discriminator,
|
||||
},
|
||||
applicationId: String(appRes.data.id),
|
||||
};
|
||||
} catch (err) {
|
||||
const status = err.response?.status;
|
||||
if (status === 401) {
|
||||
return { ok: false, error: "Discord rejected this bot token (401 Unauthorized)." };
|
||||
}
|
||||
return {
|
||||
ok: false,
|
||||
error: err.response?.data?.message || err.message || "Could not verify bot token.",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function validateOAuthCredentials(clientId, clientSecret, botToken) {
|
||||
const id = String(clientId || "").trim();
|
||||
const secret = String(clientSecret || "").trim();
|
||||
|
||||
if (!isDiscordSnowflake(id)) {
|
||||
return { ok: false, error: "Client ID must be a numeric Discord application ID." };
|
||||
}
|
||||
if (isPlaceholder(secret) || secret.length < 20) {
|
||||
return { ok: false, error: "Enter the OAuth Client Secret from the Discord portal." };
|
||||
}
|
||||
|
||||
if (botToken) {
|
||||
const botCheck = await validateBotToken(botToken);
|
||||
if (botCheck.ok && botCheck.applicationId && botCheck.applicationId !== id) {
|
||||
return {
|
||||
ok: false,
|
||||
error: `Client ID does not match the bot application (${botCheck.applicationId}). Use the same app's credentials.`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await axios.post(
|
||||
`${DISCORD_API}/oauth2/token`,
|
||||
new URLSearchParams({ grant_type: "client_credentials", scope: "identify" }),
|
||||
{
|
||||
auth: { username: id, password: secret },
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
timeout: 12_000,
|
||||
},
|
||||
);
|
||||
return { ok: true, clientId: id };
|
||||
} catch (err) {
|
||||
const status = err.response?.status;
|
||||
if (botToken) {
|
||||
const botCheck = await validateBotToken(botToken);
|
||||
if (botCheck.ok && botCheck.applicationId === id) {
|
||||
return {
|
||||
ok: true,
|
||||
clientId: id,
|
||||
warning:
|
||||
"Could not verify the client secret with Discord (token endpoint). ID matches your bot — double-check the secret in the portal.",
|
||||
};
|
||||
}
|
||||
}
|
||||
if (status === 401 || status === 400) {
|
||||
return { ok: false, error: "Invalid Client ID or Client Secret." };
|
||||
}
|
||||
return {
|
||||
ok: false,
|
||||
error: err.response?.data?.error_description || err.message || "OAuth validation failed.",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function validateRedirectUri(uri) {
|
||||
const trimmed = String(uri || "").trim();
|
||||
if (!isValidRedirectUriConfigured(trimmed)) {
|
||||
return {
|
||||
ok: false,
|
||||
error: `Redirect URL must end with ${CALLBACK_PATH} and use http or https.`,
|
||||
};
|
||||
}
|
||||
return { ok: true, redirectUri: trimmed };
|
||||
}
|
||||
|
||||
async function validateOwnerUserIds(ownerIds, botToken) {
|
||||
const ids = (Array.isArray(ownerIds) ? ownerIds : [ownerIds])
|
||||
.map((id) => String(id).trim())
|
||||
.filter(Boolean);
|
||||
|
||||
if (ids.length === 0) {
|
||||
return { ok: false, error: "Add at least one Discord user ID." };
|
||||
}
|
||||
|
||||
const invalid = ids.filter((id) => !isDiscordSnowflake(id));
|
||||
if (invalid.length) {
|
||||
return { ok: false, error: `Invalid user ID: ${invalid[0]}` };
|
||||
}
|
||||
|
||||
if (!botToken || !isValidBotToken(botToken)) {
|
||||
return { ok: true, ownerUserIds: ids, warning: "User IDs format OK (bot token not verified yet)." };
|
||||
}
|
||||
|
||||
const users = [];
|
||||
try {
|
||||
for (const id of ids) {
|
||||
const res = await axios.get(`${DISCORD_API}/users/${id}`, {
|
||||
headers: { Authorization: `Bot ${String(botToken).trim()}` },
|
||||
timeout: 12_000,
|
||||
});
|
||||
users.push({
|
||||
id: res.data.id,
|
||||
username: res.data.username,
|
||||
globalName: res.data.global_name || null,
|
||||
});
|
||||
}
|
||||
return { ok: true, ownerUserIds: ids, users };
|
||||
} catch (err) {
|
||||
if (err.response?.status === 404) {
|
||||
return { ok: false, error: "One or more user IDs were not found. Check Developer Mode → Copy User ID." };
|
||||
}
|
||||
if (err.response?.status === 403) {
|
||||
return {
|
||||
ok: false,
|
||||
error: "Bot could not look up users. Ensure the bot is in a server with those users, or verify IDs manually.",
|
||||
};
|
||||
}
|
||||
return { ok: false, error: err.message || "Failed to verify owner user IDs." };
|
||||
}
|
||||
}
|
||||
|
||||
function validateSecretKey(secretKey) {
|
||||
if (!isValidSecretKey(secretKey)) {
|
||||
return { ok: false, error: "Use at least 16 characters (not CHANGE_ME)." };
|
||||
}
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
async function validateSetupPayload(payload) {
|
||||
const results = {};
|
||||
|
||||
if (payload.botToken != null) {
|
||||
results.botToken = await validateBotToken(payload.botToken);
|
||||
}
|
||||
if (payload.secretKey != null) {
|
||||
results.secretKey = validateSecretKey(payload.secretKey);
|
||||
}
|
||||
if (payload.clientId != null && payload.clientSecret != null) {
|
||||
results.oauth = await validateOAuthCredentials(
|
||||
payload.clientId,
|
||||
payload.clientSecret,
|
||||
payload.botToken,
|
||||
);
|
||||
}
|
||||
if (payload.redirectUri != null) {
|
||||
results.redirectUri = validateRedirectUri(payload.redirectUri);
|
||||
}
|
||||
if (payload.ownerUserIds != null) {
|
||||
results.owners = await validateOwnerUserIds(payload.ownerUserIds, payload.botToken);
|
||||
}
|
||||
if (payload.emeraldApiKey != null) {
|
||||
const key = String(payload.emeraldApiKey).trim();
|
||||
if (key === "") {
|
||||
results.emeraldApi = { ok: true };
|
||||
} else if (key.length < 10) {
|
||||
results.emeraldApi = { ok: false, error: "Invalid API key." };
|
||||
} else {
|
||||
results.emeraldApi = { ok: true };
|
||||
}
|
||||
}
|
||||
|
||||
const allOk = Object.values(results).every((r) => r?.ok);
|
||||
return { ok: allOk, results };
|
||||
}
|
||||
|
||||
function ensureApiFile() {
|
||||
if (fs.existsSync(API_FILE)) return;
|
||||
const example = path.join(CONFIG_DIR, "api.yml.example");
|
||||
if (fs.existsSync(example)) {
|
||||
fs.copyFileSync(example, API_FILE);
|
||||
configStore.reloadFile("api");
|
||||
} else {
|
||||
throw new Error("Configs/api.yml is missing. Copy Configs/api.yml.example to api.yml.");
|
||||
}
|
||||
}
|
||||
|
||||
function applySetup(payload) {
|
||||
ensureApiFile();
|
||||
|
||||
const ownerIds = (Array.isArray(payload.ownerUserIds)
|
||||
? payload.ownerUserIds
|
||||
: String(payload.ownerUserIds || "").split(/[\s,]+/)
|
||||
)
|
||||
.map((id) => String(id).trim())
|
||||
.filter(Boolean);
|
||||
|
||||
const apiDoc = YAML.parseDocument(fs.readFileSync(API_FILE, "utf8"));
|
||||
if (!apiDoc.get("API")) apiDoc.set("API", {});
|
||||
|
||||
apiDoc.setIn(["API", "Enabled"], true);
|
||||
apiDoc.setIn(["API", "SecretKey"], String(payload.secretKey).trim());
|
||||
if (payload.emeraldApiKey != null) {
|
||||
apiDoc.setIn(["API", "EmeraldAPIKey"], String(payload.emeraldApiKey).trim());
|
||||
}
|
||||
if (payload.port != null && payload.port !== "") {
|
||||
apiDoc.setIn(["API", "Port"], Number(payload.port) || 3000);
|
||||
}
|
||||
if (!apiDoc.getIn(["API", "OAuth"])) apiDoc.setIn(["API", "OAuth"], {});
|
||||
|
||||
apiDoc.setIn(["API", "OAuth", "Enabled"], true);
|
||||
apiDoc.setIn(["API", "OAuth", "ClientId"], String(payload.clientId).trim());
|
||||
apiDoc.setIn(["API", "OAuth", "ClientSecret"], String(payload.clientSecret).trim());
|
||||
apiDoc.setIn(["API", "OAuth", "RedirectUri"], String(payload.redirectUri).trim());
|
||||
apiDoc.setIn(["API", "OAuth", "OwnerUserIds"], ownerIds);
|
||||
apiDoc.setIn(["API", "OAuth", "AllowedUserIds"], []);
|
||||
|
||||
fs.writeFileSync(API_FILE, apiDoc.toString());
|
||||
|
||||
const sbDoc = YAML.parseDocument(fs.readFileSync(SUPPORTBOT_FILE, "utf8"));
|
||||
sbDoc.setIn(["General", "Token"], String(payload.botToken).trim());
|
||||
fs.writeFileSync(SUPPORTBOT_FILE, sbDoc.toString());
|
||||
|
||||
configStore.reloadFile("api");
|
||||
configStore.reloadFile("supportbot");
|
||||
|
||||
return {
|
||||
ownerUserIds: ownerIds,
|
||||
api: configStore.api?.API || configStore.api,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
isSetupComplete,
|
||||
getSetupStatus,
|
||||
validateBotToken,
|
||||
validateOAuthCredentials,
|
||||
validateRedirectUri,
|
||||
validateOwnerUserIds,
|
||||
validateSecretKey,
|
||||
validateSetupPayload,
|
||||
applySetup,
|
||||
};
|
||||
@@ -0,0 +1,291 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const {
|
||||
CONFIG_FILES,
|
||||
ROLES,
|
||||
fullPermissions,
|
||||
permissionsFromRole,
|
||||
normalizePermissions,
|
||||
hasPermission,
|
||||
canAccessConfig,
|
||||
} = require("./DashboardPermissions.js");
|
||||
const { discordAvatarUrl } = require("./DiscordOAuth.js");
|
||||
|
||||
const STORE_PATH = path.join(__dirname, "../Data/dashboard-users.json");
|
||||
|
||||
function ensureDataDir() {
|
||||
const dir = path.dirname(STORE_PATH);
|
||||
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
function emptyStore() {
|
||||
return { version: 1, users: {} };
|
||||
}
|
||||
|
||||
function readStore() {
|
||||
ensureDataDir();
|
||||
if (!fs.existsSync(STORE_PATH)) return emptyStore();
|
||||
try {
|
||||
const raw = JSON.parse(fs.readFileSync(STORE_PATH, "utf8"));
|
||||
if (!raw.users || typeof raw.users !== "object") return emptyStore();
|
||||
return raw;
|
||||
} catch {
|
||||
return emptyStore();
|
||||
}
|
||||
}
|
||||
|
||||
function writeStore(store) {
|
||||
ensureDataDir();
|
||||
fs.writeFileSync(STORE_PATH, JSON.stringify(store, null, 2), "utf8");
|
||||
}
|
||||
|
||||
function getOwnerIds(oauth) {
|
||||
const owners = new Set();
|
||||
for (const id of oauth?.OwnerUserIds || []) owners.add(String(id));
|
||||
for (const id of oauth?.AllowedUserIds || []) owners.add(String(id));
|
||||
return owners;
|
||||
}
|
||||
|
||||
function isYamlOwner(userId, oauth) {
|
||||
return getOwnerIds(oauth).has(String(userId));
|
||||
}
|
||||
|
||||
function bootstrapFromOAuth(oauth) {
|
||||
const store = readStore();
|
||||
const owners = getOwnerIds(oauth);
|
||||
let changed = false;
|
||||
|
||||
for (const id of owners) {
|
||||
if (!store.users[id]) {
|
||||
store.users[id] = {
|
||||
id,
|
||||
username: null,
|
||||
globalName: null,
|
||||
avatar: null,
|
||||
role: "owner",
|
||||
permissions: null,
|
||||
enabled: true,
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
createdBy: "system",
|
||||
};
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (changed) writeStore(store);
|
||||
return store;
|
||||
}
|
||||
|
||||
function syncDiscordProfile(userId, profile) {
|
||||
const store = readStore();
|
||||
const entry = store.users[userId];
|
||||
if (!entry) return;
|
||||
|
||||
entry.username = profile.username ?? entry.username;
|
||||
entry.globalName = profile.globalName ?? entry.globalName;
|
||||
entry.avatar = profile.avatar ?? entry.avatar;
|
||||
entry.updatedAt = Date.now();
|
||||
writeStore(store);
|
||||
}
|
||||
|
||||
function resolvePermissions(entry, oauth, userId) {
|
||||
if (isYamlOwner(userId, oauth)) {
|
||||
return { permissions: fullPermissions(), role: "owner", isOwner: true };
|
||||
}
|
||||
if (!entry?.enabled) return null;
|
||||
|
||||
const role = entry.role || "viewer";
|
||||
const permissions =
|
||||
role === "custom"
|
||||
? permissionsFromRole("custom", entry.permissions)
|
||||
: permissionsFromRole(role);
|
||||
|
||||
return { permissions, role, isOwner: false };
|
||||
}
|
||||
|
||||
function getAccess(userId, oauth) {
|
||||
bootstrapFromOAuth(oauth);
|
||||
const store = readStore();
|
||||
const id = String(userId);
|
||||
|
||||
if (isYamlOwner(id, oauth)) {
|
||||
const entry = store.users[id];
|
||||
return {
|
||||
entry: entry || { id, role: "owner" },
|
||||
permissions: fullPermissions(),
|
||||
role: "owner",
|
||||
isOwner: true,
|
||||
yamlOwner: true,
|
||||
};
|
||||
}
|
||||
|
||||
const entry = store.users[id];
|
||||
const resolved = resolvePermissions(entry, oauth, id);
|
||||
if (!resolved) return null;
|
||||
|
||||
return {
|
||||
entry,
|
||||
permissions: resolved.permissions,
|
||||
role: resolved.role,
|
||||
isOwner: false,
|
||||
yamlOwner: false,
|
||||
};
|
||||
}
|
||||
|
||||
function canLogin(userId, oauth) {
|
||||
return Boolean(getAccess(userId, oauth));
|
||||
}
|
||||
|
||||
function formatPublicUser(entry, access, discordProfile) {
|
||||
const id = String(entry?.id || discordProfile?.id);
|
||||
const avatarHash = entry?.avatar ?? discordProfile?.avatar ?? null;
|
||||
|
||||
return {
|
||||
id,
|
||||
username: entry?.username || discordProfile?.username || "Unknown",
|
||||
globalName: entry?.globalName ?? discordProfile?.global_name ?? null,
|
||||
avatar: avatarHash
|
||||
? discordAvatarUrl({ id, avatar: avatarHash })
|
||||
: discordProfile
|
||||
? discordAvatarUrl(discordProfile)
|
||||
: discordAvatarUrl({ id, avatar: null }),
|
||||
role: access.role,
|
||||
isOwner: access.isOwner,
|
||||
permissions: access.permissions,
|
||||
};
|
||||
}
|
||||
|
||||
function listUsers(oauth) {
|
||||
bootstrapFromOAuth(oauth);
|
||||
const store = readStore();
|
||||
const yamlOwners = getOwnerIds(oauth);
|
||||
|
||||
return Object.values(store.users).map((entry) => {
|
||||
const access = resolvePermissions(entry, oauth, entry.id) || {
|
||||
permissions: permissionsFromRole("viewer"),
|
||||
role: entry.role,
|
||||
isOwner: false,
|
||||
};
|
||||
|
||||
return {
|
||||
id: entry.id,
|
||||
username: entry.username,
|
||||
globalName: entry.globalName,
|
||||
avatar: entry.avatar
|
||||
? discordAvatarUrl({ id: entry.id, avatar: entry.avatar })
|
||||
: discordAvatarUrl({ id: entry.id, avatar: null }),
|
||||
role: yamlOwners.has(String(entry.id)) ? "owner" : entry.role,
|
||||
permissions:
|
||||
entry.role === "custom"
|
||||
? normalizePermissions(entry.permissions)
|
||||
: access.permissions,
|
||||
enabled: entry.enabled !== false,
|
||||
isOwner: yamlOwners.has(String(entry.id)),
|
||||
yamlOwner: yamlOwners.has(String(entry.id)),
|
||||
createdAt: entry.createdAt,
|
||||
updatedAt: entry.updatedAt,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function addUser(payload, actorId, oauth) {
|
||||
const id = String(payload.id || "").trim();
|
||||
if (!/^\d{17,20}$/.test(id)) {
|
||||
throw new Error("Invalid Discord user ID");
|
||||
}
|
||||
|
||||
const role = payload.role || "viewer";
|
||||
if (!ROLES.includes(role)) throw new Error("Invalid role");
|
||||
if (role === "owner" && !isYamlOwner(actorId, oauth)) {
|
||||
throw new Error("Only api.yml owners can assign the owner role");
|
||||
}
|
||||
|
||||
const store = readStore();
|
||||
if (store.users[id]) throw new Error("User already exists");
|
||||
|
||||
store.users[id] = {
|
||||
id,
|
||||
username: payload.username || null,
|
||||
globalName: payload.globalName || null,
|
||||
avatar: null,
|
||||
role,
|
||||
permissions:
|
||||
role === "custom" ? normalizePermissions(payload.permissions) : null,
|
||||
enabled: payload.enabled !== false,
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
createdBy: String(actorId),
|
||||
};
|
||||
|
||||
writeStore(store);
|
||||
return listUsers(oauth).find((u) => u.id === id);
|
||||
}
|
||||
|
||||
function updateUser(id, payload, actorId, oauth) {
|
||||
const store = readStore();
|
||||
const entry = store.users[String(id)];
|
||||
if (!entry) throw new Error("User not found");
|
||||
|
||||
if (isYamlOwner(id, oauth) && payload.enabled === false) {
|
||||
throw new Error("Cannot disable an owner defined in api.yml");
|
||||
}
|
||||
|
||||
if (payload.role) {
|
||||
if (!ROLES.includes(payload.role)) throw new Error("Invalid role");
|
||||
if (payload.role === "owner" && !isYamlOwner(actorId, oauth)) {
|
||||
throw new Error("Only api.yml owners can assign the owner role");
|
||||
}
|
||||
if (isYamlOwner(id, oauth) && payload.role !== "owner") {
|
||||
throw new Error("Cannot change role of an api.yml owner");
|
||||
}
|
||||
entry.role = payload.role;
|
||||
}
|
||||
|
||||
if (payload.enabled !== undefined) entry.enabled = Boolean(payload.enabled);
|
||||
if (payload.username !== undefined) entry.username = payload.username;
|
||||
if (payload.globalName !== undefined) entry.globalName = payload.globalName;
|
||||
|
||||
if (payload.permissions !== undefined) {
|
||||
entry.permissions = normalizePermissions(payload.permissions);
|
||||
if (entry.role !== "custom") entry.role = "custom";
|
||||
}
|
||||
|
||||
if (entry.role === "custom" && payload.permissions === undefined && payload.role !== "custom") {
|
||||
entry.permissions = null;
|
||||
}
|
||||
|
||||
entry.updatedAt = Date.now();
|
||||
writeStore(store);
|
||||
return listUsers(oauth).find((u) => u.id === String(id));
|
||||
}
|
||||
|
||||
function removeUser(id, oauth) {
|
||||
if (isYamlOwner(id, oauth)) {
|
||||
throw new Error("Remove this user from OwnerUserIds in api.yml instead");
|
||||
}
|
||||
|
||||
const store = readStore();
|
||||
if (!store.users[String(id)]) throw new Error("User not found");
|
||||
delete store.users[String(id)];
|
||||
writeStore(store);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
CONFIG_FILES,
|
||||
STORE_PATH,
|
||||
getOwnerIds,
|
||||
isYamlOwner,
|
||||
bootstrapFromOAuth,
|
||||
syncDiscordProfile,
|
||||
getAccess,
|
||||
canLogin,
|
||||
formatPublicUser,
|
||||
listUsers,
|
||||
addUser,
|
||||
updateUser,
|
||||
removeUser,
|
||||
hasPermission,
|
||||
canAccessConfig,
|
||||
ROLES,
|
||||
};
|
||||
@@ -0,0 +1,474 @@
|
||||
// SupportBot | Emerald Services
|
||||
const Database = require("better-sqlite3");
|
||||
const path = require("path");
|
||||
|
||||
const db = new Database(path.join(__dirname, "../Data/supportbot.db"));
|
||||
|
||||
db.prepare(
|
||||
`
|
||||
CREATE TABLE IF NOT EXISTS suggestions (
|
||||
thread_id TEXT PRIMARY KEY,
|
||||
author_id TEXT,
|
||||
text TEXT,
|
||||
status TEXT DEFAULT 'Created'
|
||||
)
|
||||
`,
|
||||
).run();
|
||||
|
||||
db.prepare(
|
||||
`
|
||||
CREATE TABLE IF NOT EXISTS profiles (
|
||||
user_id TEXT PRIMARY KEY,
|
||||
bio TEXT,
|
||||
timezone TEXT,
|
||||
clockedIn INTEGER DEFAULT 0
|
||||
)
|
||||
`,
|
||||
).run();
|
||||
|
||||
db.prepare(
|
||||
`
|
||||
CREATE TABLE IF NOT EXISTS tickets (
|
||||
ticket_id TEXT PRIMARY KEY,
|
||||
user_id TEXT,
|
||||
status TEXT DEFAULT 'open',
|
||||
created_at INTEGER,
|
||||
updated_at INTEGER,
|
||||
subject TEXT,
|
||||
description TEXT
|
||||
)
|
||||
`,
|
||||
).run();
|
||||
|
||||
// Safe column upgrades
|
||||
const safeAlter = (sql) => {
|
||||
try {
|
||||
db.prepare(sql).run();
|
||||
} catch (e) {
|
||||
if (!String(e).includes("duplicate column name")) throw e;
|
||||
}
|
||||
};
|
||||
|
||||
safeAlter(`ALTER TABLE tickets ADD COLUMN voiceChannelId TEXT`);
|
||||
safeAlter(`ALTER TABLE tickets ADD COLUMN department TEXT DEFAULT 'general'`);
|
||||
safeAlter(`ALTER TABLE tickets ADD COLUMN priority TEXT DEFAULT 'medium'`);
|
||||
safeAlter(`ALTER TABLE tickets ADD COLUMN aiModeEnabled INTEGER DEFAULT 0`);
|
||||
safeAlter(`ALTER TABLE tickets ADD COLUMN aiChannelId TEXT`);
|
||||
safeAlter(`ALTER TABLE tickets ADD COLUMN aiType TEXT`);
|
||||
safeAlter(`ALTER TABLE tickets ADD COLUMN aiEnabledBy TEXT`);
|
||||
safeAlter(`ALTER TABLE tickets ADD COLUMN aiEnabledAt INTEGER`);
|
||||
safeAlter(`ALTER TABLE tickets ADD COLUMN questionAnswers TEXT`);
|
||||
safeAlter(`ALTER TABLE tickets ADD COLUMN ticketName TEXT`);
|
||||
safeAlter(`ALTER TABLE tickets ADD COLUMN ticketNumber TEXT`);
|
||||
|
||||
db.prepare(
|
||||
`
|
||||
CREATE TABLE IF NOT EXISTS ticket_panel (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
message_id TEXT,
|
||||
channel_id TEXT,
|
||||
created_at INTEGER
|
||||
)
|
||||
`,
|
||||
).run();
|
||||
|
||||
db.prepare(
|
||||
`
|
||||
CREATE TABLE IF NOT EXISTS ticket_users (
|
||||
ticket_id TEXT,
|
||||
user_id TEXT,
|
||||
PRIMARY KEY(ticket_id, user_id)
|
||||
)
|
||||
`,
|
||||
).run();
|
||||
|
||||
db.prepare(
|
||||
`
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
id INTEGER PRIMARY KEY,
|
||||
data TEXT
|
||||
)
|
||||
`,
|
||||
).run();
|
||||
|
||||
function ensureProfile(userId) {
|
||||
const existing = db
|
||||
.prepare(
|
||||
`
|
||||
SELECT user_id FROM profiles WHERE user_id = ?
|
||||
`,
|
||||
)
|
||||
.get(userId);
|
||||
|
||||
if (!existing) {
|
||||
db.prepare(
|
||||
`
|
||||
INSERT INTO profiles (user_id, bio, timezone, clockedIn)
|
||||
VALUES (?, '', '', 0)
|
||||
`,
|
||||
).run(userId);
|
||||
}
|
||||
}
|
||||
|
||||
function normaliseTicketRow(row) {
|
||||
if (!row) return null;
|
||||
|
||||
let questionAnswers = [];
|
||||
|
||||
if (row.questionAnswers) {
|
||||
try {
|
||||
questionAnswers = JSON.parse(row.questionAnswers);
|
||||
} catch {
|
||||
questionAnswers = [];
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...row,
|
||||
aiModeEnabled: Boolean(row.aiModeEnabled),
|
||||
clockedIn:
|
||||
row.clockedIn !== undefined ? Boolean(row.clockedIn) : row.clockedIn,
|
||||
questionAnswers,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
addTicket(ticket) {
|
||||
const now = Date.now();
|
||||
|
||||
db.prepare(
|
||||
`
|
||||
INSERT INTO tickets (
|
||||
ticket_id,
|
||||
user_id,
|
||||
subject,
|
||||
description,
|
||||
created_at,
|
||||
updated_at,
|
||||
department,
|
||||
priority,
|
||||
aiModeEnabled,
|
||||
aiChannelId,
|
||||
aiType,
|
||||
aiEnabledBy,
|
||||
aiEnabledAt,
|
||||
questionAnswers,
|
||||
ticketName,
|
||||
ticketNumber
|
||||
)
|
||||
VALUES (
|
||||
@ticket_id,
|
||||
@user_id,
|
||||
@subject,
|
||||
@description,
|
||||
@created_at,
|
||||
@updated_at,
|
||||
@department,
|
||||
@priority,
|
||||
@aiModeEnabled,
|
||||
@aiChannelId,
|
||||
@aiType,
|
||||
@aiEnabledBy,
|
||||
@aiEnabledAt,
|
||||
@questionAnswers,
|
||||
@ticketName,
|
||||
@ticketNumber
|
||||
)
|
||||
ON CONFLICT(ticket_id) DO NOTHING
|
||||
`,
|
||||
).run({
|
||||
ticket_id: ticket.id,
|
||||
user_id: ticket.user,
|
||||
subject: ticket.reason || "",
|
||||
description: ticket.description || "",
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
department: ticket.department || "general",
|
||||
priority: ticket.priority || "medium",
|
||||
aiModeEnabled: ticket.aiModeEnabled ? 1 : 0,
|
||||
aiChannelId: ticket.aiChannelId || null,
|
||||
aiType: ticket.aiType || null,
|
||||
aiEnabledBy: ticket.aiEnabledBy || null,
|
||||
aiEnabledAt: ticket.aiEnabledAt || null,
|
||||
questionAnswers: JSON.stringify(ticket.questionAnswers || []),
|
||||
ticketName: ticket.name || null,
|
||||
ticketNumber: ticket.number || null,
|
||||
});
|
||||
},
|
||||
|
||||
updateTicketQuestionAnswers(ticketId, questionAnswers = []) {
|
||||
db.prepare(
|
||||
`
|
||||
UPDATE tickets
|
||||
SET questionAnswers=@questionAnswers, updated_at=@updatedAt
|
||||
WHERE ticket_id=@ticketId
|
||||
`,
|
||||
).run({
|
||||
ticketId,
|
||||
questionAnswers: JSON.stringify(questionAnswers),
|
||||
updatedAt: Date.now(),
|
||||
});
|
||||
},
|
||||
|
||||
updateTicketVoice(ticketId, voiceChannelId) {
|
||||
db.prepare(
|
||||
`
|
||||
UPDATE tickets
|
||||
SET voiceChannelId=@voiceChannelId, updated_at=@updatedAt
|
||||
WHERE ticket_id=@ticketId
|
||||
`,
|
||||
).run({
|
||||
ticketId,
|
||||
voiceChannelId,
|
||||
updatedAt: Date.now(),
|
||||
});
|
||||
},
|
||||
|
||||
updateTicketStatus(ticketId, status) {
|
||||
db.prepare(
|
||||
`
|
||||
UPDATE tickets
|
||||
SET status=@status, updated_at=@updatedAt
|
||||
WHERE ticket_id=@ticketId
|
||||
`,
|
||||
).run({
|
||||
status,
|
||||
updatedAt: Date.now(),
|
||||
ticketId,
|
||||
});
|
||||
},
|
||||
|
||||
updateTicketDepartment(ticketId, department) {
|
||||
db.prepare(
|
||||
`
|
||||
UPDATE tickets
|
||||
SET department=@department, updated_at=@updatedAt
|
||||
WHERE ticket_id=@ticketId
|
||||
`,
|
||||
).run({
|
||||
ticketId,
|
||||
department,
|
||||
updatedAt: Date.now(),
|
||||
});
|
||||
},
|
||||
|
||||
updateTicketPriority(ticketId, priority) {
|
||||
db.prepare(
|
||||
`
|
||||
UPDATE tickets
|
||||
SET priority=@priority, updated_at=@updatedAt
|
||||
WHERE ticket_id=@ticketId
|
||||
`,
|
||||
).run({
|
||||
ticketId,
|
||||
priority,
|
||||
updatedAt: Date.now(),
|
||||
});
|
||||
},
|
||||
|
||||
setTicketAIState(ticketId, aiState = {}) {
|
||||
db.prepare(
|
||||
`
|
||||
UPDATE tickets
|
||||
SET
|
||||
aiModeEnabled=@aiModeEnabled,
|
||||
aiChannelId=@aiChannelId,
|
||||
aiType=@aiType,
|
||||
aiEnabledBy=@aiEnabledBy,
|
||||
aiEnabledAt=@aiEnabledAt,
|
||||
updated_at=@updatedAt
|
||||
WHERE ticket_id=@ticketId
|
||||
`,
|
||||
).run({
|
||||
ticketId,
|
||||
aiModeEnabled: aiState.enabled ? 1 : 0,
|
||||
aiChannelId: aiState.aiChannelId || null,
|
||||
aiType: aiState.aiType || null,
|
||||
aiEnabledBy: aiState.enabledBy || null,
|
||||
aiEnabledAt: aiState.enabledAt
|
||||
? new Date(aiState.enabledAt).getTime()
|
||||
: aiState.enabled
|
||||
? Date.now()
|
||||
: null,
|
||||
updatedAt: Date.now(),
|
||||
});
|
||||
},
|
||||
|
||||
enableTicketAI(
|
||||
ticketId,
|
||||
aiChannelId = null,
|
||||
aiType = null,
|
||||
enabledBy = null,
|
||||
) {
|
||||
db.prepare(
|
||||
`
|
||||
UPDATE tickets
|
||||
SET
|
||||
aiModeEnabled=1,
|
||||
aiChannelId=@aiChannelId,
|
||||
aiType=@aiType,
|
||||
aiEnabledBy=@aiEnabledBy,
|
||||
aiEnabledAt=@aiEnabledAt,
|
||||
updated_at=@updatedAt
|
||||
WHERE ticket_id=@ticketId
|
||||
`,
|
||||
).run({
|
||||
ticketId,
|
||||
aiChannelId,
|
||||
aiType,
|
||||
aiEnabledBy,
|
||||
aiEnabledAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
});
|
||||
},
|
||||
|
||||
disableTicketAI(ticketId) {
|
||||
db.prepare(
|
||||
`
|
||||
UPDATE tickets
|
||||
SET
|
||||
aiModeEnabled=0,
|
||||
updated_at=@updatedAt
|
||||
WHERE ticket_id=@ticketId
|
||||
`,
|
||||
).run({
|
||||
ticketId,
|
||||
updatedAt: Date.now(),
|
||||
});
|
||||
},
|
||||
|
||||
getTicket(ticketId) {
|
||||
const row = db
|
||||
.prepare(
|
||||
`
|
||||
SELECT * FROM tickets WHERE ticket_id=@ticketId
|
||||
`,
|
||||
)
|
||||
.get({ ticketId });
|
||||
|
||||
return normaliseTicketRow(row);
|
||||
},
|
||||
|
||||
getTicketByAIChannel(aiChannelId) {
|
||||
const row = db
|
||||
.prepare(
|
||||
`
|
||||
SELECT * FROM tickets WHERE aiChannelId=@aiChannelId LIMIT 1
|
||||
`,
|
||||
)
|
||||
.get({ aiChannelId });
|
||||
|
||||
return normaliseTicketRow(row);
|
||||
},
|
||||
|
||||
getAllTickets() {
|
||||
return db.prepare(`SELECT * FROM tickets`).all().map(normaliseTicketRow);
|
||||
},
|
||||
|
||||
saveTicketPanel(messageId, channelId) {
|
||||
db.prepare(
|
||||
`
|
||||
INSERT INTO ticket_panel (message_id, channel_id, created_at)
|
||||
VALUES (@messageId, @channelId, @createdAt)
|
||||
`,
|
||||
).run({ messageId, channelId, createdAt: Date.now() });
|
||||
},
|
||||
|
||||
getTicketPanel() {
|
||||
return db
|
||||
.prepare(
|
||||
`
|
||||
SELECT * FROM ticket_panel ORDER BY id DESC LIMIT 1
|
||||
`,
|
||||
)
|
||||
.get();
|
||||
},
|
||||
|
||||
addUserToTicket(ticketId, userId) {
|
||||
db.prepare(
|
||||
`
|
||||
INSERT INTO ticket_users (ticket_id, user_id)
|
||||
VALUES (?, ?)
|
||||
ON CONFLICT(ticket_id, user_id) DO NOTHING
|
||||
`,
|
||||
).run(ticketId, userId);
|
||||
},
|
||||
|
||||
removeUserFromTicket(ticketId, userId) {
|
||||
db.prepare(
|
||||
`
|
||||
DELETE FROM ticket_users WHERE ticket_id=? AND user_id=?
|
||||
`,
|
||||
).run(ticketId, userId);
|
||||
},
|
||||
|
||||
getTicketUsers(ticketId) {
|
||||
return db
|
||||
.prepare(
|
||||
`
|
||||
SELECT user_id FROM ticket_users WHERE ticket_id=?
|
||||
`,
|
||||
)
|
||||
.all(ticketId)
|
||||
.map((r) => r.user_id);
|
||||
},
|
||||
|
||||
getSettings() {
|
||||
const row = db.prepare(`SELECT data FROM settings WHERE id=1`).get();
|
||||
if (!row) return {};
|
||||
try {
|
||||
return JSON.parse(row.data);
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
|
||||
getProfile(userId) {
|
||||
ensureProfile(userId);
|
||||
|
||||
const row = db
|
||||
.prepare(
|
||||
`
|
||||
SELECT * FROM profiles WHERE user_id = ?
|
||||
`,
|
||||
)
|
||||
.get(userId);
|
||||
|
||||
if (!row) {
|
||||
return {
|
||||
user_id: userId,
|
||||
bio: "",
|
||||
timezone: "",
|
||||
clockedIn: false,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...row,
|
||||
clockedIn: Boolean(row.clockedIn),
|
||||
};
|
||||
},
|
||||
|
||||
setClockedIn(userId, clockedIn) {
|
||||
ensureProfile(userId);
|
||||
|
||||
db.prepare(
|
||||
`
|
||||
UPDATE profiles
|
||||
SET clockedIn = ?
|
||||
WHERE user_id = ?
|
||||
`,
|
||||
).run(clockedIn ? 1 : 0, userId);
|
||||
},
|
||||
|
||||
saveSettings(obj) {
|
||||
const data = JSON.stringify(obj);
|
||||
db.prepare(
|
||||
`
|
||||
INSERT INTO settings (id, data)
|
||||
VALUES (1, @data)
|
||||
ON CONFLICT(id) DO UPDATE SET data = excluded.data
|
||||
`,
|
||||
).run({ data });
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,233 @@
|
||||
const crypto = require("crypto");
|
||||
const axios = require("axios");
|
||||
|
||||
const DISCORD_API = "https://discord.com/api";
|
||||
const OAUTH_STATE_COOKIE = "sb_oauth_state";
|
||||
const OAUTH_REDIRECT_COOKIE = "sb_oauth_redirect";
|
||||
const OAUTH_STATE_MAX_AGE_MS = 10 * 60 * 1000;
|
||||
const CALLBACK_PATH = "/api/auth/discord/callback";
|
||||
|
||||
function discordAvatarUrl(user) {
|
||||
if (user.avatar) {
|
||||
const ext = user.avatar.startsWith("a_") ? "gif" : "png";
|
||||
return `https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}.${ext}?size=128`;
|
||||
}
|
||||
const index = Number((BigInt(user.id) >> 22n) % 6n);
|
||||
return `https://cdn.discordapp.com/embed/avatars/${index}.png`;
|
||||
}
|
||||
|
||||
function getTrustProxySetting(apiConfig) {
|
||||
if (apiConfig?.TrustProxy === false) return false;
|
||||
if (typeof apiConfig?.TrustProxy === "number") return apiConfig.TrustProxy;
|
||||
if (apiConfig?.TrustProxy === true) return 1;
|
||||
return false;
|
||||
}
|
||||
|
||||
function getRequestProto(req, apiConfig) {
|
||||
if (apiConfig?.TrustProxy !== false) {
|
||||
const forwarded = req.get("x-forwarded-proto");
|
||||
if (forwarded) {
|
||||
return forwarded.split(",")[0].trim().toLowerCase();
|
||||
}
|
||||
}
|
||||
return (req.protocol || "http").toLowerCase();
|
||||
}
|
||||
|
||||
function getRequestHost(req) {
|
||||
const forwarded = req.get("x-forwarded-host");
|
||||
if (forwarded) {
|
||||
return forwarded.split(",")[0].trim();
|
||||
}
|
||||
return req.get("host") || "";
|
||||
}
|
||||
|
||||
function getRequestOrigin(req, apiConfig) {
|
||||
const proto = getRequestProto(req, apiConfig);
|
||||
const host = getRequestHost(req);
|
||||
if (!host) return null;
|
||||
return `${proto}://${host}`;
|
||||
}
|
||||
|
||||
function normalizeRedirectUri(uri) {
|
||||
try {
|
||||
const u = new URL(uri);
|
||||
u.hash = "";
|
||||
u.search = "";
|
||||
return u.toString();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function redirectUriMatchesHostPath(a, b) {
|
||||
try {
|
||||
const ua = new URL(a);
|
||||
const ub = new URL(b);
|
||||
return ua.host === ub.host && ua.pathname === ub.pathname;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function collectConfiguredRedirectUris(oauth) {
|
||||
const list = [];
|
||||
if (oauth?.RedirectUri) {
|
||||
const normalized = normalizeRedirectUri(oauth.RedirectUri);
|
||||
if (normalized) list.push(normalized);
|
||||
}
|
||||
if (Array.isArray(oauth?.RedirectUris)) {
|
||||
for (const entry of oauth.RedirectUris) {
|
||||
const normalized = normalizeRedirectUri(entry);
|
||||
if (normalized && !list.includes(normalized)) {
|
||||
list.push(normalized);
|
||||
}
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pick the OAuth redirect_uri for this request. When UseRequestOrigin is enabled
|
||||
* (default), uses the same host/path as RedirectUri but the protocol the user
|
||||
* actually used (http vs https). Register every variant in the Discord portal.
|
||||
*/
|
||||
function resolveOAuthRedirectUri(req, oauth, apiConfig) {
|
||||
const configured = collectConfiguredRedirectUris(oauth);
|
||||
const fallback = configured[0] || oauth?.RedirectUri || "";
|
||||
|
||||
const useRequestOrigin = oauth?.UseRequestOrigin !== false;
|
||||
if (!useRequestOrigin || !req) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
const origin = getRequestOrigin(req, apiConfig);
|
||||
if (!origin) return fallback;
|
||||
|
||||
const dynamic = `${origin}${CALLBACK_PATH}`;
|
||||
const normalizedDynamic = normalizeRedirectUri(dynamic);
|
||||
if (!normalizedDynamic) return fallback;
|
||||
|
||||
if (configured.includes(normalizedDynamic)) {
|
||||
return normalizedDynamic;
|
||||
}
|
||||
|
||||
const matchesConfigured = configured.some((uri) =>
|
||||
redirectUriMatchesHostPath(uri, normalizedDynamic),
|
||||
);
|
||||
if (matchesConfigured) {
|
||||
return normalizedDynamic;
|
||||
}
|
||||
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function isSecureRequest(req, oauth, apiConfig) {
|
||||
if (oauth?.CookieSecure === true) return true;
|
||||
if (oauth?.CookieSecure === false) return false;
|
||||
if (req) {
|
||||
if (typeof req.secure === "boolean") return req.secure;
|
||||
return getRequestProto(req, apiConfig) === "https";
|
||||
}
|
||||
return process.env.NODE_ENV === "production";
|
||||
}
|
||||
|
||||
function buildAuthorizeUrl(oauth, state, redirectUri) {
|
||||
const params = new URLSearchParams({
|
||||
client_id: oauth.ClientId,
|
||||
redirect_uri: redirectUri,
|
||||
response_type: "code",
|
||||
scope: "identify",
|
||||
state,
|
||||
prompt: "none",
|
||||
});
|
||||
return `${DISCORD_API}/oauth2/authorize?${params.toString()}`;
|
||||
}
|
||||
|
||||
async function exchangeCode(oauth, code, redirectUri) {
|
||||
const body = new URLSearchParams({
|
||||
client_id: oauth.ClientId,
|
||||
client_secret: oauth.ClientSecret,
|
||||
grant_type: "authorization_code",
|
||||
code,
|
||||
redirect_uri: redirectUri,
|
||||
});
|
||||
|
||||
const { data } = await axios.post(`${DISCORD_API}/oauth2/token`, body, {
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
timeout: 15000,
|
||||
});
|
||||
|
||||
return data.access_token;
|
||||
}
|
||||
|
||||
async function fetchDiscordUser(accessToken) {
|
||||
const { data } = await axios.get(`${DISCORD_API}/users/@me`, {
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
timeout: 15000,
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
function createOAuthState() {
|
||||
return crypto.randomBytes(24).toString("hex");
|
||||
}
|
||||
|
||||
function oauthStateCookieOptions(req, oauth, apiConfig) {
|
||||
return {
|
||||
httpOnly: true,
|
||||
secure: isSecureRequest(req, oauth, apiConfig),
|
||||
sameSite: "lax",
|
||||
maxAge: OAUTH_STATE_MAX_AGE_MS,
|
||||
path: "/",
|
||||
};
|
||||
}
|
||||
|
||||
function oauthRedirectCookieOptions(req, oauth, apiConfig) {
|
||||
return oauthStateCookieOptions(req, oauth, apiConfig);
|
||||
}
|
||||
|
||||
function isOAuthConfigured(oauth) {
|
||||
return Boolean(
|
||||
oauth?.Enabled &&
|
||||
oauth.ClientId &&
|
||||
oauth.ClientSecret &&
|
||||
oauth.RedirectUri,
|
||||
);
|
||||
}
|
||||
|
||||
function isUserAllowed(userId, oauth) {
|
||||
const allowed = oauth?.AllowedUserIds;
|
||||
if (!Array.isArray(allowed) || allowed.length === 0) {
|
||||
return false;
|
||||
}
|
||||
return allowed.map(String).includes(String(userId));
|
||||
}
|
||||
|
||||
function formatPublicUser(user) {
|
||||
return {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
globalName: user.global_name || null,
|
||||
avatar: discordAvatarUrl(user),
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
OAUTH_STATE_COOKIE,
|
||||
OAUTH_REDIRECT_COOKIE,
|
||||
OAUTH_STATE_MAX_AGE_MS,
|
||||
CALLBACK_PATH,
|
||||
getTrustProxySetting,
|
||||
resolveOAuthRedirectUri,
|
||||
isSecureRequest,
|
||||
buildAuthorizeUrl,
|
||||
exchangeCode,
|
||||
fetchDiscordUser,
|
||||
createOAuthState,
|
||||
oauthStateCookieOptions,
|
||||
oauthRedirectCookieOptions,
|
||||
isOAuthConfigured,
|
||||
isUserAllowed,
|
||||
formatPublicUser,
|
||||
discordAvatarUrl,
|
||||
};
|
||||
@@ -1,5 +1,6 @@
|
||||
// SupportBot | Emerald Services
|
||||
// Event Structure
|
||||
|
||||
class Event {
|
||||
constructor(event, run) {
|
||||
this.event = event;
|
||||
|
||||
Loaded 100 of 119 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user