import { Client, GatewayIntentBits, EmbedBuilder, ChannelType, PermissionFlagsBits } 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'; import { initGuildSettings, getGuildSettings, getAllGuildSettings, setGuildSettings, deleteGuildSettings } from '../db/init.js'; const __dirname = dirname(fileURLToPath(import.meta.url)); let client = null; // State tracking for alerts (per guild) 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: 'pz.zeasy.dev:16261' }, vrising: { name: 'V Rising', icon: '🧛', color: 0xDC2626, address: 'vrising.zeasy.dev' }, palworld: { name: 'Palworld', icon: '🦎', color: 0x00D4AA, address: 'palworld.zeasy.dev:8211' } }; function loadConfig() { return JSON.parse(readFileSync(join(__dirname, '..', 'config.json'), 'utf-8')); } // ============================================ // Channel Setup Functions // ============================================ async function setupGuildChannels(guild) { console.log('[DiscordBot] Setting up channels for guild: ' + guild.name); try { // Create category const category = await guild.channels.create({ name: '🎮 Gameserver', type: ChannelType.GuildCategory, permissionOverwrites: [ { id: guild.id, // @everyone deny: [PermissionFlagsBits.SendMessages], allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.ReadMessageHistory] }, { id: client.user.id, // Bot allow: [PermissionFlagsBits.SendMessages, PermissionFlagsBits.ManageMessages, PermissionFlagsBits.EmbedLinks] } ] }); // Create info channel const infoChannel = await guild.channels.create({ name: 'ℹ️│info', type: ChannelType.GuildText, parent: category.id, topic: 'Informationen zum Gameserver Management System' }); // Create status channel const statusChannel = await guild.channels.create({ name: '📊│status', type: ChannelType.GuildText, parent: category.id, topic: 'Live-Status aller Gameserver' }); // Create alerts channel const alertsChannel = await guild.channels.create({ name: '📢│alerts', type: ChannelType.GuildText, parent: category.id, topic: 'Benachrichtigungen über Server-Events und Spieler' }); // Create updates channel const updatesChannel = await guild.channels.create({ name: '📰│updates', type: ChannelType.GuildText, parent: category.id, topic: 'Ankündigungen zu neuen Gameservern' }); // Create discussion channel (users can write) const discussionChannel = await guild.channels.create({ name: '💬│diskussion', type: ChannelType.GuildText, parent: category.id, topic: 'Diskutiert hier über die Gameserver', permissionOverwrites: [ { id: guild.id, allow: [PermissionFlagsBits.SendMessages, PermissionFlagsBits.ViewChannel, PermissionFlagsBits.ReadMessageHistory] } ] }); // Create requests forum channel const requestsChannel = await guild.channels.create({ name: '💡│requests', type: ChannelType.GuildForum, parent: category.id, topic: 'Schlage neue Gameserver vor', defaultAutoArchiveDuration: 10080, // 7 days permissionOverwrites: [ { id: guild.id, allow: [PermissionFlagsBits.SendMessages, PermissionFlagsBits.ViewChannel, PermissionFlagsBits.ReadMessageHistory, PermissionFlagsBits.CreatePublicThreads] } ] }); // Create info thread in requests forum const infoThread = await requestsChannel.threads.create({ name: '📌 Wie funktioniert dieser Kanal?', message: { embeds: [new EmbedBuilder() .setTitle('💡 Gameserver Vorschläge') .setDescription( 'In diesem Kanal kannst du neue Gameserver vorschlagen!\n\n' + '**So funktioniert\'s:**\n' + '1. Erstelle einen neuen Beitrag mit dem Namen des Spiels\n' + '2. Beschreibe warum dieser Server cool wäre\n' + '3. Andere können deinen Vorschlag diskutieren und liken\n\n' + '**Was wir berücksichtigen:**\n' + '• Interesse der Community (Likes & Diskussion)\n' + '• Technische Machbarkeit\n' + '• Serverkosten\n\n' + 'Beliebte Vorschläge werden von uns geprüft!' ) .setColor(0x5865F2) .setFooter({ text: 'Zeasy Gameserver' }) ] } }); // Pin the info thread await infoThread.pin(); // Create info message 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 (Palworld, Zomboid)\n\n' + '**Zugang:**\n' + 'Melde dich mit deinem Discord-Account an.' ) .setColor(0x5865F2) .addFields({ name: '🔗 Web-Interface', value: '[server.zeasy.dev](https://server.zeasy.dev)', inline: false }) .setFooter({ text: 'Zeasy Software' }) .setTimestamp(); await infoChannel.send({ embeds: [infoEmbed] }); // Create initial status message const statusMsg = await statusChannel.send({ embeds: [createLoadingEmbed()] }); // Save settings to database const settings = { category_id: category.id, info_channel_id: infoChannel.id, status_channel_id: statusChannel.id, status_message_id: statusMsg.id, alerts_channel_id: alertsChannel.id, updates_channel_id: updatesChannel.id, discussion_channel_id: discussionChannel.id, requests_channel_id: requestsChannel.id, requests_info_thread_id: infoThread.id }; setGuildSettings(guild.id, settings); console.log('[DiscordBot] Setup complete for guild: ' + guild.name); // Send welcome message in alerts const welcomeEmbed = new EmbedBuilder() .setTitle('🎉 Gameserver Bot eingerichtet!') .setDescription( 'Die Gameserver-Kanäle wurden erfolgreich erstellt.\n\n' + '**Kanäle:**\n' + '• <#' + infoChannel.id + '> - Allgemeine Infos\n' + '• <#' + statusChannel.id + '> - Live Server-Status\n' + '• <#' + alertsChannel.id + '> - Benachrichtigungen\n' + '• <#' + updatesChannel.id + '> - Server-Ankündigungen\n' + '• <#' + discussionChannel.id + '> - Diskussion\n' + '• <#' + requestsChannel.id + '> - Server-Vorschläge' ) .setColor(0x22C55E) .setTimestamp(); await alertsChannel.send({ embeds: [welcomeEmbed] }); return settings; } catch (err) { console.error('[DiscordBot] Error setting up guild channels:', err); throw err; } } // ============================================ // Alert Functions // ============================================ async function sendAlertToAllGuilds(embed) { const allSettings = getAllGuildSettings(); for (const settings of allSettings) { if (!settings.alerts_channel_id) continue; try { const channel = await client.channels.fetch(settings.alerts_channel_id); if (channel) { await channel.send({ embeds: [embed] }); } } catch (err) { console.error('[DiscordBot] Error sending alert to guild ' + settings.guild_id + ':', 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(); } if (embed) { await sendAlertToAllGuilds(embed); } } // Check player changes (only if server is online) if (server.status === 'online' && server.playerList) { const currentPlayers = server.playerList; 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 sendAlertToAllGuilds(embed); } } 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 sendAlertToAllGuilds(embed); } } previousPlayerLists.set(server.id, [...currentPlayers]); } else { previousPlayerLists.set(server.id, []); } previousServerState.set(server.id, server.status); } } // ============================================ // Status Update Functions // ============================================ function createLoadingEmbed() { return new EmbedBuilder() .setTitle('🎮 Gameserver Status') .setDescription('Lade Server-Status...') .setColor(0x6B7280) .setTimestamp(); } function createStatusEmbeds(serverStatuses) { 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); } return embeds; } async function fetchServerStatuses() { const config = loadConfig(); return 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: [] }; } })); } async function updateAllStatusMessages(skipAlerts = false) { if (!client) return; try { const serverStatuses = await fetchServerStatuses(); const embeds = createStatusEmbeds(serverStatuses); // Send alerts if (!skipAlerts) { await checkAndSendAlerts(serverStatuses); } else { // Just populate initial state for (const server of serverStatuses) { previousServerState.set(server.id, server.status); previousPlayerLists.set(server.id, server.playerList || []); } } // Update status message in all guilds const allSettings = getAllGuildSettings(); for (const settings of allSettings) { if (!settings.status_channel_id || !settings.status_message_id) continue; try { const channel = await client.channels.fetch(settings.status_channel_id); const message = await channel.messages.fetch(settings.status_message_id); await message.edit({ embeds }); } catch (err) { console.error('[DiscordBot] Error updating status for guild ' + settings.guild_id + ':', err.message); } } } catch (err) { console.error('[DiscordBot] Error updating status messages:', err.message); } } // ============================================ // Public Functions // ============================================ export async function sendUpdateToAllGuilds(embed) { const allSettings = getAllGuildSettings(); for (const settings of allSettings) { if (!settings.updates_channel_id) continue; try { const channel = await client.channels.fetch(settings.updates_channel_id); if (channel) { await channel.send({ embeds: [embed] }); } } catch (err) { console.error('[DiscordBot] Error sending update to guild ' + settings.guild_id + ':', err.message); } } } export async function initDiscordBot() { const token = process.env.DISCORD_BOT_TOKEN; if (!token) { console.log('[DiscordBot] Missing DISCORD_BOT_TOKEN, bot disabled'); return; } // Initialize database table initGuildSettings(); client = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages] }); // Bot joined a new server client.on('guildCreate', async (guild) => { console.log('[DiscordBot] Joined new guild: ' + guild.name + ' (' + guild.id + ')'); try { await setupGuildChannels(guild); } catch (err) { console.error('[DiscordBot] Failed to setup guild:', err.message); } }); // Bot was removed from a server client.on('guildDelete', (guild) => { console.log('[DiscordBot] Left guild: ' + guild.name + ' (' + guild.id + ')'); deleteGuildSettings(guild.id); }); client.once('ready', async () => { console.log('[DiscordBot] Logged in as ' + client.user.tag); // First run - populate state without alerts await updateAllStatusMessages(true); // Regular updates every 60 seconds setInterval(() => updateAllStatusMessages(false), 60000); }); client.login(token).catch(err => { console.error('[DiscordBot] Failed to login:', err.message); }); } export function getDiscordClient() { return client; } // Export setup function for manual setup via API export { setupGuildChannels };