This commit is contained in:
Louis Mazin 2026-04-15 23:36:05 +02:00
parent 900ac9d536
commit 115401be41
31 changed files with 5110 additions and 0 deletions

22
.env.example Normal file
View File

@ -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

2
.gitattributes vendored Normal file
View File

@ -0,0 +1,2 @@
# Auto detect text files and perform LF normalization
* text=auto

3
.gitignore vendored
View File

@ -12,3 +12,6 @@
# Built Visual Studio Code Extensions # Built Visual Studio Code Extensions
*.vsix *.vsix
node_modules/*
.env

View File

@ -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.');
}
},
};

View File

@ -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.');
}
},
};

View File

@ -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 });
}
},
};

View File

@ -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.');
}
},
};

98
commands/trad/trad.js Normal file
View File

@ -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.');
}
}
},
};

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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
});
}
},
};

View File

@ -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 = `<t:${Math.floor(lastDate.getTime() / 1000)}:R>`;
}
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 = `<t:${Math.floor(lastDate.getTime() / 1000)}:R>`;
}
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
});
}
},
};

View File

@ -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 = `<t:${Math.floor(lastDate.getTime() / 1000)}:R>`;
}
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
});
}
},
};

View File

@ -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
});
}
},
};

145
commands/utility/delier.js Normal file
View File

@ -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
});
});
}
},
};

View File

@ -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(() => {});
}
},
};

71
commands/utility/info.js Normal file
View File

@ -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
? `<t:${Math.floor(new Date(link.lastConnection).getTime() / 1000)}:R>`
: 'Jamais vu';
const linkedDate = `<t:${Math.floor(new Date(link.linked_at).getTime() / 1000)}:F>`;
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
});
}
},
};

View File

@ -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));
}
},
};

117
commands/utility/lier.js Normal file
View File

@ -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
});
}
},
};

309
commands/utility/panel.js Normal file
View File

@ -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é');
});
}

140
index.js Normal file
View File

@ -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);

1629
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

23
package.json Normal file
View File

@ -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"
}
}

View File

@ -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] <Lili Asuna> 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
};

245
src/core/database.js Normal file
View File

@ -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
};

View File

@ -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);
}
})();
}

View File

@ -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 naffiche 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] <LouisMazin> !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 lAPI 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 sarrê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 };

View File

@ -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
};

View File

@ -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;

View File

@ -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;

View File

@ -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,
};