- Bot creates category and channels automatically when joining a server - Channel structure: info, status, alerts, updates, diskussion, requests (forum) - Add guild_settings database table for per-server configuration - Add Discord bot invite button to Dashboard - Add display settings API functions - Add comprehensive Discord bot documentation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
522 lines
16 KiB
JavaScript
522 lines
16 KiB
JavaScript
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 };
|