edit
This commit is contained in:
parent
900ac9d536
commit
115401be41
22
.env.example
Normal file
22
.env.example
Normal 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
2
.gitattributes
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
# Auto detect text files and perform LF normalization
|
||||
* text=auto
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@ -12,3 +12,6 @@
|
||||
# Built Visual Studio Code Extensions
|
||||
*.vsix
|
||||
|
||||
node_modules/*
|
||||
|
||||
.env
|
||||
60
commands/server/ram-status.js
Normal file
60
commands/server/ram-status.js
Normal 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.');
|
||||
}
|
||||
},
|
||||
};
|
||||
91
commands/server/reboot-server.js
Normal file
91
commands/server/reboot-server.js
Normal 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.');
|
||||
}
|
||||
},
|
||||
};
|
||||
120
commands/server/server-stats.js
Normal file
120
commands/server/server-stats.js
Normal 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 });
|
||||
}
|
||||
},
|
||||
};
|
||||
65
commands/server/start-server.js
Normal file
65
commands/server/start-server.js
Normal 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
98
commands/trad/trad.js
Normal 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.');
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
41
commands/transfer-save/serverMessage.json
Normal file
41
commands/transfer-save/serverMessage.json
Normal 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"
|
||||
}
|
||||
41
commands/transfer-save/soloMessage.json
Normal file
41
commands/transfer-save/soloMessage.json
Normal 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"
|
||||
}
|
||||
54
commands/transfer-save/transfer-save.js
Normal file
54
commands/transfer-save/transfer-save.js
Normal 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
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
123
commands/utility/afficher-inactifs.js
Normal file
123
commands/utility/afficher-inactifs.js
Normal 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
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
67
commands/utility/afficher-lies.js
Normal file
67
commands/utility/afficher-lies.js
Normal 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
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
134
commands/utility/delier-rygainland.js
Normal file
134
commands/utility/delier-rygainland.js
Normal 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
145
commands/utility/delier.js
Normal 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
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
97
commands/utility/diagnostique-ws.js
Normal file
97
commands/utility/diagnostique-ws.js
Normal 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
71
commands/utility/info.js
Normal 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
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
58
commands/utility/lier-rygainland.js
Normal file
58
commands/utility/lier-rygainland.js
Normal 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
117
commands/utility/lier.js
Normal 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
309
commands/utility/panel.js
Normal 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
140
index.js
Normal 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
1629
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
23
package.json
Normal file
23
package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
198
src/bridge/palworld-bridge.js
Normal file
198
src/bridge/palworld-bridge.js
Normal 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
245
src/core/database.js
Normal 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
|
||||
};
|
||||
41
src/discord/deploy_command.js
Normal file
41
src/discord/deploy_command.js
Normal 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);
|
||||
}
|
||||
})();
|
||||
}
|
||||
659
src/monitoring/consoleMonitor.js
Normal file
659
src/monitoring/consoleMonitor.js
Normal 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 n’affiche pas les déconnexions)
|
||||
const connectedPlayers = new Map(); // steamId -> { name, playerId, lastSeen }
|
||||
// Suivi des logs déjà traités pour éviter les doublons
|
||||
const processedLogs = new Set(); // Garde les 500 derniers logs traités
|
||||
const MAX_PROCESSED_LOGS = 500;
|
||||
const parseLogMessage = (log) => {
|
||||
// Format réel de log Palworld:
|
||||
// [2025-12-09 13:28:23] [CHAT] <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 l’API Palworld
|
||||
const fetchCurrentPlayers = async () => {
|
||||
try {
|
||||
const response = await axios({
|
||||
method: 'get',
|
||||
maxBodyLength: Infinity,
|
||||
url: 'http://play.louismazin.ovh:8212/v1/api/players',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Basic ${process.env.PALWORLD_API_TOKEN}`
|
||||
}
|
||||
});
|
||||
const players = response.data.players || {};
|
||||
const list = [];
|
||||
for (const [, player] of Object.entries(players)) {
|
||||
list.push({
|
||||
steamId: player.userId,
|
||||
playerId: player.playerId,
|
||||
name: player.name
|
||||
});
|
||||
}
|
||||
return list;
|
||||
} catch (error) {
|
||||
// API inaccessible quand le serveur est down
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// Mettre à jour le set des joueurs et détecter les départs silencieux
|
||||
const pollPlayersAndDetectDisconnects = async (serverState = null) => {
|
||||
const list = await fetchCurrentPlayers();
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
if (list && list.length > 0) {
|
||||
// Marquer présents
|
||||
for (const p of list) {
|
||||
connectedPlayers.set(p.steamId, { name: p.name, playerId: p.playerId, lastSeen: now });
|
||||
}
|
||||
// Détecter ceux qui ont disparu depuis le dernier poll
|
||||
for (const [steamId, info] of connectedPlayers) {
|
||||
const stillHere = list.find(x => x.steamId === steamId);
|
||||
if (!stillHere) {
|
||||
try {
|
||||
const result = await updateLastConnection(steamId);
|
||||
if (result.changes > 0) {
|
||||
console.log(`✅ Départ silencieux détecté: ${info.name} (${steamId}) -> lastConnection mis à jour`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`❌ Erreur update lastConnection pour ${steamId}:`, e.message);
|
||||
}
|
||||
connectedPlayers.delete(steamId);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Pas de liste (serveur inaccessible) ou vide:
|
||||
// Si le serveur est arrêté/stopping/offline, considérer tous comme déconnectés
|
||||
if (serverState && ['offline', 'stopping', 'stopped'].includes(serverState)) {
|
||||
for (const [steamId, info] of connectedPlayers) {
|
||||
try {
|
||||
const result = await updateLastConnection(steamId);
|
||||
if (result.changes > 0) {
|
||||
console.log(`✅ Serveur ${serverState}: déconnexion implicite de ${info.name} (${steamId})`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`❌ Erreur update lastConnection pour ${steamId}:`, e.message);
|
||||
}
|
||||
}
|
||||
connectedPlayers.clear();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const checkAndManageWebSocket = async () => {
|
||||
const { cleanExpiredCodes } = require('../core/database.js');
|
||||
try {
|
||||
await cleanExpiredCodes();
|
||||
|
||||
// Fallback polling toutes les 20s
|
||||
await pollPlayersAndDetectDisconnects();
|
||||
|
||||
// Le WebSocket doit toujours être connecté maintenant (pour les déconnexions)
|
||||
if (!ws && !isConnecting) {
|
||||
console.log('🔌 Connexion au WebSocket pour surveillance...');
|
||||
const serverId = process.env.PTERODACTYL_SERVER_ID;
|
||||
const pterodactylToken = process.env.PTERODACTYL_API_TOKEN;
|
||||
await connectWebSocket(pterodactylToken, serverId);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la vérification du WebSocket:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const resetReconnectBackoff = () => {
|
||||
reconnectDelayMs = 5000;
|
||||
};
|
||||
|
||||
const scheduleReconnect = () => {
|
||||
if (!isMonitoring) return;
|
||||
if (reconnectTimeout) clearTimeout(reconnectTimeout);
|
||||
const delay = reconnectDelayMs;
|
||||
reconnectDelayMs = Math.min(reconnectDelayMs * 2, RECONNECT_DELAY_MAX_MS);
|
||||
console.log(`🔄 [WEBSOCKET] Reconnexion WebSocket programmée dans ${Math.round(delay / 1000)}s... (Monitoring actif: ${isMonitoring})`);
|
||||
reconnectTimeout = setTimeout(() => {
|
||||
console.log('🔄 [WEBSOCKET] Tentative de reconnexion maintenant...');
|
||||
checkAndManageWebSocket();
|
||||
}, delay);
|
||||
};
|
||||
|
||||
const connectWebSocket = async (pterodactylToken, serverId) => {
|
||||
if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) {
|
||||
console.log('⚠️ [WEBSOCKET] Déjà connecté ou en cours de connexion (état:', ws.readyState, ')');
|
||||
return;
|
||||
}
|
||||
if (isConnecting) {
|
||||
console.log('⚠️ [WEBSOCKET] Connexion déjà en cours, abandon de cette tentative');
|
||||
return;
|
||||
}
|
||||
|
||||
isConnecting = true;
|
||||
connectionTimestamp = Date.now();
|
||||
console.log(`📅 [WEBSOCKET] Timestamp de connexion: ${new Date(connectionTimestamp).toISOString()}`);
|
||||
|
||||
try {
|
||||
// Toujours rafraîchir les credentials pour éviter les tokens expirés après redémarrage serveur
|
||||
console.log('🔑 [WEBSOCKET] Récupération de nouveaux credentials...');
|
||||
const credentials = await getWebSocketCredentials(pterodactylToken, serverId);
|
||||
console.log('✅ [WEBSOCKET] Credentials récupérés avec succès');
|
||||
|
||||
ws = new WebSocket(credentials.socket, {
|
||||
origin: process.env.PTERODACTYL_API_URL
|
||||
});
|
||||
|
||||
ws.on('open', () => {
|
||||
console.log('✅ [WEBSOCKET] WebSocket Pterodactyl connecté avec succès');
|
||||
isConnecting = false;
|
||||
resetReconnectBackoff();
|
||||
lastHeartbeatResponse = Date.now(); // Initialiser à maintenant
|
||||
|
||||
console.log('🔐 [WEBSOCKET] Envoi de l\'authentification...');
|
||||
ws.send(JSON.stringify({
|
||||
event: 'auth',
|
||||
args: [credentials.token]
|
||||
}));
|
||||
});
|
||||
|
||||
// Pas besoin de handler 'pong' - on vérifie l'accès aux logs directement
|
||||
|
||||
|
||||
ws.on('message', async (data) => {
|
||||
try {
|
||||
const message = JSON.parse(data.toString());
|
||||
|
||||
if (message.event === 'auth success') {
|
||||
console.log('✅ [WEBSOCKET] Authentification WebSocket réussie');
|
||||
console.log('📡 [WEBSOCKET] Demande de réception des logs console...');
|
||||
|
||||
ws.send(JSON.stringify({
|
||||
event: 'send logs',
|
||||
args: [null]
|
||||
}));
|
||||
|
||||
// Configurer le heartbeat avec détection agressive
|
||||
if (heartbeatInterval) clearInterval(heartbeatInterval);
|
||||
if (heartbeatTimeout) clearTimeout(heartbeatTimeout);
|
||||
|
||||
lastHeartbeatResponse = Date.now();
|
||||
|
||||
heartbeatInterval = setInterval(() => {
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
try {
|
||||
const timeSinceLastLog = Date.now() - lastHeartbeatResponse;
|
||||
|
||||
// Si on n'a pas reçu de log depuis plus de 40s, c'est que l'accès aux logs est perdu
|
||||
if (timeSinceLastLog > 19000) {
|
||||
console.error(`💔 [WEBSOCKET] Aucun log reçu depuis ${Math.round(timeSinceLastLog/1000)}s - accès aux logs perdu! Reconnexion forcée...`);
|
||||
if (ws) {
|
||||
ws.terminate(); // Fermeture immédiate sans attendre
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Redemander l'accès aux logs pour s'assurer qu'on les reçoit toujours
|
||||
ws.send(JSON.stringify({
|
||||
event: 'send logs',
|
||||
args: [null]
|
||||
}));
|
||||
console.log(`📡 [WEBSOCKET] Vérification accès aux logs (dernier reçu: ${Math.round(timeSinceLastLog/1000)}s)`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ [WEBSOCKET] Erreur lors de la vérification des logs:', error.message);
|
||||
if (ws) {
|
||||
ws.terminate();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.warn('⚠️ [WEBSOCKET] Vérification impossible: WebSocket non ouvert (état:', ws ? ws.readyState : 'null', ')');
|
||||
if (heartbeatInterval) {
|
||||
clearInterval(heartbeatInterval);
|
||||
heartbeatInterval = null;
|
||||
}
|
||||
}
|
||||
}, 20000); // Vérifier toutes les 20s
|
||||
console.log('📡 [WEBSOCKET] Vérification d\'accès aux logs configurée (20s, timeout 40s)');
|
||||
}
|
||||
|
||||
// Détecter les réponses du serveur (console output, status, etc) pour mettre à jour le lastHeartbeatResponse
|
||||
if (message.event === 'console output' || message.event === 'status' || message.event === 'stats') {
|
||||
lastHeartbeatResponse = Date.now();
|
||||
}
|
||||
if (message.event === 'console output') {
|
||||
const log = message.args[0];
|
||||
|
||||
// Protection 0: Vérifier si ce log a déjà été traité (évite les doublons lors des 'send logs' répétés)
|
||||
if (processedLogs.has(log)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isChatMessage = log.includes('[CHAT]');
|
||||
|
||||
// Extraire le timestamp du log
|
||||
const timestampMatch = log.match(/\[(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\]/);
|
||||
|
||||
if (timestampMatch) {
|
||||
const logTimestamp = new Date(timestampMatch[1]).getTime();
|
||||
const now = Date.now();
|
||||
|
||||
// Protection 1: Ignorer les messages très anciens (>15 min) pour éviter l'historique complet
|
||||
if (now - logTimestamp > 15 * 60 * 1000) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Protection 2: Ignorer les messages antérieurs au démarrage du monitoring
|
||||
// S'applique à TOUS les messages (y compris chat) pour éviter de traiter l'historique au démarrage
|
||||
if (monitoringStartTimestamp && logTimestamp < monitoringStartTimestamp) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Marquer ce log comme traité
|
||||
processedLogs.add(log);
|
||||
|
||||
// Limiter la taille du Set pour éviter une fuite mémoire
|
||||
if (processedLogs.size > MAX_PROCESSED_LOGS) {
|
||||
const firstItem = processedLogs.values().next().value;
|
||||
processedLogs.delete(firstItem);
|
||||
}
|
||||
|
||||
// Détecter si c'est un message de chat
|
||||
if (isChatMessage) {
|
||||
console.log(`💬 [CONSOLE] Message de chat détecté: ${log.substring(0, 100)}`);
|
||||
|
||||
// Transférer le message de chat vers Discord via le bridge
|
||||
console.log(`📋 [CONSOLE] Traitement du log pour le bridge...`);
|
||||
try {
|
||||
await handlePalworldChat(log);
|
||||
} catch (bridgeError) {
|
||||
console.error('❌ [CONSOLE] Erreur lors du transfert vers le bridge:', bridgeError);
|
||||
}
|
||||
}
|
||||
|
||||
const linkData = parseLogMessage(log);
|
||||
|
||||
if (linkData) {
|
||||
if (linkData.type === 'link') {
|
||||
// Traiter de manière asynchrone sans bloquer le handler WebSocket
|
||||
(async () => {
|
||||
try {
|
||||
const playerData = await getSteamIdFromPlayerName(linkData.playerName);
|
||||
|
||||
if (playerData) {
|
||||
await handleLinkCommand(linkData.playerName, playerData, linkData.code);
|
||||
} else {
|
||||
console.log(`❌ Impossible de trouver le Steam ID pour ${linkData.playerName}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`❌ Erreur lors du traitement de !lier pour ${linkData.playerName}:`, error);
|
||||
}
|
||||
})();
|
||||
} else if (linkData.type === 'disconnect') {
|
||||
// Traiter de manière asynchrone sans bloquer le handler WebSocket
|
||||
(async () => {
|
||||
try {
|
||||
const result = await updateLastConnection(linkData.userId);
|
||||
if (result.changes > 0) {
|
||||
console.log(`✅ Dernière connexion mise à jour pour ${linkData.playerName} (${linkData.userId})`);
|
||||
} else {
|
||||
console.log(`ℹ️ Aucun compte lié trouvé pour ${linkData.playerName} (${linkData.userId})`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la mise à jour de la dernière connexion:', error);
|
||||
}
|
||||
})();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (message.event === 'status') {
|
||||
const state = message.args[0];
|
||||
console.log('📊 Statut du serveur:', state);
|
||||
|
||||
// Si le serveur s’arrête/offline, forcer la mise à jour des joueurs connectés (déconnexions implicites)
|
||||
if (['offline', 'stopping', 'stopped'].includes(state)) {
|
||||
await pollPlayersAndDetectDisconnects(state);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur parsing message WebSocket:', error);
|
||||
}
|
||||
});
|
||||
|
||||
ws.on('error', (error) => {
|
||||
console.error('❌ [WEBSOCKET] Erreur WebSocket:', error.message);
|
||||
console.error('❌ [WEBSOCKET] Code erreur:', error.code || 'N/A');
|
||||
isConnecting = false;
|
||||
});
|
||||
|
||||
ws.on('close', (code, reason) => {
|
||||
const reasonStr = reason ? reason.toString() : 'Aucune raison fournie';
|
||||
console.log(`⚠️ [WEBSOCKET] WebSocket Pterodactyl déconnecté (Code: ${code}, Raison: ${reasonStr})`);
|
||||
console.log(`🔍 [WEBSOCKET] État actuel: isMonitoring=${isMonitoring}, isConnecting=${isConnecting}`);
|
||||
ws = null;
|
||||
isConnecting = false;
|
||||
connectionTimestamp = null;
|
||||
|
||||
if (heartbeatInterval) {
|
||||
clearInterval(heartbeatInterval);
|
||||
heartbeatInterval = null;
|
||||
console.log('💔 [WEBSOCKET] Heartbeat arrêté');
|
||||
}
|
||||
|
||||
if (heartbeatTimeout) {
|
||||
clearTimeout(heartbeatTimeout);
|
||||
heartbeatTimeout = null;
|
||||
}
|
||||
|
||||
// Planifier une reconnexion avec backoff (utile lors des redémarrages quotidiens du serveur)
|
||||
if (isMonitoring) {
|
||||
scheduleReconnect();
|
||||
} else {
|
||||
console.log('⚠️ [WEBSOCKET] Monitoring désactivé, pas de reconnexion');
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ [WEBSOCKET] Erreur lors de la connexion WebSocket:', error.message);
|
||||
console.error('❌ [WEBSOCKET] Stack:', error.stack);
|
||||
isConnecting = false;
|
||||
connectionTimestamp = null;
|
||||
// Échec immédiat -> planifier reconnexion avec backoff
|
||||
if (isMonitoring) {
|
||||
scheduleReconnect();
|
||||
} else {
|
||||
console.log('⚠️ [WEBSOCKET] Monitoring désactivé, pas de reconnexion');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const stopWebSocketOnly = () => {
|
||||
if (heartbeatInterval) {
|
||||
clearInterval(heartbeatInterval);
|
||||
heartbeatInterval = null;
|
||||
}
|
||||
|
||||
if (heartbeatTimeout) {
|
||||
clearTimeout(heartbeatTimeout);
|
||||
heartbeatTimeout = null;
|
||||
}
|
||||
|
||||
if (refreshCredentialsInterval) {
|
||||
clearInterval(refreshCredentialsInterval);
|
||||
refreshCredentialsInterval = null;
|
||||
}
|
||||
|
||||
if (ws) {
|
||||
ws.close();
|
||||
ws = null;
|
||||
}
|
||||
|
||||
isConnecting = false;
|
||||
lastHeartbeatResponse = null;
|
||||
};
|
||||
|
||||
const forceReconnectToRefreshCredentials = async () => {
|
||||
if (!isMonitoring) return;
|
||||
|
||||
console.log('🔄 Rafraîchissement des credentials WebSocket (reconnexion préventive)...');
|
||||
|
||||
// Fermer la connexion actuelle proprement
|
||||
if (ws) {
|
||||
ws.close();
|
||||
ws = null;
|
||||
}
|
||||
|
||||
// Attendre un peu avant de reconnecter
|
||||
setTimeout(async () => {
|
||||
if (isMonitoring) {
|
||||
await checkAndManageWebSocket();
|
||||
}
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
const startConsoleMonitoring = (discordClient, pterodactylToken) => {
|
||||
if (isMonitoring) {
|
||||
console.log('⚠️ Surveillance déjà active');
|
||||
return;
|
||||
}
|
||||
|
||||
client = discordClient;
|
||||
isMonitoring = true;
|
||||
monitoringStartTimestamp = Date.now();
|
||||
console.log(`📅 Démarrage du monitoring: ${new Date(monitoringStartTimestamp).toISOString()}`);
|
||||
resetReconnectBackoff();
|
||||
|
||||
const serverId = process.env.PTERODACTYL_SERVER_ID;
|
||||
if (!serverId) {
|
||||
console.error('❌ PTERODACTYL_SERVER_ID non défini dans .env');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('🔍 Surveillance de la console Pterodactyl démarrée (mode intelligent)');
|
||||
|
||||
// Vérifier immédiatement
|
||||
checkAndManageWebSocket();
|
||||
|
||||
// Vérifier toutes les 20 secondes (nettoyage codes et reconnect si besoin)
|
||||
if (checkInterval) clearInterval(checkInterval);
|
||||
checkInterval = setInterval(checkAndManageWebSocket, 20000); // 20s pour détecter vite les départs silencieux
|
||||
|
||||
// Rafraîchir les credentials toutes les 55 minutes pour éviter l'expiration
|
||||
if (refreshCredentialsInterval) clearInterval(refreshCredentialsInterval);
|
||||
refreshCredentialsInterval = setInterval(forceReconnectToRefreshCredentials, CREDENTIALS_REFRESH_INTERVAL);
|
||||
console.log(`⏱️ Rafraîchissement automatique des credentials programmé toutes les ${CREDENTIALS_REFRESH_INTERVAL / 60000} minutes`);
|
||||
};
|
||||
|
||||
const stopConsoleMonitoring = () => {
|
||||
stopWebSocketOnly();
|
||||
|
||||
if (checkInterval) {
|
||||
clearInterval(checkInterval);
|
||||
checkInterval = null;
|
||||
}
|
||||
|
||||
if (refreshCredentialsInterval) {
|
||||
clearInterval(refreshCredentialsInterval);
|
||||
refreshCredentialsInterval = null;
|
||||
}
|
||||
|
||||
if (reconnectTimeout) {
|
||||
clearTimeout(reconnectTimeout);
|
||||
reconnectTimeout = null;
|
||||
}
|
||||
|
||||
isMonitoring = false;
|
||||
monitoringStartTimestamp = null;
|
||||
processedLogs.clear(); // Nettoyer le cache des logs traités
|
||||
console.log('🔌 Surveillance de la console arrêtée');
|
||||
};
|
||||
|
||||
// Fonction pour forcer la reconnexion du WebSocket (utilisée par ramMonitor)
|
||||
const forceWebSocketReconnect = async () => {
|
||||
console.log('🔄 [CONSOLE] Reconnexion forcée du WebSocket...');
|
||||
await forceReconnectToRefreshCredentials();
|
||||
};
|
||||
|
||||
// Fonction pour obtenir l'état du WebSocket (pour diagnostic)
|
||||
const getWebSocketStatus = () => {
|
||||
const wsStates = {
|
||||
[WebSocket.CONNECTING]: 'CONNECTING',
|
||||
[WebSocket.OPEN]: 'OPEN',
|
||||
[WebSocket.CLOSING]: 'CLOSING',
|
||||
[WebSocket.CLOSED]: 'CLOSED'
|
||||
};
|
||||
|
||||
return {
|
||||
isMonitoring,
|
||||
isConnecting,
|
||||
hasWebSocket: !!ws,
|
||||
wsState: ws ? wsStates[ws.readyState] : 'N/A',
|
||||
wsStateRaw: ws ? ws.readyState : null,
|
||||
connectionTimestamp: connectionTimestamp ? new Date(connectionTimestamp).toISOString() : null,
|
||||
monitoringStartTimestamp: monitoringStartTimestamp ? new Date(monitoringStartTimestamp).toISOString() : null,
|
||||
lastHeartbeatResponse: lastHeartbeatResponse ? new Date(lastHeartbeatResponse).toISOString() : null,
|
||||
timeSinceLastResponse: lastHeartbeatResponse ? Math.round((Date.now() - lastHeartbeatResponse) / 1000) : null,
|
||||
reconnectDelayMs,
|
||||
hasHeartbeat: !!heartbeatInterval,
|
||||
hasHeartbeatTimeout: !!heartbeatTimeout,
|
||||
hasCheckInterval: !!checkInterval,
|
||||
hasRefreshInterval: !!refreshCredentialsInterval,
|
||||
hasPendingReconnect: !!reconnectTimeout
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = { startConsoleMonitoring, stopConsoleMonitoring, forceWebSocketReconnect, getWebSocketStatus };
|
||||
209
src/monitoring/ramMonitor.js
Normal file
209
src/monitoring/ramMonitor.js
Normal 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
|
||||
};
|
||||
64
src/pterodactyl/cleaner.js
Normal file
64
src/pterodactyl/cleaner.js
Normal 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;
|
||||
49
src/pterodactyl/displayer.js
Normal file
49
src/pterodactyl/displayer.js
Normal 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;
|
||||
135
src/pterodactyl/pterodactylFiles.js
Normal file
135
src/pterodactyl/pterodactylFiles.js
Normal 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,
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user