import { NodeSSH } from "node-ssh"; import { readFileSync } from "fs"; import { dirname, join } from "path"; import { fileURLToPath } from "url"; import { EmbedBuilder } from "discord.js"; import { sendUpdateToAllGuilds } from "./discordBot.js"; const __dirname = dirname(fileURLToPath(import.meta.url)); function loadConfig() { return JSON.parse(readFileSync(join(__dirname, "..", "config.json"), "utf-8")); } // Server logos for Discord embeds const serverLogos = { minecraft: 'https://gsm.zeasy.dev/minecraft.png', factorio: 'https://gsm.zeasy.dev/factorio.png', zomboid: 'https://gsm.zeasy.dev/zomboid.png', vrising: 'https://gsm.zeasy.dev/vrising.png', palworld: 'https://gsm.zeasy.dev/palworld.png', terraria: 'https://gsm.zeasy.dev/terraria.png', openttd: 'https://gsm.zeasy.dev/openttd.png', hytale: 'https://gsm.zeasy.dev/hytale.png' }; // SSH connection helper (reuse existing pattern) const sshConnections = new Map(); const SSH_TIMEOUT = 30000; // Longer timeout for update operations async function getConnection(host, username = "root") { const key = username + "@" + host; if (sshConnections.has(key)) { const conn = sshConnections.get(key); if (conn.isConnected()) return conn; sshConnections.delete(key); } const ssh = new NodeSSH(); await ssh.connect({ host, username, privateKeyPath: "/root/.ssh/id_ed25519", readyTimeout: SSH_TIMEOUT }); sshConnections.set(key, ssh); return ssh; } /** * Send Discord notification about server update */ async function sendUpdateNotification(server, updateInfo) { try { const embed = new EmbedBuilder() .setTitle(`${server.name} wurde aktualisiert!`) .setDescription(updateInfo.message || "Der Server wurde erfolgreich aktualisiert.") .setColor(0x00FF00) // Green for success .setTimestamp(); if (serverLogos[server.type]) { embed.setThumbnail(serverLogos[server.type]); } if (updateInfo.oldVersion && updateInfo.newVersion) { embed.addFields( { name: "Alte Version", value: updateInfo.oldVersion, inline: true }, { name: "Neue Version", value: updateInfo.newVersion, inline: true } ); } await sendUpdateToAllGuilds(embed); console.log(`[Update] Discord notification sent for ${server.name}`); } catch (err) { console.error(`[Update] Failed to send Discord notification:`, err.message); // Don't throw - update was successful, just notification failed } } // ============================================================================= // Update Handler Registry - Add new game handlers here // ============================================================================= const updateHandlers = { // Factorio (Docker-based) factorio: { supportsUpdate: true, gameName: "Factorio", async checkForUpdate(server) { const ssh = await getConnection(server.host, server.sshUser); // Get current image ID const currentResult = await ssh.execCommand( `docker inspect --format='{{.Image}}' ${server.containerName} 2>/dev/null` ); const currentImageId = currentResult.stdout.trim(); if (!currentImageId) { return { hasUpdate: false, error: "Container not found" }; } // Pull latest image and check if different console.log("[Update] Checking for Factorio updates..."); const pullResult = await ssh.execCommand( "docker pull factoriotools/factorio:latest 2>&1", { execOptions: { timeout: 120000 } } ); // Check if image was updated const isUpToDate = pullResult.stdout.includes("Image is up to date") || pullResult.stdout.includes("Status: Image is up to date"); if (isUpToDate) { return { hasUpdate: false, currentVersion: "latest", message: "Server ist auf dem neuesten Stand" }; } // Get new image ID const newResult = await ssh.execCommand( "docker inspect --format='{{.Id}}' factoriotools/factorio:latest 2>/dev/null" ); const newImageId = newResult.stdout.trim(); // Compare image IDs const hasUpdate = currentImageId !== newImageId && newImageId.length > 0; return { hasUpdate, currentVersion: currentImageId.substring(7, 19), newVersion: newImageId.substring(7, 19), message: hasUpdate ? "Neues Update verfügbar!" : "Server ist auf dem neuesten Stand" }; }, async performUpdate(server) { const ssh = await getConnection(server.host, server.sshUser); // Verify server is stopped const statusResult = await ssh.execCommand( `docker inspect --format='{{.State.Status}}' ${server.containerName} 2>/dev/null` ); if (statusResult.stdout.trim() === "running") { throw new Error("Server muss gestoppt sein um zu updaten"); } // Get current image ID for version comparison const oldImageResult = await ssh.execCommand( `docker inspect --format='{{.Image}}' ${server.containerName} 2>/dev/null` ); const oldImageId = oldImageResult.stdout.trim().substring(7, 19); console.log("[Update] Starting Factorio update..."); // Pull latest image const pullResult = await ssh.execCommand( "docker pull factoriotools/factorio:latest 2>&1", { execOptions: { timeout: 180000 } } ); console.log("[Update] Pull result:", pullResult.stdout); // Get current container config for recreation const configResult = await ssh.execCommand(` docker inspect ${server.containerName} --format='{{range .Config.Env}}{{println .}}{{end}}' 2>/dev/null `); // Parse environment variables const envVars = {}; configResult.stdout.split('\n').forEach(line => { const [key, ...valueParts] = line.split('='); if (key && valueParts.length > 0) { envVars[key] = valueParts.join('='); } }); // Remove old container console.log("[Update] Removing old container..."); await ssh.execCommand(`docker stop ${server.containerName} 2>/dev/null || true`); await ssh.execCommand(`docker rm ${server.containerName} 2>/dev/null || true`); // Build environment string const envString = Object.entries(envVars) .filter(([k]) => ['SAVE_NAME', 'LOAD_LATEST_SAVE', 'TZ', 'PUID', 'PGID'].includes(k)) .map(([k, v]) => `-e ${k}="${v}"`) .join(' '); // Create new container with updated image console.log("[Update] Creating new container with updated image..."); const createResult = await ssh.execCommand(` docker create \ --name ${server.containerName} \ -p 34197:34197/udp \ -p 27015:27015/tcp \ -v /srv/docker/factorio/data:/factorio \ ${envString || '-e TZ=Europe/Berlin -e PUID=845 -e PGID=845'} \ --restart=unless-stopped \ factoriotools/factorio:latest `); if (createResult.code !== 0) { throw new Error("Container creation failed: " + createResult.stderr); } // Get new image ID const newImageResult = await ssh.execCommand( "docker inspect --format='{{.Id}}' factoriotools/factorio:latest 2>/dev/null" ); const newImageId = newImageResult.stdout.trim().substring(7, 19); // Clean up old images console.log("[Update] Cleaning up old images..."); await ssh.execCommand("docker image prune -f 2>/dev/null || true"); return { success: true, message: "Update erfolgreich! Server kann jetzt gestartet werden.", oldVersion: oldImageId, newVersion: newImageId }; } }, // Add more game handlers here in the future // Example template for other games: /* minecraft: { supportsUpdate: true, gameName: "Minecraft", async checkForUpdate(server) { // Implementation for Minecraft updates return { hasUpdate: false, message: "Not implemented" }; }, async performUpdate(server) { // Implementation for Minecraft updates return { success: false, message: "Not implemented" }; } }, */ }; // ============================================================================= // Public API // ============================================================================= /** * Check if a server type supports updates */ export function supportsUpdate(serverType) { return updateHandlers[serverType]?.supportsUpdate === true; } /** * Get list of all server types that support updates */ export function getUpdatableServerTypes() { return Object.entries(updateHandlers) .filter(([_, handler]) => handler.supportsUpdate) .map(([type]) => type); } /** * Check if an update is available for a server */ export async function checkForUpdate(server) { const handler = updateHandlers[server.type]; if (!handler || !handler.supportsUpdate) { return { hasUpdate: false, supported: false, message: "Updates für diesen Servertyp nicht unterstützt" }; } try { const result = await handler.checkForUpdate(server); return { ...result, supported: true }; } catch (err) { console.error(`[Update] Check failed for ${server.id}:`, err.message); return { hasUpdate: false, supported: true, error: err.message }; } } /** * Perform update for a server (with automatic Discord notification) */ export async function performUpdate(server, sendDiscordNotification = true) { const handler = updateHandlers[server.type]; if (!handler || !handler.supportsUpdate) { throw new Error("Updates für diesen Servertyp nicht unterstützt"); } const result = await handler.performUpdate(server); // Send Discord notification on successful update if (result.success && sendDiscordNotification) { await sendUpdateNotification(server, result); } return result; }