Files
d4rkbot/src/bot/server-monitor.js
T
D4rkst3randClaude Opus 4.8 075ea36bd2 Moderation & Kontakt + Game-Server-Monitor
- Modmail: DMs an den Bot landen als Threads im privaten Staff-Kanal,
  Thread-Antworten gehen als DM zurück (Zustell-Feedback per Reaktion)
- Willkommens-Embed für neue Member (GuildMemberAdd; Server-Members-Intent nötig)
- Mod-Log: gelöschte/bearbeitete User-Nachrichten in privaten Log-Kanal
- Server-Monitor: FiveM-kompatible Endpoints (/dynamic.json) alle 2 min,
  persistentes Status-Embed (edit in place) + Spielerzahl als Bot-Presence
- Neue Intents: DirectMessages, GuildMembers; Partials.Channel
- Setup-Seite: Sektionen // Moderation & Kontakt und // Game-Server

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 13:14:18 +02:00

98 lines
3.6 KiB
JavaScript

// Game-Server-Monitor: pollt FiveM-kompatible HTTP-Endpoints (/dynamic.json)
// und pflegt ein persistentes Status-Embed im Status-Kanal + Bot-Presence.
// Setting "gameservers": kommagetrennt "Name=http://ip:30120, Name2=…"
import { ActivityType, EmbedBuilder, Events } from 'discord.js';
import { getSetting, setSetting } from '../db.js';
const CHECK_INTERVAL_MS = 2 * 60 * 1000;
const TIMEOUT_MS = 8000;
const BRAND_YELLOW = 0xf5c518;
function serverList() {
return (getSetting('gameservers') ?? '')
.split(',')
.map((entry) => {
const [name, ...rest] = entry.split('=');
const url = rest.join('=').trim().replace(/\/$/, '');
return name?.trim() && /^https?:\/\//.test(url) ? { name: name.trim(), url } : null;
})
.filter(Boolean);
}
/** Einen Server abfragen (FiveM-Format: clients, sv_maxclients, hostname) */
async function queryServer(server) {
try {
const res = await fetch(`${server.url}/dynamic.json`, {
signal: AbortSignal.timeout(TIMEOUT_MS),
headers: { 'User-Agent': 'd4rkbot-monitor/1.0' },
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
return {
...server,
online: true,
players: Number(data.clients) || 0,
max: Number(data.sv_maxclients) || 0,
map: data.mapname ?? null,
};
} catch {
return { ...server, online: false, players: 0, max: 0, map: null };
}
}
/** Status-Embed bauen */
function buildEmbed(results) {
const lines = results.map((r) =>
r.online
? `🟢 **${r.name}** — ${r.players}/${r.max} Spieler${r.map ? ` · ${r.map}` : ''}`
: `🔴 **${r.name}** — offline`
);
return new EmbedBuilder()
.setColor(BRAND_YELLOW)
.setTitle('🎮 Server-Status')
.setDescription(lines.join('\n'))
.setFooter({ text: 'D4RKST3R // STATUS • aktualisiert alle 2 min' })
.setTimestamp();
}
/** Ein Poll-Durchlauf — exportiert für Tests und den Timer */
export async function monitorTick(client) {
const servers = serverList();
const channelId = getSetting('status_channel_id');
if (servers.length === 0) return;
const results = await Promise.all(servers.map(queryServer));
// Bot-Presence: Gesamtspielerzahl (nur wenn mindestens ein Server online)
const online = results.filter((r) => r.online);
const totalPlayers = online.reduce((sum, r) => sum + r.players, 0);
try {
client.user?.setActivity?.(
online.length > 0 ? `${totalPlayers} Spieler online` : 'Server offline',
{ type: ActivityType.Watching }
);
} catch { /* Presence ist nice-to-have */ }
// Persistentes Embed im Status-Kanal pflegen (einmal posten, danach editieren)
if (!channelId) return;
const channel = await client.channels.fetch(channelId).catch(() => null);
if (!channel?.isTextBased()) return;
const embed = buildEmbed(results);
const messageId = getSetting('status_message_id');
if (messageId) {
const existing = await channel.messages.fetch(messageId).catch(() => null);
if (existing) {
await existing.edit({ embeds: [embed] });
return;
}
}
const message = await channel.send({ embeds: [embed] });
setSetting('status_message_id', message.id);
}
export function startServerMonitor(client) {
setInterval(() => monitorTick(client).catch((e) => console.error('[monitor] Fehler:', e)), CHECK_INTERVAL_MS);
client.once(Events.ClientReady, () => monitorTick(client).catch(() => {}));
}