const axios = require('axios'); const WebSocket = require('ws'); const { verifyLinkCode, updateUserLinkWithUsername, updateLastConnection } = require('./database.js'); let ws = null; let reconnectTimeout = null; let client = null; let heartbeatInterval = null; let checkInterval = null; let isMonitoring = false; let isConnecting = false; let connectionTimestamp = null; // Backoff de reconnexion let reconnectDelayMs = 5000; const RECONNECT_DELAY_MAX_MS = 5 * 60 * 1000; // 5 min const parseLogMessage = (log) => { // Format réel de log Palworld: // [2025-12-09 13:28:23] [CHAT] !link X2NMAY const linkRegex = /\[.*?\]\s*\[CHAT\]\s*<(.+?)>\s*!lier\s+([A-Z0-9]{6})/i; const match = log.match(linkRegex); if (match) { const playerName = match[1].trim(); const code = match[2].toUpperCase(); console.log(`✅ Commande !lier détectée: ${playerName} avec le code ${code}`); return { type: 'link', playerName: playerName, code: code, needsSteamId: true }; } // Détecter les déconnexions: [2025-12-09 18:55:19] [LOG] Nami left the server. (User id: gdk_2535420062888893) const disconnectRegex = /\[.*?\]\s*\[LOG\]\s*(.+?)\s+left the server\.\s*\(User id:\s*(.+?)\)/i; const disconnectMatch = log.match(disconnectRegex); if (disconnectMatch) { const playerName = disconnectMatch[1].trim(); const userId = disconnectMatch[2].trim(); console.log(`👋 Déconnexion détectée: ${playerName} (${userId})`); return { type: 'disconnect', playerName: playerName, userId: userId }; } return null; }; const getSteamIdFromPlayerName = async (playerName) => { try { const response = await axios({ method: 'get', maxBodyLength: Infinity, url: 'http://play.louismazin.ovh:8212/v1/api/players', headers: { 'Accept': 'application/json', 'Authorization': `Basic ${process.env.PALWORLD_API_TOKEN}` } }); const players = response.data.players || {}; console.log(`🔍 Recherche du Steam ID pour le joueur: ${playerName}`); // Chercher le joueur par nom for (const [steamId, player] of Object.entries(players)) { if (player.name === playerName) { return { steamId: player.userId, playerId: player.playerId, name: player.name }; } } return null; } catch (error) { console.error('Erreur lors de la récupération du Steam ID:', error.message); return null; } }; const handleLinkCommand = async (playerName, playerData, code) => { try { console.log(`🔗 Tentative de liaison détectée: ${playerName} (${playerData.steamId}) avec le code ${code}`); const result = await verifyLinkCode(code, playerData.steamId, playerData.name, playerData.playerId); if (result.success) { console.log(`✅ Liaison réussie pour ${playerName}`); if (client) { const user = await client.users.fetch(result.discordId).catch(() => null); if (user) { await updateUserLinkWithUsername(result.discordId, user.tag); 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; } }; const checkAndManageWebSocket = async () => { const { hasActiveLinkCodes, cleanExpiredCodes } = require('./database.js'); try { await cleanExpiredCodes(); // 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(`🔄 Reconnexion WebSocket dans ${Math.round(delay / 1000)}s...`); reconnectTimeout = setTimeout(() => { 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'); return; } if (isConnecting) { console.log('⚠️ Connexion WebSocket déjà en cours'); return; } isConnecting = true; connectionTimestamp = Date.now(); console.log(`📅 Timestamp de connexion: ${new Date(connectionTimestamp).toISOString()}`); try { // Toujours rafraîchir les credentials pour éviter les tokens expirés après redémarrage serveur const credentials = await getWebSocketCredentials(pterodactylToken, serverId); ws = new WebSocket(credentials.socket, { origin: process.env.PTERODACTYL_API_URL }); ws.on('open', () => { console.log('✅ WebSocket Pterodactyl connecté'); isConnecting = false; resetReconnectBackoff(); ws.send(JSON.stringify({ event: 'auth', args: [credentials.token] })); }); ws.on('message', async (data) => { try { const message = JSON.parse(data.toString()); if (message.event === 'auth success') { console.log('✅ Authentification WebSocket réussie'); ws.send(JSON.stringify({ event: 'send logs', args: [null] })); if (heartbeatInterval) clearInterval(heartbeatInterval); heartbeatInterval = setInterval(() => { if (ws && ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ event: 'send heartbeat', args: [] })); } }, 30000); } if (message.event === 'console output') { const log = message.args[0]; // 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(); // Ignorer les messages antérieurs à la connexion if (logTimestamp < connectionTimestamp) { return; } } const linkData = parseLogMessage(log); if (linkData) { if (linkData.type === 'link') { const playerData = await getSteamIdFromPlayerName(linkData.playerName); if (playerData) { await handleLinkCommand(linkData.playerName, playerData, linkData.code); // Ne plus vérifier si on doit fermer le WebSocket } else { console.log(`❌ Impossible de trouver le Steam ID pour ${linkData.playerName}`); } } else if (linkData.type === 'disconnect') { 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') { console.log('📊 Statut du serveur:', message.args[0]); } } catch (error) { console.error('Erreur parsing message WebSocket:', error); } }); ws.on('error', (error) => { console.error('❌ Erreur WebSocket:', error.message); isConnecting = false; }); ws.on('close', (code, reason) => { console.log(`⚠️ WebSocket Pterodactyl déconnecté (Code: ${code})`); ws = null; isConnecting = false; connectionTimestamp = null; if (heartbeatInterval) { clearInterval(heartbeatInterval); heartbeatInterval = null; } // Planifier une reconnexion avec backoff (utile lors des redémarrages quotidiens du serveur) scheduleReconnect(); }); } catch (error) { console.error('Erreur lors de la connexion WebSocket:', error); isConnecting = false; connectionTimestamp = null; // Échec immédiat -> planifier reconnexion avec backoff scheduleReconnect(); } }; const stopWebSocketOnly = () => { if (heartbeatInterval) { clearInterval(heartbeatInterval); heartbeatInterval = null; } if (ws) { ws.close(); ws = null; } isConnecting = false; }; const startConsoleMonitoring = (discordClient, pterodactylToken) => { if (isMonitoring) { console.log('⚠️ Surveillance déjà active'); return; } client = discordClient; isMonitoring = true; 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 30 secondes (nettoyage codes et reconnect si besoin) if (checkInterval) clearInterval(checkInterval); checkInterval = setInterval(checkAndManageWebSocket, 30000); }; const stopConsoleMonitoring = () => { stopWebSocketOnly(); if (checkInterval) { clearInterval(checkInterval); checkInterval = null; } if (reconnectTimeout) { clearTimeout(reconnectTimeout); reconnectTimeout = null; } isMonitoring = false; console.log('🔌 Surveillance de la console arrêtée'); }; module.exports = { startConsoleMonitoring, stopConsoleMonitoring };