ja
This commit is contained in:
353
discordBot.js
Normal file
353
discordBot.js
Normal file
@@ -0,0 +1,353 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user