- 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>
257 lines
11 KiB
JavaScript
257 lines
11 KiB
JavaScript
// 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);
|
||
}
|
||
});
|
||
}
|