From 6c9241f349e1fd1f8b323642ed5789c1b8524723 Mon Sep 17 00:00:00 2001 From: Arthur Puechberty Date: Sat, 17 Jan 2026 17:23:49 +0100 Subject: [PATCH] finish organization event/command & add guild command in folder commands --- app/bot.js | 288 +------------------------------- app/commands/level.js | 91 ++++++++++ app/commands/leveltop.js | 101 +++++++++++ app/commands/ping.js | 190 +++++++-------------- app/db.js | 18 ++ app/events/clientReady.js | 11 ++ app/events/guildMemberAdd.js | 39 +++++ app/events/guildMemberRemove.js | 26 +++ app/events/messageCreate.js | 136 +++++++++++++++ app/events/voiceStateUpdate.js | 41 +++++ app/fonctions/addCommand.js | 234 ++++++++++---------------- app/slash_commands.js | 78 ++++----- 12 files changed, 654 insertions(+), 599 deletions(-) create mode 100644 app/commands/level.js create mode 100644 app/commands/leveltop.js create mode 100644 app/events/clientReady.js create mode 100644 app/events/guildMemberAdd.js create mode 100644 app/events/guildMemberRemove.js create mode 100644 app/events/messageCreate.js create mode 100644 app/events/voiceStateUpdate.js diff --git a/app/bot.js b/app/bot.js index b49142d..3cbd573 100644 --- a/app/bot.js +++ b/app/bot.js @@ -1,298 +1,12 @@ -const loadSlashCommands = require('./slash_commands.js'); - const db = require("./db"); -const { Client, GatewayIntentBits, ActivityType, Events } = require("discord.js"); -const e = require('express'); +const { Client, GatewayIntentBits, Events } = require("discord.js"); 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); - client.user.setActivity("LazyBot à votre service !", { type: ActivityType.Custom }); -}); - - -client.on(Events.InteractionCreate, async interaction => { - if (!interaction.isChatInputCommand()) return; - - else if (interaction.commandName === 'level') { - const guildId = interaction.guild.id; - const userId = interaction.user.id; - - db.get( - `SELECT xp, level FROM user_levels WHERE guild_id = ? AND user_id = ?`, - [guildId, userId], - (err, row) => { - if (err) { - console.error("DB error fetching user level", err); - return interaction.reply("Une erreur est survenue en récupérant votre niveau."); - } - - if (!row) { - return interaction.reply("Vous n'avez pas encore de niveau dans ce serveur."); - } - - return interaction.reply(`Vous êtes au niveau ${row.level} avec ${row.xp} XP.`); - } - ); - } else if (interaction.commandName === 'leveltop') { - const guildId = interaction.guild.id; - - db.all( - `SELECT user_id, xp, level FROM user_levels WHERE guild_id = ? ORDER BY level DESC, xp DESC LIMIT 10`, - [guildId], - (err, rows) => { - if (err) { - console.error("DB error fetching level top", err); - return interaction.reply("Une erreur est survenue en récupérant le classement des niveaux."); - } - - if (rows.length === 0) { - return interaction.reply("Aucun utilisateur n'a encore de niveau dans ce serveur."); - } - - let replyMessage = "🏆 **Top 10 des niveaux :**\n"; - rows.forEach((row, index) => { - replyMessage += `${index + 1}. <@${row.user_id}> - Niveau ${row.level} (${row.xp} XP)\n`; - }); - - return interaction.reply(replyMessage); - } - ); - } -}); - - -client.on(Events.GuildMemberAdd, member => { - db.get( - "SELECT enabled, channel_id, message FROM welcome_config WHERE guild_id = ?", - [member.guild.id], - (err, row) => { - if (err || !row || !row.enabled) return; - - let msg = row.message; - - msg = msg - .replace("{user}", member.user.username) - .replace("{mention}", `<@${member.id}>`) - .replace("{server}", member.guild.name); - - const channel = member.guild.channels.cache.get(row.channel_id); - if (channel) { - channel.send(msg); - } - } - ); - db.get( - "SELECT enabled, role_id FROM autorole_newuser_config WHERE guild_id = ?", - [member.guild.id], - (err, row) => { - if (err || !row || !row.enabled) return; - - const role = member.guild.roles.cache.get(row.role_id); - if (role) { - member.roles.add(role); - } - } - ); -}); - - -client.on(Events.GuildMemberRemove, member => { - db.get( - "SELECT enabled, channel_id, message FROM goodbye_config WHERE guild_id = ?", - [member.guild.id], - (err, row) => { - if (err || !row || !row.enabled) return; - - let msg = row.message; - - msg = msg - .replace("{user}", member.user.username) - .replace("{server}", member.guild.name); - - const channel = member.guild.channels.cache.get(row.channel_id); - if (channel) { - channel.send(msg); - } - } - ); -}); - - -client.on(Events.VoiceStateUpdate, (oldState, newState) => { - if (newState.member.user.bot) return; - - const guildId = newState.guild.id; - - db.get( - "SELECT enabled, role_id, exclude_channel_ids FROM autorole_vocal_config WHERE guild_id = ?", - [guildId], - (err, row) => { - if (err || !row || !row.enabled) return; - - let excludeChannelIds = []; - try { - excludeChannelIds = row.exclude_channel_ids - ? JSON.parse(row.exclude_channel_ids) - : []; - } catch (err) { - console.error("Erreur parsing exclude_channel_ids", err); - excludeChannelIds = []; - } - - const role = newState.guild.roles.cache.get(row.role_id); - if (!role) return; - - // User joins a voice channel and it's not excluded et a pas déjà le rôle - if (newState.channelId && !excludeChannelIds.includes(newState.channelId) && !newState.member.roles.cache.has(role.id)) { - newState.member.roles.add(role); - } - // User leaves a voice channel or joins an excluded one - else if (!newState.channelId || excludeChannelIds.includes(newState.channelId)) { - newState.member.roles.remove(role); - } - } - ); -}); - - -client.on(Events.MessageCreate, message => { - if (message.author.bot) return; - - const guildId = message.guild.id; - - db.get( - `SELECT - enabled, - level_announcements_enabled, - level_announcements_channel_id, - level_announcements_message, - xp_courbe_type, - multiplier_courbe_for_level, - level_annoncement_every_level, - level_max, - role_with_without_type, - role_with_without_xp, - salon_with_without_type, - salon_with_without_xp, - gain_xp_on_message, - gain_xp_message_lower_bound, - gain_xp_message_upper_bound, - cooldown_xp_message_seconds - FROM levels_config WHERE guild_id = ?`, - [guildId], - (err, row) => { - if (err || !row || !row.enabled || !row.gain_xp_on_message) return; - if (row.role_with_without_type === "with") { - const userRoles = message.member.roles.cache; - const requiredRoles = JSON.parse(row.role_with_without_xp || "[]"); - if (!requiredRoles.some(roleId => userRoles.has(roleId))) { - return; // User has an excluded role - } - } else if (row.role_with_without_type === "without") { - const userRoles = message.member.roles.cache; - const excludedRoles = JSON.parse(row.role_with_without_xp || "[]"); - if (excludedRoles.some(roleId => userRoles.has(roleId))) { - return; // User does not have any of the required roles - } - } else if (row.salon_with_without_type === "with") { - const channelId = message.channel.id; - const requiredChannels = JSON.parse(row.salon_with_without_xp || "[]"); - if (!requiredChannels.includes(channelId)) { - return; // Message not in a required channel - } - } else if (row.salon_with_without_type === "without") { - const channelId = message.channel.id; - const excludedChannels = JSON.parse(row.salon_with_without_xp || "[]"); - if (excludedChannels.includes(channelId)) { - return; // Message in an excluded channel - } - } - // Logic to award XP for message goes here - const now = Date.now(); - db.get( - `SELECT xp, level, last_xp_message_timestamp FROM user_levels WHERE guild_id = ? AND user_id = ?`, - [guildId, message.author.id], - (err, userRow) => { - if (err) return; - - const lastTimestamp = userRow ? userRow.last_xp_message_timestamp || 0 : 0; - if (now - lastTimestamp < row.cooldown_xp_message_seconds * 1000) { - return; // Still in cooldown - } - - const minXp = row.gain_xp_message_lower_bound; - const maxXp = row.gain_xp_message_upper_bound; - const xpToAdd = Math.floor(Math.random() * (maxXp - minXp + 1)) + minXp; - - let newXp; - let newLevel; - - if (userRow) { - newXp = userRow.xp + xpToAdd; - newLevel = userRow.level; - } else { - newXp = xpToAdd; - newLevel = 1; - } - - // Level up logic based on xp_courbe_type and multiplier goes here - const multiplier = row.multiplier_courbe_for_level; - let fonction_courbe; - - if (row.xp_courbe_type === "constante") { - fonction_courbe = (level) => multiplier; - } else if (row.xp_courbe_type === "linear") { - fonction_courbe = (level) => (level) * multiplier; - } else if (row.xp_courbe_type === "quadratic") { - fonction_courbe = (level) => (level) * (level) * multiplier; - } else if (row.xp_courbe_type === "exponential") { - fonction_courbe = (level) => Math.pow(2, (level - 1)) * multiplier; - } - - let xpForNextLevel = fonction_courbe(newLevel); - while (newXp >= xpForNextLevel && (row.level_max === 0 || newLevel < row.level_max)) { - newXp -= xpForNextLevel; - newLevel += 1; - xpForNextLevel = fonction_courbe(newLevel); - - // Announce level up if enabled and meets the criteria - if (row.level_announcements_enabled && (newLevel % row.level_annoncement_every_level === 0)) { - const channel = message.guild.channels.cache.get(row.level_announcements_channel_id); - console.log("Channel for level announcement:", channel); - if (channel) { - let announcementMsg = row.level_announcements_message; - announcementMsg = announcementMsg - .replace("{user}", message.author.username) - .replace("{mention}", `<@${message.author.id}>`) - .replace("{level}", newLevel) - .replace("{level-xp}", xpForNextLevel); - channel.send(announcementMsg); - } - } - } - - db.run( - `INSERT INTO user_levels (guild_id, user_id, xp, level, last_xp_message_timestamp) - VALUES (?, ?, ?, ?, ?) - ON CONFLICT(guild_id, user_id) DO UPDATE SET - xp = excluded.xp, - level = excluded.level, - last_xp_message_timestamp = excluded.last_xp_message_timestamp`, - [guildId, message.author.id, newXp, newLevel, now] - ); - } - ); - } - ); -}); - setInterval(() => { // vérification des membres vocaux pour leur faire gagner de l'xp diff --git a/app/commands/level.js b/app/commands/level.js new file mode 100644 index 0000000..a35d3cd --- /dev/null +++ b/app/commands/level.js @@ -0,0 +1,91 @@ +const addCommand = require("../fonctions/addCommand"); +const { EmbedBuilder } = require("discord.js"); +const db = require("../db"); + +module.exports = addCommand({ + name: "level", + description: "Affiche votre niveau et XP sur ce serveur.", + aliases: ["lvl", "xp", "niveau"], + permissions: [], + botOwnerOnly: false, + dm: false, + scope: "guild", + + guildCondition: async (guildId) => { + return new Promise((resolve) => { + db.get( + "SELECT enabled FROM levels_config WHERE guild_id = ?", + [guildId], + (err, row) => { + if (err) { + console.error(`DB error in guildCondition for guild ${guildId}`, err); + return resolve(false); + } + resolve(!!row?.enabled); + } + ); + }); + }, + + executePrefix: async (client, message, args) => { + const guildId = message.guild.id; + const userId = message.author.id; + + db.get( + `SELECT xp, level FROM user_levels WHERE guild_id = ? AND user_id = ?`, + [guildId, userId], + async (err, row) => { + if (err) { + console.error("DB error fetching user level", err); + return message.reply("Une erreur est survenue en récupérant votre niveau."); + } + + if (!row) { + return message.reply("Vous n'avez pas encore de niveau dans ce serveur."); + } + + const embed = new EmbedBuilder() + .setTitle(`${message.author.username} — Niveau`) + .setDescription(`Vous êtes au **niveau ${row.level}** avec **${row.xp} XP**.`) + .setColor("#00FF00") + .setTimestamp() + .setFooter({ text: `ID: ${userId}` }); + + await message.reply({ embeds: [embed] }); + } + ); + }, + + executeSlash: async (client, interaction) => { + const guildId = interaction.guild.id; + const userId = interaction.user.id; + + db.get( + `SELECT xp, level FROM user_levels WHERE guild_id = ? AND user_id = ?`, + [guildId, userId], + async (err, row) => { + if (err) { + console.error("DB error fetching user level", err); + return interaction.reply({ content: "Une erreur est survenue en récupérant votre niveau.", ephemeral: true }); + } + + if (!row) { + return interaction.reply({ content: "Vous n'avez pas encore de niveau dans ce serveur.", ephemeral: true }); + } + + const embed = new EmbedBuilder() + .setTitle(`${interaction.user.username} — Niveau`) + .setDescription(`Vous êtes au **niveau ${row.level}** avec **${row.xp} XP**.`) + .setColor("#00FF00") + .setTimestamp() + .setFooter({ text: `ID: ${userId}` }); + + await interaction.reply({ embeds: [embed] }); + } + ); + }, + + slashData: new (require("discord.js").SlashCommandBuilder)() + .setName("level") + .setDescription("Affiche votre niveau et XP sur ce serveur."), +}); diff --git a/app/commands/leveltop.js b/app/commands/leveltop.js new file mode 100644 index 0000000..6e5beb7 --- /dev/null +++ b/app/commands/leveltop.js @@ -0,0 +1,101 @@ +const addCommand = require("../fonctions/addCommand"); +const { EmbedBuilder, SlashCommandBuilder } = require("discord.js"); +const db = require("../db"); + +module.exports = addCommand({ + name: "leveltop", + description: "Affiche le top 10 des niveaux du serveur.", + aliases: ["toplevel", "topxp", "leaderboard"], + permissions: [], + botOwnerOnly: false, + dm: false, + scope: "guild", + + guildCondition: async (guildId) => { + return new Promise((resolve) => { + db.get( + "SELECT enabled FROM levels_config WHERE guild_id = ?", + [guildId], + (err, row) => { + if (err) { + console.error(`DB error in guildCondition for guild ${guildId}`, err); + return resolve(false); + } + resolve(!!row?.enabled); + } + ); + }); + }, + + executePrefix: async (client, message, args) => { + const guildId = message.guild.id; + + db.all( + `SELECT user_id, xp, level FROM user_levels WHERE guild_id = ? ORDER BY level DESC, xp DESC LIMIT 10`, + [guildId], + async (err, rows) => { + if (err) { + console.error("DB error fetching level top", err); + return message.reply("Une erreur est survenue en récupérant le classement des niveaux."); + } + + if (rows.length === 0) { + return message.reply("Aucun utilisateur n'a encore de niveau dans ce serveur."); + } + + const embed = new EmbedBuilder() + .setTitle("🏆 Top 10 des niveaux") + .setColor("#FFD700") + .setTimestamp() + .setDescription( + rows + .map( + (row, index) => + `**${index + 1}.** <@${row.user_id}> - Niveau **${row.level}** (${row.xp} XP)` + ) + .join("\n") + ); + + await message.reply({ embeds: [embed] }); + } + ); + }, + + executeSlash: async (client, interaction) => { + const guildId = interaction.guild.id; + + db.all( + `SELECT user_id, xp, level FROM user_levels WHERE guild_id = ? ORDER BY level DESC, xp DESC LIMIT 10`, + [guildId], + async (err, rows) => { + if (err) { + console.error("DB error fetching level top", err); + return interaction.reply({ content: "Une erreur est survenue en récupérant le classement des niveaux.", ephemeral: true }); + } + + if (rows.length === 0) { + return interaction.reply({ content: "Aucun utilisateur n'a encore de niveau dans ce serveur.", ephemeral: true }); + } + + const embed = new EmbedBuilder() + .setTitle("🏆 Top 10 des niveaux") + .setColor("#FFD700") + .setTimestamp() + .setDescription( + rows + .map( + (row, index) => + `**${index + 1}.** <@${row.user_id}> - Niveau **${row.level}** (${row.xp} XP)` + ) + .join("\n") + ); + + await interaction.reply({ embeds: [embed] }); + } + ); + }, + + slashData: new SlashCommandBuilder() + .setName("leveltop") + .setDescription("Affiche le top 10 des niveaux du serveur."), +}); diff --git a/app/commands/ping.js b/app/commands/ping.js index 58727ce..4e11c8f 100644 --- a/app/commands/ping.js +++ b/app/commands/ping.js @@ -7,14 +7,26 @@ const { 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) => { +module.exports = addCommand({ + name: "ping", + description: "Cette commande permet de vérifier la latence du bot.", + aliases: ["latency", "lag", "responseTime"], + permissions: [], + botOwnerOnly: false, + dm: true, + scope: "global", + + slashOptions: [ + { + type: "BOOLEAN", + name: "actualiser", + description: + "Actualiser automatiquement la latence du bot toutes les 10 secondes pendant 2 minutes.", + required: false, + }, + ], + + executePrefix: async (client, message, args) => { const pingBtn = new ButtonBuilder() .setCustomId("pingBtn") .setLabel("🔄") @@ -22,61 +34,35 @@ module.exports = addCommand( 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() + const embed = (user) => + 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(), - }); + .setFooter({ text: `Demandé par ${user.tag}`, iconURL: user.displayAvatarURL() }); - sendMessage.edit({ embeds: [embed], components: [row] }); - i.reply({ content: "La latence a été rafraichie.", ephemeral: true }); + const sendMessage = await message.reply({ embeds: [embed(message.author)], components: [row] }); + + const filter = (i) => i.customId === "pingBtn"; + const collector = sendMessage.createMessageComponentCollector({ filter, time: 120_000 }); + + 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 }); + + await sendMessage.edit({ embeds: [embed(i.user)], components: [row] }); + await 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: [] }); + await sendMessage.edit({ embeds: [embed(message.author)], components: [] }); }); - }), - (this.executeSlash = async (client, interaction) => { + }, + + executeSlash: async (client, interaction) => { + const actualiser = interaction.options.getBoolean("actualiser") ?? false; + const pingBtn = new ButtonBuilder() .setCustomId("pingBtn") .setLabel("🔄") @@ -84,87 +70,41 @@ module.exports = addCommand( 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(), - }); + const embed = (user) => + new EmbedBuilder() + .setTitle("Pong !") + .setDescription(`La latence du bot est de \`${client.ws.ping}\`ms.`) + .setColor("#0099FF") + .setTimestamp() + .setFooter({ text: `Demandé par ${user.tag}`, iconURL: user.displayAvatarURL() }); - if (interaction.options.getBoolean("actualiser") === false) { - const sendMessage = await interaction.reply({ - embeds: [embed], - components: [row], - }); + const sendMessage = await interaction.reply({ + embeds: [embed(interaction.user)], + components: actualiser ? [] : [row], + fetchReply: true, + }); + if (!actualiser) { const filter = (i) => i.customId === "pingBtn"; - const collector = sendMessage.createMessageComponentCollector({ - filter, - time: 120000, - }); + const collector = sendMessage.createMessageComponentCollector({ filter, time: 120_000 }); + 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(), - }); + return i.reply({ content: "Vous n'êtes pas l'auteur du message.", ephemeral: true }); - sendMessage.edit({ embeds: [embed], components: [row] }); - i.reply({ content: "La latence a été rafraichie.", ephemeral: true }); + await sendMessage.edit({ embeds: [embed(i.user)], components: [row] }); + await 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: [] }); + await sendMessage.edit({ embeds: [embed(interaction.user)], components: [] }); }); } else { - const sendMessage = await interaction.reply({ embeds: [embed] }); + const interval = setInterval(async () => { + await sendMessage.edit({ embeds: [embed(interaction.user)] }); + }, 10_000); - 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); + setTimeout(() => clearInterval(interval), 120_000); } - }), - (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 4eb717a..44dedc0 100644 --- a/app/db.js +++ b/app/db.js @@ -9,6 +9,24 @@ const db = new sqlite3.Database( } ); +db.getAsync = (sql, params = []) => { + return new Promise((resolve, reject) => { + db.get(sql, params, (err, row) => { + if (err) reject(err); + else resolve(row); + }); + }); +}; + +db.allAsync = (sql, params = []) => { + return new Promise((resolve, reject) => { + db.all(sql, params, (err, rows) => { + if (err) reject(err); + else resolve(rows); + }); + }); +}; + // Création de la table si elle n'existe pas db.exec(` CREATE TABLE IF NOT EXISTS welcome_config ( diff --git a/app/events/clientReady.js b/app/events/clientReady.js new file mode 100644 index 0000000..706b041 --- /dev/null +++ b/app/events/clientReady.js @@ -0,0 +1,11 @@ +const { Events, ActivityType } = require("discord.js"); +const loadSlashCommands = require('../slash_commands.js'); + +module.exports = { + name: Events.ClientReady, + async execute(client) { + console.log(`[READY] ${client.user.tag} est prêt | ${client.guilds.cache.size} serveurs | ${client.guilds.cache.reduce((acc, guild) => acc + guild.memberCount, 0)} utilisateurs`); + await loadSlashCommands(client); + client.user.setActivity("LazyBot à votre service !", { type: ActivityType.Custom }); + } +}; \ No newline at end of file diff --git a/app/events/guildMemberAdd.js b/app/events/guildMemberAdd.js new file mode 100644 index 0000000..a5e74b8 --- /dev/null +++ b/app/events/guildMemberAdd.js @@ -0,0 +1,39 @@ +const { Events } = require("discord.js"); +const db = require("../db"); + +module.exports = { + name: Events.GuildMemberAdd, + async execute(client, member) { + db.get( + "SELECT enabled, channel_id, message FROM welcome_config WHERE guild_id = ?", + [member.guild.id], + (err, row) => { + if (err || !row || !row.enabled) return; + + let msg = row.message; + + msg = msg + .replace("{user}", member.user.username) + .replace("{mention}", `<@${member.id}>`) + .replace("{server}", member.guild.name); + + const channel = member.guild.channels.cache.get(row.channel_id); + if (channel) { + channel.send(msg); + } + } + ); + db.get( + "SELECT enabled, role_id FROM autorole_newuser_config WHERE guild_id = ?", + [member.guild.id], + (err, row) => { + if (err || !row || !row.enabled) return; + + const role = member.guild.roles.cache.get(row.role_id); + if (role) { + member.roles.add(role); + } + } + ); + }, +}; diff --git a/app/events/guildMemberRemove.js b/app/events/guildMemberRemove.js new file mode 100644 index 0000000..18ba1ac --- /dev/null +++ b/app/events/guildMemberRemove.js @@ -0,0 +1,26 @@ +const { Events } = require("discord.js"); +const db = require("../db"); + +module.exports = { + name: Events.GuildMemberRemove, + async execute(client, member) { + db.get( + "SELECT enabled, channel_id, message FROM goodbye_config WHERE guild_id = ?", + [member.guild.id], + (err, row) => { + if (err || !row || !row.enabled) return; + + let msg = row.message; + + msg = msg + .replace("{user}", member.user.username) + .replace("{server}", member.guild.name); + + const channel = member.guild.channels.cache.get(row.channel_id); + if (channel) { + channel.send(msg); + } + } + ); + }, +}; diff --git a/app/events/messageCreate.js b/app/events/messageCreate.js new file mode 100644 index 0000000..ce7f767 --- /dev/null +++ b/app/events/messageCreate.js @@ -0,0 +1,136 @@ +const { Events } = require("discord.js"); +const db = require("../db"); + +module.exports = { + name: Events.MessageCreate, + async execute(client, message) { + if (message.author.bot) return; + + const guildId = message.guild.id; + + db.get( + `SELECT + enabled, + level_announcements_enabled, + level_announcements_channel_id, + level_announcements_message, + xp_courbe_type, + multiplier_courbe_for_level, + level_annoncement_every_level, + level_max, + role_with_without_type, + role_with_without_xp, + salon_with_without_type, + salon_with_without_xp, + gain_xp_on_message, + gain_xp_message_lower_bound, + gain_xp_message_upper_bound, + cooldown_xp_message_seconds + FROM levels_config WHERE guild_id = ?`, + [guildId], + (err, row) => { + if (err || !row || !row.enabled || !row.gain_xp_on_message) return; + if (row.role_with_without_type === "with") { + const userRoles = message.member.roles.cache; + const requiredRoles = JSON.parse(row.role_with_without_xp || "[]"); + if (!requiredRoles.some(roleId => userRoles.has(roleId))) { + return; // User has an excluded role + } + } else if (row.role_with_without_type === "without") { + const userRoles = message.member.roles.cache; + const excludedRoles = JSON.parse(row.role_with_without_xp || "[]"); + if (excludedRoles.some(roleId => userRoles.has(roleId))) { + return; // User does not have any of the required roles + } + } else if (row.salon_with_without_type === "with") { + const channelId = message.channel.id; + const requiredChannels = JSON.parse(row.salon_with_without_xp || "[]"); + if (!requiredChannels.includes(channelId)) { + return; // Message not in a required channel + } + } else if (row.salon_with_without_type === "without") { + const channelId = message.channel.id; + const excludedChannels = JSON.parse(row.salon_with_without_xp || "[]"); + if (excludedChannels.includes(channelId)) { + return; // Message in an excluded channel + } + } + // Logic to award XP for message goes here + const now = Date.now(); + db.get( + `SELECT xp, level, last_xp_message_timestamp FROM user_levels WHERE guild_id = ? AND user_id = ?`, + [guildId, message.author.id], + (err, userRow) => { + if (err) return; + + const lastTimestamp = userRow ? userRow.last_xp_message_timestamp || 0 : 0; + if (now - lastTimestamp < row.cooldown_xp_message_seconds * 1000) { + return; // Still in cooldown + } + + const minXp = row.gain_xp_message_lower_bound; + const maxXp = row.gain_xp_message_upper_bound; + const xpToAdd = Math.floor(Math.random() * (maxXp - minXp + 1)) + minXp; + + let newXp; + let newLevel; + + if (userRow) { + newXp = userRow.xp + xpToAdd; + newLevel = userRow.level; + } else { + newXp = xpToAdd; + newLevel = 1; + } + + // Level up logic based on xp_courbe_type and multiplier goes here + const multiplier = row.multiplier_courbe_for_level; + let fonction_courbe; + + if (row.xp_courbe_type === "constante") { + fonction_courbe = (level) => multiplier; + } else if (row.xp_courbe_type === "linear") { + fonction_courbe = (level) => (level) * multiplier; + } else if (row.xp_courbe_type === "quadratic") { + fonction_courbe = (level) => (level) * (level) * multiplier; + } else if (row.xp_courbe_type === "exponential") { + fonction_courbe = (level) => Math.pow(2, (level - 1)) * multiplier; + } + + let xpForNextLevel = fonction_courbe(newLevel); + while (newXp >= xpForNextLevel && (row.level_max === 0 || newLevel < row.level_max)) { + newXp -= xpForNextLevel; + newLevel += 1; + xpForNextLevel = fonction_courbe(newLevel); + + // Announce level up if enabled and meets the criteria + if (row.level_announcements_enabled && (newLevel % row.level_annoncement_every_level === 0)) { + const channel = message.guild.channels.cache.get(row.level_announcements_channel_id); + console.log("Channel for level announcement:", channel); + if (channel) { + let announcementMsg = row.level_announcements_message; + announcementMsg = announcementMsg + .replace("{user}", message.author.username) + .replace("{mention}", `<@${message.author.id}>`) + .replace("{level}", newLevel) + .replace("{level-xp}", xpForNextLevel); + channel.send(announcementMsg); + } + } + } + + db.run( + `INSERT INTO user_levels (guild_id, user_id, xp, level, last_xp_message_timestamp) + VALUES (?, ?, ?, ?, ?) + ON CONFLICT(guild_id, user_id) DO UPDATE SET + xp = excluded.xp, + level = excluded.level, + last_xp_message_timestamp = excluded.last_xp_message_timestamp`, + [guildId, message.author.id, newXp, newLevel, now] + ); + } + ); + } + ); + }, +}; diff --git a/app/events/voiceStateUpdate.js b/app/events/voiceStateUpdate.js new file mode 100644 index 0000000..7da298c --- /dev/null +++ b/app/events/voiceStateUpdate.js @@ -0,0 +1,41 @@ +const { Events } = require("discord.js"); +const db = require("../db"); + +module.exports = { + name: Events.VoiceStateUpdate, + async execute(client, oldState, newState) { + if (newState.member.user.bot) return; + + const guildId = newState.guild.id; + + db.get( + "SELECT enabled, role_id, exclude_channel_ids FROM autorole_vocal_config WHERE guild_id = ?", + [guildId], + (err, row) => { + if (err || !row || !row.enabled) return; + + let excludeChannelIds = []; + try { + excludeChannelIds = row.exclude_channel_ids + ? JSON.parse(row.exclude_channel_ids) + : []; + } catch (err) { + console.error("Erreur parsing exclude_channel_ids", err); + excludeChannelIds = []; + } + + const role = newState.guild.roles.cache.get(row.role_id); + if (!role) return; + + // User joins a voice channel and it's not excluded et a pas déjà le rôle + if (newState.channelId && !excludeChannelIds.includes(newState.channelId) && !newState.member.roles.cache.has(role.id)) { + newState.member.roles.add(role); + } + // User leaves a voice channel or joins an excluded one + else if (!newState.channelId || excludeChannelIds.includes(newState.channelId)) { + newState.member.roles.remove(role); + } + } + ); + }, +}; diff --git a/app/fonctions/addCommand.js b/app/fonctions/addCommand.js index 3b8d15f..c9b7276 100644 --- a/app/fonctions/addCommand.js +++ b/app/fonctions/addCommand.js @@ -3,170 +3,108 @@ 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. + * @param {Object} options + * @param {string} options.name - Nom de la commande + * @param {string} options.description - Description de la commande + * @param {Array} [options.aliases=[]] - Alias + * @param {Array} [options.permissions=[]] - Permissions requises + * @param {boolean} [options.botOwnerOnly=false] - Réservé au propriétaire du bot + * @param {boolean} [options.dm=false] - Disponible en DM + * @param {"global"|"guild"} [options.scope="global"] - Portée de la commande + * @param {(guildId: string) => Promise | boolean} [options.guildCondition=null] - Condition pour les commandes de guild + * @param {Function} options.executePrefix - Fonction à exécuter en préfixe + * @param {Function} options.executeSlash - Fonction à exécuter en slash + * @param {Array} [options.slashOptions=[]] - Options pour la commande slash * - * @returns {void} Ne retourne rien. + * @returns {Object} Commande prête à être ajoutée au bot */ -function addCommand( +function addCommand({ name, description, - aliases, - permissions, - botOwnerOnly, - dm, + aliases = [], + permissions = [], + botOwnerOnly = false, + dm = false, + scope = "global", + guildCondition = null, 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".', - ); + slashOptions = [] +}) { + if (!name || !description) throw new Error("name et description requis"); + + if (scope === "guild" && typeof guildCondition !== "function") { + throw new Error("guildCondition requise pour scope=guild"); } - const command = { + if (typeof executePrefix !== "function") throw new Error("executePrefix requis et doit être une fonction"); + if (typeof executeSlash !== "function") throw new Error("executeSlash requis et doit être une fonction"); + + // Permissions + let defaultMemberPermissions = null; + if (permissions.length > 0) { + defaultMemberPermissions = new PermissionsBitField(); + permissions.forEach(p => defaultMemberPermissions.add(p)); + } + + // Création du SlashCommandBuilder + const slashData = new SlashCommandBuilder() + .setName(name.toLowerCase()) + .setDescription(description) + .setDMPermission(dm) + .setDefaultMemberPermissions(defaultMemberPermissions); + + // Ajouter les options + slashOptions.forEach(opt => { + switch (opt.type) { + case "STRING": + slashData.addStringOption(o => + o.setName(opt.name).setDescription(opt.description || "No description").setRequired(!!opt.required) + ); + break; + case "INTEGER": + slashData.addIntegerOption(o => + o.setName(opt.name).setDescription(opt.description || "No description").setRequired(!!opt.required) + ); + break; + case "BOOLEAN": + slashData.addBooleanOption(o => + o.setName(opt.name).setDescription(opt.description || "No description").setRequired(!!opt.required) + ); + break; + case "USER": + slashData.addUserOption(o => + o.setName(opt.name).setDescription(opt.description || "No description").setRequired(!!opt.required) + ); + break; + case "CHANNEL": + slashData.addChannelOption(o => + o.setName(opt.name).setDescription(opt.description || "No description").setRequired(!!opt.required) + ); + break; + case "ROLE": + slashData.addRoleOption(o => + o.setName(opt.name).setDescription(opt.description || "No description").setRequired(!!opt.required) + ); + break; + default: + console.warn(`Option type inconnu pour ${opt.name}: ${opt.type}`); + } + }); + + return { name, description, aliases, permissions, botOwnerOnly, dm, + scope, + guildCondition, executePrefix, executeSlash, + data: slashData }; - - 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 +module.exports = addCommand; diff --git a/app/slash_commands.js b/app/slash_commands.js index 5a17581..2b520b7 100644 --- a/app/slash_commands.js +++ b/app/slash_commands.js @@ -1,11 +1,10 @@ -const { REST, Routes } = require('discord.js'); -const db = require('./db'); +const { REST, Routes } = require("discord.js"); -module.exports = async (client, guildId = null) => { +module.exports = async function loadSlashCommands(client, guildId = null) { const TOKEN = process.env.BOT_TOKEN; const CLIENT_ID = process.env.CLIENT_ID; - const rest = new REST({ version: '10' }).setToken(TOKEN); + const rest = new REST({ version: "10" }).setToken(TOKEN); /* ========================= 1️⃣ COMMANDES GLOBALES @@ -14,19 +13,21 @@ module.exports = async (client, guildId = null) => { if (!guildId) { const globalCommands = []; - client.commands.forEach((command) => { + + for (const command of client.commands.values()) { + if (command.scope === "guild") continue; globalCommands.push(command.data.toJSON()); - }); + } try { - console.log('Refreshing GLOBAL slash commands...'); + console.log("Refreshing GLOBAL slash commands..."); await rest.put( - Routes.applicationCommands(client.user.id), + Routes.applicationCommands(CLIENT_ID), { body: globalCommands } ); - console.log('Global slash commands loaded'); + console.log("Global slash commands loaded"); } catch (err) { - console.error('Global commands error', err); + console.error("Global commands error", err); } } @@ -36,45 +37,44 @@ module.exports = async (client, guildId = null) => { const guildsToProcess = guildId ? [client.guilds.cache.get(guildId)] - : client.guilds.cache.values(); + : [...client.guilds.cache.values()]; for (const guild of guildsToProcess) { if (!guild) continue; - db.get( - "SELECT enabled FROM levels_config WHERE guild_id = ?", - [guild.id], - async (err, row) => { - if (err) { - console.error(`DB error ${guild.name}`, err); - return; - } + const guildCommands = []; - const guildCommands = []; - - if (row?.enabled) { - guildCommands.push( - { - name: 'level', - description: 'Check your level and XP', - }, - { - name: 'leveltop', - description: 'Show the top levels', - } - ); - } + for (const command of client.commands.values()) { + if (command.scope === "global") continue; + if (command.guildCondition) { + let conditionMet = false; try { - await rest.put( - Routes.applicationGuildCommands(client.user.id, guild.id), - { body: guildCommands } - ); - console.log(`Guild commands updated for ${guild.name}`); + conditionMet = await command.guildCondition(guild.id); } catch (err) { - console.error(`Guild commands error ${guild.name}`, err); + console.error( + `Guild condition error for command ${command.name} in guild ${guild.name}`, + err + ); } + if (!conditionMet) continue; } + + guildCommands.push(command.data.toJSON()); + } + + console.log( + `Refreshing GUILD slash commands for ${guild.name} (${guildCommands.length})` ); + + try { + await rest.put( + Routes.applicationGuildCommands(CLIENT_ID, guild.id), + { body: guildCommands } + ); + console.log(`Guild commands updated for ${guild.name}`); + } catch (err) { + console.error(`Guild commands error ${guild.name}`, err); + } } };