Files
d4rkbot/src/bot/mod-tools.js
T
D4rkst3randClaude Fable 5 00249f9d8b Server-Monitor v3, Composer v2, Temp-Voice-Panel und Embed-Polish
- Server-Monitor v3: gamedig-Integration (300+ Spiele), GSM-Stil-Embeds
  (Spieler-Balken mit %, Map, Spieler-Liste, klickbarer Connect-Link,
  Ping im Footer), Down/Up-Alerts (2 Fails → 🚨, Recovery →  mit
  Downtime) in konfigurierbaren Alert-Kanal; Server-Tab mit 19
  Spiel-Presets, Host/Port bzw. Query-URL und Connect-URL
- Composer v2: visueller Embed-Builder mit Live-Discord-Preview
  (Autor, Felder, Bilder, Farbe, Footer, Timestamp), speicherbare
  Vorlagen (/api/templates), /api/compose versteht volle Embeds
- Temp-Voice: Control-Panel im erstellten Kanal (Umbenennen, Sperren,
  Limit, Löschen) — nur der Kanal-Besitzer darf bedienen
- Embed-Polish: brandEmbed-Factory (src/embeds.js), Footer überall
  mit Bot-Avatar-Icon

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 02:17:30 +02:00

257 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Moderation & Kontakt: Modmail (DM ↔ Staff-Thread), Willkommens-Embed, Mod-Log
import { ChannelType, EmbedBuilder, Events } from 'discord.js';
import { saveModmail, modmailByUser, modmailByThread, saveStickyRoles, stickyRolesOf } from '../db.js';
import { modmailChannelId, welcomeChannelId, modlogChannelId, publicUrl, autoroleId, stickyRolesEnabled, brandColor, brandColor2, brandFooter } from '../runtime-settings.js';
/* ── Modmail ───────────────────────────────────────── */
/** Staff-Thread für einen User finden oder anlegen */
async function ensureModmailThread(client, user) {
const channelId = modmailChannelId();
if (!channelId) return null;
const channel = await client.channels.fetch(channelId).catch(() => null);
if (!channel?.threads) return null;
const existing = modmailByUser(user.id);
if (existing) {
const thread = await client.channels.fetch(existing.thread_id).catch(() => null);
if (thread) {
if (thread.archived) await thread.setArchived(false).catch(() => {});
return thread;
}
}
const thread = await channel.threads.create({
name: `📬 ${user.username}`,
autoArchiveDuration: 10080, // 7 Tage
type: ChannelType.PublicThread,
});
saveModmail(user.id, thread.id, user.username);
await thread.send({
embeds: [
new EmbedBuilder()
.setColor(brandColor())
.setDescription(
`Neue Modmail-Konversation mit **${user.username}** (<@${user.id}>).\n` +
'Antworten in diesem Thread gehen als DM an den User.'
)
.setFooter({ text: brandFooter('MODMAIL'), iconURL: member.client?.user?.displayAvatarURL?.({ size: 64 }) }),
],
});
return thread;
}
/** DM vom User → in den Staff-Thread spiegeln */
async function handleIncomingDm(client, message) {
const thread = await ensureModmailThread(client, message.author);
if (!thread) return; // Feature aus → DM ignorieren
const files = [...message.attachments.values()].map((a) => a.url).slice(0, 5);
await thread.send({
content: `**${message.author.username}:** ${message.content || ''}${
files.length ? `\n${files.join('\n')}` : ''
}`.slice(0, 2000),
allowedMentions: { parse: [] },
});
await message.react('📬').catch(() => {});
}
/** Staff-Antwort im Thread → als DM an den User */
async function handleThreadReply(client, message) {
const row = modmailByThread(message.channelId);
if (!row) return;
const user = await client.users.fetch(row.user_id).catch(() => null);
if (!user) return;
try {
await user.send(
`**${message.member?.displayName ?? message.author.username}:** ${message.content}`.slice(0, 2000)
);
await message.react('✅').catch(() => {});
} catch {
await message.react('⚠️').catch(() => {});
await message.reply('DM nicht zustellbar (User blockt DMs).').catch(() => {});
}
}
/* ── Willkommens-Embed ─────────────────────────────── */
async function handleMemberAdd(member) {
const channelId = welcomeChannelId();
if (!channelId) return;
const channel = await member.client.channels.fetch(channelId).catch(() => null);
if (!channel?.isTextBased()) return;
const embed = new EmbedBuilder()
.setColor(brandColor())
.setTitle(`👋 Willkommen, ${member.displayName}!`)
.setThumbnail(member.user.displayAvatarURL({ size: 128 }))
.setDescription(
`Schön, dass du da bist — du bist Mitglied **#${member.guild.memberCount}**.\n\n` +
`📔 Devlogs & Roadmap: ${publicUrl()}\n` +
'🧪 Playtester werden? Schau nach dem Bewerbungs-Post!\n' +
'🐛 Bug gefunden? Einfach `/bug` benutzen.'
)
.setFooter({ text: brandFooter('COMMUNITY'), iconURL: member.client?.user?.displayAvatarURL?.({ size: 64 }) })
.setTimestamp();
await channel.send({ content: `<@${member.id}>`, embeds: [embed] });
}
/* ── Auto- & Sticky-Roles ──────────────────────────── */
/** Beim Join: gesicherte Rollen wiederherstellen + Auto-Rolle vergeben */
async function restoreRoles(member) {
const roleIds = new Set();
if (stickyRolesEnabled()) {
for (const id of stickyRolesOf(member.id)) roleIds.add(id);
}
const auto = autoroleId();
if (auto) roleIds.add(auto);
for (const id of roleIds) {
const role = member.guild.roles.cache.get(id);
if (!role || role.managed || role.id === member.guild.id) continue;
await member.roles.add(id).catch((e) =>
console.error(`[roles] Rolle ${id} bei Join fehlgeschlagen: ${e.message}`)
);
}
}
/* ── Mod-Log ───────────────────────────────────────── */
async function logToModlog(client, embed) {
const channelId = modlogChannelId();
if (!channelId) return;
const channel = await client.channels.fetch(channelId).catch(() => null);
if (!channel?.isTextBased()) return;
await channel.send({ embeds: [embed] });
}
function trimmed(text) {
const t = (text ?? '').trim();
return t.length > 900 ? `${t.slice(0, 900)}…` : t || '*[kein Text / nicht im Cache]*';
}
/* ── Registrierung ─────────────────────────────────── */
export function registerModTools(client) {
// Modmail: eingehende DMs + Staff-Antworten in Threads
client.on(Events.MessageCreate, async (message) => {
try {
if (message.author?.bot) return;
if (message.channel?.type === ChannelType.DM) {
await handleIncomingDm(client, message);
} else if (
message.channel?.isThread?.() &&
message.channel.parentId === modmailChannelId()
) {
await handleThreadReply(client, message);
}
} catch (error) {
console.error('[modmail] Fehler:', error);
}
});
// Willkommens-Embed + Auto-/Sticky-Roles + Join-Log
client.on(Events.GuildMemberAdd, async (member) => {
try {
await handleMemberAdd(member);
await restoreRoles(member);
await logToModlog(client, new EmbedBuilder()
.setColor(0x00ff88)
.setTitle('📥 Member beigetreten')
.setDescription(`**${member.user.username}** (<@${member.id}>) — Mitglied #${member.guild.memberCount}`)
.setTimestamp());
} catch (error) {
console.error('[welcome] Fehler:', error);
}
});
// Leave: Rollen für Sticky-Restore sichern + Log
client.on(Events.GuildMemberRemove, async (member) => {
try {
const roleIds = [...(member.roles?.cache?.values() ?? [])]
.filter((r) => r.id !== member.guild.id && !r.managed)
.map((r) => r.id);
if (roleIds.length > 0) saveStickyRoles(member.id, roleIds);
await logToModlog(client, new EmbedBuilder()
.setColor(brandColor2())
.setTitle('📤 Member gegangen')
.setDescription(`**${member.user?.username ?? member.id}**${roleIds.length ? ` — Rollen gesichert (${roleIds.length})` : ''}`)
.setTimestamp());
} catch (error) {
console.error('[modlog] Leave-Handling fehlgeschlagen:', error);
}
});
// Nick-/Rollen-Änderungen loggen
client.on(Events.GuildMemberUpdate, async (oldMember, newMember) => {
try {
const changes = [];
if (oldMember.nickname !== newMember.nickname) {
changes.push({
name: 'Nickname',
value: `\`${oldMember.nickname ?? oldMember.user?.username ?? '—'}\` → \`${newMember.nickname ?? newMember.user?.username ?? '—'}\``,
});
}
const oldIds = new Set(oldMember.roles?.cache?.keys?.() ?? []);
const newIds = new Set(newMember.roles?.cache?.keys?.() ?? []);
const added = [...newIds].filter((id) => !oldIds.has(id) && id !== newMember.guild.id);
const removed = [...oldIds].filter((id) => !newIds.has(id) && id !== newMember.guild.id);
if (added.length) changes.push({ name: 'Rollen +', value: added.map((id) => `<@&${id}>`).join(' ') });
if (removed.length) changes.push({ name: 'Rollen ', value: removed.map((id) => `<@&${id}>`).join(' ') });
if (changes.length === 0) return;
await logToModlog(client, new EmbedBuilder()
.setColor(brandColor())
.setTitle('👤 Member geändert')
.setDescription(`**${newMember.user?.username ?? newMember.id}**`)
.addFields(changes)
.setTimestamp());
} catch (error) {
console.error('[modlog] Member-Update fehlgeschlagen:', error);
}
});
// Mod-Log: gelöschte + bearbeitete Nachrichten (User-Nachrichten in Guild-Kanälen)
client.on(Events.MessageDelete, async (message) => {
try {
if (!message.guildId || message.author?.bot) return;
if (message.channelId === modlogChannelId()) return;
await logToModlog(client, new EmbedBuilder()
.setColor(brandColor2())
.setTitle('🗑️ Nachricht gelöscht')
.addFields(
{ name: 'User', value: message.author ? `${message.author.username}` : 'unbekannt', inline: true },
{ name: 'Kanal', value: `<#${message.channelId}>`, inline: true },
{ name: 'Inhalt', value: trimmed(message.content) }
)
.setTimestamp());
} catch (error) {
console.error('[modlog] Fehler:', error);
}
});
client.on(Events.MessageUpdate, async (oldMessage, newMessage) => {
try {
if (!newMessage.guildId || newMessage.author?.bot) return;
if (oldMessage.content === newMessage.content) return; // z. B. nur Embed-Update
await logToModlog(client, new EmbedBuilder()
.setColor(brandColor())
.setTitle('✏️ Nachricht bearbeitet')
.addFields(
{ name: 'User', value: newMessage.author?.username ?? 'unbekannt', inline: true },
{ name: 'Kanal', value: `<#${newMessage.channelId}>`, inline: true },
{ name: 'Vorher', value: trimmed(oldMessage.content) },
{ name: 'Nachher', value: trimmed(newMessage.content) }
)
.setTimestamp());
} catch (error) {
console.error('[modlog] Fehler:', error);
}
});
}