diff --git a/app/commands/🔧 Administration/rolepanel.js b/app/commands/🔧 Administration/rolepanel.js new file mode 100644 index 0000000..e7b2b89 --- /dev/null +++ b/app/commands/🔧 Administration/rolepanel.js @@ -0,0 +1,382 @@ +const { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, ChannelType } = require('discord.js'); +const addCommand = require('../../fonctions/addCommand'); +const db = require('../../db'); + +const buttonStyles = { + 'primary': ButtonStyle.Primary, + 'secondary': ButtonStyle.Secondary, + 'success': ButtonStyle.Success, + 'danger': ButtonStyle.Danger +}; + +// Fonction pour créer/mettre à jour le message du panel +async function updatePanelMessage(client, panel, buttons) { + try { + const guild = client.guilds.cache.get(panel.guild_id); + if (!guild) return null; + + const channel = guild.channels.cache.get(panel.channel_id); + if (!channel) return null; + + // Créer l'embed + const embed = new EmbedBuilder() + .setColor(panel.color || '#5865F2') + .setTitle(panel.title || '🎭 Choisissez vos rôles') + .setDescription(panel.description || 'Cliquez sur les boutons ci-dessous pour obtenir ou retirer des rôles.'); + + if (panel.image_url) embed.setImage(panel.image_url); + if (panel.thumbnail_url) embed.setThumbnail(panel.thumbnail_url); + + // Infos sur le mode + const modeText = panel.exclusive + ? '⚠️ *Un seul rôle possible à la fois*' + : (panel.mode === 'toggle' ? '💡 *Cliquez à nouveau pour retirer un rôle*' : ''); + + if (modeText) { + embed.setFooter({ text: modeText }); + } + + // Créer les boutons (max 5 par ligne, max 5 lignes) + const rows = []; + const enabledButtons = buttons.filter(b => b.enabled).sort((a, b) => a.position - b.position); + + for (let i = 0; i < enabledButtons.length && rows.length < 5; i += 5) { + const row = new ActionRowBuilder(); + const rowButtons = enabledButtons.slice(i, i + 5); + + for (const btn of rowButtons) { + const button = new ButtonBuilder() + .setCustomId(`role_panel_${btn.id}`) + .setLabel(btn.label) + .setStyle(buttonStyles[btn.style] || ButtonStyle.Primary); + + if (btn.emoji) { + // Vérifier si c'est un emoji custom ou unicode + if (btn.emoji.match(/^\d+$/)) { + button.setEmoji({ id: btn.emoji }); + } else { + button.setEmoji(btn.emoji); + } + } + + row.addComponents(button); + } + + if (row.components.length > 0) { + rows.push(row); + } + } + + // Mettre à jour ou créer le message + if (panel.message_id) { + try { + const message = await channel.messages.fetch(panel.message_id); + await message.edit({ embeds: [embed], components: rows }); + return panel.message_id; + } catch { + // Message introuvable, en créer un nouveau + } + } + + const message = await channel.send({ embeds: [embed], components: rows }); + + // Sauvegarder l'ID du message + await new Promise((resolve, reject) => { + db.run("UPDATE role_panels SET message_id = ? WHERE id = ?", [message.id, panel.id], (err) => { + if (err) reject(err); + else resolve(); + }); + }); + + return message.id; + + } catch (err) { + console.error('Erreur mise à jour panel:', err); + return null; + } +} + +module.exports = addCommand({ + name: 'rolepanel', + description: 'Créer un panneau de rôles interactif', + aliases: ['rp', 'rolebuttons', 'reactionroles'], + permissions: ['ManageRoles', 'ManageMessages'], + botOwnerOnly: false, + dm: false, + scope: 'global', + slashOptions: [ + { type: 'STRING', name: 'action', description: 'Action à effectuer', required: true, choices: [ + { name: 'Créer un panel', value: 'create' }, + { name: 'Ajouter un bouton', value: 'addbutton' }, + { name: 'Supprimer un panel', value: 'delete' }, + { name: 'Liste des panels', value: 'list' }, + { name: 'Actualiser un panel', value: 'refresh' } + ]}, + { type: 'STRING', name: 'nom', description: 'Nom du panel', required: false }, + { type: 'CHANNEL', name: 'salon', description: 'Salon où envoyer le panel', required: false }, + { type: 'ROLE', name: 'role', description: 'Rôle à associer (pour addbutton)', required: false }, + { type: 'STRING', name: 'label', description: 'Texte du bouton', required: false }, + { type: 'STRING', name: 'emoji', description: 'Emoji du bouton', required: false } + ], + + executeSlash: async (client, interaction) => { + const action = interaction.options.getString('action'); + const guildId = interaction.guild.id; + + switch (action) { + case 'create': { + const name = interaction.options.getString('nom'); + const channel = interaction.options.getChannel('salon'); + + if (!name || !channel) { + return interaction.reply({ + content: '❌ Vous devez spécifier un nom et un salon.\nUsage: `/rolepanel create nom:MonPanel salon:#roles`', + ephemeral: true + }); + } + + if (channel.type !== ChannelType.GuildText) { + return interaction.reply({ + content: '❌ Le salon doit être un salon textuel.', + ephemeral: true + }); + } + + // Vérifier si un panel avec ce nom existe déjà + const existing = await db.getAsync( + "SELECT id FROM role_panels WHERE guild_id = ? AND name = ?", + [guildId, name] + ); + + if (existing) { + return interaction.reply({ + content: '❌ Un panel avec ce nom existe déjà.', + ephemeral: true + }); + } + + // Créer le panel + const panelId = await new Promise((resolve, reject) => { + db.run( + "INSERT INTO role_panels (guild_id, channel_id, name, title, description) VALUES (?, ?, ?, ?, ?)", + [guildId, channel.id, name, `🎭 ${name}`, 'Cliquez sur les boutons ci-dessous pour obtenir vos rôles.'], + function(err) { + if (err) reject(err); + else resolve(this.lastID); + } + ); + }); + + const embed = new EmbedBuilder() + .setColor(0x57F287) + .setTitle('✅ Panel créé') + .setDescription(`Le panel **${name}** a été créé.`) + .addFields( + { name: '📁 Salon', value: `${channel}`, inline: true }, + { name: '🆔 ID', value: `${panelId}`, inline: true } + ) + .setFooter({ text: 'Ajoutez des boutons avec /rolepanel addbutton' }); + + return interaction.reply({ embeds: [embed], ephemeral: true }); + } + + case 'addbutton': { + const name = interaction.options.getString('nom'); + const role = interaction.options.getRole('role'); + const label = interaction.options.getString('label'); + const emoji = interaction.options.getString('emoji'); + + if (!name || !role) { + return interaction.reply({ + content: '❌ Vous devez spécifier le nom du panel et un rôle.\nUsage: `/rolepanel addbutton nom:MonPanel role:@MonRole label:Mon Rôle emoji:🎮`', + ephemeral: true + }); + } + + // Trouver le panel + const panel = await db.getAsync( + "SELECT * FROM role_panels WHERE guild_id = ? AND name = ?", + [guildId, name] + ); + + if (!panel) { + return interaction.reply({ + content: `❌ Panel "${name}" non trouvé. Utilisez \`/rolepanel list\` pour voir les panels existants.`, + ephemeral: true + }); + } + + // Vérifier le nombre de boutons (max 25) + const buttonCount = await db.getAsync( + "SELECT COUNT(*) as count FROM role_panel_buttons WHERE panel_id = ?", + [panel.id] + ); + + if (buttonCount.count >= 25) { + return interaction.reply({ + content: '❌ Ce panel a atteint la limite de 25 boutons.', + ephemeral: true + }); + } + + // Ajouter le bouton + await new Promise((resolve, reject) => { + db.run( + "INSERT INTO role_panel_buttons (panel_id, role_id, label, emoji, position) VALUES (?, ?, ?, ?, ?)", + [panel.id, role.id, label || role.name, emoji || null, buttonCount.count], + (err) => { + if (err) reject(err); + else resolve(); + } + ); + }); + + // Mettre à jour le message + const buttons = await db.allAsync( + "SELECT * FROM role_panel_buttons WHERE panel_id = ?", + [panel.id] + ); + + await updatePanelMessage(client, panel, buttons); + + const embed = new EmbedBuilder() + .setColor(0x57F287) + .setTitle('✅ Bouton ajouté') + .addFields( + { name: '📋 Panel', value: name, inline: true }, + { name: '🎭 Rôle', value: `${role}`, inline: true }, + { name: '🏷️ Label', value: label || role.name, inline: true } + ); + + if (emoji) embed.addFields({ name: '😀 Emoji', value: emoji, inline: true }); + + return interaction.reply({ embeds: [embed], ephemeral: true }); + } + + case 'delete': { + const name = interaction.options.getString('nom'); + + if (!name) { + return interaction.reply({ + content: '❌ Vous devez spécifier le nom du panel à supprimer.', + ephemeral: true + }); + } + + const panel = await db.getAsync( + "SELECT * FROM role_panels WHERE guild_id = ? AND name = ?", + [guildId, name] + ); + + if (!panel) { + return interaction.reply({ + content: `❌ Panel "${name}" non trouvé.`, + ephemeral: true + }); + } + + // Supprimer le message + try { + const channel = interaction.guild.channels.cache.get(panel.channel_id); + if (channel && panel.message_id) { + const message = await channel.messages.fetch(panel.message_id).catch(() => null); + if (message) await message.delete(); + } + } catch {} + + // Supprimer de la DB + await new Promise((resolve, reject) => { + db.run("DELETE FROM role_panel_buttons WHERE panel_id = ?", [panel.id], (err) => { + if (err) reject(err); + else resolve(); + }); + }); + + await new Promise((resolve, reject) => { + db.run("DELETE FROM role_panels WHERE id = ?", [panel.id], (err) => { + if (err) reject(err); + else resolve(); + }); + }); + + return interaction.reply({ + content: `✅ Panel **${name}** supprimé avec succès.`, + ephemeral: true + }); + } + + case 'list': { + const panels = await db.allAsync( + "SELECT rp.*, COUNT(rpb.id) as button_count FROM role_panels rp LEFT JOIN role_panel_buttons rpb ON rp.id = rpb.panel_id WHERE rp.guild_id = ? GROUP BY rp.id", + [guildId] + ); + + if (!panels || panels.length === 0) { + return interaction.reply({ + content: 'ℹ️ Aucun panel de rôles configuré. Créez-en un avec `/rolepanel create`.', + ephemeral: true + }); + } + + const embed = new EmbedBuilder() + .setColor(0x5865F2) + .setTitle('📋 Panels de rôles') + .setDescription(panels.map(p => { + const status = p.enabled ? '🟢' : '🔴'; + return `${status} **${p.name}** - ${p.button_count} boutons - <#${p.channel_id}>`; + }).join('\n')); + + return interaction.reply({ embeds: [embed], ephemeral: true }); + } + + case 'refresh': { + const name = interaction.options.getString('nom'); + + if (!name) { + return interaction.reply({ + content: '❌ Vous devez spécifier le nom du panel à actualiser.', + ephemeral: true + }); + } + + const panel = await db.getAsync( + "SELECT * FROM role_panels WHERE guild_id = ? AND name = ?", + [guildId, name] + ); + + if (!panel) { + return interaction.reply({ + content: `❌ Panel "${name}" non trouvé.`, + ephemeral: true + }); + } + + const buttons = await db.allAsync( + "SELECT * FROM role_panel_buttons WHERE panel_id = ?", + [panel.id] + ); + + const messageId = await updatePanelMessage(client, panel, buttons); + + if (messageId) { + return interaction.reply({ + content: `✅ Panel **${name}** actualisé !`, + ephemeral: true + }); + } else { + return interaction.reply({ + content: '❌ Erreur lors de l\'actualisation du panel.', + ephemeral: true + }); + } + } + } + }, + + executePrefix: async (client, message, args) => { + return message.reply('❌ Cette commande est disponible uniquement en slash command. Utilisez `/rolepanel`.'); + } +}); + +// Exporter la fonction de mise à jour pour l'API +module.exports.updatePanelMessage = updatePanelMessage; diff --git a/app/db.js b/app/db.js index 6f0f6d2..c92f0bb 100644 --- a/app/db.js +++ b/app/db.js @@ -410,6 +410,37 @@ db.exec(` last_channel_activity INTEGER, created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')) ); + + -- Système de rôles par boutons + CREATE TABLE IF NOT EXISTS role_panels ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + guild_id TEXT NOT NULL, + channel_id TEXT NOT NULL, + message_id TEXT, + name TEXT NOT NULL, + title TEXT, + description TEXT, + color TEXT DEFAULT '#5865F2', + image_url TEXT, + thumbnail_url TEXT, + mode TEXT NOT NULL DEFAULT 'toggle', + exclusive INTEGER NOT NULL DEFAULT 0, + required_role_id TEXT, + enabled INTEGER NOT NULL DEFAULT 1, + created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')) + ); + + CREATE TABLE IF NOT EXISTS role_panel_buttons ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + panel_id INTEGER NOT NULL, + role_id TEXT NOT NULL, + label TEXT NOT NULL, + emoji TEXT, + style TEXT NOT NULL DEFAULT 'primary', + position INTEGER NOT NULL DEFAULT 0, + enabled INTEGER NOT NULL DEFAULT 1, + FOREIGN KEY (panel_id) REFERENCES role_panels(id) ON DELETE CASCADE + ); `); module.exports = db; diff --git a/app/events/interactionCreate.js b/app/events/interactionCreate.js new file mode 100644 index 0000000..0bc6201 --- /dev/null +++ b/app/events/interactionCreate.js @@ -0,0 +1,109 @@ +const { Events, EmbedBuilder } = require("discord.js"); +const db = require("../db"); + +module.exports = { + name: Events.InteractionCreate, + async execute(client, interaction) { + // Gérer uniquement les boutons de rôles + if (!interaction.isButton()) return; + if (!interaction.customId.startsWith('role_panel_')) return; + + const buttonId = interaction.customId.replace('role_panel_', ''); + + try { + // Récupérer les infos du bouton + const button = await db.getAsync( + "SELECT rpb.*, rp.mode, rp.exclusive, rp.required_role_id, rp.enabled as panel_enabled FROM role_panel_buttons rpb JOIN role_panels rp ON rpb.panel_id = rp.id WHERE rpb.id = ?", + [buttonId] + ); + + if (!button) { + return interaction.reply({ + content: '❌ Ce bouton n\'est plus valide.', + ephemeral: true + }); + } + + if (!button.panel_enabled || !button.enabled) { + return interaction.reply({ + content: '❌ Ce système de rôles est actuellement désactivé.', + ephemeral: true + }); + } + + // Vérifier le rôle requis + if (button.required_role_id) { + if (!interaction.member.roles.cache.has(button.required_role_id)) { + return interaction.reply({ + content: `❌ Vous devez avoir le rôle <@&${button.required_role_id}> pour utiliser ce menu.`, + ephemeral: true + }); + } + } + + const role = interaction.guild.roles.cache.get(button.role_id); + if (!role) { + return interaction.reply({ + content: '❌ Le rôle associé à ce bouton n\'existe plus.', + ephemeral: true + }); + } + + // Vérifier que le bot peut gérer ce rôle + if (role.position >= interaction.guild.members.me.roles.highest.position) { + return interaction.reply({ + content: '❌ Je ne peux pas gérer ce rôle car il est au-dessus de mon rôle le plus élevé.', + ephemeral: true + }); + } + + const hasRole = interaction.member.roles.cache.has(role.id); + + // Mode exclusif : retirer les autres rôles du même panel + if (button.exclusive && !hasRole) { + const panelButtons = await db.allAsync( + "SELECT role_id FROM role_panel_buttons WHERE panel_id = ? AND id != ?", + [button.panel_id, buttonId] + ); + + for (const btn of panelButtons) { + if (interaction.member.roles.cache.has(btn.role_id)) { + await interaction.member.roles.remove(btn.role_id).catch(() => {}); + } + } + } + + // Gérer le rôle selon le mode + if (hasRole) { + // Mode toggle : retirer le rôle + if (button.mode === 'toggle') { + await interaction.member.roles.remove(role); + return interaction.reply({ + content: `✅ Le rôle ${role} vous a été retiré.`, + ephemeral: true + }); + } else { + // Mode add-only : ne pas retirer + return interaction.reply({ + content: `ℹ️ Vous avez déjà le rôle ${role}.`, + ephemeral: true + }); + } + } else { + // Ajouter le rôle + await interaction.member.roles.add(role); + return interaction.reply({ + content: `✅ Le rôle ${role} vous a été attribué !`, + ephemeral: true + }); + } + + } catch (err) { + console.error('Erreur interaction role panel:', err); + return interaction.reply({ + content: '❌ Une erreur est survenue.', + ephemeral: true + }); + } + } +}; diff --git a/app/public/guild.css b/app/public/guild.css index 45a8499..229b4d8 100644 --- a/app/public/guild.css +++ b/app/public/guild.css @@ -1286,3 +1286,245 @@ body { color: var(--error-color); background: rgba(237, 66, 69, 0.1); } + +/* ===== Modal ===== */ +.modal { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.7); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + padding: var(--spacing-lg); +} + +.modal-content { + background: var(--bg-card); + border-radius: var(--radius-lg); + width: 100%; + max-width: 500px; + max-height: 90vh; + overflow-y: auto; + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3); +} + +.modal-content.modal-lg { + max-width: 700px; +} + +.modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: var(--spacing-lg); + border-bottom: 1px solid var(--border-color); +} + +.modal-header h3 { + margin: 0; + font-size: 1.2rem; +} + +.modal-close { + background: none; + border: none; + color: var(--text-muted); + font-size: 1.5rem; + cursor: pointer; + padding: 0; + line-height: 1; +} + +.modal-close:hover { + color: var(--text-primary); +} + +.modal-body { + padding: var(--spacing-lg); +} + +.modal-footer { + padding: var(--spacing-lg); + border-top: 1px solid var(--border-color); + display: flex; + justify-content: flex-end; + gap: var(--spacing-sm); + align-items: center; +} + +/* ===== Role Panels ===== */ +.panels-list { + display: flex; + flex-direction: column; + gap: var(--spacing-md); +} + +.panel-item { + background: var(--bg-secondary); + border-radius: var(--radius-md); + overflow: hidden; +} + +.panel-item-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: var(--spacing-md); + border-bottom: 1px solid var(--border-color); +} + +.panel-item-info { + display: flex; + align-items: center; + gap: var(--spacing-sm); +} + +.panel-item-info h4 { + margin: 0; + font-size: 1rem; +} + +.panel-status { + width: 10px; + height: 10px; + border-radius: 50%; +} + +.panel-status.status-active { + background: #57F287; +} + +.panel-status.status-inactive { + background: #ED4245; +} + +.panel-meta { + color: var(--text-muted); + font-size: 0.85rem; +} + +.panel-item-actions { + display: flex; + gap: var(--spacing-xs); +} + +.panel-item-preview { + padding: var(--spacing-md); +} + +.preview-embed { + background: var(--bg-main); + border-left: 4px solid #5865F2; + border-radius: var(--radius-sm); + padding: var(--spacing-md); +} + +.preview-title { + font-weight: 600; + margin-bottom: var(--spacing-xs); +} + +.preview-desc { + color: var(--text-muted); + font-size: 0.9rem; + margin-bottom: var(--spacing-sm); +} + +.preview-buttons { + display: flex; + flex-wrap: wrap; + gap: var(--spacing-xs); +} + +.preview-btn { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 4px 12px; + border-radius: 4px; + font-size: 0.85rem; + font-weight: 500; +} + +.preview-btn-primary { + background: #5865F2; + color: white; +} + +.preview-btn-secondary { + background: #4f545c; + color: white; +} + +.preview-btn-success { + background: #57F287; + color: black; +} + +.preview-btn-danger { + background: #ED4245; + color: white; +} + +.preview-more { + color: var(--text-muted); + font-size: 0.85rem; + padding: 4px 8px; +} + +/* Buttons list in modal */ +.buttons-list { + display: flex; + flex-direction: column; + gap: var(--spacing-sm); +} + +.button-item { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--spacing-md); + padding: var(--spacing-sm) var(--spacing-md); + background: var(--bg-secondary); + border-radius: var(--radius-sm); +} + +.button-item.button-disabled { + opacity: 0.5; +} + +.button-preview { + flex-shrink: 0; +} + +.button-info { + flex: 1; + color: var(--text-muted); + font-size: 0.9rem; +} + +.button-actions { + display: flex; + gap: var(--spacing-xs); +} + +.btn-xs { + padding: 4px 8px; + font-size: 0.75rem; +} + +.btn-sm { + padding: 6px 12px; + font-size: 0.85rem; +} + +.empty-message, .loading-message { + text-align: center; + color: var(--text-muted); + padding: var(--spacing-lg); +} + diff --git a/app/public/guild.html b/app/public/guild.html index 0bb4a41..b4d098c 100644 --- a/app/public/guild.html +++ b/app/public/guild.html @@ -82,6 +82,10 @@ 🛡️ Anti-Raid + + 🎭 + Rôles par boutons + @@ -1898,6 +1902,164 @@ + + + + + + 🎭 + Rôles par boutons + + + + + Créez des panneaux de rôles interactifs. Les utilisateurs pourront cliquer sur des boutons pour obtenir ou retirer des rôles. + + + + + Chargement des panels... + + + + + ➕ Créer un nouveau panel + + + + + + + + + Créer un panel + × + + + + + + 📝 Nom du panel (identifiant unique) + + + + + 📁 Salon + + + + + 🏷️ Titre de l'embed + + + + + 📄 Description + + + + + + 🎨 Couleur + + + + ⚙️ Mode + + Toggle (ajouter/retirer) + Ajouter uniquement + + + + + + + + + Rôles exclusifs (un seul à la fois) + + + + + + 🔒 Rôle requis (optionnel) + + Aucun + + Seuls les membres avec ce rôle pourront utiliser ce panel. + + + + + 🖼️ Image URL (optionnel) + + + + 🔲 Thumbnail URL (optionnel) + + + + + + + + + + + + + Boutons du panel + × + + + + + + + + + + ➕ Ajouter un bouton + + + 🎭 Rôle + + + + 🏷️ Label + + + + + + 😀 Emoji (optionnel) + + + + 🎨 Style + + Bleu (Primary) + Gris (Secondary) + Vert (Success) + Rouge (Danger) + + + + ➕ Ajouter le bouton + + + + + + + @@ -1920,5 +2082,6 @@ +
+ Créez des panneaux de rôles interactifs. Les utilisateurs pourront cliquer sur des boutons pour obtenir ou retirer des rôles. +