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>
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
// 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(() => {}));
|
||||
}
|
||||
Reference in New Issue
Block a user