- 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>
98 lines
3.6 KiB
JavaScript
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(() => {}));
|
|
}
|