diff --git a/app/db.js b/app/db.js index 15c3fda..111c4a7 100644 --- a/app/db.js +++ b/app/db.js @@ -161,6 +161,21 @@ db.exec(` purchased_at INTEGER NOT NULL, FOREIGN KEY (item_id) REFERENCES shop_items(id) ); + + CREATE TABLE IF NOT EXISTS privateroom_config ( + guild_id TEXT PRIMARY KEY, + enabled INTEGER NOT NULL DEFAULT 0, + creator_channel_id TEXT, + category_id TEXT, + channel_name_format TEXT NOT NULL DEFAULT '🔊 Salon de {user}' + ); + + CREATE TABLE IF NOT EXISTS temp_voice_channels ( + channel_id TEXT PRIMARY KEY, + guild_id TEXT NOT NULL, + owner_id TEXT NOT NULL, + created_at INTEGER NOT NULL + ); `); module.exports = db; diff --git a/app/events/voiceStateUpdate.js b/app/events/voiceStateUpdate.js index 146d9a8..19bdddb 100644 --- a/app/events/voiceStateUpdate.js +++ b/app/events/voiceStateUpdate.js @@ -1,4 +1,4 @@ -const { Events } = require("discord.js"); +const { Events, ChannelType, PermissionFlagsBits } = require("discord.js"); const db = require("../db"); // Store voice join times and intervals for economy @@ -8,6 +8,9 @@ const voiceMoneyIntervals = new Map(); // guildId_userId -> intervalId module.exports = { name: Events.VoiceStateUpdate, async execute(client, oldState, newState) { + // ===== PRIVATE ROOM (TEMP VOICE CHANNELS) ===== + await handlePrivateRoom(client, oldState, newState); + if (newState.member.user.bot) return; const guildId = newState.guild.id; @@ -114,3 +117,108 @@ module.exports = { ); }, }; + +// ===== PRIVATE ROOM HANDLER ===== +async function handlePrivateRoom(client, oldState, newState) { + const guildId = newState.guild.id; + const member = newState.member; + + // RĂ©cupĂ©rer la configuration + const config = await db.getAsync( + "SELECT enabled, creator_channel_id, category_id, channel_name_format FROM privateroom_config WHERE guild_id = ?", + [guildId] + ); + + if (!config || !config.enabled) { + // MĂȘme si dĂ©sactivĂ©, vĂ©rifier si un salon temp doit ĂȘtre supprimĂ© + await checkAndDeleteEmptyTempChannel(oldState); + return; + } + + // Utilisateur rejoint le salon crĂ©ateur + if (newState.channelId === config.creator_channel_id) { + try { + // Formater le nom du salon + let channelName = config.channel_name_format || '🔊 Salon de {user}'; + channelName = channelName + .replace(/{user}/g, member.user.username) + .replace(/{displayname}/g, member.displayName); + + // CrĂ©er le salon vocal + const newChannel = await newState.guild.channels.create({ + name: channelName, + type: ChannelType.GuildVoice, + parent: config.category_id || null, + permissionOverwrites: [ + { + id: member.id, + allow: [ + PermissionFlagsBits.ManageChannels, + PermissionFlagsBits.MuteMembers, + PermissionFlagsBits.DeafenMembers, + PermissionFlagsBits.MoveMembers, + PermissionFlagsBits.Connect, + PermissionFlagsBits.Speak + ] + } + ] + }); + + // Enregistrer le salon dans la base de donnĂ©es + await new Promise((resolve, reject) => { + db.run( + "INSERT INTO temp_voice_channels (channel_id, guild_id, owner_id, created_at) VALUES (?, ?, ?, ?)", + [newChannel.id, guildId, member.id, Date.now()], + (err) => { + if (err) reject(err); + else resolve(); + } + ); + }); + + // DĂ©placer l'utilisateur dans le nouveau salon + await member.voice.setChannel(newChannel); + } catch (err) { + console.error("Erreur crĂ©ation salon temporaire:", err); + } + } + + // VĂ©rifier si l'ancien salon Ă©tait un salon temporaire vide + await checkAndDeleteEmptyTempChannel(oldState); +} + +async function checkAndDeleteEmptyTempChannel(oldState) { + if (!oldState.channelId) return; + + const oldChannel = oldState.guild.channels.cache.get(oldState.channelId); + if (!oldChannel) return; + + // VĂ©rifier si c'est un salon temporaire + const tempChannel = await db.getAsync( + "SELECT channel_id FROM temp_voice_channels WHERE channel_id = ?", + [oldState.channelId] + ); + + if (!tempChannel) return; + + // VĂ©rifier si le salon est vide + if (oldChannel.members.size === 0) { + try { + // Supprimer le salon + await oldChannel.delete("Salon temporaire vide"); + + // Supprimer de la base de donnĂ©es + db.run( + "DELETE FROM temp_voice_channels WHERE channel_id = ?", + [oldState.channelId] + ); + } catch (err) { + console.error("Erreur suppression salon temporaire:", err); + // Si le salon n'existe plus, le supprimer quand mĂȘme de la DB + db.run( + "DELETE FROM temp_voice_channels WHERE channel_id = ?", + [oldState.channelId] + ); + } + } +} diff --git a/app/public/guild.css b/app/public/guild.css index bc30c73..4262820 100644 --- a/app/public/guild.css +++ b/app/public/guild.css @@ -516,6 +516,24 @@ body { color: var(--text-muted); } +/* ===== Info Box ===== */ +.info-box { + background: linear-gradient(135deg, rgba(88, 101, 242, 0.1), rgba(88, 101, 242, 0.05)); + border: 1px solid rgba(88, 101, 242, 0.3); + border-radius: var(--border-radius); + padding: var(--spacing-md); + margin-bottom: var(--spacing-lg); + color: var(--text-secondary); + font-size: 0.9rem; + line-height: 1.5; +} + +.info-box strong { + color: var(--primary); + display: block; + margin-bottom: var(--spacing-xs); +} + /* ===== Mobile Sidebar Toggle ===== */ .mobile-toggle { display: none; diff --git a/app/public/guild.html b/app/public/guild.html index c5499fd..4876129 100644 --- a/app/public/guild.html +++ b/app/public/guild.html @@ -50,6 +50,10 @@ 💰 Économie + + 🔊 + Salons temporaires + @@ -599,6 +603,58 @@ + +
+
+
+
+ 🔊 +

Salons vocaux temporaires

+
+ +
+
+
+ 💡 Comment ça marche ?
+ Quand un membre rejoint le salon "créateur", un nouveau salon vocal est automatiquement créé pour lui. + Le salon est supprimé quand il devient vide. +
+ +
+ + Le salon vocal que les membres rejoignent pour créer leur salon + +
+ +
+ + Les salons temporaires seront créés dans cette catégorie + +
+ +
+ + +
+ +
+
Variables disponibles
+
+ {user} → nom d'utilisateur + {displayname} → pseudo serveur +
+
+
+ +
+
+ @@ -613,5 +669,6 @@ + diff --git a/app/public/guild/privateroomForm.js b/app/public/guild/privateroomForm.js new file mode 100644 index 0000000..b6cc235 --- /dev/null +++ b/app/public/guild/privateroomForm.js @@ -0,0 +1,104 @@ +// ===== PRIVATE ROOM FORM ===== +(async function () { + const enabledCheckbox = document.getElementById("privateroom-enabled"); + const creatorChannelSelect = document.getElementById("privateroom-creator-channel"); + const categorySelect = document.getElementById("privateroom-category"); + const nameFormatInput = document.getElementById("privateroom-name-format"); + const saveBtn = document.getElementById("save-privateroom"); + const statusDiv = document.getElementById("status-privateroom-form"); + + // Charger les salons vocaux + async function loadVoiceChannels() { + try { + const res = await fetch(`/api/bot/get-voice-channels/${guildId}`); + const channels = await res.json(); + creatorChannelSelect.innerHTML = ''; + channels.forEach(ch => { + const opt = document.createElement("option"); + opt.value = ch.id; + opt.textContent = ch.name; + creatorChannelSelect.appendChild(opt); + }); + } catch (err) { + console.error("Erreur chargement salons vocaux:", err); + } + } + + // Charger les catĂ©gories + async function loadCategories() { + try { + const res = await fetch(`/api/bot/get-categories/${guildId}`); + const categories = await res.json(); + categorySelect.innerHTML = ''; + categories.forEach(cat => { + const opt = document.createElement("option"); + opt.value = cat.id; + opt.textContent = cat.name; + categorySelect.appendChild(opt); + }); + } catch (err) { + console.error("Erreur chargement catĂ©gories:", err); + } + } + + // Charger la configuration + async function loadConfig() { + try { + const res = await fetch(`/api/bot/get-privateroom-config/${guildId}`); + const data = await res.json(); + + enabledCheckbox.checked = data.enabled; + nameFormatInput.value = data.channelNameFormat || '🔊 Salon de {user}'; + + // Attendre que les selects soient remplis + await Promise.all([loadVoiceChannels(), loadCategories()]); + + if (data.creatorChannelId) { + creatorChannelSelect.value = data.creatorChannelId; + } + if (data.categoryId) { + categorySelect.value = data.categoryId; + } + } catch (err) { + console.error("Erreur chargement config privateroom:", err); + } + } + + // Sauvegarder + saveBtn.addEventListener("click", async () => { + saveBtn.disabled = true; + saveBtn.textContent = "Sauvegarde..."; + + const data = { + guildId, + enabled: enabledCheckbox.checked, + creatorChannelId: creatorChannelSelect.value || null, + categoryId: categorySelect.value || null, + channelNameFormat: nameFormatInput.value || '🔊 Salon de {user}' + }; + + try { + const res = await fetch("/api/bot/save-privateroom-config", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(data) + }); + const result = await res.json(); + + if (result.success) { + showStatus("status-privateroom-form", "Configuration sauvegardĂ©e ✅", "success"); + } else { + showStatus("status-privateroom-form", "Erreur lors de la sauvegarde ❌", "error"); + } + } catch (err) { + console.error("Erreur sauvegarde:", err); + showStatus("status-privateroom-form", "Erreur de connexion ❌", "error"); + } + + saveBtn.disabled = false; + saveBtn.textContent = "Sauvegarder"; + }); + + // Init + loadConfig(); +})(); diff --git a/app/public/index.html b/app/public/index.html index 0d79011..fe0888c 100644 --- a/app/public/index.html +++ b/app/public/index.html @@ -72,6 +72,11 @@

RĂŽles Automatiques

Attribuez automatiquement des rĂŽles aux nouveaux membres ou aux utilisateurs en vocal. Configurez les salons Ă  exclure.

+
+
🔊
+

Salons Temporaires

+

Créez des salons vocaux privés à la demande. Quand un membre rejoint le salon créateur, un salon personnel est créé automatiquement.

+
⚙

Dashboard Intuitif

diff --git a/app/routes/api.js b/app/routes/api.js index 59b6487..715c063 100644 --- a/app/routes/api.js +++ b/app/routes/api.js @@ -748,5 +748,86 @@ module.exports = (app, db, client) => { ); }); + // ===== PRIVATE ROOM CONFIG ===== + router.post("/bot/save-privateroom-config", express.json(), (req, res) => { + const { guildId, enabled, creatorChannelId, categoryId, channelNameFormat } = req.body; + + if (!req.session.guilds) { + return res.status(401).json({ success: false }); + } + + const isAdmin = req.session.guilds.find( + g => g.id === guildId && (BigInt(g.permissions) & 0x8n) === 0x8n + ); + + if (!isAdmin) { + return res.status(403).json({ success: false }); + } + + db.run( + `INSERT INTO privateroom_config (guild_id, enabled, creator_channel_id, category_id, channel_name_format) + VALUES (?, ?, ?, ?, ?) + ON CONFLICT(guild_id) DO UPDATE SET + enabled = ?, creator_channel_id = ?, category_id = ?, channel_name_format = ?`, + [ + guildId, + enabled ? 1 : 0, + creatorChannelId, + categoryId, + channelNameFormat || '🔊 Salon de {user}', + enabled ? 1 : 0, + creatorChannelId, + categoryId, + channelNameFormat || '🔊 Salon de {user}' + ], + err => { + if (err) { + console.error(err); + return res.status(500).json({ success: false }); + } + res.json({ success: true }); + } + ); + }); + + router.get("/bot/get-privateroom-config/:guildId", (req, res) => { + const { guildId } = req.params; + + db.get( + "SELECT enabled, creator_channel_id, category_id, channel_name_format FROM privateroom_config WHERE guild_id = ?", + [guildId], + (err, row) => { + if (err || !row) { + return res.json({ + enabled: false, + creatorChannelId: null, + categoryId: null, + channelNameFormat: '🔊 Salon de {user}' + }); + } + res.json({ + enabled: !!row.enabled, + creatorChannelId: row.creator_channel_id, + categoryId: row.category_id, + channelNameFormat: row.channel_name_format || '🔊 Salon de {user}' + }); + } + ); + }); + + router.get("/bot/get-categories/:guildId", (req, res) => { + const { guildId } = req.params; + const guild = client.guilds.cache.get(guildId); + if (!guild) { + return res.status(404).json({ error: "Serveur non trouvĂ©" }); + } + + const categories = guild.channels.cache + .filter(c => c.type === 4) // 4 = GuildCategory + .map(c => ({ id: c.id, name: c.name })); + + res.json(categories); + }); + app.use("/api", router); };