Server-Tab (DiscordGSM-Stil), Team-Rechte und Bot-Profil-Beschreibung
- Game-Server: eigene Tabelle statt CSV-Setting (automatische Migration), eigener Setup-Tab mit Server-Builder (Name, Typ fivem/http, Query-URL, Anzeige-Adresse); pro Server ein Live-Embed (gruen/rot, Spieler-Balken, Map, Adresse als Code-Block), edit in place; Embed wird beim Server-Loeschen mit entfernt - Team-System: web_admins mit Bereichs-Scopes (content/community/rollen/bewerbungen/ server/settings/devlogs); Owner behaelt Brand/System/API-Keys/Team exklusiv; 32 Routen-Guards auf Scopes umgestellt, sensible PUT-Felder fuer Team gestrippt; Team-Tab (Owner) + Tab-Filterung im Frontend, /api/me liefert Scopes - Bot-Karte: Profil-Beschreibung (Ueber mich) via application.edit - Fix: settings-Tabelle wird vor der Gameserver-Migration angelegt (frische DBs crashten sonst beim Start) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+67
-57
@@ -1,96 +1,106 @@
|
||||
// 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=…"
|
||||
// Game-Server-Monitor (DiscordGSM-Stil): pro Server ein eigenes Status-Embed
|
||||
// im Status-Kanal (edit in place, grün/rot), alle 2 Minuten aktualisiert.
|
||||
// Server werden im Setup-Tab "Server" verwaltet (Typ: fivem | http).
|
||||
import { ActivityType, EmbedBuilder, Events } from 'discord.js';
|
||||
import { getSetting, setSetting } from '../db.js';
|
||||
import { brandColor, brandFooter, botStatusText } from '../runtime-settings.js';
|
||||
import { getSetting, listGameservers, setGameserverMessage } from '../db.js';
|
||||
import { brandFooter, botStatusText } from '../runtime-settings.js';
|
||||
|
||||
const CHECK_INTERVAL_MS = 2 * 60 * 1000;
|
||||
const TIMEOUT_MS = 8000;
|
||||
const GREEN = 0x23a55a;
|
||||
const RED = 0xf23f43;
|
||||
|
||||
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) {
|
||||
/** Einen Server abfragen — je nach Typ */
|
||||
export async function queryServer(server) {
|
||||
const base = { ...server, online: false, players: null, max: null, map: null };
|
||||
try {
|
||||
const res = await fetch(`${server.url}/dynamic.json`, {
|
||||
if (server.type === 'fivem') {
|
||||
const res = await fetch(`${server.query_url.replace(/\/$/, '')}/dynamic.json`, {
|
||||
signal: AbortSignal.timeout(TIMEOUT_MS),
|
||||
headers: { 'User-Agent': 'd4rkbot-monitor/2.0' },
|
||||
});
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
const data = await res.json();
|
||||
return {
|
||||
...base,
|
||||
online: true,
|
||||
players: Number(data.clients) || 0,
|
||||
max: Number(data.sv_maxclients) || 0,
|
||||
map: data.mapname ?? null,
|
||||
};
|
||||
}
|
||||
// Typ 'http': generischer Healthcheck (2xx/3xx/4xx = erreichbar)
|
||||
const res = await fetch(server.query_url, {
|
||||
signal: AbortSignal.timeout(TIMEOUT_MS),
|
||||
headers: { 'User-Agent': 'd4rkbot-monitor/1.0' },
|
||||
redirect: 'follow',
|
||||
headers: { 'User-Agent': 'd4rkbot-monitor/2.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,
|
||||
};
|
||||
return { ...base, online: res.status < 500 };
|
||||
} catch {
|
||||
return { ...server, online: false, players: 0, max: 0, map: null };
|
||||
return base;
|
||||
}
|
||||
}
|
||||
|
||||
/** 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(brandColor())
|
||||
.setTitle('🎮 Server-Status')
|
||||
.setDescription(lines.join('\n'))
|
||||
.setFooter({ text: `${brandFooter('STATUS')} • aktualisiert alle 2 min` })
|
||||
/** Status-Embed für einen Server bauen (DiscordGSM-Look) */
|
||||
export function buildServerEmbed(r) {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(r.online ? GREEN : RED)
|
||||
.setTitle(`${r.online ? '🟢' : '🔴'} ${r.name}`)
|
||||
.addFields({ name: 'Status', value: r.online ? '**Online**' : '**Offline**', inline: true })
|
||||
.setFooter({ text: `${brandFooter('STATUS')} • alle 2 min` })
|
||||
.setTimestamp();
|
||||
|
||||
if (r.online && r.players != null && r.max) {
|
||||
const pct = Math.min(1, r.players / r.max);
|
||||
const bar = '█'.repeat(Math.round(pct * 10)).padEnd(10, '░');
|
||||
embed.addFields({ name: 'Spieler', value: `\`${bar}\` ${r.players}/${r.max}`, inline: true });
|
||||
}
|
||||
if (r.map) embed.addFields({ name: 'Map', value: r.map, inline: true });
|
||||
if (r.address) embed.addFields({ name: 'Adresse', value: `\`\`\`\n${r.address}\n\`\`\`` });
|
||||
return embed;
|
||||
}
|
||||
|
||||
/** Ein Poll-Durchlauf — exportiert für Tests und den Timer */
|
||||
export async function monitorTick(client) {
|
||||
const servers = serverList();
|
||||
const channelId = getSetting('status_channel_id');
|
||||
const servers = listGameservers();
|
||||
if (servers.length === 0) return;
|
||||
|
||||
const results = await Promise.all(servers.map(queryServer));
|
||||
|
||||
// Bot-Presence: Gesamtspielerzahl — nur wenn kein eigener Status (Brand-Tab) gesetzt ist
|
||||
const online = results.filter((r) => r.online);
|
||||
const totalPlayers = online.reduce((sum, r) => sum + r.players, 0);
|
||||
const withPlayers = results.filter((r) => r.online && r.players != null);
|
||||
const totalPlayers = withPlayers.reduce((sum, r) => sum + r.players, 0);
|
||||
if (!botStatusText()) {
|
||||
try {
|
||||
client.user?.setActivity?.(
|
||||
online.length > 0 ? `${totalPlayers} Spieler online` : 'Server offline',
|
||||
results.some((r) => r.online) ? `${totalPlayers} Spieler online` : 'Server offline',
|
||||
{ type: ActivityType.Watching }
|
||||
);
|
||||
} catch { /* Presence ist nice-to-have */ }
|
||||
}
|
||||
|
||||
// Persistentes Embed im Status-Kanal pflegen (einmal posten, danach editieren)
|
||||
// Pro Server ein Embed pflegen
|
||||
const channelId = getSetting('status_channel_id');
|
||||
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;
|
||||
for (const r of results) {
|
||||
const payload = { embeds: [buildServerEmbed(r)] };
|
||||
try {
|
||||
if (r.message_id) {
|
||||
const existing = await channel.messages.fetch(r.message_id).catch(() => null);
|
||||
if (existing) {
|
||||
await existing.edit(payload);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
const message = await channel.send(payload);
|
||||
setGameserverMessage(r.id, message.id);
|
||||
} catch (error) {
|
||||
console.error(`[monitor] Embed für ${r.name} fehlgeschlagen:`, error.message);
|
||||
}
|
||||
}
|
||||
const message = await channel.send({ embeds: [embed] });
|
||||
setSetting('status_message_id', message.id);
|
||||
}
|
||||
|
||||
export function startServerMonitor(client) {
|
||||
|
||||
Reference in New Issue
Block a user