chore: 🎉 initial commit
This commit is contained in:
@@ -0,0 +1,32 @@
|
|||||||
|
const { SlashCommandBuilder, EmbedBuilder } = require("discord.js");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
data: new SlashCommandBuilder()
|
||||||
|
.setName("help")
|
||||||
|
.setDescription("Affiche la liste des commandes disponibles"),
|
||||||
|
|
||||||
|
async execute(interaction, client) {
|
||||||
|
const globalCommands = await client.application.commands.fetch();
|
||||||
|
|
||||||
|
const commandList = globalCommands.map(
|
||||||
|
cmd => `</${cmd.name}:${cmd.id}> — ${cmd.description || "Pas de description"}`
|
||||||
|
).join("\n");
|
||||||
|
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setTitle("📖 Aide du bot")
|
||||||
|
.setDescription(
|
||||||
|
commandList.length > 0
|
||||||
|
? "Voici la liste des commandes disponibles :\n\n" + commandList
|
||||||
|
: "❌ Aucune commande trouvée."
|
||||||
|
)
|
||||||
|
.setColor(0x5e99ff)
|
||||||
|
.setThumbnail(client.user.displayAvatarURL())
|
||||||
|
.setFooter({
|
||||||
|
text: `QuantumCraft Studios • Demandé par ${interaction.user.tag}`,
|
||||||
|
iconURL: interaction.user.displayAvatarURL()
|
||||||
|
})
|
||||||
|
.setTimestamp();
|
||||||
|
|
||||||
|
await interaction.reply({ embeds: [embed], flags: 64 });
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
const { SlashCommandBuilder } = require("discord.js");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
data: new SlashCommandBuilder()
|
||||||
|
.setName("ping")
|
||||||
|
.setDescription("Répond avec Pong !"),
|
||||||
|
|
||||||
|
async execute(interaction) {
|
||||||
|
await interaction.reply("🏓 Pong !");
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
const { SlashCommandBuilder, PermissionFlagsBits } = require("discord.js");
|
||||||
|
const { config, lang } = require("../../handlers/configLoader");
|
||||||
|
const { getTicketByChannel } = require("../../handlers/database");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
data: new SlashCommandBuilder()
|
||||||
|
.setName("add")
|
||||||
|
.setDescription(lang.commands.add.description)
|
||||||
|
.addUserOption(option =>
|
||||||
|
option.setName("utilisateur")
|
||||||
|
.setDescription("L'utilisateur à ajouter au ticket")
|
||||||
|
.setRequired(true)
|
||||||
|
),
|
||||||
|
|
||||||
|
async execute(interaction) {
|
||||||
|
const member = interaction.member;
|
||||||
|
|
||||||
|
const staffRoles = (Array.isArray(config.staffRoles) ? config.staffRoles : [config.staffRole || config.staffRoles])
|
||||||
|
.filter(r => r && r.trim() !== "");
|
||||||
|
|
||||||
|
if (!staffRoles.some(roleId => member.roles.cache.has(roleId))) {
|
||||||
|
return interaction.reply({
|
||||||
|
content: lang.permissions.staff_only,
|
||||||
|
flags: 64
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const ticket = getTicketByChannel(interaction.channel.id);
|
||||||
|
if (!ticket) {
|
||||||
|
return interaction.reply({
|
||||||
|
content: lang.ticket.not_in_ticket,
|
||||||
|
flags: 64
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const userToAdd = interaction.options.getUser("utilisateur");
|
||||||
|
|
||||||
|
const existingOverwrite = interaction.channel.permissionOverwrites.cache.get(userToAdd.id);
|
||||||
|
if (existingOverwrite && existingOverwrite.allow.has(PermissionFlagsBits.ViewChannel)) {
|
||||||
|
return interaction.reply({
|
||||||
|
content: lang.commands.add.already_added.replace("{user}", userToAdd),
|
||||||
|
flags: 64
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await interaction.channel.permissionOverwrites.edit(userToAdd.id, {
|
||||||
|
[PermissionFlagsBits.ViewChannel]: true,
|
||||||
|
[PermissionFlagsBits.SendMessages]: true,
|
||||||
|
[PermissionFlagsBits.ReadMessageHistory]: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
await interaction.reply({
|
||||||
|
content: lang.commands.add.success.replace("{user}", userToAdd),
|
||||||
|
flags: 64
|
||||||
|
});
|
||||||
|
|
||||||
|
await interaction.channel.send(
|
||||||
|
lang.commands.add.welcome.replace("{user}", userToAdd)
|
||||||
|
);
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
await interaction.reply({
|
||||||
|
content: lang.commands.add.error,
|
||||||
|
flags: 64
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
const {
|
||||||
|
SlashCommandBuilder,
|
||||||
|
EmbedBuilder,
|
||||||
|
ButtonBuilder,
|
||||||
|
ButtonStyle,
|
||||||
|
ActionRowBuilder
|
||||||
|
} = require("discord.js");
|
||||||
|
const { config, lang } = require("../../handlers/configLoader");
|
||||||
|
const { getTicketByChannel, closeTicket } = require("../../handlers/database");
|
||||||
|
const { scheduleTicketClosure } = require("../../handlers/alertManager");
|
||||||
|
const ms = require("ms");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
data: new SlashCommandBuilder()
|
||||||
|
.setName("alert")
|
||||||
|
.setDescription(lang.commands.alert.description),
|
||||||
|
|
||||||
|
async execute(interaction, client) {
|
||||||
|
const ticket = getTicketByChannel(interaction.channel.id);
|
||||||
|
if (!ticket) {
|
||||||
|
return interaction.reply({
|
||||||
|
content: lang.ticket.not_in_ticket,
|
||||||
|
flags: 64
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const member = interaction.member;
|
||||||
|
const isStaff = config.staffRoles.some(roleId => member.roles.cache.has(roleId));
|
||||||
|
if (!isStaff) {
|
||||||
|
return interaction.reply({
|
||||||
|
content: lang.permissions.staff_only,
|
||||||
|
flags: 64
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const alertDuration = ms(config.TicketAlert.Time || "1h");
|
||||||
|
const now = Date.now();
|
||||||
|
const inactiveTime = `<t:${Math.floor(now / 1000)}:R>`;
|
||||||
|
|
||||||
|
// === Boutons ===
|
||||||
|
const closeBtn = new ButtonBuilder()
|
||||||
|
.setCustomId("closeTicket")
|
||||||
|
.setLabel(lang.commands.alert.close_now_label || "🔒 Fermer maintenant")
|
||||||
|
.setStyle(ButtonStyle.Danger);
|
||||||
|
|
||||||
|
const cancelBtn = new ButtonBuilder()
|
||||||
|
.setCustomId("cancelClosure")
|
||||||
|
.setLabel(lang.commands.alert.cancel_label || "🚫 Annuler la fermeture")
|
||||||
|
.setStyle(ButtonStyle.Secondary);
|
||||||
|
|
||||||
|
const linkBtn = new ButtonBuilder()
|
||||||
|
.setLabel("🔗 Voir le ticket")
|
||||||
|
.setStyle(ButtonStyle.Link)
|
||||||
|
.setURL(`https://discord.com/channels/${interaction.guild.id}/${interaction.channel.id}`);
|
||||||
|
|
||||||
|
const row1 = new ActionRowBuilder().addComponents(closeBtn, cancelBtn);
|
||||||
|
const row2 = new ActionRowBuilder().addComponents(linkBtn);
|
||||||
|
|
||||||
|
// === Embed principal ===
|
||||||
|
const alertEmbed = new EmbedBuilder()
|
||||||
|
.setColor(0xe67e22)
|
||||||
|
.setDescription(
|
||||||
|
lang.commands.alert.sent
|
||||||
|
.replace("{time}", `<t:${Math.floor((now + alertDuration) / 1000)}:R>`)
|
||||||
|
.replace("{inactive-time}", inactiveTime)
|
||||||
|
)
|
||||||
|
.setTimestamp();
|
||||||
|
|
||||||
|
const dmEmbed = new EmbedBuilder()
|
||||||
|
.setColor(0xe67e22)
|
||||||
|
.setDescription(
|
||||||
|
lang.commands.alert.sent
|
||||||
|
.replace("{time}", `<t:${Math.floor((now + alertDuration) / 1000)}:R>`)
|
||||||
|
.replace("{inactive-time}", inactiveTime)
|
||||||
|
)
|
||||||
|
.setTimestamp();
|
||||||
|
|
||||||
|
// === Envoi DM si activé ===
|
||||||
|
if (config.TicketAlert?.DMUser && ticket.userId) {
|
||||||
|
const ticketCreator = await client.users.fetch(ticket.userId).catch(() => null);
|
||||||
|
if (ticketCreator) {
|
||||||
|
try {
|
||||||
|
await ticketCreator.send({ embeds: [dmEmbed], components: [row2] });
|
||||||
|
} catch {
|
||||||
|
await interaction.reply({
|
||||||
|
content: lang.commands.alert.dm_unreachable,
|
||||||
|
flags: 64
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Envoi dans le ticket (non éphémère) ===
|
||||||
|
await interaction.reply({
|
||||||
|
content: ticket.userId ? `<@${ticket.userId}>` : null,
|
||||||
|
embeds: [alertEmbed],
|
||||||
|
components: [row1]
|
||||||
|
});
|
||||||
|
|
||||||
|
// === Planification de la fermeture auto ===
|
||||||
|
scheduleTicketClosure(interaction.channel, alertDuration, async () => {
|
||||||
|
closeTicket(interaction.channel.id);
|
||||||
|
await interaction.channel.delete().catch(() => {});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
const { SlashCommandBuilder, EmbedBuilder, MessageFlags } = require("discord.js");
|
||||||
|
const { getTicketByChannel, closeTicket } = require("../../handlers/database");
|
||||||
|
const { config, lang } = require("../../handlers/configLoader");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
data: new SlashCommandBuilder()
|
||||||
|
.setName("close")
|
||||||
|
.setDescription("Fermer un ticket"),
|
||||||
|
|
||||||
|
async execute(interaction) {
|
||||||
|
const ticket = getTicketByChannel(interaction.channel.id);
|
||||||
|
|
||||||
|
if (!ticket) {
|
||||||
|
return interaction.reply({ content: lang.ticket.not_in_ticket, flags: MessageFlags.Ephemeral });
|
||||||
|
}
|
||||||
|
|
||||||
|
closeTicket(interaction.channel.id);
|
||||||
|
|
||||||
|
await interaction.reply(lang.ticket.closing);
|
||||||
|
|
||||||
|
// Log fermeture
|
||||||
|
const logChannel = await interaction.client.channels.fetch(config.logsChannel).catch(() => null);
|
||||||
|
if (logChannel) {
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setTitle("🔒 Ticket fermé")
|
||||||
|
.addFields(
|
||||||
|
{ name: "Salon", value: `${interaction.channel.name}`, inline: true },
|
||||||
|
{ name: "Fermé par", value: `${interaction.user.tag} (${interaction.user.id})`, inline: true }
|
||||||
|
)
|
||||||
|
.setColor(0xe74c3c)
|
||||||
|
.setTimestamp();
|
||||||
|
logChannel.send({ embeds: [embed] });
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
interaction.channel.delete().catch(() => {});
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
const { SlashCommandBuilder, PermissionFlagsBits } = require("discord.js");
|
||||||
|
const { config, lang } = require("../../handlers/configLoader");
|
||||||
|
const { getTicketByChannel } = require("../../handlers/database");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
data: new SlashCommandBuilder()
|
||||||
|
.setName("remove")
|
||||||
|
.setDescription(lang.commands.remove.description)
|
||||||
|
.addUserOption(option =>
|
||||||
|
option.setName("utilisateur")
|
||||||
|
.setDescription("L'utilisateur à retirer du ticket")
|
||||||
|
.setRequired(true)
|
||||||
|
),
|
||||||
|
|
||||||
|
async execute(interaction) {
|
||||||
|
const member = interaction.member;
|
||||||
|
|
||||||
|
const staffRoles = (Array.isArray(config.staffRoles) ? config.staffRoles : [config.staffRole || config.staffRoles])
|
||||||
|
.filter(r => r && r.trim() !== "");
|
||||||
|
|
||||||
|
if (!staffRoles.some(roleId => member.roles.cache.has(roleId))) {
|
||||||
|
return interaction.reply({
|
||||||
|
content: lang.permissions.staff_only,
|
||||||
|
flags: 64
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const ticket = getTicketByChannel(interaction.channel.id);
|
||||||
|
if (!ticket) {
|
||||||
|
return interaction.reply({
|
||||||
|
content: lang.ticket.not_in_ticket,
|
||||||
|
flags: 64
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const userToRemove = interaction.options.getUser("utilisateur");
|
||||||
|
|
||||||
|
const existingOverwrite = interaction.channel.permissionOverwrites.cache.get(userToRemove.id);
|
||||||
|
if (!existingOverwrite || !existingOverwrite.allow.has(PermissionFlagsBits.ViewChannel)) {
|
||||||
|
return interaction.reply({
|
||||||
|
content: lang.commands.remove.not_in_ticket.replace("{user}", userToRemove),
|
||||||
|
flags: 64
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await interaction.channel.permissionOverwrites.edit(userToRemove.id, {
|
||||||
|
[PermissionFlagsBits.ViewChannel]: false,
|
||||||
|
[PermissionFlagsBits.SendMessages]: false,
|
||||||
|
[PermissionFlagsBits.ReadMessageHistory]: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
await interaction.reply({
|
||||||
|
content: lang.commands.remove.success.replace("{user}", userToRemove),
|
||||||
|
flags: 64
|
||||||
|
});
|
||||||
|
|
||||||
|
await interaction.channel.send(
|
||||||
|
lang.commands.remove.goodbye.replace("{user}", userToRemove)
|
||||||
|
);
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
await interaction.reply({
|
||||||
|
content: lang.commands.remove.error,
|
||||||
|
flags: 64
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
+130
@@ -0,0 +1,130 @@
|
|||||||
|
# =========================
|
||||||
|
# Configuration générale
|
||||||
|
# =========================
|
||||||
|
|
||||||
|
logsChannel: "1409149170131931237"
|
||||||
|
|
||||||
|
# Liste des rôles qui auront les permissions "staff" (accès aux tickets, commandes /add /remove, etc.)
|
||||||
|
# Tu peux mettre un seul rôle :
|
||||||
|
# staffRoles:
|
||||||
|
# - "ID_DU_ROLE"
|
||||||
|
#
|
||||||
|
# Ou plusieurs rôles :
|
||||||
|
# staffRoles:
|
||||||
|
# - "ID_ROLE_1"
|
||||||
|
# - "ID_ROLE_2"
|
||||||
|
# - "ID_ROLE_3"
|
||||||
|
staffRoles:
|
||||||
|
- "1066466155574857884"
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# Configuration du panel de tickets
|
||||||
|
# =========================
|
||||||
|
TicketPanel:
|
||||||
|
Panel:
|
||||||
|
# Nom interne du panel (juste informatif, affiché dans les logs)
|
||||||
|
Name: "Support Panel"
|
||||||
|
|
||||||
|
# Channel textuel où le panel sera envoyé (un seul ID ici)
|
||||||
|
Channel: ["1409140845738856498"]
|
||||||
|
|
||||||
|
# Catégorie où seront créés les tickets
|
||||||
|
Categories: ["1368955351512387634"]
|
||||||
|
|
||||||
|
# Méthode d'interaction pour ouvrir un ticket :
|
||||||
|
# - "buttons" = un bouton par type de ticket
|
||||||
|
# - "select" = un menu déroulant avec plusieurs choix
|
||||||
|
InteractionType: "buttons"
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# Paramètres de l'embed du panel
|
||||||
|
# =========================
|
||||||
|
Embed:
|
||||||
|
Title: "🎟️ Support Tickets" # Titre de l'embed
|
||||||
|
Description: "> Cliquez sur un bouton ou sélectionnez un type de ticket."
|
||||||
|
Color: "#5e99ff" # Couleur de l'embed (hexadécimal ou int)
|
||||||
|
PanelImage: "https://i.imgur.com/wOifew6.png" # Image principale de l'embed
|
||||||
|
Timestamp: false # true = ajoute un timestamp, false = désactivé
|
||||||
|
Footer:
|
||||||
|
Enabled: true # Active/désactive le footer
|
||||||
|
Text: "QuantumCraft Studios" # Texte affiché en bas de l'embed
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# Boutons du panel (si InteractionType = "buttons")
|
||||||
|
# =========================
|
||||||
|
Buttons:
|
||||||
|
- Label: "🎟️ Support Général"
|
||||||
|
Style: "Primary"
|
||||||
|
CustomId: "ticket_general"
|
||||||
|
- Label: "⚙️ Support Technique"
|
||||||
|
Style: "Secondary"
|
||||||
|
CustomId: "ticket_tech"
|
||||||
|
- Label: "💰 Facturation"
|
||||||
|
Style: "Success"
|
||||||
|
CustomId: "ticket_billing"
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# Menu déroulant (si InteractionType = "select")
|
||||||
|
# =========================
|
||||||
|
SelectMenu:
|
||||||
|
Placeholder: "Choisis le type de ticket..."
|
||||||
|
Options:
|
||||||
|
- Label: "🎟️ Support Général"
|
||||||
|
Value: "ticket_general"
|
||||||
|
Description: "Ouvrir un ticket général"
|
||||||
|
- Label: "⚙️ Support Technique"
|
||||||
|
Value: "ticket_tech"
|
||||||
|
Description: "Ouvrir un ticket technique"
|
||||||
|
- Label: "💰 Facturation"
|
||||||
|
Value: "ticket_billing"
|
||||||
|
Description: "Ouvrir un ticket lié à la facturation"
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# Configuration des Modals (formulaires)
|
||||||
|
# =========================
|
||||||
|
Modals:
|
||||||
|
ticket_general:
|
||||||
|
Enabled: true
|
||||||
|
Title: "Support Général"
|
||||||
|
Inputs:
|
||||||
|
- CustomId: "subject"
|
||||||
|
Label: "Sujet du ticket"
|
||||||
|
Style: "Short"
|
||||||
|
Required: true
|
||||||
|
- CustomId: "description"
|
||||||
|
Label: "Décris ton problème"
|
||||||
|
Style: "Paragraph"
|
||||||
|
Required: true
|
||||||
|
|
||||||
|
ticket_billing:
|
||||||
|
Enabled: true
|
||||||
|
Title: "Facturation"
|
||||||
|
Inputs:
|
||||||
|
- CustomId: "invoice"
|
||||||
|
Label: "ID de facture"
|
||||||
|
Style: "Short"
|
||||||
|
Required: true
|
||||||
|
- CustomId: "description"
|
||||||
|
Label: "Explique ton problème"
|
||||||
|
Style: "Paragraph"
|
||||||
|
Required: true
|
||||||
|
|
||||||
|
# Modal désactivé : le ticket sera créé directement sans formulaire
|
||||||
|
ticket_tech:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# Configuration des alertes de ticket
|
||||||
|
# =========================
|
||||||
|
TicketAlert:
|
||||||
|
Enabled: true # true = active la commande /alert, false = désactive totalement la fonctionnalité
|
||||||
|
DMUser: false # true = envoie aussi une alerte en message privé à l'auteur du ticket (si ses MP sont ouverts)
|
||||||
|
# false = aucune alerte en MP, uniquement dans le ticket
|
||||||
|
Time: "1m" # durée avant la fermeture automatique du ticket après une alerte
|
||||||
|
# ⚠️ format supporté :
|
||||||
|
# - "1m" = 1 minute
|
||||||
|
# - "10m" = 10 minutes
|
||||||
|
# - "1h" = 1 heure
|
||||||
|
# - "12h" = 12 heures
|
||||||
|
# - "1d" = 1 jour
|
||||||
|
# Après ce délai, si personne n'annule le ticket se ferme automatiquement
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
const { REST, Routes } = require("discord.js");
|
||||||
|
const { getCommandsJSON } = require("../../handlers/commandHandler");
|
||||||
|
const logger = require("../../handlers/logger");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
name: "clientReady",
|
||||||
|
once: true,
|
||||||
|
async execute(client) {
|
||||||
|
const commands = getCommandsJSON();
|
||||||
|
const rest = new REST({ version: "10" }).setToken(process.env.DISCORD_TOKEN);
|
||||||
|
|
||||||
|
try {
|
||||||
|
logger.info("⏳ Déploiement des slash commands...");
|
||||||
|
await rest.put(
|
||||||
|
Routes.applicationCommands(process.env.CLIENT_ID),
|
||||||
|
{ body: commands }
|
||||||
|
);
|
||||||
|
logger.success(`✅ ${commands.length} slash commands déployées avec succès !`);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`❌ Erreur lors du déploiement des commandes : ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
const {
|
||||||
|
ActionRowBuilder,
|
||||||
|
ButtonBuilder,
|
||||||
|
ButtonStyle,
|
||||||
|
EmbedBuilder,
|
||||||
|
StringSelectMenuBuilder
|
||||||
|
} = require("discord.js");
|
||||||
|
const { config } = require("../../handlers/configLoader");
|
||||||
|
const logger = require("../../handlers/logger");
|
||||||
|
|
||||||
|
function parseColor(raw) {
|
||||||
|
if (!raw) return 0x5865F2;
|
||||||
|
if (typeof raw === "number") return raw;
|
||||||
|
if (typeof raw === "string") {
|
||||||
|
if (raw.startsWith("0x")) return parseInt(raw, 16);
|
||||||
|
if (raw.startsWith("#")) return parseInt(raw.slice(1), 16);
|
||||||
|
const asInt = parseInt(raw);
|
||||||
|
if (!isNaN(asInt)) return asInt;
|
||||||
|
}
|
||||||
|
return 0x5865F2;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
name: "clientReady",
|
||||||
|
once: true,
|
||||||
|
async execute(client) {
|
||||||
|
const panelConfig = config.TicketPanel.Panel;
|
||||||
|
const channelId = Array.isArray(panelConfig.Channel)
|
||||||
|
? panelConfig.Channel[0]
|
||||||
|
: panelConfig.Channel;
|
||||||
|
|
||||||
|
const channel = await client.channels.fetch(channelId).catch(() => null);
|
||||||
|
|
||||||
|
if (!channel) {
|
||||||
|
return logger.error("❌ Impossible de trouver le salon défini pour le panel de ticket.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Embed principal ===
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setTitle(panelConfig.Embed.Title || "")
|
||||||
|
.setDescription(panelConfig.Embed.Description || "")
|
||||||
|
.setColor(parseColor(panelConfig.Embed.Color));
|
||||||
|
|
||||||
|
if (panelConfig.Embed.PanelImage) embed.setImage(panelConfig.Embed.PanelImage);
|
||||||
|
if (panelConfig.Embed.CustomThumbnailURL) embed.setThumbnail(panelConfig.Embed.CustomThumbnailURL);
|
||||||
|
if (panelConfig.Embed.Timestamp) embed.setTimestamp();
|
||||||
|
|
||||||
|
if (panelConfig.Embed.Footer && panelConfig.Embed.Footer.Enabled) {
|
||||||
|
embed.setFooter({
|
||||||
|
text: panelConfig.Embed.Footer.Text || "",
|
||||||
|
iconURL: panelConfig.Embed.Footer.CustomIconURL || null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let row;
|
||||||
|
|
||||||
|
// === Gestion des interactions ===
|
||||||
|
if (panelConfig.InteractionType === "select" && panelConfig.SelectMenu) {
|
||||||
|
// 📌 Mode Select Menu
|
||||||
|
const menu = new StringSelectMenuBuilder()
|
||||||
|
.setCustomId("ticket_select")
|
||||||
|
.setPlaceholder(panelConfig.SelectMenu.Placeholder || "Choisis une option...");
|
||||||
|
|
||||||
|
for (const opt of panelConfig.SelectMenu.Options) {
|
||||||
|
const option = {
|
||||||
|
label: opt.Label,
|
||||||
|
value: opt.Value,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (opt.Description) option.description = opt.Description;
|
||||||
|
if (opt.Emoji) option.emoji = opt.Emoji;
|
||||||
|
|
||||||
|
menu.addOptions(option);
|
||||||
|
}
|
||||||
|
|
||||||
|
row = new ActionRowBuilder().addComponents(menu);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// 📌 Mode Boutons par défaut
|
||||||
|
row = new ActionRowBuilder();
|
||||||
|
if (panelConfig.Buttons && Array.isArray(panelConfig.Buttons)) {
|
||||||
|
for (const btn of panelConfig.Buttons) {
|
||||||
|
row.addComponents(
|
||||||
|
new ButtonBuilder()
|
||||||
|
.setCustomId(btn.CustomId)
|
||||||
|
.setLabel(btn.Label)
|
||||||
|
.setStyle(ButtonStyle[btn.Style])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Nettoyage & envoi ===
|
||||||
|
await channel.bulkDelete(10).catch(() => {});
|
||||||
|
await channel.send({ embeds: [embed], components: [row] });
|
||||||
|
|
||||||
|
logger.success(`✅ Panel "${panelConfig.Name}" envoyé avec succès !`);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
const logger = require("../../handlers/logger");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
name: "clientReady",
|
||||||
|
once: true,
|
||||||
|
execute(client) {
|
||||||
|
logger.success(`Connecté en tant que ${client.user.tag}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
module.exports = {
|
||||||
|
name: "interactionCreate",
|
||||||
|
once: false,
|
||||||
|
async execute(interaction, client) {
|
||||||
|
try {
|
||||||
|
// Slash commands
|
||||||
|
if (interaction.isChatInputCommand()) {
|
||||||
|
const command = client.commands.get(interaction.commandName);
|
||||||
|
if (command) return command.execute(interaction, client);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buttons
|
||||||
|
if (interaction.isButton()) {
|
||||||
|
for (const button of client.buttons.values()) {
|
||||||
|
if (typeof button.id === "string" && button.id === interaction.customId) {
|
||||||
|
return button.execute(interaction, client);
|
||||||
|
}
|
||||||
|
if (button.id instanceof RegExp && button.id.test(interaction.customId)) {
|
||||||
|
return button.execute(interaction, client);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modals
|
||||||
|
if (interaction.isModalSubmit()) {
|
||||||
|
for (const modal of client.modals.values()) {
|
||||||
|
if (typeof modal.id === "string" && modal.id === interaction.customId) {
|
||||||
|
return modal.execute(interaction, client);
|
||||||
|
}
|
||||||
|
if (modal.id instanceof RegExp && modal.id.test(interaction.customId)) {
|
||||||
|
return modal.execute(interaction, client);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Menus
|
||||||
|
if (
|
||||||
|
interaction.isStringSelectMenu() ||
|
||||||
|
interaction.isUserSelectMenu() ||
|
||||||
|
interaction.isRoleSelectMenu() ||
|
||||||
|
interaction.isChannelSelectMenu()
|
||||||
|
) {
|
||||||
|
for (const menu of client.menus.values()) {
|
||||||
|
if (typeof menu.id === "string" && menu.id === interaction.customId) {
|
||||||
|
return menu.execute(interaction, client);
|
||||||
|
}
|
||||||
|
if (menu.id instanceof RegExp && menu.id.test(interaction.customId)) {
|
||||||
|
return menu.execute(interaction, client);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
if (interaction.replied || interaction.deferred) {
|
||||||
|
await interaction.followUp({ content: "❌ Une erreur est survenue.", flags: 64 });
|
||||||
|
} else {
|
||||||
|
await interaction.reply({ content: "❌ Une erreur est survenue.", flags: 64 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
const { getTicketByChannel } = require("../../handlers/database");
|
||||||
|
const { cancelTicketClosure } = require("../../handlers/alertManager");
|
||||||
|
const { lang } = require("../../handlers/configLoader");
|
||||||
|
const { EmbedBuilder } = require("discord.js");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
name: "messageCreate",
|
||||||
|
once: false,
|
||||||
|
async execute(message) {
|
||||||
|
if (message.author.bot) return;
|
||||||
|
|
||||||
|
const ticket = getTicketByChannel(message.channel.id);
|
||||||
|
if (!ticket) return;
|
||||||
|
|
||||||
|
if (message.author.id === ticket.userId) {
|
||||||
|
if (cancelTicketClosure(message.channel.id)) {
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setDescription(lang.commands.alert.auto_cancelled)
|
||||||
|
.setColor(0x2ecc71)
|
||||||
|
.setTimestamp();
|
||||||
|
|
||||||
|
await message.channel.send({ embeds: [embed] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
const activeAlerts = new Map();
|
||||||
|
|
||||||
|
|
||||||
|
function scheduleTicketClosure(channel, duration, closeFn) {
|
||||||
|
if (activeAlerts.has(channel.id)) {
|
||||||
|
clearTimeout(activeAlerts.get(channel.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeout = setTimeout(async () => {
|
||||||
|
activeAlerts.delete(channel.id);
|
||||||
|
|
||||||
|
await closeFn();
|
||||||
|
}, duration);
|
||||||
|
|
||||||
|
activeAlerts.set(channel.id, timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelTicketClosure(channelId) {
|
||||||
|
if (activeAlerts.has(channelId)) {
|
||||||
|
clearTimeout(activeAlerts.get(channelId));
|
||||||
|
activeAlerts.delete(channelId);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { scheduleTicketClosure, cancelTicketClosure };
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
const { Collection } = require("discord.js");
|
||||||
|
|
||||||
|
function loadButtons(client) {
|
||||||
|
const interactionsPath = path.join(__dirname, "../interactions");
|
||||||
|
client.buttons = new Collection();
|
||||||
|
|
||||||
|
if (!fs.existsSync(interactionsPath)) return client.buttons;
|
||||||
|
|
||||||
|
const files = fs.readdirSync(interactionsPath).filter(f => f.endsWith(".js"));
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const button = require(path.join(interactionsPath, file));
|
||||||
|
|
||||||
|
if (button.id) {
|
||||||
|
client.buttons.set(button.id, button);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return client.buttons;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleButton(interaction, client) {
|
||||||
|
const button = client.buttons.get(interaction.customId);
|
||||||
|
|
||||||
|
if (button) return button.execute(interaction, client);
|
||||||
|
|
||||||
|
for (const btn of client.buttons.values()) {
|
||||||
|
if (btn.id instanceof RegExp && btn.id.test(interaction.customId)) {
|
||||||
|
return btn.execute(interaction, client);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { loadButtons, handleButton };
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
const { Collection } = require("discord.js");
|
||||||
|
|
||||||
|
function walkCommands(dir, callback) {
|
||||||
|
const files = fs.readdirSync(dir, { withFileTypes: true });
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const filePath = path.join(dir, file.name);
|
||||||
|
|
||||||
|
if (file.isDirectory()) {
|
||||||
|
walkCommands(filePath, callback);
|
||||||
|
} else if (file.name.endsWith(".js")) {
|
||||||
|
const command = require(filePath);
|
||||||
|
callback(command);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadCommands(client) {
|
||||||
|
const commandsPath = path.join(__dirname, "../SlashCommand");
|
||||||
|
client.commands = new Collection();
|
||||||
|
|
||||||
|
walkCommands(commandsPath, (command) => {
|
||||||
|
client.commands.set(command.data.name, command);
|
||||||
|
});
|
||||||
|
|
||||||
|
return client.commands;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCommandsJSON() {
|
||||||
|
const commandsPath = path.join(__dirname, "../SlashCommand");
|
||||||
|
const commands = [];
|
||||||
|
|
||||||
|
walkCommands(commandsPath, (command) => {
|
||||||
|
commands.push(command.data.toJSON());
|
||||||
|
});
|
||||||
|
|
||||||
|
return commands;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { loadCommands, getCommandsJSON };
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
const YAML = require("yaml");
|
||||||
|
|
||||||
|
function loadYAML(file) {
|
||||||
|
const filePath = path.join(__dirname, "..", file);
|
||||||
|
const content = fs.readFileSync(filePath, "utf8");
|
||||||
|
return YAML.parse(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = loadYAML("config.yml");
|
||||||
|
const lang = loadYAML("lang.yml");
|
||||||
|
|
||||||
|
module.exports = { config, lang };
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
const initSqlJs = require("sql.js");
|
||||||
|
|
||||||
|
const dbPath = path.join(__dirname, "../tickets.db");
|
||||||
|
|
||||||
|
let db;
|
||||||
|
|
||||||
|
async function initDB() {
|
||||||
|
const SQL = await initSqlJs();
|
||||||
|
|
||||||
|
if (fs.existsSync(dbPath)) {
|
||||||
|
const fileBuffer = fs.readFileSync(dbPath);
|
||||||
|
db = new SQL.Database(fileBuffer);
|
||||||
|
} else {
|
||||||
|
db = new SQL.Database();
|
||||||
|
db.run(`
|
||||||
|
CREATE TABLE IF NOT EXISTS tickets (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
userId TEXT NOT NULL,
|
||||||
|
channelId TEXT NOT NULL,
|
||||||
|
status TEXT NOT NULL DEFAULT 'open'
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
saveDB();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveDB() {
|
||||||
|
const data = db.export();
|
||||||
|
fs.writeFileSync(dbPath, Buffer.from(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
function createTicket(userId, channelId) {
|
||||||
|
db.run(
|
||||||
|
"INSERT INTO tickets (userId, channelId, status) VALUES (?, ?, 'open')",
|
||||||
|
[String(userId), String(channelId)]
|
||||||
|
);
|
||||||
|
saveDB();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTicketByChannel(channelId) {
|
||||||
|
const stmt = db.prepare(
|
||||||
|
"SELECT * FROM tickets WHERE channelId = ? AND status = 'open'"
|
||||||
|
);
|
||||||
|
stmt.bind([String(channelId)]);
|
||||||
|
const row = stmt.step() ? stmt.getAsObject() : null;
|
||||||
|
stmt.free();
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeTicket(channelId) {
|
||||||
|
db.run("UPDATE tickets SET status = 'closed' WHERE channelId = ?", [
|
||||||
|
String(channelId),
|
||||||
|
]);
|
||||||
|
saveDB();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { initDB, createTicket, getTicketByChannel, closeTicket };
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
function loadEvents(client) {
|
||||||
|
const eventsPath = path.join(__dirname, "../events");
|
||||||
|
|
||||||
|
function walk(dir) {
|
||||||
|
const files = fs.readdirSync(dir, { withFileTypes: true });
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const filePath = path.join(dir, file.name);
|
||||||
|
|
||||||
|
if (file.isDirectory()) {
|
||||||
|
walk(filePath);
|
||||||
|
} else if (file.name.endsWith(".js")) {
|
||||||
|
const event = require(filePath);
|
||||||
|
|
||||||
|
if (event.once) {
|
||||||
|
client.once(event.name, (...args) => event.execute(...args, client));
|
||||||
|
} else {
|
||||||
|
client.on(event.name, (...args) => event.execute(...args, client));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
walk(eventsPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { loadEvents };
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
const chalk = require("chalk");
|
||||||
|
|
||||||
|
function timestamp() {
|
||||||
|
return chalk.gray(`[${new Date().toLocaleTimeString("fr-FR")}]`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const logger = {
|
||||||
|
info: (msg) => {
|
||||||
|
console.log(`${timestamp()} ${chalk.blue("[INFO]")} ${msg}`);
|
||||||
|
},
|
||||||
|
success: (msg) => {
|
||||||
|
console.log(`${timestamp()} ${chalk.green("[SUCCESS]")} ${msg}`);
|
||||||
|
},
|
||||||
|
warn: (msg) => {
|
||||||
|
console.warn(`${timestamp()} ${chalk.yellow("[WARN]")} ${msg}`);
|
||||||
|
},
|
||||||
|
error: (msg) => {
|
||||||
|
console.error(`${timestamp()} ${chalk.red("[ERROR]")} ${msg}`);
|
||||||
|
},
|
||||||
|
debug: (msg) => {
|
||||||
|
if (process.env.DEBUG === "true") {
|
||||||
|
console.log(`${timestamp()} ${chalk.magenta("[DEBUG]")} ${msg}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = logger;
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
const { Collection } = require("discord.js");
|
||||||
|
|
||||||
|
function loadMenus(client) {
|
||||||
|
const interactionsPath = path.join(__dirname, "../interactions");
|
||||||
|
client.menus = new Collection();
|
||||||
|
|
||||||
|
if (!fs.existsSync(interactionsPath)) return client.menus;
|
||||||
|
|
||||||
|
const files = fs.readdirSync(interactionsPath).filter(f => f.toLowerCase().includes("menu") && f.endsWith(".js"));
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const menu = require(path.join(interactionsPath, file));
|
||||||
|
client.menus.set(menu.id, menu);
|
||||||
|
}
|
||||||
|
|
||||||
|
return client.menus;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleMenu(interaction, client) {
|
||||||
|
for (const menu of client.menus.values()) {
|
||||||
|
if (typeof menu.id === "string" && menu.id === interaction.customId) {
|
||||||
|
return menu.execute(interaction, client);
|
||||||
|
}
|
||||||
|
if (menu.id instanceof RegExp && menu.id.test(interaction.customId)) {
|
||||||
|
return menu.execute(interaction, client);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { loadMenus, handleMenu };
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
const { Collection } = require("discord.js");
|
||||||
|
const { config } = require("./configLoader");
|
||||||
|
|
||||||
|
function loadModals(client) {
|
||||||
|
const interactionsPath = path.join(__dirname, "../interactions");
|
||||||
|
client.modals = new Collection();
|
||||||
|
|
||||||
|
const files = fs.readdirSync(interactionsPath).filter(f => f.toLowerCase().includes("modal") && f.endsWith(".js"));
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const modal = require(path.join(interactionsPath, file));
|
||||||
|
client.modals.set(modal.id, modal);
|
||||||
|
}
|
||||||
|
|
||||||
|
return client.modals;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getModal(customId) {
|
||||||
|
return config.TicketPanel?.Modals?.[customId] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { loadModals, getModal };
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
const { config, lang } = require("../handlers/configLoader");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
id: "cancelClosure",
|
||||||
|
async execute(interaction) {
|
||||||
|
const member = interaction.member;
|
||||||
|
const isStaff = config.staffRoles.some(roleId => member.roles.cache.has(roleId));
|
||||||
|
|
||||||
|
if (!isStaff) {
|
||||||
|
return interaction.reply({
|
||||||
|
content: lang.permissions.staff_only,
|
||||||
|
flags: 64
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await interaction.reply({
|
||||||
|
content: lang.commands.alert.cancelled,
|
||||||
|
flags: 64
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
const { PermissionFlagsBits } = require("discord.js");
|
||||||
|
const { closeTicket } = require("../handlers/database");
|
||||||
|
const { config, lang } = require("../handlers/configLoader");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
id: "closeTicket",
|
||||||
|
async execute(interaction) {
|
||||||
|
const member = interaction.member;
|
||||||
|
const isStaff = member.roles.cache.some(role => config.staffRoles.includes(role.id));
|
||||||
|
|
||||||
|
if (!isStaff) {
|
||||||
|
return interaction.reply({ content: lang.permissions.staff_only, flags: 64 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const channel = interaction.channel;
|
||||||
|
|
||||||
|
await interaction.reply({ content: lang.ticket.closing });
|
||||||
|
|
||||||
|
setTimeout(async () => {
|
||||||
|
closeTicket(channel.id);
|
||||||
|
await channel.delete().catch(() => {});
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
const {
|
||||||
|
ModalBuilder,
|
||||||
|
TextInputBuilder,
|
||||||
|
TextInputStyle,
|
||||||
|
ActionRowBuilder,
|
||||||
|
PermissionFlagsBits,
|
||||||
|
EmbedBuilder
|
||||||
|
} = require("discord.js");
|
||||||
|
const { getModal } = require("../handlers/modalHandler");
|
||||||
|
const { createTicket } = require("../handlers/database");
|
||||||
|
const { config } = require("../handlers/configLoader");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
id: /^ticket_.+$/,
|
||||||
|
async execute(interaction, client) {
|
||||||
|
const modalConfig = getModal(interaction.customId);
|
||||||
|
|
||||||
|
const staffRoles = (Array.isArray(config.staffRoles) ? config.staffRoles : [config.staffRoles])
|
||||||
|
.filter(r => r && r.trim() !== "");
|
||||||
|
|
||||||
|
if (!modalConfig || modalConfig.Enabled === false) {
|
||||||
|
const guild = interaction.guild;
|
||||||
|
const member = interaction.member;
|
||||||
|
|
||||||
|
const categories = config.TicketPanel.Panel.Categories;
|
||||||
|
const parentCategory = categories && categories.length > 0 ? categories[0] : null;
|
||||||
|
|
||||||
|
const overwrites = [
|
||||||
|
{ id: guild.id, deny: [PermissionFlagsBits.ViewChannel] },
|
||||||
|
{
|
||||||
|
id: member.id,
|
||||||
|
allow: [
|
||||||
|
PermissionFlagsBits.ViewChannel,
|
||||||
|
PermissionFlagsBits.SendMessages,
|
||||||
|
PermissionFlagsBits.ReadMessageHistory
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const roleId of staffRoles) {
|
||||||
|
overwrites.push({
|
||||||
|
id: roleId,
|
||||||
|
allow: [
|
||||||
|
PermissionFlagsBits.ViewChannel,
|
||||||
|
PermissionFlagsBits.SendMessages,
|
||||||
|
PermissionFlagsBits.ReadMessageHistory
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const channel = await guild.channels.create({
|
||||||
|
name: `ticket-${member.user.username}`.toLowerCase(),
|
||||||
|
type: 0,
|
||||||
|
parent: parentCategory || undefined,
|
||||||
|
permissionOverwrites: overwrites
|
||||||
|
});
|
||||||
|
|
||||||
|
createTicket(member.id, channel.id);
|
||||||
|
|
||||||
|
await interaction.reply({
|
||||||
|
content: `✅ Ton ticket a été créé : ${channel}`,
|
||||||
|
flags: 64
|
||||||
|
});
|
||||||
|
|
||||||
|
await channel.send(`🎟️ Bonjour ${member}, un membre du staff va bientôt te répondre.`);
|
||||||
|
|
||||||
|
const logChannel = await client.channels.fetch(config.logsChannel).catch(() => null);
|
||||||
|
if (logChannel) {
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setTitle("🎟️ Nouveau ticket (sans formulaire)")
|
||||||
|
.addFields(
|
||||||
|
{ name: "Utilisateur", value: `${member.user.tag} (${member.id})` },
|
||||||
|
{ name: "Salon", value: `${channel}` }
|
||||||
|
)
|
||||||
|
.setColor(0x2ecc71)
|
||||||
|
.setTimestamp();
|
||||||
|
|
||||||
|
await logChannel.send({ embeds: [embed] });
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const modal = new ModalBuilder()
|
||||||
|
.setCustomId(`ticket_modal_${interaction.customId}`)
|
||||||
|
.setTitle(modalConfig.Title || "Création de ticket");
|
||||||
|
|
||||||
|
modalConfig.Inputs.forEach(input => {
|
||||||
|
const textInput = new TextInputBuilder()
|
||||||
|
.setCustomId(input.CustomId)
|
||||||
|
.setLabel(input.Label)
|
||||||
|
.setStyle(TextInputStyle[input.Style] || TextInputStyle.Short)
|
||||||
|
.setRequired(input.Required ?? false);
|
||||||
|
|
||||||
|
modal.addComponents(new ActionRowBuilder().addComponents(textInput));
|
||||||
|
});
|
||||||
|
|
||||||
|
await interaction.showModal(modal);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
const { ModalBuilder, TextInputBuilder, TextInputStyle, ActionRowBuilder, PermissionFlagsBits, EmbedBuilder } = require("discord.js");
|
||||||
|
const { getModal } = require("../handlers/modalHandler");
|
||||||
|
const { createTicket } = require("../handlers/database");
|
||||||
|
const { config } = require("../handlers/configLoader");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
id: "ticket_select",
|
||||||
|
async execute(interaction, client) {
|
||||||
|
const selected = interaction.values[0];
|
||||||
|
const modalConfig = getModal(selected);
|
||||||
|
|
||||||
|
if (!modalConfig || modalConfig.Enabled === false) {
|
||||||
|
const guild = interaction.guild;
|
||||||
|
const member = interaction.member;
|
||||||
|
|
||||||
|
const categories = config.TicketPanel.Panel.Categories;
|
||||||
|
const parentCategory = categories && categories.length > 0 ? categories[0] : null;
|
||||||
|
|
||||||
|
const channel = await guild.channels.create({
|
||||||
|
name: `ticket-${member.user.username}`.toLowerCase(),
|
||||||
|
type: 0,
|
||||||
|
parent: parentCategory || undefined,
|
||||||
|
permissionOverwrites: [
|
||||||
|
{ id: guild.id, deny: [PermissionFlagsBits.ViewChannel] },
|
||||||
|
{
|
||||||
|
id: member.id,
|
||||||
|
allow: [
|
||||||
|
PermissionFlagsBits.ViewChannel,
|
||||||
|
PermissionFlagsBits.SendMessages,
|
||||||
|
PermissionFlagsBits.ReadMessageHistory
|
||||||
|
]
|
||||||
|
},
|
||||||
|
...config.staffRoles.map(roleId => ({
|
||||||
|
id: roleId,
|
||||||
|
allow: [
|
||||||
|
PermissionFlagsBits.ViewChannel,
|
||||||
|
PermissionFlagsBits.SendMessages,
|
||||||
|
PermissionFlagsBits.ReadMessageHistory
|
||||||
|
]
|
||||||
|
}))
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
createTicket(member.id, channel.id);
|
||||||
|
|
||||||
|
await interaction.reply({
|
||||||
|
content: `✅ Ton ticket a été créé : ${channel}`,
|
||||||
|
flags: 64
|
||||||
|
});
|
||||||
|
|
||||||
|
await channel.send(`🎟️ Bonjour ${member}, un membre du staff va bientôt te répondre.`);
|
||||||
|
|
||||||
|
const logChannel = await client.channels.fetch(config.logsChannel).catch(() => null);
|
||||||
|
if (logChannel) {
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setTitle("🎟️ Nouveau ticket (sans formulaire - via SelectMenu)")
|
||||||
|
.addFields(
|
||||||
|
{ name: "Utilisateur", value: `${member.user.tag} (${member.id})` },
|
||||||
|
{ name: "Salon", value: `${channel}` },
|
||||||
|
{ name: "Type de ticket", value: selected }
|
||||||
|
)
|
||||||
|
.setColor(0x2ecc71)
|
||||||
|
.setTimestamp();
|
||||||
|
|
||||||
|
await logChannel.send({ embeds: [embed] });
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const modal = new ModalBuilder()
|
||||||
|
.setCustomId(`ticket_modal_${selected}`)
|
||||||
|
.setTitle(modalConfig.Title || "Création de ticket");
|
||||||
|
|
||||||
|
modalConfig.Inputs.forEach(input => {
|
||||||
|
const textInput = new TextInputBuilder()
|
||||||
|
.setCustomId(input.CustomId)
|
||||||
|
.setLabel(input.Label)
|
||||||
|
.setStyle(TextInputStyle[input.Style] || TextInputStyle.Short)
|
||||||
|
.setRequired(input.Required ?? false);
|
||||||
|
|
||||||
|
modal.addComponents(new ActionRowBuilder().addComponents(textInput));
|
||||||
|
});
|
||||||
|
|
||||||
|
await interaction.showModal(modal);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
const { EmbedBuilder, PermissionFlagsBits } = require("discord.js");
|
||||||
|
const { createTicket } = require("../handlers/database");
|
||||||
|
const { config } = require("../handlers/configLoader");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
id: /^ticket_modal_.+$/,
|
||||||
|
async execute(interaction, client) {
|
||||||
|
const originalId = interaction.customId.replace("ticket_modal_", "");
|
||||||
|
const modalConfig = config.TicketPanel?.Modals?.[originalId];
|
||||||
|
|
||||||
|
if (!modalConfig) {
|
||||||
|
return interaction.reply({
|
||||||
|
content: "❌ Aucun formulaire configuré pour ce ticket.",
|
||||||
|
flags: 64
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const member = interaction.member;
|
||||||
|
const guild = interaction.guild;
|
||||||
|
|
||||||
|
const categories = config.TicketPanel.Panel.Categories;
|
||||||
|
const parentCategory = categories && categories.length > 0 ? categories[0] : null;
|
||||||
|
|
||||||
|
const staffRoles = (Array.isArray(config.staffRoles) ? config.staffRoles : [config.staffRoles])
|
||||||
|
.filter(r => r && r.trim() !== "");
|
||||||
|
|
||||||
|
const overwrites = [
|
||||||
|
{ id: guild.id, deny: [PermissionFlagsBits.ViewChannel] },
|
||||||
|
{
|
||||||
|
id: member.id,
|
||||||
|
allow: [
|
||||||
|
PermissionFlagsBits.ViewChannel,
|
||||||
|
PermissionFlagsBits.SendMessages,
|
||||||
|
PermissionFlagsBits.ReadMessageHistory
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const roleId of staffRoles) {
|
||||||
|
overwrites.push({
|
||||||
|
id: roleId,
|
||||||
|
allow: [
|
||||||
|
PermissionFlagsBits.ViewChannel,
|
||||||
|
PermissionFlagsBits.SendMessages,
|
||||||
|
PermissionFlagsBits.ReadMessageHistory
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const channel = await guild.channels.create({
|
||||||
|
name: `ticket-${member.user.username}`.toLowerCase(),
|
||||||
|
type: 0,
|
||||||
|
parent: parentCategory || undefined,
|
||||||
|
permissionOverwrites: overwrites
|
||||||
|
});
|
||||||
|
|
||||||
|
createTicket(member.id, channel.id);
|
||||||
|
|
||||||
|
const fieldsOutput = modalConfig.Inputs.map(input => {
|
||||||
|
const value = interaction.fields.getTextInputValue(input.CustomId);
|
||||||
|
return `**${input.Label}** : ${value}`;
|
||||||
|
}).join("\n");
|
||||||
|
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setTitle(`🎟️ ${modalConfig.Title || "Nouveau ticket"}`)
|
||||||
|
.setDescription(`👤 Ouvert par: ${member}\n\n${fieldsOutput}`)
|
||||||
|
.setColor(0x5e99ff)
|
||||||
|
.setTimestamp();
|
||||||
|
|
||||||
|
await channel.send({
|
||||||
|
content: staffRoles.map(r => `<@&${r}>`).join(" "),
|
||||||
|
embeds: [embed]
|
||||||
|
});
|
||||||
|
|
||||||
|
await interaction.reply({
|
||||||
|
content: `✅ Ton ticket a été créé : ${channel}`,
|
||||||
|
flags: 64
|
||||||
|
});
|
||||||
|
|
||||||
|
const logChannel = await client.channels.fetch(config.logsChannel).catch(() => null);
|
||||||
|
if (logChannel) {
|
||||||
|
const logEmbed = new EmbedBuilder()
|
||||||
|
.setTitle("🎟️ Nouveau ticket")
|
||||||
|
.setDescription(`Formulaire utilisé: \`${originalId}\``)
|
||||||
|
.addFields(
|
||||||
|
{ name: "Utilisateur", value: `${member.user.tag} (${member.id})` },
|
||||||
|
{ name: "Salon", value: `${channel}` },
|
||||||
|
{ name: "Réponses", value: fieldsOutput }
|
||||||
|
)
|
||||||
|
.setColor(0x2ecc71)
|
||||||
|
.setTimestamp();
|
||||||
|
|
||||||
|
await logChannel.send({ embeds: [logEmbed] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
ticket:
|
||||||
|
already_open: "❌ Tu as déjà un ticket ouvert."
|
||||||
|
created: "✅ Ticket créé : {channel}"
|
||||||
|
greeting: "🎟️ Bonjour {user}, un membre du staff va bientôt te répondre."
|
||||||
|
closing: "⏳ Fermeture du ticket dans 5 secondes..."
|
||||||
|
closed: "✅ Ticket fermé avec succès."
|
||||||
|
not_in_ticket: "❌ Cette commande doit être utilisée dans un ticket."
|
||||||
|
no_permission: "❌ Tu n'as pas la permission de fermer ce ticket."
|
||||||
|
logs_message: "📝 Ticket fermé par {user} (ID: {userId})"
|
||||||
|
|
||||||
|
errors:
|
||||||
|
generic: "❌ Une erreur est survenue."
|
||||||
|
not_found: "❌ Ressource introuvable."
|
||||||
|
db_error: "❌ Une erreur est survenue avec la base de données."
|
||||||
|
missing_config: "⚠️ Paramètre manquant dans config.yml."
|
||||||
|
|
||||||
|
commands:
|
||||||
|
close:
|
||||||
|
description: "Fermer un ticket ouvert"
|
||||||
|
success: "✅ Le ticket sera fermé sous peu..."
|
||||||
|
error: "❌ Impossible de fermer ce ticket."
|
||||||
|
add:
|
||||||
|
description: "Ajouter un utilisateur à un ticket"
|
||||||
|
success: "✅ {user} a bien été ajouté au ticket."
|
||||||
|
already_added: "⚠️ {user} est déjà présent dans ce ticket."
|
||||||
|
welcome: "👋 Bienvenue {user}, tu as été ajouté à ce ticket."
|
||||||
|
error: "❌ Impossible d'ajouter cet utilisateur au ticket."
|
||||||
|
remove:
|
||||||
|
description: "Retirer un utilisateur du ticket"
|
||||||
|
success: "✅ {user} a bien été retiré du ticket."
|
||||||
|
not_in_ticket: "⚠️ {user} n'est pas présent dans ce ticket."
|
||||||
|
goodbye: "👋 {user} a été retiré de ce ticket."
|
||||||
|
error: "❌ Impossible de retirer cet utilisateur."
|
||||||
|
alert:
|
||||||
|
description: "Alerter l’auteur que le ticket va être fermé"
|
||||||
|
cancelled: "✅ La fermeture automatique du ticket a été annulée."
|
||||||
|
auto_cancelled: "✅ Réponse détectée, la fermeture automatique du ticket a été annulée."
|
||||||
|
dm_unreachable: "⚠️ Impossible d’envoyer un DM à l’auteur du ticket."
|
||||||
|
sent: "⏳ Une alerte de fermeture a été envoyée. Le ticket sera fermé {time} si aucune réponse n’est donnée. Inactivité détectée depuis {inactive-time}."
|
||||||
|
close_now_label: "🔴 Fermer maintenant"
|
||||||
|
cancel_label: "🚫 Annuler la fermeture"
|
||||||
|
close_confirm: "✅ Le ticket va être fermé immédiatement."
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
staff_only: "❌ Cette commande est réservée au staff."
|
||||||
|
user_only: "❌ Seuls les utilisateurs peuvent utiliser cette commande."
|
||||||
Reference in New Issue
Block a user