From 115401be4129a313bd48be52dcf64faefa2b78df Mon Sep 17 00:00:00 2001 From: Louis Mazin Date: Wed, 15 Apr 2026 23:36:05 +0200 Subject: [PATCH] edit --- .env.example | 22 + .gitattributes | 2 + .gitignore | 3 + commands/server/ram-status.js | 60 + commands/server/reboot-server.js | 91 ++ commands/server/server-stats.js | 120 ++ commands/server/start-server.js | 65 + commands/trad/trad.js | 98 ++ commands/transfer-save/serverMessage.json | 41 + commands/transfer-save/soloMessage.json | 41 + commands/transfer-save/transfer-save.js | 54 + commands/utility/afficher-inactifs.js | 123 ++ commands/utility/afficher-lies.js | 67 + commands/utility/delier-rygainland.js | 134 ++ commands/utility/delier.js | 145 ++ commands/utility/diagnostique-ws.js | 97 ++ commands/utility/info.js | 71 + commands/utility/lier-rygainland.js | 58 + commands/utility/lier.js | 117 ++ commands/utility/panel.js | 309 ++++ index.js | 140 ++ package-lock.json | 1629 +++++++++++++++++++++ package.json | 23 + src/bridge/palworld-bridge.js | 198 +++ src/core/database.js | 245 ++++ src/discord/deploy_command.js | 41 + src/monitoring/consoleMonitor.js | 659 +++++++++ src/monitoring/ramMonitor.js | 209 +++ src/pterodactyl/cleaner.js | 64 + src/pterodactyl/displayer.js | 49 + src/pterodactyl/pterodactylFiles.js | 135 ++ 31 files changed, 5110 insertions(+) create mode 100644 .env.example create mode 100644 .gitattributes create mode 100644 commands/server/ram-status.js create mode 100644 commands/server/reboot-server.js create mode 100644 commands/server/server-stats.js create mode 100644 commands/server/start-server.js create mode 100644 commands/trad/trad.js create mode 100644 commands/transfer-save/serverMessage.json create mode 100644 commands/transfer-save/soloMessage.json create mode 100644 commands/transfer-save/transfer-save.js create mode 100644 commands/utility/afficher-inactifs.js create mode 100644 commands/utility/afficher-lies.js create mode 100644 commands/utility/delier-rygainland.js create mode 100644 commands/utility/delier.js create mode 100644 commands/utility/diagnostique-ws.js create mode 100644 commands/utility/info.js create mode 100644 commands/utility/lier-rygainland.js create mode 100644 commands/utility/lier.js create mode 100644 commands/utility/panel.js create mode 100644 index.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/bridge/palworld-bridge.js create mode 100644 src/core/database.js create mode 100644 src/discord/deploy_command.js create mode 100644 src/monitoring/consoleMonitor.js create mode 100644 src/monitoring/ramMonitor.js create mode 100644 src/pterodactyl/cleaner.js create mode 100644 src/pterodactyl/displayer.js create mode 100644 src/pterodactyl/pterodactylFiles.js diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..9782d5d --- /dev/null +++ b/.env.example @@ -0,0 +1,22 @@ +# Configuration Discord +DISCORD_TOKEN=votre_token_discord +GUILD_ID=votre_guild_id +BRIDGE_CHANNEL_ID=1234567891234567891 + +# Configuration Pterodactyl +PTERODACTYL_API_URL=https://votre-panel.com +PTERODACTYL_API_TOKEN=votre_token_pterodactyl +PTERODACTYL_SERVER_ID=votre_server_id + +# Configuration Palworld +PALWORLD_API_TOKEN=votre_token_palworld_api + +# Configuration Base de données +DB_HOST=localhost +DB_PORT=3306 +DB_USER=votre_utilisateur +DB_PASSWORD=votre_mot_de_passe +DB_NAME=nom_de_la_base + +# Configuration DeepL (optionnel) +DEEPL_TOKEN=votre_token_deepl diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/.gitignore b/.gitignore index 8c2b884..a2043c4 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,6 @@ # Built Visual Studio Code Extensions *.vsix +node_modules/* + +.env \ No newline at end of file diff --git a/commands/server/ram-status.js b/commands/server/ram-status.js new file mode 100644 index 0000000..d3bb63a --- /dev/null +++ b/commands/server/ram-status.js @@ -0,0 +1,60 @@ +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/reboot-server.js b/commands/server/reboot-server.js new file mode 100644 index 0000000..eb857bf --- /dev/null +++ b/commands/server/reboot-server.js @@ -0,0 +1,91 @@ +const { SlashCommandBuilder } = require('discord.js'); +const axios = require('axios'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('reboot-server') + .setDescription('Redémarre le serveur Palworld'), + async execute(interaction, headers) { + + 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 { + // Vérifier l'état actuel du serveur + const statusResponse = await axios.get('https://panel.louismazin.ovh/api/client/servers/ae4a628f/resources', { + headers: headers + }); + + const currentState = statusResponse.data.attributes.current_state; + + if (currentState !== 'running') { + await interaction.editReply('⚠️ Le serveur n\'est pas en cours d\'exécution. Utilisez `/start-server` pour le démarrer.'); + return; + } + + // Étape 1: Sauvegarder le serveur + await interaction.editReply('💾 Sauvegarde du serveur en cours...'); + await axios.post('https://panel.louismazin.ovh/api/client/servers/ae4a628f/command', { + command: 'save' + }, { + headers: headers + }); + + await axios.post('https://panel.louismazin.ovh/api/client/servers/ae4a628f/command', { + command: "broadcast 'Redemarrage du serveur (Ca prends 20 secondes)'" + }, { + headers: headers + }); + + // Attendre un peu pour la sauvegarde + await new Promise(resolve => setTimeout(resolve, 2000)); + + // Étape 2: Arrêter le serveur + await interaction.editReply('⏹️ Arrêt du serveur...'); + await axios.post('https://panel.louismazin.ovh/api/client/servers/ae4a628f/power', { + signal: 'stop' + }, { + headers: headers + }); + + // Étape 3: Attendre 25 secondes + await interaction.editReply('⏳ Attente de 25 secondes...'); + await new Promise(resolve => setTimeout(resolve, 25000)); + + // Étape 4: Redémarrer le serveur + await interaction.editReply('🚀 Redémarrage du serveur...'); + await axios.post('https://panel.louismazin.ovh/api/client/servers/ae4a628f/power', { + signal: 'start' + }, { + headers: headers + }); + + var run = false; + while (!run) { + await new Promise(resolve => setTimeout(resolve, 5000)); + try { + const checkResponse = await axios.get('https://panel.louismazin.ovh/api/client/servers/ae4a628f/resources', { + headers: headers + }); + + const newState = checkResponse.data.attributes.current_state; + + if (newState === 'running') { + run = true; + await interaction.editReply('✅ Le serveur Palworld a été redémarré avec succès !'); + } + } catch (error) { + console.error('Erreur lors de la vérification de l\'état du serveur:', error); + } + } + + } catch (error) { + console.error('Erreur lors du redémarrage du serveur:', error); + await interaction.editReply('❌ Erreur lors du redémarrage du serveur. Veuillez réessayer plus tard.'); + } + }, +}; \ No newline at end of file diff --git a/commands/server/server-stats.js b/commands/server/server-stats.js new file mode 100644 index 0000000..a9f32bd --- /dev/null +++ b/commands/server/server-stats.js @@ -0,0 +1,120 @@ +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/server/start-server.js b/commands/server/start-server.js new file mode 100644 index 0000000..3ddb213 --- /dev/null +++ b/commands/server/start-server.js @@ -0,0 +1,65 @@ +const { SlashCommandBuilder } = require('discord.js'); +const axios = require('axios'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('start-server') + .setDescription('Démarre le serveur Palworld s\'il n\'est pas déjà en cours d\'exécution'), + async execute(interaction, headers) { + + 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 { + // Vérifier l'état actuel du serveur + const statusResponse = await axios.get('https://panel.louismazin.ovh/api/client/servers/ae4a628f/resources', { + headers: headers + }); + + const currentState = statusResponse.data.attributes.current_state; + + if (currentState === 'running') { + await interaction.editReply('✅ Le serveur Palworld est déjà en cours d\'exécution !'); + return; + } + + // Démarrer le serveur + await axios.post('https://panel.louismazin.ovh/api/client/servers/ae4a628f/power', { + signal: 'start' + }, { + headers: headers + }); + + await interaction.editReply('🚀 Commande de démarrage envoyée au serveur Palworld ! Le serveur va démarrer dans quelques instants...'); + + // Vérifier l'état du serveur toutes les 5 secondes jusqu'à ce qu'il soit en cours d'exécution + + var run = false; + while (!run) { + await new Promise(resolve => setTimeout(resolve, 5000)); + try { + const checkResponse = await axios.get('https://panel.louismazin.ovh/api/client/servers/ae4a628f/resources', { + headers: headers + }); + + const newState = checkResponse.data.attributes.current_state; + + if (newState === 'running') { + run = true; + await interaction.editReply('✅ Le serveur Palworld a été démarré avec succès !'); + } + } catch (error) { + console.error('Erreur lors de la vérification de l\'état du serveur:', error); + } + } + + } catch (error) { + console.error('Erreur lors du démarrage du serveur:', error); + await interaction.editReply('❌ Erreur lors du démarrage du serveur. Veuillez réessayer plus tard.'); + } + }, +}; diff --git a/commands/trad/trad.js b/commands/trad/trad.js new file mode 100644 index 0000000..02bc3fa --- /dev/null +++ b/commands/trad/trad.js @@ -0,0 +1,98 @@ +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 new file mode 100644 index 0000000..71be93c --- /dev/null +++ b/commands/transfer-save/serverMessage.json @@ -0,0 +1,41 @@ +{ + "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 new file mode 100644 index 0000000..d2c85dc --- /dev/null +++ b/commands/transfer-save/soloMessage.json @@ -0,0 +1,41 @@ +{ + "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 new file mode 100644 index 0000000..e1f1cbe --- /dev/null +++ b/commands/transfer-save/transfer-save.js @@ -0,0 +1,54 @@ +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 new file mode 100644 index 0000000..5fc8344 --- /dev/null +++ b/commands/utility/afficher-inactifs.js @@ -0,0 +1,123 @@ +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 new file mode 100644 index 0000000..5ea87c9 --- /dev/null +++ b/commands/utility/afficher-lies.js @@ -0,0 +1,67 @@ +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 new file mode 100644 index 0000000..684d51e --- /dev/null +++ b/commands/utility/delier-rygainland.js @@ -0,0 +1,134 @@ +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 new file mode 100644 index 0000000..92655f5 --- /dev/null +++ b/commands/utility/delier.js @@ -0,0 +1,145 @@ +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 new file mode 100644 index 0000000..934aad0 --- /dev/null +++ b/commands/utility/diagnostique-ws.js @@ -0,0 +1,97 @@ +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 new file mode 100644 index 0000000..29bdfd6 --- /dev/null +++ b/commands/utility/info.js @@ -0,0 +1,71 @@ +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 new file mode 100644 index 0000000..d2de40c --- /dev/null +++ b/commands/utility/lier-rygainland.js @@ -0,0 +1,58 @@ +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 new file mode 100644 index 0000000..ac80cc9 --- /dev/null +++ b/commands/utility/lier.js @@ -0,0 +1,117 @@ +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 new file mode 100644 index 0000000..a8f9d09 --- /dev/null +++ b/commands/utility/panel.js @@ -0,0 +1,309 @@ +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/index.js b/index.js new file mode 100644 index 0000000..98d0da8 --- /dev/null +++ b/index.js @@ -0,0 +1,140 @@ +require('dotenv').config(); +const fs = require('node:fs'); +const deepl = require('deepl-node'); +const path = require('node:path'); +const deploy = require('./src/discord/deploy_command.js') +const update = require('./src/pterodactyl/displayer.js'); +const clean = require('./src/pterodactyl/cleaner.js'); +const { Client, GatewayIntentBits, Collection, Events, Partials } = require('discord.js'); +const { initDatabase, createTables, cleanExpiredCodes } = require('./src/core/database.js'); +const { startConsoleMonitoring, forceWebSocketReconnect } = require('./src/monitoring/consoleMonitor.js'); +const { initPalworldBridge } = require('./src/bridge/palworld-bridge.js'); +const { startRAMMonitoring } = require('./src/monitoring/ramMonitor.js'); + +const client = new Client({ intents: + [ + GatewayIntentBits.GuildMembers, + GatewayIntentBits.GuildMessages, + GatewayIntentBits.GuildMessageReactions, + GatewayIntentBits.MessageContent, + GatewayIntentBits.Guilds + ], + partials: [Partials.Message, Partials.Channel, Partials.Reaction, Partials.User], +}); + +const headers = { + "Accept": "application/json", + "Content-Type": "application/json", + "Authorization": "Bearer " + process.env.PTERODACTYL_API_TOKEN +}; +const numbers=["𝟎","𝟏","𝟐","𝟑","𝟒","𝟓","𝟔","𝟕","𝟖","𝟗","𝟏𝟎","𝟏𝟏","𝟏𝟐","𝟏𝟑","𝟏𝟖","𝟏𝟕","𝟏𝟖","𝟏𝟗","𝟐𝟎","𝟐𝟏","𝟐𝟐","𝟐𝟑","𝟐𝟒","𝟐𝟓","𝟐𝟔","𝟐𝟕","𝟐𝟖","𝟐𝟗","𝟑𝟎","𝟑𝟏","𝟑𝟐"]; + +const translator = process.env.DEEPL_TOKEN ? new deepl.Translator(process.env.DEEPL_TOKEN) : null; + +client.commands = new Collection(); +const foldersPath = path.join(__dirname, 'commands'); +const commandFolders = fs.readdirSync(foldersPath); + +for (const folder of commandFolders) { + const commandsPath = path.join(foldersPath, folder); + const commandFiles = fs.readdirSync(commandsPath).filter(file => file.endsWith('.js')); + for (const file of commandFiles) { + const filePath = path.join(commandsPath, file); + const command = require(filePath); + // Set a new item in the Collection with the key as the command name and the value as the exported module + if ('data' in command && 'execute' in command) { + client.commands.set(command.data.name, command); + } else { + console.log(`[WARNING] The command at ${filePath} is missing a required "data" or "execute" property.`); + } + } +} + +client.once('ready', async () => { + client.user.setPresence({ activities: [{ name: 'Rygain', type: 'WATCHING' }], status: 'online' }); + console.log('Bot started !'); + + try { + await initDatabase(); + await createTables(); + + // Nettoyer les codes expirés au démarrage + await cleanExpiredCodes(); + console.log('🧹 Codes expirés nettoyés'); + + console.log('📦 Système de liaison activé'); + + // Initialiser le pont Palworld-Discord + const bridgeChannelId = process.env.BRIDGE_CHANNEL_ID || '1467491354924814411'; + console.log(`🔧 Configuration du pont: Salon ${bridgeChannelId}`); + + // Vérifier que le salon existe + try { + const bridgeChannel = await client.channels.fetch(bridgeChannelId); + if (bridgeChannel) { + console.log(`✅ Salon du pont trouvé: #${bridgeChannel.name}`); + initPalworldBridge(client, bridgeChannelId); + } else { + console.error(`❌ Salon du pont introuvable (ID: ${bridgeChannelId})`); + } + } catch (channelError) { + console.error(`❌ Erreur lors de la récupération du salon du pont:`, channelError.message); + console.error(`⚠️ Le pont Discord-Palworld est désactivé`); + } + + startConsoleMonitoring(client, process.env.PTERODACTYL_API_TOKEN); + + // Démarrer la surveillance RAM avec callback de reconnexion WebSocket + startRAMMonitoring(forceWebSocketReconnect); + console.log('✅ Surveillance RAM activée (seuil: 19 Go)'); + } catch (error) { + console.error('⚠️ Erreur lors de l\'initialisation de la base de données'); + console.error('⚠️ Le système de liaison est désactivé'); + console.error('⚠️ Les autres fonctionnalités du bot restent actives'); + } + + deploy(process.env.DISCORD_TOKEN); + clean(client); +}); + +client.on(Events.InteractionCreate, async interaction => { + if (!interaction.isChatInputCommand()) return; + + // Vérifier que la commande provient du bon serveur + if (interaction.guildId !== process.env.GUILD_ID) { + return interaction.reply({ + content: 'Ce bot ne peut être utilisé que sur un serveur spécifique.', + ephemeral: true + }); + } + + const command = interaction.client.commands.get(interaction.commandName); + + if (!command) { + console.error(`No command matching ${interaction.commandName} was found.`); + return; + } + + try { + if (interaction.commandName === 'server-stats') { + await command.execute(interaction, process.env.PALWORLD_API_TOKEN); + } else if (interaction.commandName === 'trad') { + await command.execute(interaction, translator); + } else if (interaction.commandName === 'start-server' || interaction.commandName === 'reboot-server') { + await command.execute(interaction, headers); + } else { + await command.execute(interaction); + } + } catch (error) { + console.error(error); + if (interaction.replied || interaction.deferred) { + await interaction.followUp({ content: 'There was an error while executing this command!', ephemeral: true }); + } else { + await interaction.reply({ content: 'There was an error while executing this command!', ephemeral: true }); + } + } +}); + +client.login(process.env.DISCORD_TOKEN); + +setInterval(()=>{update(headers,numbers,client,process.env.PALWORLD_API_TOKEN)}, 300000); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..9329991 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1629 @@ +{ + "name": "bot-discord", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "bot-discord", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "axios": "^1.7.7", + "deepl-node": "^1.19.0", + "discord.js": "^14.16.3", + "dotenv": "^17.2.3", + "express": "^5.2.1", + "mysql2": "^3.15.3", + "node": "^18.20.5", + "node-fetch": "^3.3.2", + "pterosocket": "^1.0.6" + } + }, + "node_modules/@discordjs/builders": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.13.1.tgz", + "integrity": "sha512-cOU0UDHc3lp/5nKByDxkmRiNZBpdp0kx55aarbiAfakfKJHlxv/yFW1zmIqCAmwH5CRlrH9iMFKJMpvW4DPB+w==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/formatters": "^0.6.2", + "@discordjs/util": "^1.2.0", + "@sapphire/shapeshift": "^4.0.0", + "discord-api-types": "^0.38.33", + "fast-deep-equal": "^3.1.3", + "ts-mixer": "^6.0.4", + "tslib": "^2.6.3" + }, + "engines": { + "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/collection": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-1.5.3.tgz", + "integrity": "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=16.11.0" + } + }, + "node_modules/@discordjs/formatters": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.6.2.tgz", + "integrity": "sha512-y4UPwWhH6vChKRkGdMB4odasUbHOUwy7KL+OVwF86PvT6QVOwElx+TiI1/6kcmcEe+g5YRXJFiXSXUdabqZOvQ==", + "license": "Apache-2.0", + "dependencies": { + "discord-api-types": "^0.38.33" + }, + "engines": { + "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/rest": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.6.0.tgz", + "integrity": "sha512-RDYrhmpB7mTvmCKcpj+pc5k7POKszS4E2O9TYc+U+Y4iaCP+r910QdO43qmpOja8LRr1RJ0b3U+CqVsnPqzf4w==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/collection": "^2.1.1", + "@discordjs/util": "^1.1.1", + "@sapphire/async-queue": "^1.5.3", + "@sapphire/snowflake": "^3.5.3", + "@vladfrangu/async_event_emitter": "^2.4.6", + "discord-api-types": "^0.38.16", + "magic-bytes.js": "^1.10.0", + "tslib": "^2.6.3", + "undici": "6.21.3" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/rest/node_modules/@discordjs/collection": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz", + "integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/util": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.2.0.tgz", + "integrity": "sha512-3LKP7F2+atl9vJFhaBjn4nOaSWahZ/yWjOvA4e5pnXkt2qyXRCHLxoBQy81GFtLGCq7K9lPm9R517M1U+/90Qg==", + "license": "Apache-2.0", + "dependencies": { + "discord-api-types": "^0.38.33" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/ws": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-1.2.3.tgz", + "integrity": "sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/collection": "^2.1.0", + "@discordjs/rest": "^2.5.1", + "@discordjs/util": "^1.1.0", + "@sapphire/async-queue": "^1.5.2", + "@types/ws": "^8.5.10", + "@vladfrangu/async_event_emitter": "^2.2.4", + "discord-api-types": "^0.38.1", + "tslib": "^2.6.2", + "ws": "^8.17.0" + }, + "engines": { + "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/ws/node_modules/@discordjs/collection": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz", + "integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@sapphire/async-queue": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.5.tgz", + "integrity": "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@sapphire/shapeshift": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-4.0.0.tgz", + "integrity": "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "lodash": "^4.17.21" + }, + "engines": { + "node": ">=v16" + } + }, + "node_modules/@sapphire/snowflake": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.3.tgz", + "integrity": "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ==", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@types/node": { + "version": "24.0.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.3.tgz", + "integrity": "sha512-R4I/kzCYAdRLzfiCabn9hxWfbuHS573x+r0dJMkkzThEa7pbrcDWK+9zu3e7aBOouf+rQAciqPFMnxwr0aWgKg==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.8.0" + } + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@vladfrangu/async_event_emitter": { + "version": "2.4.7", + "resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.4.7.tgz", + "integrity": "sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g==", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/adm-zip": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz", + "integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==", + "license": "MIT", + "engines": { + "node": ">=12.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/body-parser": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz", + "integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deepl-node": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/deepl-node/-/deepl-node-1.19.0.tgz", + "integrity": "sha512-icg7dw/IXxsEZ4rf441XBWUpMKYyqa3JfLFbiFEj07f+2SNRseHwoDCJeA6CxYgQdIuX1jy2ShPvY2DAlzXSQw==", + "license": "MIT", + "dependencies": { + "@types/node": ">=12.0", + "adm-zip": "^0.5.16", + "axios": "^1.7.4", + "form-data": "^3.0.0", + "loglevel": ">=1.6.2", + "uuid": "^8.3.2" + }, + "engines": { + "node": ">=12.0" + } + }, + "node_modules/deepl-node/node_modules/form-data": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.4.tgz", + "integrity": "sha512-f0cRzm6dkyVYV3nPoooP8XlccPQukegwhAnpoLcXy+X+A8KfpGOoXwDr9FLZd3wzgLaBGQBE3lY93Zm/i1JvIQ==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/discord-api-types": { + "version": "0.38.37", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.37.tgz", + "integrity": "sha512-Cv47jzY1jkGkh5sv0bfHYqGgKOWO1peOrGMkDFM4UmaGMOTgOW8QSexhvixa9sVOiz8MnVOBryWYyw/CEVhj7w==", + "license": "MIT", + "workspaces": [ + "scripts/actions/documentation" + ] + }, + "node_modules/discord.js": { + "version": "14.25.1", + "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.25.1.tgz", + "integrity": "sha512-2l0gsPOLPs5t6GFZfQZKnL1OJNYFcuC/ETWsW4VtKVD/tg4ICa9x+jb9bkPffkMdRpRpuUaO/fKkHCBeiCKh8g==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/builders": "^1.13.0", + "@discordjs/collection": "1.5.3", + "@discordjs/formatters": "^0.6.2", + "@discordjs/rest": "^2.6.0", + "@discordjs/util": "^1.2.0", + "@discordjs/ws": "^1.2.3", + "@sapphire/snowflake": "3.5.3", + "discord-api-types": "^0.38.33", + "fast-deep-equal": "3.1.3", + "lodash.snakecase": "4.1.1", + "magic-bytes.js": "^1.10.0", + "tslib": "^2.6.3", + "undici": "6.21.3" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/dotenv": { + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", + "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "license": "MIT", + "dependencies": { + "is-property": "^1.0.2" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "license": "MIT" + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash.snakecase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", + "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==", + "license": "MIT" + }, + "node_modules/loglevel": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz", + "integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + }, + "funding": { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/loglevel" + } + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/lru.min": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.3.tgz", + "integrity": "sha512-Lkk/vx6ak3rYkRR0Nhu4lFUT2VDnQSxBe8Hbl7f36358p6ow8Bnvr8lrLt98H8J1aGxfhbX4Fs5tYg2+FTwr5Q==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, + "node_modules/magic-bytes.js": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.12.1.tgz", + "integrity": "sha512-ThQLOhN86ZkJ7qemtVRGYM+gRgR8GEXNli9H/PMvpnZsE44Xfh3wx9kGJaldg314v85m+bFW6WBMaVHJc/c3zA==", + "license": "MIT" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mysql2": { + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.15.3.tgz", + "integrity": "sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg==", + "license": "MIT", + "dependencies": { + "aws-ssl-profiles": "^1.1.1", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.7.0", + "long": "^5.2.1", + "lru.min": "^1.0.0", + "named-placeholders": "^1.1.3", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.2" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.4.tgz", + "integrity": "sha512-/qfG0Kk/bLJIvej4FcPQ2KYUJP8iQdU1CTxysNb/U2wUNb+/4K485yeio8iNoiwfqJnsTInXoRPTza0dZWHVJQ==", + "license": "MIT", + "dependencies": { + "lru.min": "^1.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node": { + "version": "18.20.5", + "resolved": "https://registry.npmjs.org/node/-/node-18.20.5.tgz", + "integrity": "sha512-v4kVqf+wQAEGAODwH6GZLH8ur8gCcGESjO1lpfgIEMT4DzU4XgYhnq4yJ5sNwO7MTsxTG9m66vMjVzRf/4Okqg==", + "hasInstallScript": true, + "license": "ISC", + "dependencies": { + "node-bin-setup": "^1.0.0" + }, + "bin": { + "node": "bin/node" + }, + "engines": { + "npm": ">=5.0.0" + } + }, + "node_modules/node-bin-setup": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/node-bin-setup/-/node-bin-setup-1.1.3.tgz", + "integrity": "sha512-opgw9iSCAzT2+6wJOETCpeRYAQxSopqQ2z+N6BXwIMsQQ7Zj5M8MaafQY8JMlolRR6R1UXg2WmhKp0p9lSOivg==", + "license": "ISC" + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/pterosocket": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/pterosocket/-/pterosocket-1.0.6.tgz", + "integrity": "sha512-5zkWwb4/19PM3iQGSP7QgYjr3+OSORPbdE5pWMbl/kedSzAKwGmS404+/3et6KvoAohXfoZeQSF+rJ+4QVBu3Q==", + "license": "MIT", + "dependencies": { + "node-fetch": "^3.2.4" + } + }, + "node_modules/qs": { + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/send/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/send/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" + }, + "node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/ts-mixer": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.4.tgz", + "integrity": "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==", + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/type-is/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/type-is/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/undici": { + "version": "6.21.3", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz", + "integrity": "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==", + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/undici-types": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", + "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..81f2e5b --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "bot-discord", + "version": "1.0.0", + "main": "index.js", + "scripts": { + "deploy": "node src/discord/deploy_command.js" + }, + "keywords": [], + "author": "", + "license": "ISC", + "description": "", + "dependencies": { + "axios": "^1.7.7", + "deepl-node": "^1.19.0", + "discord.js": "^14.16.3", + "dotenv": "^17.2.3", + "express": "^5.2.1", + "mysql2": "^3.15.3", + "node": "^18.20.5", + "node-fetch": "^3.3.2", + "pterosocket": "^1.0.6" + } +} diff --git a/src/bridge/palworld-bridge.js b/src/bridge/palworld-bridge.js new file mode 100644 index 0000000..8d98432 --- /dev/null +++ b/src/bridge/palworld-bridge.js @@ -0,0 +1,198 @@ +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 new file mode 100644 index 0000000..a7501be --- /dev/null +++ b/src/core/database.js @@ -0,0 +1,245 @@ +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/discord/deploy_command.js b/src/discord/deploy_command.js new file mode 100644 index 0000000..3595a60 --- /dev/null +++ b/src/discord/deploy_command.js @@ -0,0 +1,41 @@ +const { REST, Routes } = require('discord.js'); +const fs = require('node:fs'); +const path = require('node:path'); +module.exports = (token) => { + const commands = []; + // Grab all the command folders from the commands directory you created earlier + const foldersPath = path.join(__dirname, '../../commands'); + const commandFolders = fs.readdirSync(foldersPath); + + for (const folder of commandFolders) { + // Grab all the command files from the commands directory you created earlier + const commandsPath = path.join(foldersPath, folder); + const commandFiles = fs.readdirSync(commandsPath).filter(file => file.endsWith('.js')); + // Grab the SlashCommandBuilder#toJSON() output of each command's data for deployment + for (const file of commandFiles) { + const filePath = path.join(commandsPath, file); + const command = require(filePath); + if ('data' in command && 'execute' in command) { + commands.push(command.data.toJSON()); + } else { + console.log(`[WARNING] The command at ${filePath} is missing a required "data" or "execute" property.`); + } + } + } + // Construct and prepare an instance of the REST module + const rest = new REST().setToken(token); + + // and deploy your commands! + (async () => { + try { + console.log(`Started refreshing ${commands.length} application (/) commands.`); + const data = await rest.put( + Routes.applicationGuildCommands(1256304109393547305n, "1068240252092813373"), + { body: commands }, + ).then(data=>console.log(`Successfully reloaded ${data.length} application (/) commands.`)) + } catch (error) { + // And of course, make sure you catch and log any errors! + console.error(error); + } + })(); +} \ No newline at end of file diff --git a/src/monitoring/consoleMonitor.js b/src/monitoring/consoleMonitor.js new file mode 100644 index 0000000..43aa9eb --- /dev/null +++ b/src/monitoring/consoleMonitor.js @@ -0,0 +1,659 @@ +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 new file mode 100644 index 0000000..5afe481 --- /dev/null +++ b/src/monitoring/ramMonitor.js @@ -0,0 +1,209 @@ +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/cleaner.js b/src/pterodactyl/cleaner.js new file mode 100644 index 0000000..3db8c6a --- /dev/null +++ b/src/pterodactyl/cleaner.js @@ -0,0 +1,64 @@ +const axios = require('axios'); + + +const cleanChannel = async (client, channelId) => { + try { + const channel = await client.channels.fetch(channelId); + + if (!channel) { + console.log(`⚠️ Salon ${channelId} introuvable`); + return; + } + + const messages = await channel.messages.fetch({ limit: 100 }); + + // Trier les messages par date (du plus ancien au plus récent) + const sortedMessages = Array.from(messages.values()).sort((a, b) => a.createdTimestamp - b.createdTimestamp); + + // Exclure le premier message et les messages épinglés + const messagesToDelete = sortedMessages.slice(1).filter(m => !m.pinned); + + if (messagesToDelete.length > 0) { + await channel.bulkDelete(messagesToDelete); + console.log(`🧹 ${messagesToDelete.length} message(s) supprimé(s) dans #${channel.name}`); + } else { + console.log(`✅ Aucun message à supprimer dans #${channel.name}`); + } + + } catch (error) { + console.log(`❌ Erreur lors du nettoyage du salon ${channelId}: ${error.message}`); + } +}; + +const clean = async (client) => { + try { + const channelIds = process.env.CLEANER_CHANNEL_IDS; + + if (!channelIds) { + console.log('⚠️ CLEANER_CHANNEL_IDS non défini dans .env'); + return; + } + + // Séparer les IDs et nettoyer les espaces + const channelIdList = channelIds.split(',').map(id => id.trim()).filter(id => id); + + if (channelIdList.length === 0) { + console.log('⚠️ Aucun salon à nettoyer'); + return; + } + + console.log(`🧹 Nettoyage de ${channelIdList.length} salon(s)...`); + + // Nettoyer chaque salon + for (const channelId of channelIdList) { + await cleanChannel(client, channelId); + } + + console.log('✅ Nettoyage terminé'); + + } catch (error) { + console.log("❌ Erreur générale lors du nettoyage: " + error.message); + } +}; + +module.exports = clean; \ No newline at end of file diff --git a/src/pterodactyl/displayer.js b/src/pterodactyl/displayer.js new file mode 100644 index 0000000..056fea7 --- /dev/null +++ b/src/pterodactyl/displayer.js @@ -0,0 +1,49 @@ +const axios = require('axios'); + + +const update = async (headers,numbers,client,token) => { + try { + let state = "🔴"; + let players = "0"; + let config = { + method: 'get', + maxBodyLength: Infinity, + url: 'http://play.louismazin.ovh:8212/v1/api/metrics', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Authorization': `Basic ${token}` + } + }; + + axios(config) + .then((response) => { + players = response.data["currentplayernum"]; + }) + .catch((error) => { + console.log("Erreur lors de l'appel à l'api palworld (serveur injoignable)"); + players = "0"; + }); + + const state_reponse = await fetch("https://panel.louismazin.ovh/api/client/servers/ae4a628f/resources", { method : "GET", headers }); + const state_data = await state_reponse.json(); + if(state_data["attributes"]["current_state"] === "running"){ + state = "🟢"; + }else{ + state = "🔴"; + } + const title = "𝐒𝐞𝐫𝐯𝐞𝐫 : "+state+" 𝐉𝐨𝐮𝐞𝐮𝐫𝐬 : "+numbers[parseInt(players)]; + client.channels.fetch(1263481798667796623n) + .then(channel => { + if(state !== channel.name.split(" ")[2] || numbers[parseInt(players)] !== channel.name.split(" ")[5]){ + channel.setName(title); + console.log("Channel's name changed for : "+title); + } + }) + .catch(error => {console.log("Bot : error :"+error);}); + + } catch (error) { + console.log("Bot : "+error); + } +}; +module.exports = update; \ No newline at end of file diff --git a/src/pterodactyl/pterodactylFiles.js b/src/pterodactyl/pterodactylFiles.js new file mode 100644 index 0000000..3145dcd --- /dev/null +++ b/src/pterodactyl/pterodactylFiles.js @@ -0,0 +1,135 @@ +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, +};