diff --git a/commands/server/ram-status.js b/commands/server/ram-status.js deleted file mode 100644 index d3bb63a..0000000 --- a/commands/server/ram-status.js +++ /dev/null @@ -1,60 +0,0 @@ -const { SlashCommandBuilder } = require('discord.js'); -const { checkRAMUsage, getMonitoringStatus } = require('../../src/monitoring/ramMonitor.js'); - -module.exports = { - data: new SlashCommandBuilder() - .setName('ram-status') - .setDescription('Affiche l\'utilisation actuelle de la RAM du serveur Palworld'), - async execute(interaction) { - - if (!interaction.member.roles.cache.has('1444684935632912394')) { - await interaction.reply({ content: '❌ Il faut avoir le rôle Rygainland pour pouvoir utiliser cette commande.', flags: 64 }); - return; - } - - await interaction.deferReply(); - - try { - // Récupérer les informations sur la RAM - const ramInfo = await checkRAMUsage(); - const monitorStatus = getMonitoringStatus(); - - if (!ramInfo) { - await interaction.editReply('❌ Impossible de récupérer les informations de RAM du serveur.'); - return; - } - - const { ramUsedGB, currentState } = ramInfo; - const thresholdGB = (monitorStatus.threshold / 1024).toFixed(2); - const percentUsed = ((ramInfo.ramUsedMB / monitorStatus.threshold) * 100).toFixed(1); - - // Construire le message avec indicateur visuel - let indicator; - let color; - if (percentUsed < 70) { - indicator = '🟢'; - color = 'Normale'; - } else if (percentUsed < 90) { - indicator = '🟡'; - color = 'Élevée'; - } else { - indicator = '🔴'; - color = 'Critique'; - } - - const message = `${indicator} **État de la RAM du serveur Palworld**\n\n` + - `📊 **Utilisation actuelle:** ${ramUsedGB} Go / ${thresholdGB} Go (${percentUsed}%)\n` + - `⚠️ **Seuil de redémarrage:** ${thresholdGB} Go\n` + - `🔄 **État du serveur:** ${currentState}\n` + - `💡 **Niveau:** ${color}\n\n` + - `${monitorStatus.isMonitoring ? '✅ Surveillance automatique active' : '⚠️ Surveillance automatique inactive'}\n` + - `${monitorStatus.isRebooting ? '🔄 Redémarrage en cours...' : ''}`; - - await interaction.editReply(message); - - } catch (error) { - console.error('Erreur lors de la récupération de l\'état RAM:', error); - await interaction.editReply('❌ Erreur lors de la récupération des informations du serveur.'); - } - }, -}; diff --git a/commands/server/server-stats.js b/commands/server/server-stats.js deleted file mode 100644 index a9f32bd..0000000 --- a/commands/server/server-stats.js +++ /dev/null @@ -1,120 +0,0 @@ -const axios = require('axios'); -const { SlashCommandBuilder } = require('discord.js'); -const { EmbedBuilder } = require('discord.js'); -const { checkRAMUsage } = require('../../src/monitoring/ramMonitor'); - -const getPlayersNumberAndFPS = (token) => { - return new Promise((resolve, reject) => { - let infos = ""; - - axios({ - method: 'get', - maxBodyLength: Infinity, - url: 'http://play.louismazin.ovh:8212/v1/api/metrics', - headers: { - 'Accept': 'application/json', - 'Authorization': `Basic ${token}` - } - }) - .then((response) => { - infos += "## FPS du Serveur : "+response.data["serverfps"]+'\n'; - infos += "## Nombre de joueurs connectés : "+response.data["currentplayernum"]+'\n'; - resolve(infos); - }) - .catch((error) => { - console.log("Erreur lors de l'appel à l'api pterodactyl (serveur injoignable)"); - reject("Le serveur est hors ligne."); - }); - }); -} -const getPlayers = (token) => { - return new Promise((resolve, reject) => { - let infos = ""; - - axios({ - method: 'get', - maxBodyLength: Infinity, - url: 'http://play.louismazin.ovh:8212/v1/api/players', - headers: { - 'Accept': 'application/json', - 'Authorization': `Basic ${token}` - } - }) - .then((response) => { - const players = Object.entries(response.data.players); - if (players.length === 0) { - resolve(infos); - return; - } - for(const player of players) { - const joueur = player[1]; - infos += "### - "+joueur.name+' - niveau '+joueur.level+' - ping : '+Math.round(joueur.ping)+'ms\n'; - } - resolve(infos); - }) - .catch((error) => { - console.log("Erreur lors de l'appel à l'api pterodactyl (serveur injoignable)"); - reject("Le serveur est hors ligne."); - }); - }); -} -const getParams = (token) => { - return new Promise((resolve, reject) => { - let infos = "## Paramètres du Serveur : \n"; - - axios({ - method: 'get', - maxBodyLength: Infinity, - url: 'http://play.louismazin.ovh:8212/v1/api/settings', - headers: { - 'Accept': 'application/json', - 'Authorization': `Basic ${token}` - } - }) - .then((response) => { - const paramNames = {'Difficulty': 'Difficulté', 'DeathPenalty': 'Pénalité de mort', 'bEnableInvaderEnemy': 'Ennemis envahisseurs', 'BaseCampMaxNumInGuild': 'Nombre max de camps par guilde', 'BaseCampWorkerMaxNum': 'Nombre max de pals par camp'} - const params = Object.entries(response.data); - for(const [key, value] of params) { - if(Object.keys(paramNames).indexOf(key) !== -1) { - infos += "### - "+paramNames[key]+' : '+value+'\n'; - } - } - resolve(infos); - }) - .catch((error) => { - console.log("Erreur lors de l'appel à l'api pterodactyl (serveur injoignable)"); - reject("Le serveur est hors ligne."); - }); - }); -} -module.exports = { - data: new SlashCommandBuilder() - .setName('server-stats') - .setDescription('Afichez les informations sur le Serveur Palworld !') - .addUserOption(option => - option.setName('utilisateur') - .setDescription('Utilisateur à mentionner') - .setRequired(false)), - async execute(interaction,token) { - try { - const infos = await getPlayersNumberAndFPS(token); - const params = await getParams(token); - const players = await getPlayers(token); - - // Récupérer l'information de la RAM - const ramInfo = await checkRAMUsage(); - let ramText = ""; - if (ramInfo) { - ramText = `## RAM utilisée : ${ramInfo.ramUsedGB} Go\n`; - } - - const user = interaction.options.getUser('utilisateur') - const message = new EmbedBuilder() - .setColor('#0099ff') - .setDescription('# Informations sur le Serveur Palworld\n\n## :video_game: Nom du serveur :\n### RygainLand\n\n## :wireless: IP :\n### play.louismazin.ovh:1028\n\n## :no_entry: Mot de passe :\n### serverpassword\n\n## :repeat: État :\n### https://discord.com/channels/1068240252092813373/1263481798667796623\n'+infos+ramText+(players==="" ? "" : players+"\n")+'\n'+params); - await interaction.reply({ content: (user ? "||<@"+user.id+">||\n" : null), embeds: [message] }); - } catch (error) { - await interaction.reply({ content: "Une erreur est survenue : " + error, ephemeral: true }); - } - }, -}; \ No newline at end of file diff --git a/commands/trad/trad.js b/commands/trad/trad.js deleted file mode 100644 index 02bc3fa..0000000 --- a/commands/trad/trad.js +++ /dev/null @@ -1,98 +0,0 @@ -const { SlashCommandBuilder, MessageFlags } = require('discord.js'); - -module.exports = { - data: new SlashCommandBuilder() - .setName('trad') - .setDescription('Traduit le message indiqué en français') - .addStringOption(option => - option.setName('liens') - .setDescription('Lien(s) du/des message(s) à traduire (séparés par des espaces)') - .setRequired(false) - ), - async execute(interaction, translator) { - - let messagesToTranslate = []; - const messageLiens = interaction.options.getString('liens'); - - if (messageLiens) { - // Extraire les IDs des liens Discord multiples - const linkPattern = /https:\/\/discord\.com\/channels\/(\d+)\/(\d+)\/(\d+)/g; - const matches = [...messageLiens.matchAll(linkPattern)]; - - if (matches.length === 0) { - await interaction.reply({ content: '❌ Aucun lien de message valide trouvé. Utilisez des liens Discord valides.', flags: MessageFlags.Ephemeral}); - return; - } - - for (const match of matches) { - const [, guildId, channelId, messageId] = match; - - try { - const channel = await interaction.client.channels.fetch(channelId); - const message = await channel.messages.fetch(messageId); - messagesToTranslate.push(message); - } catch (error) { - console.error(`Impossible de récupérer le message ${messageId}:`, error); - } - } - } else { - // Si pas de lien, chercher le message précédent - if (interaction.channel && interaction.channel.lastMessage) { - const messages = await interaction.channel.messages.fetch({ limit: 2 }); - const messagesArray = Array.from(messages.values()); - const messageToTranslate = messagesArray[1]; - if (messageToTranslate) { - messagesToTranslate.push(messageToTranslate); - } - } - } - - if (messagesToTranslate.length === 0) { - await interaction.reply({ content: '❌ Aucun message à traduire trouvé.', flags: MessageFlags.Ephemeral }); - return; - } - - // Supprimer le message de commande - await interaction.deferReply({ flags: MessageFlags.Ephemeral }); - await interaction.deleteReply(); - - // Traiter chaque message - for (const messageToTranslate of messagesToTranslate) { - if (!messageToTranslate.content || messageToTranslate.content.trim() === '') { - continue; - } - - try { - // Traduire le message en français - const result = await translator.translateText(messageToTranslate.content, null, 'fr'); - - const footer = `\n\n*Message original de ${messageToTranslate.author.username} dans ${messageToTranslate.channel.name}*`; - const translatedText = result.text; - - // Vérifier la longueur et diviser si nécessaire - if ((translatedText + footer).length > 2000) { - const maxContentLength = 2000 - footer.length - 15; // -15 pour " *(suite...)*" - const chunks = []; - - // Diviser le texte en chunks - for (let i = 0; i < translatedText.length; i += maxContentLength) { - chunks.push(translatedText.substring(i, i + maxContentLength)); - } - - // Envoyer tous les chunks - for (let i = 0; i < chunks.length; i++) { - const isLast = i === chunks.length - 1; - const chunkMessage = chunks[i] + (isLast ? footer : ' *(suite...)*'); - await interaction.channel.send(chunkMessage); - } - } else { - await interaction.channel.send(translatedText + footer); - } - - } catch (error) { - console.error('Erreur lors de la traduction:', error); - await interaction.channel.send('❌ Erreur lors de la traduction du message.'); - } - } - }, -}; diff --git a/commands/transfer-save/serverMessage.json b/commands/transfer-save/serverMessage.json deleted file mode 100644 index 71be93c..0000000 --- a/commands/transfer-save/serverMessage.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "content": "", - "tts": false, - "embeds": [ - { - "id": 323456789, - "description": "# Pour commencer, je t'explique en quoi va consister la manoeuvre\n### Tout ce que nous allons faire, c'est modifier la sauvegarde pour que le joueur qui veut devenir l'hôte le devienne. Donc aucun des autres joueurs de ta partie n'aura à faire cette manipulation.", - "color": 3447003, - "fields": [] - }, - { - "id": 123456789, - "description": "# 📁 Comment transférer une sauvegarde SERVEUR vers une partie SOLO\n\n## **📋 Étape 1 : Copier les fichiers**\n### Copie :\n `.\\Pal\\Saved\\SaveGames\\0\\[World ID]`\n\n## **📂 Étape 2 : Coller en solo**\n### Colle les fichiers dans :\n### `%LOCALAPPDATA%\\Pal\\Saved\\SaveGames\\[Steam ID]`\n\n## **🎮 Étape 3 : Créer un nouveau personnage**\n### • Lance le monde solo\n### • Crée ton personnage\n### • Reste connecté environ 2 minutes puis ferme le jeu\n\n## **💾 Étape 4 : Sauvegarder les données**\n### • Copie le dossier : `%LOCALAPPDATA%\\Pal\\Saved\\SaveGames\\[Steam ID]\\[World ID]`\n### • Colle-le sur ton bureau\n\n## **🔧 Étape 6 : Utiliser PalworldSaveTools**\n### • Télécharge [PalworldSaveTools](https://github.com/deafdudecomputers/PalworldSaveTools/releases/latest) (premier de la liste)\n### • Ouvre l'outil et sélectionne \"Fix Host Save\"\n### • Sélectionne le fichier `Level.sav` du dossier sur ton bureau\n\n## **🔄 Étape 7 : Migrer le personnage**\n### • À gauche : sélectionne ton ancien personnage'\n### • À droite : sélectionne le joueur avec le GUID `0000...0001` (celui que tu viens de créer)\n### • Clique sur \"Migrate\"\n\n## **✅ Étape 8 : Finaliser le transfert**\n### • Ferme PalworldSaveTools une fois terminé\n### • Supprime le dossier : `%LOCALAPPDATA%\\Pal\\Saved\\SaveGames\\[Steam ID]\\[World ID]`\n### • Remplace-le par celui qui se trouve sur ton bureau\n### • Lance ton jeu, rejoins le monde et c'est parti !", - "color": 3447003, - "fields": [] - }, - { - "id": 223456789, - "description": "## **🗺️ Si la map a besoin d'être restaurée :**\n### *(Nécessaire pour tous les joueurs qui ont des problèmes avec la carte)*\n\n### **Prérequis :** Ferme complètement Palworld\n\n### **Étapes :**\n### • Va dans `%LOCALAPPDATA%\\Pal\\Saved\\SaveGames\\`\n### • Trouve le dossier correspondant à ton nouveau identifiant de monde\n### • Copie le nom de ce dossier, puis supprimes-le\n### • Renomme le dossier correspondant à ton ancien identifiant de monde grâce au nom copié précédemment.", - "color": 3447003, - "fields": [] - }, - { - "id": 423456789, - "description": "## **🎒 Si des joueurs n'ont pas leurs équipements lors de la première connexion :**\n### *(Maintenant, pour chaque joueur concerné)*\n\n### **Étapes à répéter :**\n### • Sur le serveur, copie le dossier : `%LOCALAPPDATA%\\Pal\\Saved\\SaveGames\\[Steam ID]\\[World ID]`\n### • Colle-le sur ton bureau\n\n### • Le joueur doit créer un nouveau personnage sur le serveur\n### • Utilise PalworldSaveTools avec \"Fix Host Save\"\n### • Sélectionne le fichier `Level.sav` du serveur\n### • À gauche : sélectionne l'ancien personnage du joueur\n### • À droite : sélectionne le nouveau personnage qu'il vient de créer\n### • Clique sur \"Migrate\"\n### • Supprime le dossier : `%LOCALAPPDATA%\\Pal\\Saved\\SaveGames\\[Steam ID]\\[World ID]`\n### • Remplace-le par celui qui se trouve sur ton bureau\n### • Lance ton serveur et vérifie !", - "color": 3447003, - "fields": [] - }, - { - "id": 423456789, - "description": "## **💬 Besoin d'aide ?**\n### Si tu rencontres le moindre problème pendant le processus de transfert, n'hésite pas à contacter <@391708236698615809> en message privé !\n### Il te répondra dès que possible pour t'aider à résoudre ton souci.", - "color": 3447003, - "fields": [] - } - ], - - "components": [], - "actions": {}, - "username": "Couteau Suisse", - "avatar_url": "https://srv.latostadora.com/designall.dll/couteau-suisse---dessin-drole-sketchy--i:141385141697014138520;d:1416970;w:520;b:FFFFFF;m:1.jpg" -} diff --git a/commands/transfer-save/soloMessage.json b/commands/transfer-save/soloMessage.json deleted file mode 100644 index d2c85dc..0000000 --- a/commands/transfer-save/soloMessage.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "content": "", - "tts": false, - "embeds": [ - { - "id": 323456789, - "description": "# Pour commencer, je t'explique en quoi va consister la manoeuvre\n### Tout ce que nous allons faire, c'est modifier la sauvegarde pour que le joueur qui était l'hôte puisse devenir simple joueur. Donc aucun des autres joueurs de ta partie n'aura à faire cette manipulation.\n\n## **⚙️ Prérequis :**\n### • Le serveur doit avoir été lancé et rejoint au moins 1 fois\n### • Les paramètres du serveur doivent avoir été adaptés à la partie que tu veux transférer dans :\n ### `.\\Pal\\Saved\\Config\\[Os]Server\\PalWorldSettings.ini`", - "color": 3447003, - "fields": [] - }, - { - "id": 123456789, - "description": "# 📁 Comment transférer une sauvegarde SOLO vers un SERVEUR\n\n## **📍 Étape 1 : Localiser ta sauvegarde**\n### Ta sauvegarde solo se trouve dans :\n### `%LOCALAPPDATA%\\Pal\\Saved\\SaveGames\\[Steam ID]\\[World ID]`\n\n## **📋 Étape 2 : Copier les fichiers**\n### Copie :\n### • `Players`\n### • `Level.sav`\n\n## **📂 Étape 3 : Coller sur le serveur**\n### Colle les fichiers dans :\n### `.\\Pal\\Saved\\SaveGames\\0\\[World ID]`\n\n## **🎮 Étape 4 : Créer un nouveau personnage**\n### • Lance le serveur\n### • Connecte-toi et crée ton personnage\n### • Reste connecté environ 2 minutes puis quitte\n### • Éteins le serveur\n\n## **💾 Étape 5 : Sauvegarder les données**\n### • Copie le dossier : `.\\Pal\\Saved\\SaveGames\\0\\[World ID]`\n### • Colle-le sur ton bureau\n\n## **🔧 Étape 6 : Utiliser PalworldSaveTools**\n### • Télécharge [PalworldSaveTools](https://github.com/deafdudecomputers/PalworldSaveTools/releases/latest) (premier de la liste)\n### • Ouvre l'outil et sélectionne \"Fix Host Save\"\n### • Sélectionne le fichier `Level.sav` du dossier sur ton bureau\n\n## **🔄 Étape 7 : Migrer le personnage**\n### • À gauche : sélectionne le joueur avec le GUID `0000...0001`\n### • À droite : sélectionne le joueur que tu viens de créer\n### • Clique sur \"Migrate\"\n\n## **✅ Étape 8 : Finaliser le transfert**\n### • Ferme PalworldSaveTools une fois terminé\n### • Supprime le dossier : `.\\Pal\\Saved\\SaveGames\\0\\[World ID]`\n### • Remplace-le par celui qui se trouve sur ton bureau\n### • Lance ton serveur, connecte-toi et c'est parti !", - "color": 3447003, - "fields": [] - }, - { - "id": 223456789, - "description": "## **🗺️ Si la map a besoin d'être restaurée :**\n### *(Nécessaire pour tous les joueurs qui ont des problèmes avec la carte)*\n\n### **Prérequis :** Ferme complètement Palworld\n\n### **Étapes :**\n### • Va dans `%LOCALAPPDATA%\\Pal\\Saved\\SaveGames\\`\n### • Trouve le dossier correspondant à ton nouveau identifiant de monde\n### • Copie le nom de ce dossier, puis supprimes-le\n### • Renomme le dossier correspondant à ton ancien identifiant de monde grâce au nom copié précédemment.", - "color": 3447003, - "fields": [] - }, - { - "id": 423456789, - "description": "## **🎒 Si des joueurs n'ont pas leurs équipements lors de la première connexion :**\n### *(Maintenant, pour chaque joueur concerné)*\n\n### **Étapes à répéter :**\n### • Sur le serveur, copie le dossier : `.\\Pal\\Saved\\SaveGames\\0\\[World ID]`\n### • Colle-le sur ton bureau\n\n### • Le joueur doit créer un nouveau personnage sur le serveur\n### • Utilise PalworldSaveTools avec \"Fix Host Save\"\n### • Sélectionne le fichier `Level.sav` du serveur\n### • À gauche : sélectionne l'ancien personnage du joueur\n### • À droite : sélectionne le nouveau personnage qu'il vient de créer\n### • Clique sur \"Migrate\"\n### • Supprime le dossier : `.\\Pal\\Saved\\SaveGames\\0\\[World ID]`\n### • Remplace-le par celui qui se trouve sur ton bureau\n### • Lance ton serveur et vérifie !", - "color": 3447003, - "fields": [] - }, - { - "id": 423456789, - "description": "## **💬 Besoin d'aide ?**\n### Si tu rencontres le moindre problème pendant le processus de transfert, n'hésite pas à contacter <@391708236698615809> en message privé !\n### Il te répondra dès que possible pour t'aider à résoudre ton souci.", - "color": 3447003, - "fields": [] - } - ], - - "components": [], - "actions": {}, - "username": "Couteau Suisse", - "avatar_url": "https://srv.latostadora.com/designall.dll/couteau-suisse---dessin-drole-sketchy--i:141385141697014138520;d:1416970;w:520;b:FFFFFF;m:1.jpg" -} diff --git a/commands/transfer-save/transfer-save.js b/commands/transfer-save/transfer-save.js deleted file mode 100644 index e1f1cbe..0000000 --- a/commands/transfer-save/transfer-save.js +++ /dev/null @@ -1,54 +0,0 @@ -const { SlashCommandBuilder, MessageFlags } = require('discord.js'); -const soloMessage = require("./soloMessage.json"); -const serverMessage = require("./serverMessage.json"); - -module.exports = { - data: new SlashCommandBuilder() - .setName('transfer-save') - .setDescription('Explique comment transférer une sauvegarde Palworld') - .addStringOption(option => - option.setName('type') - .setDescription('Type de sauvegarde à transférer') - .setRequired(true) - .addChoices( - { name: 'Solo', value: 'solo' }, - { name: 'Serveur', value: 'serveur' } - )) - .addUserOption(option => - option.setName('utilisateur') - .setDescription('Utilisateur à mentionner') - .setRequired(false)), - async execute(interaction) { - const type = interaction.options.getString('type'); - const mentionnedUser = interaction.options.getUser('utilisateur'); - const targetUser = mentionnedUser || interaction.user; - - try { - let messageToSend; - - if (type === 'solo') { - messageToSend = soloMessage; - } else { - messageToSend = serverMessage; - } - - // Envoyer le message en privé au bon utilisateur - await targetUser.send(messageToSend); - - // Confirmer l'envoi dans le canal - const recipientText = mentionnedUser ? ` à ${mentionnedUser.username}` : 'en message privé'; - await interaction.reply({ - content: `✅ Les instructions pour transférer une sauvegarde ${type} ont été envoyées${recipientText} !`, - flags: MessageFlags.Ephemeral - }); - - } catch (error) { - console.error('Erreur lors de l\'envoi du message privé:', error); - const userText = mentionnedUser ? `de ${mentionnedUser.username}` : 'vos'; - await interaction.reply({ - content: `❌ Impossible d'envoyer le message privé. Vérifiez que les messages privés ${userText} sont ouverts.`, - flags: MessageFlags.Ephemeral - }); - } - }, -}; diff --git a/commands/utility/afficher-inactifs.js b/commands/utility/afficher-inactifs.js deleted file mode 100644 index 5fc8344..0000000 --- a/commands/utility/afficher-inactifs.js +++ /dev/null @@ -1,123 +0,0 @@ -const { SlashCommandBuilder, EmbedBuilder, MessageFlags } = require('discord.js'); -const { getAllLinks } = require('../../src/core/database.js'); - -module.exports = { - data: new SlashCommandBuilder() - .setName('afficher-inactifs') - .setDescription('Afficher les membres inactifs ou absents du serveur'), - - async execute(interaction) { - try { - await interaction.deferReply({}); - - const links = await getAllLinks(); - - if (links.length === 0) { - return interaction.editReply({ - content: '📝 Aucun compte lié pour le moment.', - flags: MessageFlags.Ephemeral - }); - } - - const threeMonthsAgo = new Date(); - threeMonthsAgo.setMonth(threeMonthsAgo.getMonth() - 3); - - const inactiveMembers = []; - const notInServer = []; - - // Vérifier chaque lien - for (const link of links) { - // Vérifier si le membre est toujours sur le serveur - const member = await interaction.guild.members.fetch(link.discord_id).catch(() => null); - - if (!member) { - // Membre n'est plus sur le serveur - notInServer.push(link); - } else if (link.lastConnection) { - // Vérifier si inactif depuis plus de 3 mois - const lastConn = new Date(link.lastConnection); - if (lastConn < threeMonthsAgo) { - inactiveMembers.push(link); - } - } - } - - const embeds = []; - - // Embed pour les absents depuis + de 3 mois - if (inactiveMembers.length > 0) { - const embed = new EmbedBuilder() - .setColor(0xFF6B00) - .setTitle('⏰ Membres inactifs (+ de 3 mois)') - .setDescription(`Total: **${inactiveMembers.length}** membre(s) inactif(s)`) - .setTimestamp(); - - for (const link of inactiveMembers) { - const user = await interaction.client.users.fetch(link.discord_id).catch(() => null); - const discordName = user ? (user.globalName ? user.globalName : user.username) : link.discord_username; - const discordMention = `<@${link.discord_id}>`; - - // Calculer le temps depuis la dernière connexion - let lastConnectionText = ''; - if (link.lastConnection) { - const lastDate = new Date(link.lastConnection); - lastConnectionText = ``; - } - - embed.addFields({ - name: `👤 ${discordName} - 🎮 ${link.palworld_username}`, - value: `${discordMention} - Dernière connexion: ${lastConnectionText}` - }); - } - - embeds.push(embed); - } - - // Embed pour les membres qui ne sont plus sur le serveur - if (notInServer.length > 0) { - const embed = new EmbedBuilder() - .setColor(0xFF0000) - .setTitle('👻 Membres liés mais absents du serveur Discord') - .setDescription(`Total: **${notInServer.length}** membre(s) absent(s)`) - .setTimestamp(); - - for (const link of notInServer) { - const user = await interaction.client.users.fetch(link.discord_id).catch(() => null); - const discordName = user ? (user.globalName ? user.globalName : user.username) : link.discord_username; - const discordMention = `<@${link.discord_id}>`; - - // Calculer le temps depuis la dernière connexion - let lastConnectionText = 'Jamais connecté'; - if (link.lastConnection) { - const lastDate = new Date(link.lastConnection); - lastConnectionText = ``; - } - - embed.addFields({ - name: `👤 ${discordName} - 🎮 ${link.palworld_username}`, - value: `${discordMention} - Dernière connexion: ${lastConnectionText}` - }); - } - - embeds.push(embed); - } - - // Si aucun inactif - if (embeds.length === 0) { - return interaction.editReply({ - content: '✅ Aucun membre inactif ou absent du serveur !', - flags: MessageFlags.Ephemeral - }); - } - - await interaction.editReply({ embeds }); - - } catch (error) { - console.error('Erreur lors de la récupération des inactifs:', error); - await interaction.editReply({ - content: '❌ Une erreur est survenue lors de la récupération des membres inactifs.', - flags: MessageFlags.Ephemeral - }); - } - }, -}; diff --git a/commands/utility/afficher-lies.js b/commands/utility/afficher-lies.js deleted file mode 100644 index 5ea87c9..0000000 --- a/commands/utility/afficher-lies.js +++ /dev/null @@ -1,67 +0,0 @@ -const { SlashCommandBuilder, EmbedBuilder, MessageFlags } = require('discord.js'); -const { getAllLinks } = require('../../src/core/database.js'); - -module.exports = { - data: new SlashCommandBuilder() - .setName('afficher-lies') - .setDescription('Afficher tous les comptes liés'), - - async execute(interaction) { - try { - await interaction.deferReply({}); - - const links = await getAllLinks(); - - if (links.length === 0) { - return interaction.editReply({ - content: '📝 Aucun compte lié pour le moment.', - flags: MessageFlags.Ephemeral - }); - } - - const embeds = []; - const FIELDS_PER_EMBED = 24; // Maximum 25 fields par embed, on garde une marge - - // Diviser les liens en chunks de 24 pour respecter la limite Discord - for (let i = 0; i < links.length; i += FIELDS_PER_EMBED) { - const chunk = links.slice(i, i + FIELDS_PER_EMBED); - - const embed = new EmbedBuilder() - .setColor(0x0099FF) - .setTitle(i === 0 ? '🔗 Liste des comptes liés' : null) - .setDescription(i === 0 ? `Total: **${links.length}** compte(s) lié(s)` : null) - // si c'est le dernier embed, on met la date actuelle - .setTimestamp(i + FIELDS_PER_EMBED >= links.length ? new Date() : null); - - for (const link of chunk) { - const user = await interaction.client.users.fetch(link.discord_id).catch(() => null); - const discordName = user ? (user.globalName ? user.globalName : user.username) : link.discord_username; - const discordMention = `<@${link.discord_id}>`; - - // temps depuis la dernière connexion formatée pour affichage discord - let lastConnectionText = 'Jamais connecté'; - if (link.lastConnection) { - const lastDate = new Date(link.lastConnection); - lastConnectionText = ``; - } - - embed.addFields({ - name: `👤 ${discordName} - 🎮 ${link.palworld_username} - ${lastConnectionText}`, - value: `${discordMention}` - }); - } - - embeds.push(embed); - } - - await interaction.editReply({ embeds }); - - } catch (error) { - console.error('Erreur lors de la récupération des liaisons:', error); - await interaction.editReply({ - content: '❌ Une erreur est survenue lors de la récupération des liaisons.', - flags: MessageFlags.Ephemeral - }); - } - }, -}; diff --git a/commands/utility/delier-rygainland.js b/commands/utility/delier-rygainland.js deleted file mode 100644 index 684d51e..0000000 --- a/commands/utility/delier-rygainland.js +++ /dev/null @@ -1,134 +0,0 @@ -const { SlashCommandBuilder, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, MessageFlags } = require('discord.js'); -const { getUserLink, deleteUserLink } = require('../../src/core/database.js'); - -module.exports = { - data: new SlashCommandBuilder() - .setName('delier-rygainland') - .setDescription('Délier votre compte Discord de votre compte Palworld'), - - async execute(interaction) { - try { - const existingLink = await getUserLink(interaction.user.id); - - if (!existingLink) { - const embed = new EmbedBuilder() - .setColor(0xFF9900) - .setTitle('⚠️ Aucune liaison trouvée') - .setDescription('Votre compte Discord n\'est pas lié à un compte Palworld.') - .addFields( - { name: '💡 Pour vous lier', value: 'Utilisez la commande `/lier-rygainland`' } - ) - .setTimestamp(); - - return interaction.reply({ embeds: [embed], flags: MessageFlags.Ephemeral }); - } - - // Afficher un embed de confirmation avec boutons - const confirmEmbed = new EmbedBuilder() - .setColor(0xFF6600) - .setTitle('⚠️ Confirmation de déliaison') - .setDescription('Êtes-vous sûr de vouloir délier votre compte ?') - .addFields( - { name: '👤 Discord', value: `${interaction.user.globalName}`, inline: false }, - { name: '🎮 Palworld', value: `**${existingLink.palworld_username}**`, inline: true }, - { name: '🆔 Steam ID', value: `\`${existingLink.steam_id}\``, inline: true }, - { name: '🎯 Player ID', value: `\`${existingLink.player_id || 'N/A'}\``, inline: false } - ) - .setFooter({ text: 'Cette action est irréversible' }) - .setTimestamp(); - - const row = new ActionRowBuilder() - .addComponents( - new ButtonBuilder() - .setCustomId('confirm_unlink') - .setLabel('✅ Confirmer la déliaison') - .setStyle(ButtonStyle.Danger), - new ButtonBuilder() - .setCustomId('cancel_unlink') - .setLabel('❌ Annuler') - .setStyle(ButtonStyle.Secondary) - ); - - const response = await interaction.reply({ - embeds: [confirmEmbed], - components: [row], - flags: MessageFlags.Ephemeral - }); - - // Attendre la réponse de l'utilisateur (60 secondes) - const collectorFilter = i => i.user.id === interaction.user.id; - - try { - const confirmation = await response.awaitMessageComponent({ - filter: collectorFilter, - time: 60000 - }); - - if (confirmation.customId === 'confirm_unlink') { - // Supprimer la liaison - await deleteUserLink(interaction.user.id); - - // Retirer le rôle de joueur lié - try { - const guild = interaction.guild; - const member = await guild.members.fetch(interaction.user.id); - const linkedRole = guild.roles.cache.get('1467491093649035475'); - if (linkedRole && member.roles.cache.has(linkedRole.id)) { - await member.roles.remove(linkedRole); - console.log(`✅ Rôle retiré de ${interaction.user.tag}`); - } - } catch (roleError) { - console.error('Erreur lors du retrait du rôle:', roleError); - } - - const successEmbed = new EmbedBuilder() - .setColor(0x00FF00) - .setTitle('✅ Déliaison réussie') - .setDescription('Votre compte Discord a été délié avec succès de votre compte Palworld.') - .addFields( - { name: '🎮 Compte Palworld délié', value: `**${existingLink.palworld_username}**` }, - { name: '💡 Pour vous lier à nouveau', value: 'Utilisez la commande `/lier-rygainland`' } - ) - .setTimestamp(); - - await confirmation.update({ - embeds: [successEmbed], - components: [] - }); - - } else if (confirmation.customId === 'cancel_unlink') { - const cancelEmbed = new EmbedBuilder() - .setColor(0x808080) - .setTitle('❌ Déliaison annulée') - .setDescription('Votre compte reste lié.') - .setTimestamp(); - - await confirmation.update({ - embeds: [cancelEmbed], - components: [] - }); - } - - } catch (error) { - // Timeout ou erreur - const timeoutEmbed = new EmbedBuilder() - .setColor(0x808080) - .setTitle('⏱️ Temps écoulé') - .setDescription('La demande de déliaison a expiré. Votre compte reste lié.') - .setTimestamp(); - - await interaction.editReply({ - embeds: [timeoutEmbed], - components: [] - }); - } - - } catch (error) { - console.error('Erreur lors de la déliaison:', error); - await interaction.reply({ - content: '❌ Une erreur est survenue lors de la déliaison.', - flags: MessageFlags.Ephemeral - }); - } - }, -}; diff --git a/commands/utility/delier.js b/commands/utility/delier.js deleted file mode 100644 index 92655f5..0000000 --- a/commands/utility/delier.js +++ /dev/null @@ -1,145 +0,0 @@ -const { SlashCommandBuilder, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, MessageFlags } = require('discord.js'); -const { getUserLink, deleteUserLink } = require('../../src/core/database.js'); - -module.exports = { - data: new SlashCommandBuilder() - .setName('delier') - .setDescription('Délier un compte Discord de son compte Palworld (Admin)') - .addUserOption(option => - option.setName('compte-discord') - .setDescription('Le compte Discord à délier') - .setRequired(true)), - - async execute(interaction) { - try { - const discordUser = interaction.options.getUser('compte-discord'); - - const existingLink = await getUserLink(discordUser.id); - - if (!existingLink) { - const embed = new EmbedBuilder() - .setColor(0xFF9900) - .setTitle('⚠️ Aucune liaison trouvée') - .setDescription(`Le compte <@${discordUser.id}> n'est pas lié à un compte Palworld.`) - .setTimestamp(); - - return interaction.reply({ embeds: [embed]}); - } - - // Afficher un embed de confirmation avec boutons - const confirmEmbed = new EmbedBuilder() - .setColor(0xFF6600) - .setTitle('⚠️ Confirmation de déliaison (Admin)') - .setDescription(`Êtes-vous sûr de vouloir délier ce compte ?`) - .addFields( - { name: '👤 Discord', value: `${discordUser.tag} (<@${discordUser.id}>)`, inline: false }, - { name: '🎮 Palworld', value: `**${existingLink.palworld_username}**`, inline: true }, - { name: '🆔 Steam ID', value: `\`${existingLink.steam_id}\``, inline: true }, - { name: '🎯 Player ID', value: `\`${existingLink.player_id || 'N/A'}\``, inline: false } - ) - .setFooter({ text: 'Cette action est irréversible' }) - .setTimestamp(); - - const row = new ActionRowBuilder() - .addComponents( - new ButtonBuilder() - .setCustomId('admin_confirm_unlink') - .setLabel('✅ Confirmer la déliaison') - .setStyle(ButtonStyle.Danger), - new ButtonBuilder() - .setCustomId('admin_cancel_unlink') - .setLabel('❌ Annuler') - .setStyle(ButtonStyle.Secondary) - ); - - const response = await interaction.reply({ - embeds: [confirmEmbed], - components: [row] - }); - - const collectorFilter = i => i.user.id === interaction.user.id; - - try { - const confirmation = await response.awaitMessageComponent({ - filter: collectorFilter, - time: 60000 - }); - - if (confirmation.customId === 'admin_confirm_unlink') { - await deleteUserLink(discordUser.id); - - // Retirer le rôle de joueur lié - try { - const guild = interaction.guild; - const member = await guild.members.fetch(discordUser.id); - const linkedRole = guild.roles.cache.get('1467491093649035475'); - if (linkedRole && member.roles.cache.has(linkedRole.id)) { - await member.roles.remove(linkedRole); - console.log(`✅ Rôle retiré de ${discordUser.tag}`); - } - } catch (roleError) { - console.error('Erreur lors du retrait du rôle:', roleError); - } - - const successEmbed = new EmbedBuilder() - .setColor(0x00FF00) - .setTitle('✅ Liaison supprimée') - .setDescription(`Le compte Discord a été délié avec succès.`) - .addFields( - { name: '👤 Discord', value: `${discordUser.tag} (<@${discordUser.id}>)`, inline: false }, - { name: '🎮 Palworld (ancien)', value: existingLink.palworld_username, inline: true }, - { name: '🆔 Steam ID (ancien)', value: `\`${existingLink.steam_id}\``, inline: true } - ) - .setTimestamp(); - - await discordUser.send( - `🔓 **Liaison supprimée**\n\n` + - `Votre compte Discord a été délié de votre compte Palworld par un administrateur.\n` + - `Vous pouvez vous lier à nouveau avec \`/lier-rygainland\`.` - ).catch(() => {}); - - await confirmation.update({ - embeds: [successEmbed], - components: [] - }); - - } else if (confirmation.customId === 'admin_cancel_unlink') { - const cancelEmbed = new EmbedBuilder() - .setColor(0x808080) - .setTitle('❌ Déliaison annulée') - .setDescription('La liaison n\'a pas été modifiée.') - .setTimestamp(); - - await confirmation.update({ - embeds: [cancelEmbed], - components: [] - }); - } - - } catch (error) { - const timeoutEmbed = new EmbedBuilder() - .setColor(0x808080) - .setTitle('⏱️ Temps écoulé') - .setDescription('La demande de déliaison a expiré. La liaison n\'a pas été modifiée.') - .setTimestamp(); - - await interaction.editReply({ - embeds: [timeoutEmbed], - components: [] - }); - } - - } catch (error) { - console.error('Erreur lors de la suppression de la liaison:', error); - await interaction.reply({ - content: '❌ Une erreur est survenue lors de la suppression de la liaison.', - flags: MessageFlags.Ephemeral - }).catch(() => { - interaction.editReply({ - content: '❌ Une erreur est survenue lors de la suppression de la liaison.', - flags: MessageFlags.Ephemeral - }); - }); - } - }, -}; diff --git a/commands/utility/diagnostique-ws.js b/commands/utility/diagnostique-ws.js deleted file mode 100644 index 934aad0..0000000 --- a/commands/utility/diagnostique-ws.js +++ /dev/null @@ -1,97 +0,0 @@ -const { SlashCommandBuilder, EmbedBuilder, MessageFlags } = require('discord.js'); -const { getWebSocketStatus, forceWebSocketReconnect } = require('../../src/monitoring/consoleMonitor.js'); - -module.exports = { - data: new SlashCommandBuilder() - .setName('diagnostique-ws') - .setDescription('Vérifier l\'état du WebSocket et forcer une reconnexion si nécessaire') - .addBooleanOption(option => - option.setName('reconnect') - .setDescription('Forcer une reconnexion du WebSocket') - .setRequired(false) - ), - - async execute(interaction) { - try { - const forceReconnect = interaction.options.getBoolean('reconnect') || false; - - if (forceReconnect) { - await interaction.reply({ - content: '🔄 Reconnexion forcée du WebSocket en cours...', - flags: MessageFlags.Ephemeral - }); - - await forceWebSocketReconnect(); - - // Attendre un peu pour laisser le temps de se reconnecter - await new Promise(resolve => setTimeout(resolve, 3000)); - } - - const status = getWebSocketStatus(); - - const embed = new EmbedBuilder() - .setColor(status.hasWebSocket && status.wsState === 'OPEN' ? 0x00FF00 : 0xFF0000) - .setTitle('🔌 Diagnostic WebSocket') - .addFields( - { name: '📊 Monitoring Actif', value: status.isMonitoring ? '✅ Oui' : '❌ Non', inline: true }, - { name: '🔗 WebSocket Présent', value: status.hasWebSocket ? '✅ Oui' : '❌ Non', inline: true }, - { name: '📡 État Connexion', value: status.wsState || 'N/A', inline: true }, - { name: '🔄 En cours de connexion', value: status.isConnecting ? '⏳ Oui' : '✅ Non', inline: true }, - { name: '💓 Heartbeat Actif', value: status.hasHeartbeat ? '✅ Oui' : '❌ Non', inline: true }, - { name: '⏱️ Timeout Heartbeat', value: status.hasHeartbeatTimeout ? '✅ Oui' : '❌ Non', inline: true }, - { name: '🔍 Vérification Auto', value: status.hasCheckInterval ? '✅ Oui' : '❌ Non', inline: true }, - { name: '⏱️ Reconnexion Planifiée', value: status.hasPendingReconnect ? '⏳ Oui' : '❌ Non', inline: true }, - { name: '🔁 Délai Reconnexion', value: `${Math.round(status.reconnectDelayMs / 1000)}s`, inline: true }, - { name: '🔄 Refresh Credentials', value: status.hasRefreshInterval ? '✅ Oui' : '❌ Non', inline: true } - ) - .setTimestamp(); - - if (status.connectionTimestamp) { - embed.addFields({ name: '🕐 Dernière Connexion', value: status.connectionTimestamp, inline: false }); - } - - if (status.lastHeartbeatResponse) { - const timeSinceResponse = status.timeSinceLastResponse ? `${status.timeSinceLastResponse}s` : 'N/A'; - embed.addFields({ name: '💓 Dernière Réponse Serveur', value: `${status.lastHeartbeatResponse}\n⏱️ Il y a ${timeSinceResponse}`, inline: false }); - } - - if (status.monitoringStartTimestamp) { - embed.addFields({ name: '🚀 Démarrage Monitoring', value: status.monitoringStartTimestamp, inline: false }); - } - - // Recommandations - let recommendations = ''; - if (!status.isMonitoring) { - recommendations += '⚠️ Le monitoring n\'est pas actif. Redémarrer le bot.\n'; - } - if (!status.hasWebSocket || status.wsState !== 'OPEN') { - recommendations += '⚠️ Le WebSocket n\'est pas connecté. Utilisez `/diagnostique-ws reconnect:True` pour forcer une reconnexion.\n'; - } - if (status.hasPendingReconnect) { - recommendations += `ℹ️ Une reconnexion est prévue dans ${Math.round(status.reconnectDelayMs / 1000)}s.\n`; - } - if (status.timeSinceLastResponse && status.timeSinceLastResponse > 45) { - recommendations += `⚠️ Aucune réponse du serveur depuis ${status.timeSinceLastResponse}s (timeout dans ${60 - status.timeSinceLastResponse}s)\n`; - } - - if (recommendations) { - embed.addFields({ name: '💡 Recommandations', value: recommendations, inline: false }); - } else { - embed.addFields({ name: '✅ Statut', value: 'Tout fonctionne normalement !', inline: false }); - } - - if (forceReconnect) { - await interaction.editReply({ content: null, embeds: [embed], flags: MessageFlags.Ephemeral }); - } else { - await interaction.reply({ embeds: [embed], flags: MessageFlags.Ephemeral }); - } - - } catch (error) { - console.error('[DIAGNOSTIQUE-WS] Erreur:', error); - await interaction.reply({ - content: '❌ Erreur lors du diagnostic', - flags: MessageFlags.Ephemeral - }).catch(() => {}); - } - }, -}; diff --git a/commands/utility/info.js b/commands/utility/info.js deleted file mode 100644 index 29bdfd6..0000000 --- a/commands/utility/info.js +++ /dev/null @@ -1,71 +0,0 @@ -const { SlashCommandBuilder, EmbedBuilder, MessageFlags } = require('discord.js'); -const { getUserLink } = require('../../src/core/database.js'); - -module.exports = { - data: new SlashCommandBuilder() - .setName('info') - .setDescription('Afficher les informations détaillées d\'un compte lié') - .addUserOption(option => - option.setName('utilisateur') - .setDescription('L\'utilisateur Discord dont vous voulez voir les informations') - .setRequired(true)), - - async execute(interaction) { - try { - await interaction.deferReply({}); - - const targetUser = interaction.options.getUser('utilisateur'); - const link = await getUserLink(targetUser.id); - - if (!link) { - return interaction.editReply({ - content: `❌ Aucun compte lié trouvé pour ${targetUser.globalName || targetUser.username}.`, - flags: MessageFlags.Ephemeral - }); - } - - const lastConn = link.lastConnection - ? `` - : 'Jamais vu'; - - const linkedDate = ``; - - const embed = new EmbedBuilder() - .setColor(0x0099FF) - .setTitle('📋 Informations du compte lié') - .setThumbnail(targetUser.displayAvatarURL({ dynamic: true })) - .addFields( - { - name: '👤 Discord', - value: `**Nom:** ${targetUser.globalName || targetUser.username}\n**Mention:** <@${targetUser.id}>\n**ID:** \`${targetUser.id}\``, - inline: false - }, - { - name: '🎮 Palworld', - value: `**Pseudo:** ${link.palworld_username}\n**Player ID:** \`${link.player_id || 'N/A'}\``, - inline: true - }, - { - name: '🎯 Steam', - value: `**Steam ID:** \`${link.steam_id}\``, - inline: true - }, - { - name: '📅 Dates', - value: `**Lié le:** ${linkedDate}\n**Dernière connexion:** ${lastConn}`, - inline: false - } - ) - .setTimestamp(); - - await interaction.editReply({ embeds: [embed] }); - - } catch (error) { - console.error('Erreur lors de la récupération des informations:', error); - await interaction.editReply({ - content: '❌ Une erreur est survenue lors de la récupération des informations.', - flags: MessageFlags.Ephemeral - }); - } - }, -}; diff --git a/commands/utility/lier-rygainland.js b/commands/utility/lier-rygainland.js deleted file mode 100644 index d2de40c..0000000 --- a/commands/utility/lier-rygainland.js +++ /dev/null @@ -1,58 +0,0 @@ -const { SlashCommandBuilder, EmbedBuilder, MessageFlags } = require('discord.js'); -const { generateLinkCode, getUserLink } = require('../../src/core/database.js'); - -module.exports = { - data: new SlashCommandBuilder() - .setName('lier-rygainland') - .setDescription('Lier votre compte Discord à votre compte Palworld'), - - async execute(interaction) { - try { - console.log('=== [LIER-RYGAINLAND] Début de l\'exécution ==='); - console.log('[LIER-RYGAINLAND] Utilisateur:', interaction.user.tag, '(ID:', interaction.user.id + ')'); - - console.log('[LIER-RYGAINLAND] Vérification des liaisons existantes...'); - const existingLink = await getUserLink(interaction.user.id); - console.log('[LIER-RYGAINLAND] Liaison existante:', existingLink); - if (existingLink) { - console.log('[LIER-RYGAINLAND] ⚠️ Utilisateur déjà lié à:', existingLink.palworld_username); - return interaction.reply({ - content: `Vous êtes déjà lié au compte Palworld: **${existingLink.palworld_username}** (Steam ID: ${existingLink.steam_id})`, - flags: MessageFlags.Ephemeral - }); - } - - console.log('[LIER-RYGAINLAND] Génération d\'un nouveau code de liaison...'); - const code = await generateLinkCode(interaction.user.id); - console.log('[LIER-RYGAINLAND] ✅ Code généré:', code); - - const embed = new EmbedBuilder() - .setColor(0x00FF00) - .setTitle('🔗 Liaison de compte Rygainland') - .setDescription('Pour lier votre compte Discord à votre compte Palworld:') - .addFields( - { name: '1️⃣ Étape 1', value: 'Connectez-vous sur le serveur Palworld' }, - { name: '2️⃣ Étape 2', value: `Tapez dans le chat du jeu:\n\`\`\`!lier ${code}\`\`\`` }, - { name: '⏱️ Expiration', value: 'Ce code expire dans **10 minutes**' }, - { name: '🔑 Votre code', value: `\`${code}\``, inline: false } - ) - .setFooter({ text: 'Le système de surveillance démarre automatiquement' }) - .setTimestamp(); - - console.log('[LIER-RYGAINLAND] Envoi de l\'embed avec le code...'); - await interaction.reply({ embeds: [embed], flags: MessageFlags.Ephemeral }); - console.log('[LIER-RYGAINLAND] ✅ Réponse envoyée avec succès'); - console.log('[LIER-RYGAINLAND] Le système WebSocket va surveiller le code:', code); - - // Le système checkAndManageWebSocket va détecter le nouveau code automatiquement - - } catch (error) { - console.error('[LIER-RYGAINLAND] ❌ ERREUR CRITIQUE:', error); - console.error('[LIER-RYGAINLAND] Stack trace:', error.stack); - await interaction.reply({ - content: '❌ Une erreur est survenue lors de la génération du code.', - flags: MessageFlags.Ephemeral - }).catch(err => console.error('[LIER-RYGAINLAND] Impossible de répondre:', err)); - } - }, -}; diff --git a/commands/utility/lier.js b/commands/utility/lier.js deleted file mode 100644 index ac80cc9..0000000 --- a/commands/utility/lier.js +++ /dev/null @@ -1,117 +0,0 @@ -const { SlashCommandBuilder, EmbedBuilder, MessageFlags } = require('discord.js'); -const { verifyLinkCode, updateUserLinkWithUsername } = require('../../src/core/database.js'); -const axios = require('axios'); - -module.exports = { - data: new SlashCommandBuilder() - .setName('lier') - .setDescription('Lier manuellement un compte Discord à un compte Palworld (Admin)') - .addStringOption(option => - option.setName('pseudo-palworld') - .setDescription('Le pseudo du joueur sur Palworld') - .setRequired(true)) - .addUserOption(option => - option.setName('discord-account') - .setDescription('Le compte Discord à lier') - .setRequired(true)), - - async execute(interaction) { - try { - await interaction.deferReply({ flags: MessageFlags.Ephemeral }); - - const palworldName = interaction.options.getString('pseudo-palworld'); - const discordUser = interaction.options.getUser('discord-account'); - - // Récupérer le Steam ID depuis l'API Palworld - const response = await axios({ - method: 'get', - url: 'http://play.louismazin.ovh:8212/v1/api/players', - headers: { - 'Accept': 'application/json', - 'Authorization': `Basic ${process.env.PALWORLD_API_TOKEN}` - } - }); - - const players = response.data.players || {}; - let playerData = null; - - // Chercher le joueur par nom - for (const [id, player] of Object.entries(players)) { - if (player.name === palworldName) { - playerData = { - steamId: player.userId, - playerId: player.playerId, - name: player.name - }; - break; - } - } - - if (!playerData) { - return interaction.editReply({ - content: `❌ Impossible de trouver le joueur **${palworldName}** sur le serveur.\n\n` + - `💡 Le joueur doit être connecté sur le serveur Palworld.`, - flags: MessageFlags.Ephemeral - }); - } - - // Créer un code temporaire pour la liaison manuelle - const { generateLinkCode } = require('../../src/core/database.js'); - const code = await generateLinkCode(discordUser.id); - - // Effectuer la liaison immédiatement - const result = await verifyLinkCode(code, playerData.steamId, playerData.name, playerData.playerId); - - if (result.success) { - await updateUserLinkWithUsername(discordUser.id, discordUser.tag); - - // Ajouter le rôle de joueur lié - try { - const guild = interaction.guild; - const member = await guild.members.fetch(discordUser.id); - const linkedRole = guild.roles.cache.get('1467491093649035475'); - if (linkedRole && !member.roles.cache.has(linkedRole.id)) { - await member.roles.add(linkedRole); - console.log(`✅ Rôle ajouté à ${discordUser.tag}`); - } - } catch (roleError) { - console.error('Erreur lors de l\'ajout du rôle:', roleError); - } - - const embed = new EmbedBuilder() - .setColor(0x00FF00) - .setTitle('✅ Liaison manuelle réussie') - .addFields( - { name: '👤 Discord', value: `${discordUser.tag} (<@${discordUser.id}>)`, inline: false }, - { name: '🎮 Palworld', value: palworldName, inline: true }, - { name: '🆔 Steam ID', value: `\`${playerData.steamId}\``, inline: true }, - { name: '🎯 Player ID', value: `\`${playerData.playerId}\``, inline: false } - ) - .setTimestamp(); - - // Envoyer un MP au joueur lié - await discordUser.send( - `✅ **Liaison effectuée par un administrateur**\n\n` + - `Votre compte Discord a été lié à votre compte Palworld:\n` + - `🎮 Nom Palworld: **${playerData.name}**\n` + - `🆔 Steam ID: \`${playerData.steamId}\`\n` + - `🎯 Player ID: \`${playerData.playerId}\`` - ).catch(() => {}); - - await interaction.editReply({ embeds: [embed], flags: MessageFlags.Ephemeral }); - } else { - await interaction.editReply({ - content: `❌ Erreur lors de la liaison: ${result.message}`, - flags: MessageFlags.Ephemeral - }); - } - - } catch (error) { - console.error('Erreur lors de la liaison manuelle:', error); - await interaction.editReply({ - content: '❌ Une erreur est survenue lors de la liaison.', - flags: MessageFlags.Ephemeral - }); - } - }, -}; diff --git a/commands/utility/panel.js b/commands/utility/panel.js deleted file mode 100644 index a8f9d09..0000000 --- a/commands/utility/panel.js +++ /dev/null @@ -1,309 +0,0 @@ -const { SlashCommandBuilder, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, StringSelectMenuBuilder } = require('discord.js'); -const { getConfig, updateConfig, getAllConfig } = require('../../src/core/database'); -const { getMonitoringStatus } = require('../../src/monitoring/ramMonitor'); - -module.exports = { - data: new SlashCommandBuilder() - .setName('panel') - .setDescription('Panneau de configuration du bot'), - - async execute(interaction) { - // Vérifier les permissions - if (!interaction.member.roles.cache.has('1444684935632912394')) { - await interaction.reply({ - content: '❌ Il faut avoir le rôle Rygainland pour accéder au panneau de configuration.', - flags: 64 - }); - return; - } - - await showPanel(interaction); - } -}; - -function hasPanelRole(member) { - return Boolean(member?.roles?.cache?.has('1444684935632912394')); -} - -async function buildPanelPayload() { - const config = await getAllConfig(); - const monitoringStatus = getMonitoringStatus(); - - const autoRebootEnabled = config.auto_reboot_enabled === 'true'; - const ramThreshold = Number.parseInt(config.ram_threshold_gb, 10); - const consoleMonitorEnabled = config.console_monitor_enabled === 'true'; - - const embed = new EmbedBuilder() - .setColor(0x0099FF) - .setTitle('⚙️ Panneau de Configuration') - .setDescription('Gérez les paramètres du bot et du serveur') - .addFields( - { - name: '🔄 Redémarrage Automatique', - value: `**État:** ${autoRebootEnabled ? '✅ Activé' : '❌ Désactivé'}\n**Seuil RAM:** ${ramThreshold} Go\n**Surveillance:** ${monitoringStatus.isMonitoring ? '🟢 Active' : '🔴 Inactive'}`, - inline: true - }, - { - name: '📊 Surveillance Console', - value: `**État:** ${consoleMonitorEnabled ? '✅ Activée' : '❌ Désactivée'}`, - inline: true - }, - { - name: '📈 Statistiques', - value: `**En redémarrage:** ${monitoringStatus.isRebooting ? 'Oui' : 'Non'}\n**Intervalle:** ${monitoringStatus.checkInterval / 1000}s`, - inline: false - } - ) - .setTimestamp() - .setFooter({ text: 'Panneau de Configuration' }); - - const row1 = new ActionRowBuilder() - .addComponents( - new ButtonBuilder() - .setCustomId('toggle_auto_reboot') - .setLabel(autoRebootEnabled ? 'Désactiver Auto-Reboot' : 'Activer Auto-Reboot') - .setStyle(autoRebootEnabled ? ButtonStyle.Danger : ButtonStyle.Success) - .setEmoji(autoRebootEnabled ? '⏹️' : '▶️'), - new ButtonBuilder() - .setCustomId('toggle_console_monitor') - .setLabel(consoleMonitorEnabled ? 'Désactiver Console' : 'Activer Console') - .setStyle(consoleMonitorEnabled ? ButtonStyle.Danger : ButtonStyle.Success) - .setEmoji(consoleMonitorEnabled ? '📵' : '📱') - ); - - const row2 = new ActionRowBuilder() - .addComponents( - new StringSelectMenuBuilder() - .setCustomId('change_ram_threshold') - .setPlaceholder(`Seuil RAM actuel: ${ramThreshold} Go`) - .addOptions([ - { - label: '15 Go', - description: 'Redémarrage à 15 Go de RAM', - value: '15', - emoji: '🔵' - }, - { - label: '17 Go', - description: 'Redémarrage à 17 Go de RAM', - value: '17', - emoji: '🟢' - }, - { - label: '19 Go (défaut)', - description: 'Redémarrage à 19 Go de RAM', - value: '19', - emoji: '🟡', - default: ramThreshold === 19 - }, - { - label: '21 Go', - description: 'Redémarrage à 21 Go de RAM', - value: '21', - emoji: '🟠' - }, - { - label: '23 Go', - description: 'Redémarrage à 23 Go de RAM', - value: '23', - emoji: '🔴' - }, - { - label: '25 Go', - description: 'Redémarrage à 25 Go de RAM', - value: '25', - emoji: '🔴' - } - ]) - ); - - const row3 = new ActionRowBuilder() - .addComponents( - new ButtonBuilder() - .setCustomId('refresh_panel') - .setLabel('Actualiser') - .setStyle(ButtonStyle.Secondary) - .setEmoji('🔄'), - new ButtonBuilder() - .setCustomId('view_stats') - .setLabel('Voir Stats Détaillées') - .setStyle(ButtonStyle.Primary) - .setEmoji('📊') - ); - - return { embeds: [embed], components: [row1, row2, row3], content: null }; -} - -async function buildStatsPayload() { - const config = await getAllConfig(); - const monitoringStatus = getMonitoringStatus(); - const { checkRAMUsage } = require('../../src/monitoring/ramMonitor'); - - const ramData = await checkRAMUsage(); - - const embed = new EmbedBuilder() - .setColor(0x00FF00) - .setTitle('📊 Statistiques Détaillées du Monitoring') - .setDescription('Informations en temps réel sur le serveur') - .addFields( - { - name: '🎮 État du Serveur', - value: ramData ? `**État:** ${ramData.currentState}\n**RAM:** ${ramData.ramUsedGB} Go / ${config.ram_threshold_gb} Go` : 'Impossible de récupérer les données', - inline: true - }, - { - name: '⚙️ Configuration', - value: `**Auto-Reboot:** ${config.auto_reboot_enabled === 'true' ? 'Oui' : 'Non'}\n**Seuil:** ${config.ram_threshold_gb} Go\n**Console:** ${config.console_monitor_enabled === 'true' ? 'Active' : 'Inactive'}`, - inline: true - }, - { - name: '🔄 Surveillance', - value: `**Active:** ${monitoringStatus.isMonitoring ? 'Oui' : 'Non'}\n**En reboot:** ${monitoringStatus.isRebooting ? 'Oui' : 'Non'}\n**Intervalle:** ${monitoringStatus.checkInterval / 1000}s`, - inline: false - } - ) - .setTimestamp() - .setFooter({ text: 'Statistiques en temps réel' }); - - const row = new ActionRowBuilder() - .addComponents( - new ButtonBuilder() - .setCustomId('back_to_panel') - .setLabel('Retour au Panel') - .setStyle(ButtonStyle.Secondary) - .setEmoji('◀️') - ); - - return { embeds: [embed], components: [row], content: null }; -} - -function isIgnorableDiscordRestError(error) { - const code = error?.code ?? error?.rawError?.code; - return code === 10062 || code === 40060; -} - -async function safeDeferUpdate(interaction) { - if (interaction?.deferred || interaction?.replied) return; - try { - await interaction.deferUpdate(); - } catch (e) { - if (!isIgnorableDiscordRestError(e)) throw e; - } -} - -async function renderPanel(interaction) { - const payload = await buildPanelPayload(); - - // Premier affichage via la commande slash - if (interaction.isChatInputCommand?.()) { - if (interaction.replied || interaction.deferred) { - await interaction.editReply(payload); - return interaction.fetchReply(); - } - return interaction.reply({ ...payload, fetchReply: true }); - } - - // Refresh via composants: on édite directement le message - await safeDeferUpdate(interaction); - return interaction.message.edit(payload); -} - -async function renderDetailedStats(interaction) { - // IMPORTANT: defer immédiatement pour éviter 10062 si les appels réseau sont lents - await safeDeferUpdate(interaction); - - const payload = await buildStatsPayload(); - - if (interaction.isChatInputCommand?.()) { - if (interaction.replied || interaction.deferred) { - await interaction.editReply(payload); - return interaction.fetchReply(); - } - return interaction.reply({ ...payload, fetchReply: true }); - } - - return interaction.message.edit(payload); -} - -// Point d'entrée: affiche le panel et attache un collector AU message -async function showPanel(interaction) { - const message = await renderPanel(interaction); - - const collector = message.createMessageComponentCollector({ - time: 300000 // 5 minutes - }); - - collector.on('collect', async i => { - if (!hasPanelRole(i.member)) { - try { - await i.reply({ - content: '❌ Vous n\'avez pas la permission d\'utiliser ce panneau.', - flags: 64 - }); - } catch { - // ignore - } - return; - } - - try { - switch (i.customId) { - case 'toggle_auto_reboot': { - await safeDeferUpdate(i); - const currentState = (await getConfig('auto_reboot_enabled')) === 'true'; - await updateConfig('auto_reboot_enabled', (!currentState).toString()); - await renderPanel(i); - break; - } - case 'toggle_console_monitor': { - await safeDeferUpdate(i); - const currentState = (await getConfig('console_monitor_enabled')) === 'true'; - await updateConfig('console_monitor_enabled', (!currentState).toString()); - await renderPanel(i); - break; - } - case 'change_ram_threshold': { - await safeDeferUpdate(i); - const newThreshold = i.values?.[0]; - if (newThreshold) { - await updateConfig('ram_threshold_gb', newThreshold); - } - await renderPanel(i); - break; - } - case 'refresh_panel': { - await safeDeferUpdate(i); - await renderPanel(i); - break; - } - case 'view_stats': { - await renderDetailedStats(i); - break; - } - case 'back_to_panel': { - await safeDeferUpdate(i); - await renderPanel(i); - break; - } - default: { - await safeDeferUpdate(i); - break; - } - } - } catch (error) { - console.error('Erreur lors de l\'interaction avec le panel:', error); - if (isIgnorableDiscordRestError(error)) return; - - // Ne pas crash le process si une réponse échoue - try { - await i.followUp?.({ content: '❌ Une erreur est survenue', flags: 64 }); - } catch { - // ignore - } - } - }); - - collector.on('end', () => { - console.log('⏰ [Panel] Collector expiré'); - }); -} diff --git a/src/bridge/palworld-bridge.js b/src/bridge/palworld-bridge.js deleted file mode 100644 index 8d98432..0000000 --- a/src/bridge/palworld-bridge.js +++ /dev/null @@ -1,198 +0,0 @@ -const axios = require('axios'); -const { getUserLink, getAllLinks } = require('../core/database.js'); - -let bridgeClient = null; -let bridgeChannelId = null; - -// Initialiser le bridge -const initPalworldBridge = (client, channelId) => { - bridgeClient = client; - bridgeChannelId = channelId; - - console.log(`🌉 [BRIDGE] Pont Palworld-Discord initialisé (Salon: ${channelId})`); - - // Écouter les messages du salon Discord - client.on('messageCreate', async (message) => { - // Ignorer les messages du bot lui-même - if (message.author.bot) return; - - // Vérifier que c'est bien le salon du pont - if (message.channelId !== bridgeChannelId) return; - - console.log(`📨 [BRIDGE] Message Discord détecté de ${message.author.tag}: ${message.content}`); - - try { - // Récupérer le pseudo Palworld lié - console.log(`🔍 [BRIDGE] Recherche de liaison pour ${message.author.tag} (ID: ${message.author.id})`); - const userLink = await getUserLink(message.author.id); - console.log(`🔍 [BRIDGE] Liaison trouvée pour ${message.author.tag}:`, userLink); - - if (!userLink) { - // L'utilisateur n'est pas lié - console.log(`⚠️ [BRIDGE] ${message.author.tag} n'est pas lié`); - await message.reply('❌ Vous devez lier votre compte Palworld pour envoyer des messages. Utilisez `/lier-rygainland`'); - return; - } - - // Envoyer le message vers Palworld via broadcast - console.log(`🚀 [BRIDGE] Envoi vers Palworld: ${userLink.palworld_username}: ${message.content}`); - await sendToPalworld(userLink.palworld_username, message.content); - console.log(`✅ [BRIDGE] Message envoyé avec succès vers Palworld`); - - } catch (error) { - console.error('Erreur lors de l\'envoi du message vers Palworld:', error); - await message.reply('❌ Erreur lors de l\'envoi du message vers Palworld').catch(() => {}); - } - }); -}; - -// Envoyer un message vers Palworld via RCON broadcast -const sendToPalworld = async (palworldUsername, content) => { - try { - // Nettoyer le message (enlever les mentions, emojis personnalisés, etc.) - let cleanContent = content - .replace(/<@!?\d+>/g, '@utilisateur') // Remplacer les mentions - .replace(/<#\d+>/g, '#salon') // Remplacer les mentions de salons - .replace(/<:.+?:\d+>/g, '') // Enlever les emojis personnalisés - .replace(/\n/g, ' ') // Remplacer les sauts de ligne - .trim(); - - // Limiter la longueur du message - if (cleanContent.length > 200) { - cleanContent = cleanContent.substring(0, 197) + '...'; - } - - // Format du message pour Palworld - const broadcastMessage = `[Discord] ${palworldUsername}: ${cleanContent}`; - - console.log(`🚀 [BRIDGE] Envoi HTTP vers Palworld: ${broadcastMessage}`); - - // Envoyer via l'API Palworld avec timeout - const response = await axios({ - method: 'post', - url: 'http://play.louismazin.ovh:8212/v1/api/announce', - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json', - 'Authorization': `Basic ${process.env.PALWORLD_API_TOKEN}` - }, - data: { - message: broadcastMessage - }, - timeout: 10000 // Timeout de 10s - }); - - console.log(`✅ [BRIDGE] Message envoyé à Palworld (statut ${response.status}): ${broadcastMessage}`); - - } catch (error) { - if (error.code === 'ECONNABORTED') { - console.error('❌ [BRIDGE] Timeout lors de l\'envoi vers Palworld (10s)'); - } else if (error.code === 'ECONNREFUSED') { - console.error('❌ [BRIDGE] Connexion refusée par le serveur Palworld'); - } else if (error.response) { - console.error(`❌ [BRIDGE] Erreur HTTP ${error.response.status}:`, error.response.data); - } else { - console.error('❌ [BRIDGE] Erreur lors de l\'envoi vers Palworld:', error.message); - } - throw error; - } -}; - -// Parser les messages de chat Palworld et les envoyer sur Discord -const parsePalworldChatAndSend = async (log) => { - if (!bridgeClient || !bridgeChannelId) { - console.log('⚠️ [BRIDGE] Bridge non initialisé, impossible de traiter le message'); - return; - } - - // Format: [2026-02-01 13:08:02] [CHAT] coucou - const chatRegex = /\[.*?\]\s*\[CHAT\]\s*<(.+?)>\s*(.+)/i; - const match = log.match(chatRegex); - - if (!match) return; - - console.log(`📥 [BRIDGE] Message Palworld détecté dans les logs`); - - const palworldUsername = match[1].trim(); - const messageContent = match[2].trim(); - - console.log(`📥 [BRIDGE] Palworld: ${palworldUsername}: ${messageContent}`); - - // Ignorer les messages de commande !lier - if (messageContent.toLowerCase().startsWith('!lier')) { - console.log(`⏭️ [BRIDGE] Commande !lier ignorée`); - return; - } - if (messageContent.toLowerCase().startsWith('!')) { - console.log(`⏭️ [BRIDGE] Message ignoré (commence par !)`); - return; - } - try { - // Récupérer le salon - console.log(`🔍 [BRIDGE] Récupération du salon ${bridgeChannelId}...`); - const channel = await bridgeClient.channels.fetch(bridgeChannelId).catch(() => null); - if (!channel) { - console.error('❌ Impossible de trouver le salon du pont'); - return; - } - - // Chercher si le joueur a un compte Discord lié - const allLinks = await getAllLinks(); - const linkedUser = allLinks.find(link => link.palworld_username === palworldUsername); - - if (linkedUser) { - // Utilisateur lié : utiliser webhook pour afficher sa PP et son pseudo Discord - try { - const discordUser = await bridgeClient.users.fetch(linkedUser.discord_id).catch(() => null); - - if (discordUser) { - // Récupérer le membre du serveur pour avoir son displayName - const guild = channel.guild; - const member = await guild.members.fetch(linkedUser.discord_id).catch(() => null); - const displayName = member ? member.displayName : discordUser.username; - - // Créer ou récupérer un webhook pour ce salon - const webhooks = await channel.fetchWebhooks(); - let webhook = webhooks.find(wh => wh.name === 'Palworld Bridge'); - - if (!webhook) { - webhook = await channel.createWebhook({ - name: 'Palworld Bridge', - avatar: 'https://i.imgur.com/AfFp7pu.png' // Logo Palworld - }); - } - - // Envoyer via webhook avec le pseudo Discord du serveur et l'avatar - await webhook.send({ - content: messageContent, - username: displayName, - avatarURL: discordUser.displayAvatarURL({ dynamic: true, size: 256 }) - }); - - console.log(`✅ Message Palworld envoyé sur Discord (via webhook): ${displayName}: ${messageContent}`); - return; - } - } catch (error) { - console.error('Erreur lors de l\'envoi via webhook:', error); - // Fallback vers message normal - } - } - - // Utilisateur non lié ou erreur webhook : envoyer un message normal - await channel.send(`**${palworldUsername}**: ${messageContent}`); - console.log(`✅ Message Palworld envoyé sur Discord: ${palworldUsername}: ${messageContent}`); - - } catch (error) { - console.error('Erreur lors de l\'envoi du message Palworld vers Discord:', error); - } -}; - -// Fonction appelée par consoleMonitor pour traiter les messages de chat -const handlePalworldChat = async (log) => { - await parsePalworldChatAndSend(log); -}; - -module.exports = { - initPalworldBridge, - handlePalworldChat -}; diff --git a/src/core/database.js b/src/core/database.js deleted file mode 100644 index a7501be..0000000 --- a/src/core/database.js +++ /dev/null @@ -1,245 +0,0 @@ -const mysql = require('mysql2/promise'); - -let pool; - -const initDatabase = async () => { - try { - pool = mysql.createPool({ - host: process.env.DB_HOST, - port: process.env.DB_PORT, - user: process.env.DB_USER, - password: process.env.DB_PASSWORD, - database: process.env.DB_NAME, - waitForConnections: true, - connectionLimit: 10, - queueLimit: 0 - }); - - console.log('✅ Base de données connectée'); - - await createTables(); - - console.log('✅ Tables créées/vérifiées'); - } catch (error) { - console.error('❌ Erreur de connexion à la base de données:', error); - throw error; - } -}; - -const getConnection = () => { - if (!pool) { - throw new Error('La base de données n\'est pas initialisée'); - } - return pool; -}; - -const createTables = async () => { - const connection = getConnection(); - - // Table pour les codes de liaison temporaires - await connection.execute(` - CREATE TABLE IF NOT EXISTS link_codes ( - id INT AUTO_INCREMENT PRIMARY KEY, - discord_id VARCHAR(20) NOT NULL, - code VARCHAR(6) NOT NULL UNIQUE, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - expires_at TIMESTAMP NOT NULL, - used BOOLEAN DEFAULT FALSE, - INDEX idx_code (code), - INDEX idx_discord_id (discord_id) - ) - `); - - // Table pour les liaisons confirmées - await connection.execute(` - CREATE TABLE IF NOT EXISTS user_links ( - id INT AUTO_INCREMENT PRIMARY KEY, - discord_id VARCHAR(20) NOT NULL UNIQUE, - discord_username VARCHAR(100) NOT NULL, - steam_id VARCHAR(50) NOT NULL, - player_id VARCHAR(50), - palworld_username VARCHAR(100) NOT NULL, - linked_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - lastConnection TIMESTAMP NULL DEFAULT NULL, - INDEX idx_discord_id (discord_id), - INDEX idx_steam_id (steam_id), - INDEX idx_player_id (player_id) - ) - `); - - // Table pour la configuration du bot - await connection.execute(` - CREATE TABLE IF NOT EXISTS bot_config ( - id INT AUTO_INCREMENT PRIMARY KEY, - config_key VARCHAR(50) NOT NULL UNIQUE, - config_value VARCHAR(255) NOT NULL, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - INDEX idx_config_key (config_key) - ) - `); - - // Initialiser les valeurs par défaut - await connection.execute(` - INSERT IGNORE INTO bot_config (config_key, config_value) VALUES - ('auto_reboot_enabled', 'true'), - ('ram_threshold_gb', '19'), - ('console_monitor_enabled', 'true') - `); -}; - -const generateLinkCode = async (discordId) => { - const connection = getConnection(); - const code = Math.random().toString(36).substring(2, 8).toUpperCase(); - const expiresAt = new Date(Date.now() + 10 * 60 * 1000); // 10 minutes - - await connection.execute( - 'INSERT INTO link_codes (discord_id, code, expires_at) VALUES (?, ?, ?)', - [discordId, code, expiresAt] - ); - - return code; -}; - -const verifyLinkCode = async (code, steamId, palworldUsername, playerId = null) => { - const connection = getConnection(); - - const [rows] = await connection.execute( - 'SELECT * FROM link_codes WHERE code = ? AND used = FALSE AND expires_at > NOW()', - [code] - ); - - if (rows.length === 0) { - return { success: false, message: 'Code invalide ou expiré' }; - } - - const linkData = rows[0]; - - // Créer la liaison avec player_id - await connection.execute( - 'INSERT INTO user_links (discord_id, discord_username, steam_id, player_id, palworld_username) VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE steam_id = ?, player_id = ?, palworld_username = ?', - [linkData.discord_id, 'temp', steamId, playerId, palworldUsername, steamId, playerId, palworldUsername] - ); - - // Marquer le code comme utilisé - await connection.execute( - 'UPDATE link_codes SET used = TRUE WHERE code = ?', - [code] - ); - - return { success: true, discordId: linkData.discord_id }; -}; - -const getAllLinks = async () => { - const connection = getConnection(); - const [rows] = await connection.execute( - 'SELECT discord_id, discord_username, steam_id, player_id, palworld_username, lastConnection, linked_at FROM user_links ORDER BY discord_username' - ); - return rows; -}; - -const getUserLink = async (discordId) => { - const connection = getConnection(); - const [rows] = await connection.execute( - 'SELECT * FROM user_links WHERE discord_id = ?', - [discordId] - ); - return rows.length > 0 ? rows[0] : null; -}; - -const updateUserLinkWithUsername = async (discordId, discordUsername) => { - const connection = getConnection(); - await connection.execute( - 'UPDATE user_links SET discord_username = ? WHERE discord_id = ?', - [discordUsername, discordId] - ); -}; - -const deleteUserLink = async (discordId) => { - const connection = getConnection(); - await connection.execute( - 'DELETE FROM user_links WHERE discord_id = ?', - [discordId] - ); -}; - -const getPendingPlayer = async (steamId) => { - const connection = getConnection(); - const [rows] = await connection.execute( - 'SELECT * FROM pending_players WHERE steam_id = ?', - [steamId] - ); - return rows.length > 0 ? rows[0] : null; -}; - -const hasActiveLinkCodes = async () => { - const connection = getConnection(); - const [rows] = await connection.execute( - 'SELECT COUNT(*) as count FROM link_codes WHERE used = FALSE AND expires_at > NOW()' - ); - return rows[0].count > 0; -}; - -const cleanExpiredCodes = async () => { - const connection = getConnection(); - await connection.execute( - 'DELETE FROM link_codes WHERE expires_at < NOW()' - ); -}; - -const updateLastConnection = async (steamId) => { - const connection = getConnection(); - const [result] = await connection.execute( - 'UPDATE user_links SET lastConnection = NOW() WHERE steam_id = ?', - [steamId] - ); - - return { success: true, changes: result.affectedRows }; -}; - -const getConfig = async (key) => { - const connection = getConnection(); - const [rows] = await connection.execute( - 'SELECT config_value FROM bot_config WHERE config_key = ?', - [key] - ); - return rows.length > 0 ? rows[0].config_value : null; -}; - -const getAllConfig = async () => { - const connection = getConnection(); - const [rows] = await connection.execute( - 'SELECT config_key, config_value FROM bot_config' - ); - const config = {}; - rows.forEach(row => { - config[row.config_key] = row.config_value; - }); - return config; -}; - -const updateConfig = async (key, value) => { - const connection = getConnection(); - await connection.execute( - 'UPDATE bot_config SET config_value = ? WHERE config_key = ?', - [value, key] - ); -}; - -module.exports = { - initDatabase, - getConnection, - createTables, - generateLinkCode, - verifyLinkCode, - getAllLinks, - getUserLink, - updateUserLinkWithUsername, - deleteUserLink, - getPendingPlayer, - hasActiveLinkCodes, - cleanExpiredCodes, - updateLastConnection, - getConfig, - getAllConfig, - updateConfig -}; diff --git a/src/monitoring/consoleMonitor.js b/src/monitoring/consoleMonitor.js deleted file mode 100644 index 43aa9eb..0000000 --- a/src/monitoring/consoleMonitor.js +++ /dev/null @@ -1,659 +0,0 @@ -const axios = require('axios'); -const WebSocket = require('ws'); -const { verifyLinkCode, updateUserLinkWithUsername, updateLastConnection } = require('../core/database.js'); -const { handlePalworldChat } = require('../bridge/palworld-bridge.js'); - -let ws = null; -let reconnectTimeout = null; -let client = null; -let heartbeatInterval = null; -let heartbeatTimeout = null; // Timeout pour détecter si le serveur ne répond plus -let lastHeartbeatResponse = null; // Timestamp de la dernière réponse du serveur -let checkInterval = null; -let refreshCredentialsInterval = null; // Interval pour rafraîchir les credentials périodiquement -let isMonitoring = false; -let isConnecting = false; -let connectionTimestamp = null; -let monitoringStartTimestamp = null; // Timestamp du démarrage du monitoring (ne change pas lors des reconnexions) - -// Backoff de reconnexion -let reconnectDelayMs = 5000; -const RECONNECT_DELAY_MAX_MS = 5 * 60 * 1000; // 5 min -const CREDENTIALS_REFRESH_INTERVAL = 55 * 60 * 1000; // Rafraîchir toutes les 55 minutes (avant l'expiration) - -// Suivi des joueurs connectés (fallback si la console n’affiche pas les déconnexions) -const connectedPlayers = new Map(); // steamId -> { name, playerId, lastSeen } -// Suivi des logs déjà traités pour éviter les doublons -const processedLogs = new Set(); // Garde les 500 derniers logs traités -const MAX_PROCESSED_LOGS = 500; -const parseLogMessage = (log) => { - // Format réel de log Palworld: - // [2025-12-09 13:28:23] [CHAT] !link X2NMAY - const linkRegex = /\[.*?\]\s*\[CHAT\]\s*<(.+?)>\s*!lier\s+([A-Z0-9]{6})/i; - const match = log.match(linkRegex); - - if (match) { - const playerName = match[1].trim(); - const code = match[2].toUpperCase(); - - console.log(`✅ Commande !lier détectée: ${playerName} avec le code ${code}`); - - return { - type: 'link', - playerName: playerName, - code: code, - needsSteamId: true - }; - } - // Détecter les déconnexions: [2025-12-09 18:55:19] [LOG] Nami left the server. (User id: gdk_2535420062888893) - const disconnectRegex = /\[.*?\]\s*\[LOG\]\s*(.+?)\s+left the server\.\s*\(User id:\s*(.+?)\)/i; - const disconnectMatch = log.match(disconnectRegex); - if (disconnectMatch) { - const playerName = disconnectMatch[1].trim(); - const userId = disconnectMatch[2].trim(); - - console.log(`👋 Déconnexion détectée: ${playerName} (${userId})`); - - return { - type: 'disconnect', - playerName: playerName, - userId: userId - }; - } - - return null; -}; - -const getSteamIdFromPlayerName = async (playerName) => { - try { - const response = await axios({ - method: 'get', - maxBodyLength: Infinity, - url: 'http://play.louismazin.ovh:8212/v1/api/players', - headers: { - 'Accept': 'application/json', - 'Authorization': `Basic ${process.env.PALWORLD_API_TOKEN}` - } - }); - - const players = response.data.players || {}; - console.log(`🔍 Recherche du Steam ID pour le joueur: ${playerName}`); - // Chercher le joueur par nom - for (const [steamId, player] of Object.entries(players)) { - if (player.name === playerName) { - return { - steamId: player.userId, - playerId: player.playerId, - name: player.name - }; - } - } - - return null; - } catch (error) { - console.error('Erreur lors de la récupération du Steam ID:', error.message); - return null; - } -}; - -const handleLinkCommand = async (playerName, playerData, code) => { - try { - console.log(`🔗 Tentative de liaison détectée: ${playerName} (${playerData.steamId}) avec le code ${code}`); - - const result = await verifyLinkCode(code, playerData.steamId, playerData.name, playerData.playerId); - - if (result.success) { - console.log(`✅ Liaison réussie pour ${playerName}`); - - if (client) { - const user = await client.users.fetch(result.discordId).catch(() => null); - if (user) { - await updateUserLinkWithUsername(result.discordId, user.tag); - - // Ajouter le rôle de joueur lié - try { - const guild = client.guilds.cache.get(process.env.GUILD_ID); - if (guild) { - const member = await guild.members.fetch(result.discordId); - const linkedRole = guild.roles.cache.get('1467491093649035475'); - if (linkedRole && !member.roles.cache.has(linkedRole.id)) { - await member.roles.add(linkedRole); - console.log(`✅ Rôle ajouté à ${user.tag}`); - } - } - } catch (roleError) { - console.error('Erreur lors de l\'ajout du rôle:', roleError); - } - - await user.send( - `✅ **Liaison réussie !**\n\n` + - `Votre compte Discord a été lié avec succès à votre compte Palworld:\n` + - `🎮 Nom Palworld: **${playerData.name}**\n` + - `🆔 Steam ID: \`${playerData.steamId}\`\n` + - `🎯 Player ID: \`${playerData.playerId}\`` - ).catch(() => {}); - } - } - } else { - console.log(`❌ Échec de la liaison: ${result.message}`); - } - - } catch (error) { - console.error('Erreur lors du traitement de la commande !link:', error); - } -}; - -const getWebSocketCredentials = async (pterodactylToken, serverId) => { - try { - const response = await axios({ - method: 'get', - url: `${process.env.PTERODACTYL_API_URL}/api/client/servers/${serverId}/websocket`, - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${pterodactylToken}` - } - }); - - return response.data.data; - } catch (error) { - console.error('Erreur lors de la récupération des credentials WebSocket:', error.message); - throw error; - } -}; - -// Helper: récupérer la liste actuelle des joueurs via l’API Palworld -const fetchCurrentPlayers = async () => { - try { - const response = await axios({ - method: 'get', - maxBodyLength: Infinity, - url: 'http://play.louismazin.ovh:8212/v1/api/players', - headers: { - 'Accept': 'application/json', - 'Authorization': `Basic ${process.env.PALWORLD_API_TOKEN}` - } - }); - const players = response.data.players || {}; - const list = []; - for (const [, player] of Object.entries(players)) { - list.push({ - steamId: player.userId, - playerId: player.playerId, - name: player.name - }); - } - return list; - } catch (error) { - // API inaccessible quand le serveur est down - return null; - } -}; - -// Mettre à jour le set des joueurs et détecter les départs silencieux -const pollPlayersAndDetectDisconnects = async (serverState = null) => { - const list = await fetchCurrentPlayers(); - - const now = Date.now(); - - if (list && list.length > 0) { - // Marquer présents - for (const p of list) { - connectedPlayers.set(p.steamId, { name: p.name, playerId: p.playerId, lastSeen: now }); - } - // Détecter ceux qui ont disparu depuis le dernier poll - for (const [steamId, info] of connectedPlayers) { - const stillHere = list.find(x => x.steamId === steamId); - if (!stillHere) { - try { - const result = await updateLastConnection(steamId); - if (result.changes > 0) { - console.log(`✅ Départ silencieux détecté: ${info.name} (${steamId}) -> lastConnection mis à jour`); - } - } catch (e) { - console.error(`❌ Erreur update lastConnection pour ${steamId}:`, e.message); - } - connectedPlayers.delete(steamId); - } - } - } else { - // Pas de liste (serveur inaccessible) ou vide: - // Si le serveur est arrêté/stopping/offline, considérer tous comme déconnectés - if (serverState && ['offline', 'stopping', 'stopped'].includes(serverState)) { - for (const [steamId, info] of connectedPlayers) { - try { - const result = await updateLastConnection(steamId); - if (result.changes > 0) { - console.log(`✅ Serveur ${serverState}: déconnexion implicite de ${info.name} (${steamId})`); - } - } catch (e) { - console.error(`❌ Erreur update lastConnection pour ${steamId}:`, e.message); - } - } - connectedPlayers.clear(); - } - } -}; - -const checkAndManageWebSocket = async () => { - const { cleanExpiredCodes } = require('../core/database.js'); - try { - await cleanExpiredCodes(); - - // Fallback polling toutes les 20s - await pollPlayersAndDetectDisconnects(); - - // Le WebSocket doit toujours être connecté maintenant (pour les déconnexions) - if (!ws && !isConnecting) { - console.log('🔌 Connexion au WebSocket pour surveillance...'); - const serverId = process.env.PTERODACTYL_SERVER_ID; - const pterodactylToken = process.env.PTERODACTYL_API_TOKEN; - await connectWebSocket(pterodactylToken, serverId); - } - } catch (error) { - console.error('Erreur lors de la vérification du WebSocket:', error); - } -}; - -const resetReconnectBackoff = () => { - reconnectDelayMs = 5000; -}; - -const scheduleReconnect = () => { - if (!isMonitoring) return; - if (reconnectTimeout) clearTimeout(reconnectTimeout); - const delay = reconnectDelayMs; - reconnectDelayMs = Math.min(reconnectDelayMs * 2, RECONNECT_DELAY_MAX_MS); - console.log(`🔄 [WEBSOCKET] Reconnexion WebSocket programmée dans ${Math.round(delay / 1000)}s... (Monitoring actif: ${isMonitoring})`); - reconnectTimeout = setTimeout(() => { - console.log('🔄 [WEBSOCKET] Tentative de reconnexion maintenant...'); - checkAndManageWebSocket(); - }, delay); -}; - -const connectWebSocket = async (pterodactylToken, serverId) => { - if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) { - console.log('⚠️ [WEBSOCKET] Déjà connecté ou en cours de connexion (état:', ws.readyState, ')'); - return; - } - if (isConnecting) { - console.log('⚠️ [WEBSOCKET] Connexion déjà en cours, abandon de cette tentative'); - return; - } - - isConnecting = true; - connectionTimestamp = Date.now(); - console.log(`📅 [WEBSOCKET] Timestamp de connexion: ${new Date(connectionTimestamp).toISOString()}`); - - try { - // Toujours rafraîchir les credentials pour éviter les tokens expirés après redémarrage serveur - console.log('🔑 [WEBSOCKET] Récupération de nouveaux credentials...'); - const credentials = await getWebSocketCredentials(pterodactylToken, serverId); - console.log('✅ [WEBSOCKET] Credentials récupérés avec succès'); - - ws = new WebSocket(credentials.socket, { - origin: process.env.PTERODACTYL_API_URL - }); - - ws.on('open', () => { - console.log('✅ [WEBSOCKET] WebSocket Pterodactyl connecté avec succès'); - isConnecting = false; - resetReconnectBackoff(); - lastHeartbeatResponse = Date.now(); // Initialiser à maintenant - - console.log('🔐 [WEBSOCKET] Envoi de l\'authentification...'); - ws.send(JSON.stringify({ - event: 'auth', - args: [credentials.token] - })); - }); - - // Pas besoin de handler 'pong' - on vérifie l'accès aux logs directement - - - ws.on('message', async (data) => { - try { - const message = JSON.parse(data.toString()); - - if (message.event === 'auth success') { - console.log('✅ [WEBSOCKET] Authentification WebSocket réussie'); - console.log('📡 [WEBSOCKET] Demande de réception des logs console...'); - - ws.send(JSON.stringify({ - event: 'send logs', - args: [null] - })); - - // Configurer le heartbeat avec détection agressive - if (heartbeatInterval) clearInterval(heartbeatInterval); - if (heartbeatTimeout) clearTimeout(heartbeatTimeout); - - lastHeartbeatResponse = Date.now(); - - heartbeatInterval = setInterval(() => { - if (ws && ws.readyState === WebSocket.OPEN) { - try { - const timeSinceLastLog = Date.now() - lastHeartbeatResponse; - - // Si on n'a pas reçu de log depuis plus de 40s, c'est que l'accès aux logs est perdu - if (timeSinceLastLog > 19000) { - console.error(`💔 [WEBSOCKET] Aucun log reçu depuis ${Math.round(timeSinceLastLog/1000)}s - accès aux logs perdu! Reconnexion forcée...`); - if (ws) { - ws.terminate(); // Fermeture immédiate sans attendre - } - return; - } - - // Redemander l'accès aux logs pour s'assurer qu'on les reçoit toujours - ws.send(JSON.stringify({ - event: 'send logs', - args: [null] - })); - console.log(`📡 [WEBSOCKET] Vérification accès aux logs (dernier reçu: ${Math.round(timeSinceLastLog/1000)}s)`); - - } catch (error) { - console.error('❌ [WEBSOCKET] Erreur lors de la vérification des logs:', error.message); - if (ws) { - ws.terminate(); - } - } - } else { - console.warn('⚠️ [WEBSOCKET] Vérification impossible: WebSocket non ouvert (état:', ws ? ws.readyState : 'null', ')'); - if (heartbeatInterval) { - clearInterval(heartbeatInterval); - heartbeatInterval = null; - } - } - }, 20000); // Vérifier toutes les 20s - console.log('📡 [WEBSOCKET] Vérification d\'accès aux logs configurée (20s, timeout 40s)'); - } - - // Détecter les réponses du serveur (console output, status, etc) pour mettre à jour le lastHeartbeatResponse - if (message.event === 'console output' || message.event === 'status' || message.event === 'stats') { - lastHeartbeatResponse = Date.now(); - } - if (message.event === 'console output') { - const log = message.args[0]; - - // Protection 0: Vérifier si ce log a déjà été traité (évite les doublons lors des 'send logs' répétés) - if (processedLogs.has(log)) { - return; - } - - const isChatMessage = log.includes('[CHAT]'); - - // Extraire le timestamp du log - const timestampMatch = log.match(/\[(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\]/); - - if (timestampMatch) { - const logTimestamp = new Date(timestampMatch[1]).getTime(); - const now = Date.now(); - - // Protection 1: Ignorer les messages très anciens (>15 min) pour éviter l'historique complet - if (now - logTimestamp > 15 * 60 * 1000) { - return; - } - - // Protection 2: Ignorer les messages antérieurs au démarrage du monitoring - // S'applique à TOUS les messages (y compris chat) pour éviter de traiter l'historique au démarrage - if (monitoringStartTimestamp && logTimestamp < monitoringStartTimestamp) { - return; - } - } - - // Marquer ce log comme traité - processedLogs.add(log); - - // Limiter la taille du Set pour éviter une fuite mémoire - if (processedLogs.size > MAX_PROCESSED_LOGS) { - const firstItem = processedLogs.values().next().value; - processedLogs.delete(firstItem); - } - - // Détecter si c'est un message de chat - if (isChatMessage) { - console.log(`💬 [CONSOLE] Message de chat détecté: ${log.substring(0, 100)}`); - - // Transférer le message de chat vers Discord via le bridge - console.log(`📋 [CONSOLE] Traitement du log pour le bridge...`); - try { - await handlePalworldChat(log); - } catch (bridgeError) { - console.error('❌ [CONSOLE] Erreur lors du transfert vers le bridge:', bridgeError); - } - } - - const linkData = parseLogMessage(log); - - if (linkData) { - if (linkData.type === 'link') { - // Traiter de manière asynchrone sans bloquer le handler WebSocket - (async () => { - try { - const playerData = await getSteamIdFromPlayerName(linkData.playerName); - - if (playerData) { - await handleLinkCommand(linkData.playerName, playerData, linkData.code); - } else { - console.log(`❌ Impossible de trouver le Steam ID pour ${linkData.playerName}`); - } - } catch (error) { - console.error(`❌ Erreur lors du traitement de !lier pour ${linkData.playerName}:`, error); - } - })(); - } else if (linkData.type === 'disconnect') { - // Traiter de manière asynchrone sans bloquer le handler WebSocket - (async () => { - try { - const result = await updateLastConnection(linkData.userId); - if (result.changes > 0) { - console.log(`✅ Dernière connexion mise à jour pour ${linkData.playerName} (${linkData.userId})`); - } else { - console.log(`ℹ️ Aucun compte lié trouvé pour ${linkData.playerName} (${linkData.userId})`); - } - } catch (error) { - console.error('Erreur lors de la mise à jour de la dernière connexion:', error); - } - })(); - } - } - } - - if (message.event === 'status') { - const state = message.args[0]; - console.log('📊 Statut du serveur:', state); - - // Si le serveur s’arrête/offline, forcer la mise à jour des joueurs connectés (déconnexions implicites) - if (['offline', 'stopping', 'stopped'].includes(state)) { - await pollPlayersAndDetectDisconnects(state); - } - } - - } catch (error) { - console.error('Erreur parsing message WebSocket:', error); - } - }); - - ws.on('error', (error) => { - console.error('❌ [WEBSOCKET] Erreur WebSocket:', error.message); - console.error('❌ [WEBSOCKET] Code erreur:', error.code || 'N/A'); - isConnecting = false; - }); - - ws.on('close', (code, reason) => { - const reasonStr = reason ? reason.toString() : 'Aucune raison fournie'; - console.log(`⚠️ [WEBSOCKET] WebSocket Pterodactyl déconnecté (Code: ${code}, Raison: ${reasonStr})`); - console.log(`🔍 [WEBSOCKET] État actuel: isMonitoring=${isMonitoring}, isConnecting=${isConnecting}`); - ws = null; - isConnecting = false; - connectionTimestamp = null; - - if (heartbeatInterval) { - clearInterval(heartbeatInterval); - heartbeatInterval = null; - console.log('💔 [WEBSOCKET] Heartbeat arrêté'); - } - - if (heartbeatTimeout) { - clearTimeout(heartbeatTimeout); - heartbeatTimeout = null; - } - - // Planifier une reconnexion avec backoff (utile lors des redémarrages quotidiens du serveur) - if (isMonitoring) { - scheduleReconnect(); - } else { - console.log('⚠️ [WEBSOCKET] Monitoring désactivé, pas de reconnexion'); - } - }); - - } catch (error) { - console.error('❌ [WEBSOCKET] Erreur lors de la connexion WebSocket:', error.message); - console.error('❌ [WEBSOCKET] Stack:', error.stack); - isConnecting = false; - connectionTimestamp = null; - // Échec immédiat -> planifier reconnexion avec backoff - if (isMonitoring) { - scheduleReconnect(); - } else { - console.log('⚠️ [WEBSOCKET] Monitoring désactivé, pas de reconnexion'); - } - } -}; - -const stopWebSocketOnly = () => { - if (heartbeatInterval) { - clearInterval(heartbeatInterval); - heartbeatInterval = null; - } - - if (heartbeatTimeout) { - clearTimeout(heartbeatTimeout); - heartbeatTimeout = null; - } - - if (refreshCredentialsInterval) { - clearInterval(refreshCredentialsInterval); - refreshCredentialsInterval = null; - } - - if (ws) { - ws.close(); - ws = null; - } - - isConnecting = false; - lastHeartbeatResponse = null; -}; - -const forceReconnectToRefreshCredentials = async () => { - if (!isMonitoring) return; - - console.log('🔄 Rafraîchissement des credentials WebSocket (reconnexion préventive)...'); - - // Fermer la connexion actuelle proprement - if (ws) { - ws.close(); - ws = null; - } - - // Attendre un peu avant de reconnecter - setTimeout(async () => { - if (isMonitoring) { - await checkAndManageWebSocket(); - } - }, 2000); -}; - -const startConsoleMonitoring = (discordClient, pterodactylToken) => { - if (isMonitoring) { - console.log('⚠️ Surveillance déjà active'); - return; - } - - client = discordClient; - isMonitoring = true; - monitoringStartTimestamp = Date.now(); - console.log(`📅 Démarrage du monitoring: ${new Date(monitoringStartTimestamp).toISOString()}`); - resetReconnectBackoff(); - - const serverId = process.env.PTERODACTYL_SERVER_ID; - if (!serverId) { - console.error('❌ PTERODACTYL_SERVER_ID non défini dans .env'); - return; - } - - console.log('🔍 Surveillance de la console Pterodactyl démarrée (mode intelligent)'); - - // Vérifier immédiatement - checkAndManageWebSocket(); - - // Vérifier toutes les 20 secondes (nettoyage codes et reconnect si besoin) - if (checkInterval) clearInterval(checkInterval); - checkInterval = setInterval(checkAndManageWebSocket, 20000); // 20s pour détecter vite les départs silencieux - - // Rafraîchir les credentials toutes les 55 minutes pour éviter l'expiration - if (refreshCredentialsInterval) clearInterval(refreshCredentialsInterval); - refreshCredentialsInterval = setInterval(forceReconnectToRefreshCredentials, CREDENTIALS_REFRESH_INTERVAL); - console.log(`⏱️ Rafraîchissement automatique des credentials programmé toutes les ${CREDENTIALS_REFRESH_INTERVAL / 60000} minutes`); -}; - -const stopConsoleMonitoring = () => { - stopWebSocketOnly(); - - if (checkInterval) { - clearInterval(checkInterval); - checkInterval = null; - } - - if (refreshCredentialsInterval) { - clearInterval(refreshCredentialsInterval); - refreshCredentialsInterval = null; - } - - if (reconnectTimeout) { - clearTimeout(reconnectTimeout); - reconnectTimeout = null; - } - - isMonitoring = false; - monitoringStartTimestamp = null; - processedLogs.clear(); // Nettoyer le cache des logs traités - console.log('🔌 Surveillance de la console arrêtée'); -}; - -// Fonction pour forcer la reconnexion du WebSocket (utilisée par ramMonitor) -const forceWebSocketReconnect = async () => { - console.log('🔄 [CONSOLE] Reconnexion forcée du WebSocket...'); - await forceReconnectToRefreshCredentials(); -}; - -// Fonction pour obtenir l'état du WebSocket (pour diagnostic) -const getWebSocketStatus = () => { - const wsStates = { - [WebSocket.CONNECTING]: 'CONNECTING', - [WebSocket.OPEN]: 'OPEN', - [WebSocket.CLOSING]: 'CLOSING', - [WebSocket.CLOSED]: 'CLOSED' - }; - - return { - isMonitoring, - isConnecting, - hasWebSocket: !!ws, - wsState: ws ? wsStates[ws.readyState] : 'N/A', - wsStateRaw: ws ? ws.readyState : null, - connectionTimestamp: connectionTimestamp ? new Date(connectionTimestamp).toISOString() : null, - monitoringStartTimestamp: monitoringStartTimestamp ? new Date(monitoringStartTimestamp).toISOString() : null, - lastHeartbeatResponse: lastHeartbeatResponse ? new Date(lastHeartbeatResponse).toISOString() : null, - timeSinceLastResponse: lastHeartbeatResponse ? Math.round((Date.now() - lastHeartbeatResponse) / 1000) : null, - reconnectDelayMs, - hasHeartbeat: !!heartbeatInterval, - hasHeartbeatTimeout: !!heartbeatTimeout, - hasCheckInterval: !!checkInterval, - hasRefreshInterval: !!refreshCredentialsInterval, - hasPendingReconnect: !!reconnectTimeout - }; -}; - -module.exports = { startConsoleMonitoring, stopConsoleMonitoring, forceWebSocketReconnect, getWebSocketStatus }; diff --git a/src/monitoring/ramMonitor.js b/src/monitoring/ramMonitor.js deleted file mode 100644 index 5afe481..0000000 --- a/src/monitoring/ramMonitor.js +++ /dev/null @@ -1,209 +0,0 @@ -const axios = require('axios'); -const { getConfig } = require('../core/database'); - -let isMonitoring = false; -let checkInterval = null; -let isRebooting = false; -let reconnectCallback = null; // Callback pour resync le WebSocket - -const CHECK_INTERVAL_MS = 60 * 1000; // Vérifier toutes les 60 secondes - -/** - * Vérifie l'utilisation actuelle de la RAM du serveur - */ -const checkRAMUsage = async () => { - try { - const response = await axios.get( - `${process.env.PTERODACTYL_API_URL}/api/client/servers/${process.env.PTERODACTYL_SERVER_ID}/resources`, - { - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${process.env.PTERODACTYL_API_TOKEN}` - } - } - ); - - const resources = response.data.attributes.resources; - const ramUsedMB = resources.memory_bytes / (1024 * 1024); // Convertir en MB - const ramUsedGB = (ramUsedMB / 1024).toFixed(2); // Convertir en GB pour l'affichage - const currentState = response.data.attributes.current_state; - - console.log(`🔍 [RAM Monitor] Utilisation RAM: ${ramUsedGB} Go / État: ${currentState}`); - - // Récupérer la configuration depuis la base de données - const autoRebootEnabled = (await getConfig('auto_reboot_enabled')) === 'true'; - const ramThresholdGB = parseInt(await getConfig('ram_threshold_gb')) || 19; - const RAM_THRESHOLD_MB = ramThresholdGB * 1024; - - // Vérifier si on dépasse le seuil ET que le serveur est en cours d'exécution ET que l'auto-reboot est activé - if (autoRebootEnabled && ramUsedMB > RAM_THRESHOLD_MB && currentState === 'running' && !isRebooting) { - console.log(`⚠️ [RAM Monitor] SEUIL DÉPASSÉ ! ${ramUsedGB} Go > ${ramThresholdGB} Go`); - console.log(`🔄 [RAM Monitor] Déclenchement du redémarrage automatique...`); - - await rebootServer(); - } else if (!autoRebootEnabled && ramUsedMB > RAM_THRESHOLD_MB) { - console.log(`⚠️ [RAM Monitor] Seuil dépassé mais auto-reboot désactivé (${ramUsedGB} Go > ${ramThresholdGB} Go)`); - } - - return { ramUsedMB, ramUsedGB, currentState }; - } catch (error) { - console.error('❌ [RAM Monitor] Erreur lors de la vérification RAM:', error.message); - return null; - } -}; - -/** - * Redémarre le serveur automatiquement - */ -const rebootServer = async () => { - if (isRebooting) { - console.log('⚠️ [RAM Monitor] Redémarrage déjà en cours, annulation...'); - return; - } - - isRebooting = true; - - try { - const headers = { - 'Accept': 'application/json', - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${process.env.PTERODACTYL_API_TOKEN}` - }; - - const serverUrl = `${process.env.PTERODACTYL_API_URL}/api/client/servers/${process.env.PTERODACTYL_SERVER_ID}`; - - console.log('💾 [RAM Monitor] Sauvegarde du serveur...'); - await axios.post(`${serverUrl}/command`, { - command: 'save' - }, { headers }); - - // Annoncer le redémarrage aux joueurs - console.log('📢 [RAM Monitor] Annonce du redémarrage...'); - await axios.post(`${serverUrl}/command`, { - command: "broadcast 'Redémarrage automatique (RAM élevée - 60 secondes)'" - }, { headers }); - - // Attendre la sauvegarde - await new Promise(resolve => setTimeout(resolve, 3000)); - - console.log('⏹️ [RAM Monitor] Arrêt du serveur...'); - await axios.post(`${serverUrl}/power`, { - signal: 'stop' - }, { headers }); - - // Attendre 60 secondes - console.log('⏳ [RAM Monitor] Attente de 60 secondes...'); - await new Promise(resolve => setTimeout(resolve, 60000)); - - console.log('🚀 [RAM Monitor] Redémarrage du serveur...'); - await axios.post(`${serverUrl}/power`, { - signal: 'start' - }, { headers }); - - // Attendre que le serveur soit opérationnel - let isRunning = false; - let attempts = 0; - const maxAttempts = 30; // 30 tentatives * 5 secondes = 2.5 minutes max - - while (!isRunning && attempts < maxAttempts) { - attempts++; - await new Promise(resolve => setTimeout(resolve, 5000)); - - try { - const checkResponse = await axios.get(`${serverUrl}/resources`, { headers }); - const state = checkResponse.data.attributes.current_state; - - if (state === 'running') { - isRunning = true; - console.log('✅ [RAM Monitor] Serveur redémarré avec succès !'); - - // Resynchroniser le WebSocket si un callback est défini - if (reconnectCallback) { - console.log('🔄 [RAM Monitor] Resynchronisation du WebSocket...'); - await new Promise(resolve => setTimeout(resolve, 5000)); // Attendre 5s de plus - reconnectCallback(); - } - } - } catch (error) { - console.error(`❌ [RAM Monitor] Tentative ${attempts}/${maxAttempts} échouée:`, error.message); - } - } - - if (!isRunning) { - console.error('❌ [RAM Monitor] Le serveur n\'a pas redémarré après le délai maximum'); - } - - } catch (error) { - console.error('❌ [RAM Monitor] Erreur lors du redémarrage:', error.message); - } finally { - isRebooting = false; - } -}; - -/** - * Démarre la surveillance de la RAM - */ -const startRAMMonitoring = async (websocketReconnectCallback = null) => { - if (isMonitoring) { - console.log('⚠️ [RAM Monitor] Surveillance déjà active'); - return; - } - - reconnectCallback = websocketReconnectCallback; - - const ramThresholdGB = parseInt(await getConfig('ram_threshold_gb')) || 19; - - console.log(`🚀 [RAM Monitor] Démarrage de la surveillance RAM (seuil: ${ramThresholdGB} Go)`); - console.log(`⏱️ [RAM Monitor] Intervalle de vérification: ${CHECK_INTERVAL_MS / 1000}s`); - - isMonitoring = true; - - // Première vérification immédiate - checkRAMUsage(); - - // Vérifications périodiques - checkInterval = setInterval(async () => { - if (!isRebooting) { - await checkRAMUsage(); - } - }, CHECK_INTERVAL_MS); -}; - -/** - * Arrête la surveillance de la RAM - */ -const stopRAMMonitoring = () => { - if (!isMonitoring) { - console.log('⚠️ [RAM Monitor] Surveillance déjà inactive'); - return; - } - - console.log('⏹️ [RAM Monitor] Arrêt de la surveillance RAM'); - isMonitoring = false; - - if (checkInterval) { - clearInterval(checkInterval); - checkInterval = null; - } -}; - -/** - * Obtenir le statut de la surveillance - */ -const getMonitoringStatus = async () => { - const ramThresholdGB = parseInt(await getConfig('ram_threshold_gb')) || 19; - return { - isMonitoring, - isRebooting, - threshold: ramThresholdGB * 1024, // En MB - checkInterval: CHECK_INTERVAL_MS - }; -}; - -module.exports = { - startRAMMonitoring, - stopRAMMonitoring, - checkRAMUsage, - getMonitoringStatus -}; diff --git a/src/pterodactyl/pterodactylFiles.js b/src/pterodactyl/pterodactylFiles.js deleted file mode 100644 index 3145dcd..0000000 --- a/src/pterodactyl/pterodactylFiles.js +++ /dev/null @@ -1,135 +0,0 @@ -const axios = require('axios'); - -const ACCEPT_HEADER = 'Application/vnd.pterodactyl.v1+json'; - -const defaultBaseUrl = () => process.env.PTERODACTYL_API_URL || 'https://panel.louismazin.ovh'; -const defaultServerId = () => process.env.PTERODACTYL_SERVER_ID || 'ae4a628f'; - -const withAcceptHeader = (headers) => { - // Keep existing headers behavior, but ensure we can talk to Pterodactyl v1. - return { - ...(headers || {}), - Accept: headers?.Accept || ACCEPT_HEADER, - }; -}; - -const encodeQuery = (v) => encodeURIComponent(v); - -async function listDirectory({ headers, baseUrl, serverId, directory }) { - const url = `${baseUrl}/api/client/servers/${serverId}/files/list?directory=${encodeQuery(directory || '/')}`; - const res = await axios.get(url, { headers: withAcceptHeader(headers) }); - const data = res?.data?.data || res?.data?.attributes?.data || res?.data?.data; - - // Standard response: { object:'list', data:[{object:'file_object', attributes:{...}}] } - if (res?.data?.object === 'list' && Array.isArray(res?.data?.data)) { - return res.data.data.map((x) => x.attributes); - } - - // Fallback: try best-effort. - if (Array.isArray(data)) return data; - return []; -} - -async function getDownloadUrl({ headers, baseUrl, serverId, file }) { - const url = `${baseUrl}/api/client/servers/${serverId}/files/download?file=${encodeQuery(file)}`; - - // Most panels return JSON: { object:'signed_url', attributes:{ url } } - const res = await axios.get(url, { headers: withAcceptHeader(headers), validateStatus: () => true }); - - if (res.status >= 400) { - const msg = typeof res.data === 'string' ? res.data : JSON.stringify(res.data); - throw new Error(`Pterodactyl download URL failed (${res.status}): ${msg}`); - } - - if (res.data && typeof res.data === 'object') { - const signed = - res.data?.attributes?.url || - res.data?.data?.attributes?.url || - res.data?.url; - if (signed) return String(signed); - } - - // Some installations may directly return the file, but in that case we don't have a URL. - return null; -} - -async function downloadFile({ headers, baseUrl, serverId, file }) { - const signedUrl = await getDownloadUrl({ headers, baseUrl, serverId, file }); - - if (signedUrl) { - const res = await axios.get(signedUrl, { - responseType: 'arraybuffer', - maxRedirects: 5, - validateStatus: () => true, - }); - if (res.status >= 400) { - throw new Error(`Signed download failed (${res.status}) for ${file}`); - } - return Buffer.from(res.data); - } - - // Fallback attempt: direct download from API. - const direct = `${baseUrl}/api/client/servers/${serverId}/files/download?file=${encodeQuery(file)}`; - const res = await axios.get(direct, { - headers: withAcceptHeader(headers), - responseType: 'arraybuffer', - maxRedirects: 5, - validateStatus: () => true, - }); - if (res.status >= 400) { - throw new Error(`Direct download failed (${res.status}) for ${file}`); - } - return Buffer.from(res.data); -} - -async function getUploadUrl({ headers, baseUrl, serverId, directory }) { - const url = `${baseUrl}/api/client/servers/${serverId}/files/upload?directory=${encodeQuery(directory || '/')}`; - const res = await axios.get(url, { headers: withAcceptHeader(headers) }); - const signed = res?.data?.attributes?.url || res?.data?.data?.attributes?.url; - if (!signed) { - throw new Error('Could not get signed upload URL from Pterodactyl'); - } - return String(signed); -} - -async function uploadSingleFile({ headers, baseUrl, serverId, directory, filename, content }) { - const signed = await getUploadUrl({ headers, baseUrl, serverId, directory }); - - // Use fetch + FormData (Node 18+) to match Pterodactyl upload behavior. - const form = new FormData(); - const blob = new Blob([content]); - form.append('files', blob, filename); - - const uploadUrl = `${signed}${signed.includes('?') ? '&' : '?'}directory=${encodeQuery(directory || '/')}`; - - const res = await fetch(uploadUrl, { - method: 'POST', - body: form, - }); - - if (!res.ok) { - const text = await res.text().catch(() => ''); - throw new Error(`Upload failed (${res.status}): ${text}`); - } -} - -async function deleteFiles({ headers, baseUrl, serverId, root, files }) { - const url = `${baseUrl}/api/client/servers/${serverId}/files/delete`; - await axios.post( - url, - { - root, - files, - }, - { headers: withAcceptHeader(headers) } - ); -} - -module.exports = { - defaultBaseUrl, - defaultServerId, - listDirectory, - downloadFile, - uploadSingleFile, - deleteFiles, -};