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>
This commit is contained in:
197
docs/discord-bot.md
Normal file
197
docs/discord-bot.md
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
# Discord Bot
|
||||||
|
|
||||||
|
Der GSM Discord Bot bietet Live-Status-Updates für alle Gameserver direkt in Discord. Der Bot kann auf mehreren Discord-Servern gleichzeitig laufen.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Live-Status**: Automatisch aktualisiertes Embed mit Server-Status, Spielerzahlen und Metriken
|
||||||
|
- **Alerts**: Benachrichtigungen wenn Server online/offline gehen oder Spieler joinen/leaven
|
||||||
|
- **Multi-Guild**: Kann auf beliebig vielen Discord-Servern eingesetzt werden
|
||||||
|
- **Auto-Setup**: Erstellt automatisch alle nötigen Channels beim Beitreten
|
||||||
|
|
||||||
|
## Bot einladen
|
||||||
|
|
||||||
|
### Invite-Link
|
||||||
|
|
||||||
|
```
|
||||||
|
https://discord.com/oauth2/authorize?client_id=1458251194806833306&permissions=34359831568&integration_type=0&scope=bot+applications.commands
|
||||||
|
```
|
||||||
|
|
||||||
|
Der Link ist auch im GSM Dashboard unter den Server-Cards verfügbar.
|
||||||
|
|
||||||
|
### Benötigte Permissions
|
||||||
|
|
||||||
|
| Permission | Verwendung |
|
||||||
|
|------------|------------|
|
||||||
|
| View Channels | Channels sehen |
|
||||||
|
| Manage Channels | Channels erstellen |
|
||||||
|
| Send Messages | Nachrichten senden |
|
||||||
|
| Manage Messages | Eigene Nachrichten bearbeiten |
|
||||||
|
| Embed Links | Rich Embeds für Status |
|
||||||
|
| Read Message History | Alte Nachrichten lesen |
|
||||||
|
| Create Public Threads | Forum-Threads erstellen |
|
||||||
|
|
||||||
|
## Automatisch erstellte Channel-Struktur
|
||||||
|
|
||||||
|
Wenn der Bot einem Server beitritt, erstellt er automatisch folgende Struktur:
|
||||||
|
|
||||||
|
```
|
||||||
|
🎮 Gameserver (Kategorie)
|
||||||
|
├── ℹ️│info - Informationen zum GSM System
|
||||||
|
├── 📊│status - Live-Status aller Gameserver (Auto-Update)
|
||||||
|
├── 📢│alerts - Server-Events und Spieler-Benachrichtigungen
|
||||||
|
├── 📰│updates - Ankündigungen zu neuen Gameservern
|
||||||
|
├── 💬│diskussion - Diskussions-Channel (User können schreiben)
|
||||||
|
└── 💡│requests - Forum für Gameserver-Vorschläge
|
||||||
|
```
|
||||||
|
|
||||||
|
### Channel-Permissions
|
||||||
|
|
||||||
|
| Channel | @everyone | Bot |
|
||||||
|
|---------|-----------|-----|
|
||||||
|
| Kategorie | Lesen | Schreiben |
|
||||||
|
| info | Lesen | Schreiben |
|
||||||
|
| status | Lesen | Schreiben |
|
||||||
|
| alerts | Lesen | Schreiben |
|
||||||
|
| updates | Lesen | Schreiben |
|
||||||
|
| diskussion | Lesen + Schreiben | Schreiben |
|
||||||
|
| requests | Lesen + Threads erstellen | Schreiben |
|
||||||
|
|
||||||
|
## Datenbank
|
||||||
|
|
||||||
|
### guild_settings Tabelle
|
||||||
|
|
||||||
|
Speichert die Channel-IDs für jeden Discord-Server:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE guild_settings (
|
||||||
|
guild_id TEXT PRIMARY KEY,
|
||||||
|
category_id TEXT,
|
||||||
|
info_channel_id TEXT,
|
||||||
|
status_channel_id TEXT,
|
||||||
|
status_message_id TEXT,
|
||||||
|
alerts_channel_id TEXT,
|
||||||
|
updates_channel_id TEXT,
|
||||||
|
discussion_channel_id TEXT,
|
||||||
|
requests_channel_id TEXT,
|
||||||
|
requests_info_thread_id TEXT,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### DB-Funktionen
|
||||||
|
|
||||||
|
In `backend/db/init.js`:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
initGuildSettings() // Tabelle erstellen
|
||||||
|
getGuildSettings(guildId) // Settings für einen Server
|
||||||
|
getAllGuildSettings() // Alle Server-Settings
|
||||||
|
setGuildSettings(guildId, {}) // Settings speichern
|
||||||
|
deleteGuildSettings(guildId) // Settings löschen (bei Bot-Kick)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Bot-Events
|
||||||
|
|
||||||
|
### guildCreate
|
||||||
|
|
||||||
|
Wird ausgelöst wenn der Bot einem neuen Server beitritt:
|
||||||
|
|
||||||
|
1. Erstellt Kategorie und alle Channels
|
||||||
|
2. Postet Info-Nachricht im `ℹ️│info` Channel
|
||||||
|
3. Erstellt Info-Thread im `💡│requests` Forum
|
||||||
|
4. Speichert alle Channel-IDs in der Datenbank
|
||||||
|
5. Sendet erste Status-Nachricht
|
||||||
|
|
||||||
|
### guildDelete
|
||||||
|
|
||||||
|
Wird ausgelöst wenn der Bot von einem Server entfernt wird:
|
||||||
|
|
||||||
|
1. Löscht alle Settings aus der Datenbank
|
||||||
|
|
||||||
|
## Status-Updates
|
||||||
|
|
||||||
|
Der Bot aktualisiert die Status-Nachricht in allen registrierten Guilds alle 30 Sekunden:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
async function updateAllGuildStatus() {
|
||||||
|
const guilds = getAllGuildSettings();
|
||||||
|
for (const guild of guilds) {
|
||||||
|
await updateGuildStatus(guild);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Jeder Server bekommt ein eigenes Embed mit:
|
||||||
|
- Server-Name und Status (Online/Offline)
|
||||||
|
- Aktuelle Spielerzahl
|
||||||
|
- CPU und RAM Auslastung
|
||||||
|
- Verbindungsadresse
|
||||||
|
|
||||||
|
## Konfiguration
|
||||||
|
|
||||||
|
### Umgebungsvariablen (.env)
|
||||||
|
|
||||||
|
```env
|
||||||
|
DISCORD_CLIENT_ID=1458251194806833306
|
||||||
|
DISCORD_CLIENT_SECRET=xxx
|
||||||
|
DISCORD_BOT_TOKEN=xxx
|
||||||
|
DISCORD_GUILD_ID=729865854329815051 # Haupt-Server für Login
|
||||||
|
DISCORD_ADMIN_ROLE_ID=1024693717434650736
|
||||||
|
DISCORD_MOD_ROLE_ID=1024693170958766141
|
||||||
|
```
|
||||||
|
|
||||||
|
**Hinweis**: `DISCORD_GUILD_ID` wird nur für den Discord OAuth Login verwendet, nicht für den Bot selbst.
|
||||||
|
|
||||||
|
### Developer Portal Einstellungen
|
||||||
|
|
||||||
|
1. Gehe zu https://discord.com/developers/applications
|
||||||
|
2. Wähle die Bot-Application
|
||||||
|
3. **Bot** Tab:
|
||||||
|
- "Public Bot" aktivieren (damit andere einladen können)
|
||||||
|
- Privileged Gateway Intents:
|
||||||
|
- Server Members Intent: Optional
|
||||||
|
- Message Content Intent: Nicht benötigt
|
||||||
|
|
||||||
|
## Dateien
|
||||||
|
|
||||||
|
| Datei | Beschreibung |
|
||||||
|
|-------|--------------|
|
||||||
|
| `backend/services/discordBot.js` | Bot-Logik und Event-Handler |
|
||||||
|
| `backend/db/init.js` | Guild-Settings DB-Funktionen |
|
||||||
|
| `frontend/src/pages/Dashboard.jsx` | Invite-Button im Dashboard |
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Bot erstellt keine Channels
|
||||||
|
|
||||||
|
- Prüfen ob Bot "Manage Channels" Permission hat
|
||||||
|
- Prüfen ob Bot-Rolle hoch genug in der Rollen-Hierarchie ist
|
||||||
|
|
||||||
|
### Status-Nachricht wird nicht aktualisiert
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pm2 logs gameserver-backend --lines 50
|
||||||
|
```
|
||||||
|
|
||||||
|
Suche nach `[DiscordBot]` Log-Einträgen.
|
||||||
|
|
||||||
|
### Bot aus Datenbank entfernen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sqlite3 /opt/gameserver-monitor/backend/users.sqlite
|
||||||
|
DELETE FROM guild_settings WHERE guild_id = 'xxx';
|
||||||
|
```
|
||||||
|
|
||||||
|
## Login vs. Bot
|
||||||
|
|
||||||
|
| Feature | Login (OAuth) | Bot |
|
||||||
|
|---------|--------------|-----|
|
||||||
|
| Erfordert Mitgliedschaft | Haupt-Discord | Nein |
|
||||||
|
| Server-Steuerung | Ja (je nach Rolle) | Nein |
|
||||||
|
| Status sehen | Ja | Ja |
|
||||||
|
| Alerts erhalten | Nein | Ja |
|
||||||
|
| Verfügbar für | Haupt-Discord Mitglieder | Alle mit Bot |
|
||||||
|
|
||||||
|
Der Login zur Webapp erfordert Mitgliedschaft im Haupt-Discord-Server (DISCORD_GUILD_ID). Der Bot ist davon unabhängig und zeigt nur passive Status-Updates.
|
||||||
521
gsm-backend/services/discordBot.js
Normal file
521
gsm-backend/services/discordBot.js
Normal file
@@ -0,0 +1,521 @@
|
|||||||
|
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 };
|
||||||
@@ -249,3 +249,27 @@ export async function getActivityLog(token, limit = 100) {
|
|||||||
headers: { Authorization: `Bearer ${token}` },
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Display Settings
|
||||||
|
export async function getAllDisplaySettings(token) {
|
||||||
|
return fetchAPI('/servers/display-settings', {
|
||||||
|
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getDisplaySettings(token, serverId) {
|
||||||
|
return fetchAPI(`/servers/${serverId}/display-settings`, {
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function saveDisplaySettings(token, serverId, address, hint) {
|
||||||
|
return fetchAPI(`/servers/${serverId}/display-settings`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
body: JSON.stringify({ address, hint }),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alias for backwards compatibility
|
||||||
|
export const setDisplaySettings = saveDisplaySettings
|
||||||
|
|||||||
@@ -230,6 +230,34 @@ export default function Dashboard({ onLogin, onLogout }) {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Discord Bot Invite Section */}
|
||||||
|
<div className="mt-12 fade-in-up" style={{ animationDelay: '300ms' }}>
|
||||||
|
<div className="card p-6 flex flex-col sm:flex-row items-center justify-center gap-4 sm:gap-6">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<svg className="w-10 h-10 text-[#5865F2]" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"/>
|
||||||
|
</svg>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-white font-semibold">Discord Bot</h3>
|
||||||
|
<p className="text-sm text-neutral-400">
|
||||||
|
Willst du Server-Status Updates auch auf deinem Discord?
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
href="https://discord.com/oauth2/authorize?client_id=1458251194806833306&permissions=34359831568&integration_type=0&scope=bot+applications.commands"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="btn btn-primary flex items-center gap-2 whitespace-nowrap"
|
||||||
|
>
|
||||||
|
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"/>
|
||||||
|
</svg>
|
||||||
|
Bot einladen
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
{/* Modals */}
|
{/* Modals */}
|
||||||
|
|||||||
Reference in New Issue
Block a user