import { Client, GatewayIntentBits, EmbedBuilder } from 'discord.js'; import { readFileSync } from 'fs'; import { dirname, join } from 'path'; import { fileURLToPath } from 'url'; import { getServerStatus } from './ssh.js'; import { getPlayers, getPlayerList } from './rcon.js'; const __dirname = dirname(fileURLToPath(import.meta.url)); let client = null; let statusMessageId = null; let statusChannelId = null; let infoChannelId = null; let alertChannelId = null; // State tracking for alerts const previousServerState = new Map(); const previousPlayerLists = new Map(); // Server display config const serverDisplay = { minecraft: { name: 'Minecraft ATM10', icon: '⛏️', color: 0x7B5E3C, address: 'minecraft.zeasy.dev' }, factorio: { name: 'Factorio', icon: '⚙️', color: 0xF97316, address: 'factorio.zeasy.dev' }, zomboid: { name: 'Project Zomboid', icon: '🧟', color: 0x4ADE80, address: 'zomboid.zeasy.dev' }, vrising: { name: 'V Rising', icon: '🧛', color: 0xDC2626, address: 'vrising.zeasy.dev' } }; function loadConfig() { return JSON.parse(readFileSync(join(__dirname, '..', 'config.json'), 'utf-8')); } async function sendAlert(embed) { if (!client || !alertChannelId) return; try { const channel = await client.channels.fetch(alertChannelId); if (channel) { await channel.send({ embeds: [embed] }); } } catch (err) { console.error('[DiscordBot] Error sending alert:', err.message); } } async function checkAndSendAlerts(serverStatuses) { for (const server of serverStatuses) { const display = serverDisplay[server.id] || { name: server.name, icon: '🖥️', color: 0x6B7280 }; const prevState = previousServerState.get(server.id); const prevPlayers = previousPlayerLists.get(server.id) || []; // Check server status changes if (prevState !== undefined && prevState !== server.status) { let embed; if (server.status === 'online' && prevState !== 'online') { embed = new EmbedBuilder() .setTitle(display.icon + ' Server gestartet') .setDescription('**' + display.name + '** ist jetzt online') .setColor(0x22C55E) .setTimestamp(); } else if (server.status === 'offline' && prevState === 'online') { embed = new EmbedBuilder() .setTitle(display.icon + ' Server gestoppt') .setDescription('**' + display.name + '** ist jetzt offline') .setColor(0xEF4444) .setTimestamp(); } else if (server.status === 'unreachable' && prevState !== 'unreachable') { embed = new EmbedBuilder() .setTitle('⚠️ Server nicht erreichbar') .setDescription('**' + display.name + '** ist nicht erreichbar') .setColor(0xF59E0B) .setTimestamp(); } if (embed) { await sendAlert(embed); } } // Check player changes (only if server is online) if (server.status === 'online' && server.playerList) { const currentPlayers = server.playerList; // Find players who joined for (const player of currentPlayers) { if (!prevPlayers.includes(player)) { const embed = new EmbedBuilder() .setTitle('➡️ Spieler beigetreten') .setDescription('**' + player + '** hat **' + display.name + '** betreten') .setColor(0x22C55E) .setTimestamp(); await sendAlert(embed); } } // Find players who left for (const player of prevPlayers) { if (!currentPlayers.includes(player)) { const embed = new EmbedBuilder() .setTitle('⬅️ Spieler verlassen') .setDescription('**' + player + '** hat **' + display.name + '** verlassen') .setColor(0xF59E0B) .setTimestamp(); await sendAlert(embed); } } previousPlayerLists.set(server.id, [...currentPlayers]); } else { previousPlayerLists.set(server.id, []); } // Update previous state previousServerState.set(server.id, server.status); } } async function setupInfoMessage() { try { const channel = await client.channels.fetch(infoChannelId); if (!channel) { console.error('[DiscordBot] Info channel not found'); return; } const messages = await channel.messages.fetch({ limit: 10 }); const botMessage = messages.find(m => m.author.id === client.user.id); const infoEmbed = new EmbedBuilder() .setTitle('🎮 Zeasy Gameserver Management') .setDescription( 'Verwalte und überwache unsere Gameserver bequem über das Web-Interface.\n\n' + '**Features:**\n' + '• Server starten, stoppen & neustarten\n' + '• Live Server-Status & Spielerlisten\n' + '• Server-Konsole & RCON-Befehle\n' + '• CPU, RAM & Uptime Metriken\n' + '• Welten-Verwaltung (Factorio)\n' + '• Config-Editor (Project Zomboid)\n\n' + '**Zugang:**\n' + 'Melde dich mit deinem Discord-Account an. Deine Berechtigungen werden automatisch über deine Discord-Rollen bestimmt.' ) .setColor(0x5865F2) .addFields({ name: '🔗 Web-Interface', value: '[server.zeasy.dev](https://server.zeasy.dev)', inline: false }) .setFooter({ text: 'Zeasy Software' }) .setTimestamp(); if (botMessage) { await botMessage.edit({ embeds: [infoEmbed] }); console.log('[DiscordBot] Updated info message'); } else { await channel.send({ embeds: [infoEmbed] }); console.log('[DiscordBot] Created info message'); } } catch (err) { console.error('[DiscordBot] Error setting up info message:', err.message); } } export async function initDiscordBot() { const token = process.env.DISCORD_BOT_TOKEN; statusChannelId = process.env.DISCORD_STATUS_CHANNEL_ID; infoChannelId = process.env.DISCORD_INFO_CHANNEL_ID; alertChannelId = process.env.DISCORD_ALERT_CHANNEL_ID; if (!token) { console.log('[DiscordBot] Missing DISCORD_BOT_TOKEN, bot disabled'); return; } client = new Client({ intents: [GatewayIntentBits.Guilds] }); client.once('ready', async () => { console.log('[DiscordBot] Logged in as ' + client.user.tag); // Setup info channel if (infoChannelId) { await setupInfoMessage(); } // Setup status channel and alerts if (statusChannelId) { await findOrCreateStatusMessage(); // First run - just populate state without sending alerts await updateStatusMessage(true); // Then start regular updates with alerts setInterval(() => updateStatusMessage(false), 60000); } }); client.login(token).catch(err => { console.error('[DiscordBot] Failed to login:', err.message); }); } async function findOrCreateStatusMessage() { try { const channel = await client.channels.fetch(statusChannelId); if (!channel) { console.error('[DiscordBot] Status channel not found'); return; } const messages = await channel.messages.fetch({ limit: 10 }); const botMessage = messages.find(m => m.author.id === client.user.id); if (botMessage) { statusMessageId = botMessage.id; console.log('[DiscordBot] Found existing status message'); } else { const msg = await channel.send({ embeds: [createLoadingEmbed()] }); statusMessageId = msg.id; console.log('[DiscordBot] Created new status message'); } } catch (err) { console.error('[DiscordBot] Error finding/creating status message:', err.message); } } function createLoadingEmbed() { return new EmbedBuilder() .setTitle('🎮 Gameserver Status') .setDescription('Lade Server-Status...') .setColor(0x6B7280) .setTimestamp(); } async function updateStatusMessage(skipAlerts = false) { if (!client || !statusMessageId || !statusChannelId) return; try { const channel = await client.channels.fetch(statusChannelId); const message = await channel.messages.fetch(statusMessageId); const config = loadConfig(); const serverStatuses = await Promise.all(config.servers.map(async (server) => { try { const status = await getServerStatus(server); const running = status === 'online'; let players = { online: 0, max: null }; let playerList = { players: [] }; if (running && server.rconPassword) { try { players = await getPlayers(server); playerList = await getPlayerList(server); } catch (e) { // RCON might fail } } return { id: server.id, name: server.name, type: server.type, status: running ? 'online' : 'offline', running, players: players.online || 0, maxPlayers: players.max, playerList: playerList.players || [] }; } catch (err) { return { id: server.id, name: server.name, type: server.type, status: 'unreachable', running: false, players: 0, maxPlayers: null, playerList: [] }; } })); // Send alerts if enabled if (!skipAlerts && alertChannelId) { await checkAndSendAlerts(serverStatuses); } else if (skipAlerts) { // Just populate initial state for (const server of serverStatuses) { previousServerState.set(server.id, server.status); previousPlayerLists.set(server.id, server.playerList || []); } } const embeds = []; const onlineCount = serverStatuses.filter(s => s.running).length; const totalPlayers = serverStatuses.reduce((sum, s) => sum + s.players, 0); const headerEmbed = new EmbedBuilder() .setTitle('🎮 Gameserver Status') .setDescription('**' + onlineCount + '/' + serverStatuses.length + '** Server online • **' + totalPlayers + '** Spieler') .setColor(onlineCount > 0 ? 0x22C55E : 0xEF4444) .setTimestamp() .setFooter({ text: 'Aktualisiert alle 60 Sekunden' }); embeds.push(headerEmbed); for (const server of serverStatuses) { const display = serverDisplay[server.id] || { name: server.name, icon: '🖥️', color: 0x6B7280, address: '' }; const serverEmbed = new EmbedBuilder() .setTitle(display.icon + ' ' + display.name) .setColor(server.running ? display.color : 0x4B5563); if (server.running) { let description = '✅ **Online**\n'; if (display.address) { description += '```' + display.address + '```'; } description += '👥 **Spieler:** ' + server.players; if (server.maxPlayers) { description += '/' + server.maxPlayers; } if (server.playerList && server.playerList.length > 0) { const names = server.playerList.slice(0, 15).join(', '); description += '\n' + names; if (server.playerList.length > 15) { description += ' *+' + (server.playerList.length - 15) + ' mehr*'; } } serverEmbed.setDescription(description); } else { serverEmbed.setDescription('❌ **Offline**'); } embeds.push(serverEmbed); } await message.edit({ embeds }); } catch (err) { console.error('[DiscordBot] Error updating status message:', err.message); } } export function getDiscordClient() { return client; }