Files
GSM/gsm-backend/services/discordBot.js
Alexander Zielonka 4fcc111def Add multi-guild Discord bot support with auto-setup
- 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>
2026-01-07 18:29:13 +01:00

522 lines
16 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 };