// 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); } }); }