diff --git a/README.md b/README.md index 2b66915..07b9bb6 100644 --- a/README.md +++ b/README.md @@ -19,13 +19,14 @@ cd mon-bot-discord 3. Créer un fichier `.env` : ```env -TOKEN=VOTRE_TOKEN_BOT -CLIENT_ID=VOTRE_CLIENT_ID -CLIENT_SECRET=VOTRE_CLIENT_SECRET -REDIRECT_URI=http://localhost:3000/auth/discord/callback -SESSION_SECRET=un_secret_aleatoire +CLIENT_ID=VOTRE_BOT_CLIENT_ID +CLIENT_SECRET=VOTRE_BOT_CLIENT_SECRET +REDIRECT_URI=https://your_domaine.com/auth/discord/callback PORT=3000 +BOT_TOKEN=VOTRE_TOKEN_BOT +SESSION_SECRET=un_secret_aleatoire_pour_les_sessions DB_PATH=database.sqlite +OWNER=VOTRE_ID_UTILISATEUR ``` 4. Lancer le serveur : diff --git a/app/bot.js b/app/bot.js index ec53c3b..b49142d 100644 --- a/app/bot.js +++ b/app/bot.js @@ -7,6 +7,9 @@ const e = require('express'); const client = new Client({ intents: Object.values(GatewayIntentBits) }); +require("./loader/events.js")(client); +require("./loader/commands.js")(client); + client.once(Events.ClientReady, async () => { console.log(`Bot connecté en tant que ${client.user.tag}`); await loadSlashCommands(client); @@ -17,10 +20,6 @@ client.once(Events.ClientReady, async () => { client.on(Events.InteractionCreate, async interaction => { if (!interaction.isChatInputCommand()) return; - if (interaction.commandName === 'ping') { - await interaction.reply('Pong!'); - } - else if (interaction.commandName === 'level') { const guildId = interaction.guild.id; const userId = interaction.user.id; diff --git a/app/commands/ping.js b/app/commands/ping.js new file mode 100644 index 0000000..58727ce --- /dev/null +++ b/app/commands/ping.js @@ -0,0 +1,170 @@ +const addCommand = require("../fonctions/addCommand"); +const { + SlashCommandBuilder, + ButtonStyle, + ButtonBuilder, + ActionRowBuilder, + EmbedBuilder, +} = require("discord.js"); + +module.exports = addCommand( + (this.name = "ping"), + (this.description = "Cette commande permet de vérifier la latence du bot."), + (this.aliases = ["latency", "lag", "responseTime"]), + (this.permissions = []), + (this.botOwnerOnly = false), + (this.dm = true), + (this.executePrefix = async (client, message, args) => { + const pingBtn = new ButtonBuilder() + .setCustomId("pingBtn") + .setLabel("🔄") + .setStyle(ButtonStyle.Primary); + + const row = new ActionRowBuilder().addComponents(pingBtn); + + const embed = new EmbedBuilder() + .setTitle("Pong !") + .setDescription(`La latence du bot est de \`${client.ws.ping}\`ms.`) + .setColor("#0099FF") + .setTimestamp() + .setFooter({ + text: `Demandé par ${message.author.tag}`, + iconURL: message.author.displayAvatarURL(), + }); + + const sendMessage = await message.reply({ + embeds: [embed], + components: [row], + }); + + const filter = (i) => i.customId === "pingBtn"; + const collector = sendMessage.createMessageComponentCollector({ + filter, + time: 120000, + }); + collector.on("collect", async (i) => { + if (i.user.id !== message.author.id) + return i.reply({ + content: "Vous n'êtes pas l'auteur du message.", + ephemeral: true, + }); + const embed = new EmbedBuilder() + .setTitle("Pong !") + .setDescription(`La latence du bot est de \`${client.ws.ping}\`ms.`) + .setColor("#0099FF") + .setTimestamp() + .setFooter({ + text: `Demandé par ${message.author.tag}`, + iconURL: message.author.displayAvatarURL(), + }); + + sendMessage.edit({ embeds: [embed], components: [row] }); + i.reply({ content: "La latence a été rafraichie.", ephemeral: true }); + }); + + collector.on("end", async () => { + const embed = new EmbedBuilder() + .setTitle("Pong !") + .setDescription(`La latence du bot est de \`${client.ws.ping}\`ms.`) + .setColor("#0099FF") + .setTimestamp() + .setFooter({ + text: `Demandé par ${message.author.tag}`, + iconURL: message.author.displayAvatarURL(), + }); + + sendMessage.edit({ embeds: [embed], components: [] }); + }); + }), + (this.executeSlash = async (client, interaction) => { + const pingBtn = new ButtonBuilder() + .setCustomId("pingBtn") + .setLabel("🔄") + .setStyle(ButtonStyle.Primary); + + const row = new ActionRowBuilder().addComponents(pingBtn); + + const embed = new EmbedBuilder() + .setTitle("Pong !") + .setDescription(`La latence du bot est de \`${client.ws.ping}\`ms.`) + .setColor("#0099FF") + .setTimestamp() + .setFooter({ + text: `Demandé par ${interaction.user.tag}`, + iconURL: interaction.user.displayAvatarURL(), + }); + + if (interaction.options.getBoolean("actualiser") === false) { + const sendMessage = await interaction.reply({ + embeds: [embed], + components: [row], + }); + + const filter = (i) => i.customId === "pingBtn"; + const collector = sendMessage.createMessageComponentCollector({ + filter, + time: 120000, + }); + collector.on("collect", async (i) => { + if (i.user.id !== interaction.user.id) + return i.reply({ + content: "Vous n'êtes pas l'auteur du message.", + ephemeral: true, + }); + const embed = new EmbedBuilder() + .setTitle("Pong !") + .setDescription(`La latence du bot est de \`${client.ws.ping}\`ms.`) + .setColor("#0099FF") + .setTimestamp() + .setFooter({ + text: `Demandé par ${interaction.user.tag}`, + iconURL: interaction.user.displayAvatarURL(), + }); + + sendMessage.edit({ embeds: [embed], components: [row] }); + i.reply({ content: "La latence a été rafraichie.", ephemeral: true }); + }); + + collector.on("end", async () => { + const embed = new EmbedBuilder() + .setTitle("Pong !") + .setDescription(`La latence du bot est de \`${client.ws.ping}\`ms.`) + .setColor("#0099FF") + .setTimestamp() + .setFooter({ + text: `Demandé par ${interaction.user.tag}`, + iconURL: interaction.user.displayAvatarURL(), + }); + + sendMessage.edit({ embeds: [embed], components: [] }); + }); + } else { + const sendMessage = await interaction.reply({ embeds: [embed] }); + + const interval = setInterval(() => { + const embed = new EmbedBuilder() + .setTitle("Pong !") + .setDescription(`La latence du bot est de \`${client.ws.ping}\`ms.`) + .setColor("#0099FF") + .setTimestamp() + .setFooter({ + text: `Demandé par ${interaction.user.tag}`, + iconURL: interaction.user.displayAvatarURL(), + }); + + sendMessage.edit({ embeds: [embed] }); + }, 10000); + setTimeout(() => { + clearInterval(interval); + }, 120000); + } + }), + (this.slashOptions = new SlashCommandBuilder().addBooleanOption((option) => + option + .setName("actualiser") + .setDescription( + "Actualiser automatiquement la latence du bot toute les 10 secondes pandant 2 minutes.", + ) + .setRequired(false), + )), +); \ No newline at end of file diff --git a/app/db.js b/app/db.js index ae3816d..4eb717a 100644 --- a/app/db.js +++ b/app/db.js @@ -69,6 +69,12 @@ db.exec(` last_xp_message_timestamp INTEGER, PRIMARY KEY (guild_id, user_id) ); + + CREATE TABLE IF NOT EXISTS prefix ( + guildId TEXT NOT NULL, + prefix TEXT NOT NULL DEFAULT '!', + PRIMARY KEY (guildId) + ); `); module.exports = db; diff --git a/app/events/executeCommands/prefix.js b/app/events/executeCommands/prefix.js new file mode 100644 index 0000000..017c018 --- /dev/null +++ b/app/events/executeCommands/prefix.js @@ -0,0 +1,64 @@ +const getPrefix = require("../../fonctions/getPrefix"); + +module.exports = { + name: "messageCreate", + async execute(client, message) { + if (!message || !message.author) return; + if (message.author.bot) return; + if (!message.content) return; + let prefix; + if (message.channel.type === 1) { + prefix = await getPrefix(message.channel.id); + } else { + prefix = await getPrefix(message.guild.id); + } + if (!message.content.startsWith(prefix)) return; + + const args = message.content.slice(prefix.length).trim().split(/ +/); + const commandName = args.shift().toLowerCase(); + + const command = + client.commands.get(commandName) || + client.commands.find( + (cmd) => cmd.aliases && cmd.aliases.includes(commandName), + ); + if (!command) return; + + if (command.dm !== true && message.channel.type === 1) + return message + .reply({ + content: "Cette commande ne peut pas être utilisée en message privé.", + }) + .then((msg) => setTimeout(() => msg.delete(), 5000)); + if (process.env.OWNER && !process.env.OWNER === message.author.id) { + if (command.botOwnerOnly) + return message + .reply({ + content: "Cette commande est réservée au propriétaire du bot.", + }) + .then((msg) => setTimeout(() => msg.delete(), 5000)); + if ( + command.permissions && + message.channel.type !== 1 && + !command.permissions.every((permission) => + message.member.permissions.has(permission), + ) + ) + return message + .reply({ + content: "Vous n'avez pas la permission d'utiliser cette commande.", + }) + .then((msg) => setTimeout(() => msg.delete(), 5000)); + } + + try { + command.executePrefix(client, message, args); + console.log(`[CMD - PREFIX] ${message.author.tag} | ${commandName}`); + } catch (error) { + console.error( + `Erreur lors de l'exécution de la commande '${commandName}':`, + error, + ); + } + }, +}; \ No newline at end of file diff --git a/app/events/executeCommands/slash.js b/app/events/executeCommands/slash.js new file mode 100644 index 0000000..b97d4c0 --- /dev/null +++ b/app/events/executeCommands/slash.js @@ -0,0 +1,46 @@ + +module.exports = { + name: "interactionCreate", + async execute(client, interaction) { + if (!interaction.isCommand()) return; + + const command = client.commands.get(interaction.commandName); + if (!command) return; + + if (command.dm !== true && interaction.channel.type === 1) + return interaction.reply({ + content: "Cette commande ne peut pas être utilisée en message privé.", + ephemeral: true, + }); + if (process.env.OWNER && !process.env.OWNER === interaction.user.id) { + if (command.botOwnerOnly) + return interaction.reply({ + content: "Cette commande est réservée au propriétaire du bot.", + ephemeral: true, + }); + if ( + command.permissions && + interaction.channel.type !== 1 && + !command.permissions.every((permission) => + interaction.member.permissions.has(permission), + ) + ) + return interaction.reply({ + content: "Vous n'avez pas la permission d'utiliser cette commande.", + ephemeral: true, + }); + } + + try { + command.executeSlash(client, interaction); + console.log( + `[CMD - SLASH] ${interaction.user.tag} | ${interaction.commandName}`, + ); + } catch (error) { + console.error( + `Erreur lors de l'exécution de la commande slash '${interaction.commandName}':`, + error, + ); + } + }, +}; \ No newline at end of file diff --git a/app/fonctions/addCommand.js b/app/fonctions/addCommand.js new file mode 100644 index 0000000..3b8d15f --- /dev/null +++ b/app/fonctions/addCommand.js @@ -0,0 +1,172 @@ +const { SlashCommandBuilder, PermissionsBitField } = require("discord.js"); + +/** + * Ajoute une nouvelle commande au bot. + * + * @param {string} name - Le nom de la commande. + * @param {string} description - La description de la commande. + * @param {Array} aliases - Les alias de la commande. + * @param {Array} permissions - Les permissions nécessaires pour exécuter la commande. + * @param {boolean} botOwnerOnly - Si la commande est réservée au propriétaire du bot. + * @param {boolean} dm - Si la commande peut être exécutée en message privé. + * @param {Function} executePrefix - La fonction à exécuter avec un préfixe. + * @param {Function} executeSlash - La fonction à exécuter comme commande slash. + * @param {Array} slashOptions - Les options pour la commande slash. + * + * @returns {void} Ne retourne rien. + */ +function addCommand( + name, + description, + aliases, + permissions, + botOwnerOnly, + dm, + executePrefix, + executeSlash, + slashOptions, +) { + if (!name) return console.error("Le nom de la commande est requis."); + name = name.toString(); + name = name.toLowerCase(); + name = name.replace(/ /g, "_"); + if (!description) + return console.error("La description de la commande est requise."); + description = description.toString(); + if (!aliases) aliases = []; + if (!Array.isArray(aliases)) aliases = [aliases]; + aliases = aliases.map((alias) => alias.toString()); + if (!permissions) permissions = []; + if (!Array.isArray(permissions)) permissions = [permissions]; + if (!botOwnerOnly) botOwnerOnly = false; + botOwnerOnly = Boolean(botOwnerOnly); + if (!dm) dm = false; + dm = Boolean(dm); + if (!executePrefix) + return console.error("La fonction executePrefix est requise."); + if (!executeSlash) + return console.error("La fonction executeSlash est requise."); + if ( + typeof executePrefix !== "function" || + executePrefix.constructor.name !== "AsyncFunction" + ) { + return console.error( + "La fonction executePrefix doit être une fonction asynchrone.", + ); + } + if ( + typeof executeSlash !== "function" || + executeSlash.constructor.name !== "AsyncFunction" + ) { + return console.error( + "La fonction executeSlash doit être une fonction asynchrone.", + ); + } + const executePrefixParams = executePrefix + .toString() + .match(/\(([^)]+)\)/)[1] + .split(",") + .map((param) => param.trim()); + if ( + executePrefixParams.length !== 3 || + executePrefixParams[0] !== "client" || + executePrefixParams[1] !== "message" || + executePrefixParams[2] !== "args" + ) { + return console.error( + 'La fonction executePrefix doit avoir les paramètres "client", "message" et "args".', + ); + } + const executeSlashParams = executeSlash + .toString() + .match(/\(([^)]+)\)/)[1] + .split(",") + .map((param) => param.trim()); + if ( + executeSlashParams.length !== 2 || + executeSlashParams[0] !== "client" || + executeSlashParams[1] !== "interaction" + ) { + return console.error( + 'La fonction executeSlash doit avoir les paramètres "client" et "interaction".', + ); + } + + const command = { + name, + description, + aliases, + permissions, + botOwnerOnly, + dm, + executePrefix, + executeSlash, + }; + + let default_member_permissions; + if (command.permissions.length === 0) { + default_member_permissions = null; + } else { + default_member_permissions = new PermissionsBitField(); + command.permissions.forEach( + (permission) => (default_member_permissions += BigInt(permission)), + ); + } + command.data = new SlashCommandBuilder() + .setName(command.name) + .setDescription(command.description) + .setDMPermission(command.dm) + .setDefaultMemberPermissions(default_member_permissions); + + for (const key in command.data) { + if (command.data.hasOwnProperty(key)) { + const value = command.data[key]; + + if (value !== undefined && key !== "options") { + slashOptions[key] = value; + } + } + } + + command.data = slashOptions; + + let utilisation = ""; + + command.data.options.forEach((option) => { + let optionUsage = ""; + if (option.choices) { + optionUsage = option.required + ? `<${option.choices.map((choice) => choice.name).join("|")}>` + : `[${option.choices.map((choice) => choice.name).join("|")}]`; + } else { + if (option.type === 3) { + optionUsage = option.required ? `<${option.name}>` : `[${option.name}]`; + } else if (option.type === 4) { + optionUsage = option.required ? `<${option.name}>` : `[${option.name}]`; + } else if (option.type === 5) { + optionUsage = option.required ? `` : `[True|False]`; + } else if (option.type === 6) { + optionUsage = option.required ? `<@member>` : `[@member]`; + } else if (option.type === 7) { + optionUsage = option.required ? `<#channel>` : `[#channel]`; + } else if (option.type === 8) { + optionUsage = option.required ? `<@role>` : `[@role]`; + } else if (option.type === 9) { + optionUsage = option.required ? `<@mention>` : `[@mention]`; + } else if (option.type === 10) { + optionUsage = option.required ? `<${option.name}>` : `[${option.name}]`; + } else if (option.type === 11) { + optionUsage = option.required ? `<${option.name}>` : `[${option.name}]`; + } + } + + utilisation += ` ${optionUsage}`; + }); + + utilisation = utilisation.trim(); + command.utilisation = utilisation; + + return command; +} + +module.exports = addCommand; \ No newline at end of file diff --git a/app/fonctions/getPrefix.js b/app/fonctions/getPrefix.js new file mode 100644 index 0000000..e8e48cb --- /dev/null +++ b/app/fonctions/getPrefix.js @@ -0,0 +1,16 @@ +const db = require("../db.js"); + +module.exports = async function getPrefix(guildId) { + const prefix = await new Promise((resolve, reject) => { + db.get( + `SELECT prefix FROM prefix WHERE guildId = ?`, + [guildId], + (err, row) => { + if (err) reject(err); + resolve(row); + }, + ); + }); + + return prefix ? prefix.prefix : "!"; +}; \ No newline at end of file diff --git a/app/loader/commands.js b/app/loader/commands.js new file mode 100644 index 0000000..a012972 --- /dev/null +++ b/app/loader/commands.js @@ -0,0 +1,59 @@ +const fs = require("fs"); +const path = require("path"); +const { Collection } = require("discord.js"); + +module.exports = (client) => { + client.commands = new Collection(); + + function loadCommandsFromDirectory(directory) { + fs.readdir(directory, (err, files) => { + if (err) { + console.error("Erreur lors de la lecture du dossier:", err); + return; + } + + files.forEach((file) => { + const filePath = path.join(directory, file); + + fs.stat(filePath, (err, stats) => { + if (err) { + console.error( + "Erreur lors de la récupération des informations du fichier:", + err, + ); + return; + } + + if (stats.isDirectory()) { + loadCommandsFromDirectory(filePath); + } else if (stats.isFile() && file.endsWith(".js")) { + try { + const command = require(filePath); + const commandName = command.name || file.split(".")[0]; + if (!command.category) { + const parentDir = path.basename(path.dirname(filePath)); + command.category = + parentDir === "commands" ? "🌟・Other" : parentDir; + } + if (!command.dm) command.dm = false; + if (!command.botOwnerOnly) command.botOwnerOnly = false; + if (!command.permissions) command.permissions = []; + if (!command.aliases) command.aliases = []; + if (!command.description) + command.description = "Aucune description."; + client.commands.set(commandName, command); + delete require.cache[require.resolve(filePath)]; + } catch (error) { + console.error( + `Erreur lors du chargement de la commande '${file}':`, + error, + ); + } + } + }); + }); + }); + } + + loadCommandsFromDirectory(path.join(__dirname, "..", "commands")); +}; \ No newline at end of file diff --git a/app/loader/events.js b/app/loader/events.js new file mode 100644 index 0000000..8c6e4e9 --- /dev/null +++ b/app/loader/events.js @@ -0,0 +1,45 @@ +const fs = require("fs"); +const path = require("path"); + +module.exports = (client) => { + function loadEventsFromDirectory(directory) { + fs.readdir(directory, (err, files) => { + if (err) { + console.error("Erreur lors de la lecture du dossier:", err); + return; + } + + files.forEach((file) => { + const filePath = path.join(directory, file); + + fs.stat(filePath, (err, stats) => { + if (err) { + console.error( + "Erreur lors de la récupération des informations du fichier:", + err, + ); + return; + } + + if (stats.isDirectory()) { + loadEventsFromDirectory(filePath); + } else if (stats.isFile() && file.endsWith(".js")) { + try { + const event = require(filePath); + let eventName = event.name || file.split(".")[0]; + client.on(eventName, event.execute.bind(null, client)); + delete require.cache[require.resolve(filePath)]; + } catch (error) { + console.error( + `Erreur lors du chargement de l'événement '${file}':`, + error, + ); + } + } + }); + }); + }); + } + + loadEventsFromDirectory(path.join(__dirname, "..", "events")); +}; \ No newline at end of file diff --git a/app/slash_commands.js b/app/slash_commands.js index 1610f74..5a17581 100644 --- a/app/slash_commands.js +++ b/app/slash_commands.js @@ -13,17 +13,15 @@ module.exports = async (client, guildId = null) => { ========================= */ if (!guildId) { - const globalCommands = [ - { - name: 'ping', - description: 'Replies with Pong!', - }, - ]; + const globalCommands = []; + client.commands.forEach((command) => { + globalCommands.push(command.data.toJSON()); + }); try { console.log('Refreshing GLOBAL slash commands...'); await rest.put( - Routes.applicationCommands(CLIENT_ID), + Routes.applicationCommands(client.user.id), { body: globalCommands } ); console.log('Global slash commands loaded'); @@ -69,7 +67,7 @@ module.exports = async (client, guildId = null) => { try { await rest.put( - Routes.applicationGuildCommands(CLIENT_ID, guild.id), + Routes.applicationGuildCommands(client.user.id, guild.id), { body: guildCommands } ); console.log(`Guild commands updated for ${guild.name}`);