// Discord-Client: Commands laden, registrieren und Interactions verarbeiten import { Client, Collection, Events, GatewayIntentBits, MessageFlags, Partials, REST, Routes } from 'discord.js'; import { config } from '../config.js'; import { devlogChannelId, devlogPingRoleId, playtesterRoleId, ticketChannelId } from '../runtime-settings.js'; import { archiveDevlogMessage, removeDevlog } from './devlog-archive.js'; import { registerCommunityListeners } from './community.js'; import { registerModTools } from './mod-tools.js'; import { registerLevels } from './levels.js'; import { handleRoleMenuButton } from './role-menus.js'; import { addPlaytester, removePlaytester, isWish, bumpWish, getGiveaway, toggleGiveawayEntry, giveawayEntries, saveTicket, getTicket, closeTicket, openTicketOf, } from '../db.js'; import { ChannelType, ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from 'discord.js'; import * as ping from './commands/ping.js'; import * as devlogBackfill from './commands/devlog-backfill.js'; import * as bug from './commands/bug.js'; import * as playtesterSetup from './commands/playtester-setup.js'; import * as galerieBackfill from './commands/galerie-backfill.js'; import * as wunsch from './commands/wunsch.js'; import * as giveaway from './commands/giveaway.js'; import * as ticketSetup from './commands/ticket-setup.js'; import * as warn from './commands/warn.js'; import * as warns from './commands/warns.js'; import * as timeout from './commands/timeout.js'; import * as purge from './commands/purge.js'; import * as rank from './commands/rank.js'; import * as tag from './commands/tag.js'; // Alle Commands hier eintragen — jedes Modul exportiert { data, execute } const commandModules = [ ping, devlogBackfill, bug, playtesterSetup, galerieBackfill, wunsch, giveaway, ticketSetup, warn, warns, timeout, purge, rank, tag, ]; export async function startBot() { const client = new Client({ intents: [ GatewayIntentBits.Guilds, // Für das Devlog-Archiv: Nachrichten im Devlog-Kanal mitlesen // (erfordert aktivierten "Message Content Intent" im Developer Portal!) GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent, // Für das Starboard (⭐-Reaktionen) GatewayIntentBits.GuildMessageReactions, // Für Modmail (DMs) — Willkommens-Embeds brauchen zusätzlich den // privilegierten "Server Members Intent" im Developer Portal! GatewayIntentBits.DirectMessages, GatewayIntentBits.GuildMembers, ], // Partials: Delete/Reaction-Events für ungecachte Nachrichten + DM-Kanäle partials: [Partials.Message, Partials.Reaction, Partials.Channel], }); // Commands in Collection ablegen für schnellen Zugriff im Interaction-Handler client.commands = new Collection(); for (const command of commandModules) { client.commands.set(command.data.name, command); } client.once(Events.ClientReady, async (readyClient) => { console.log(`[bot] Eingeloggt als ${readyClient.user.tag}`); await registerCommands(); }); // Live-Archivierung: neue Devlogs (Webhook-Posts im Devlog-Kanal) sofort sichern client.on(Events.MessageCreate, async (message) => { if (message.channelId !== devlogChannelId()) return; try { if (await archiveDevlogMessage(message)) { console.log(`[devlog] Neues Devlog archiviert (${message.id})`); } } catch (error) { console.error('[devlog] Archivierung fehlgeschlagen:', error); } }); // 👍-Reaktionen auf Feature-Wünsche zählen (für die Rangliste auf der Webseite) const trackWishReaction = (delta) => async (reaction) => { try { if (reaction.emoji.name !== '👍') return; if (reaction.partial) await reaction.fetch().catch(() => {}); if (isWish(reaction.message.id)) bumpWish(reaction.message.id, delta); } catch (error) { console.error('[voting] Reaction-Tracking fehlgeschlagen:', error); } }; client.on(Events.MessageReactionAdd, trackWishReaction(1)); client.on(Events.MessageReactionRemove, trackWishReaction(-1)); // In Discord gelöscht = im Archiv gelöscht (inkl. lokaler Bilder) client.on(Events.MessageDelete, async (message) => { if (message.channelId !== devlogChannelId()) return; try { if (await removeDevlog(message.id)) { console.log(`[devlog] Archiv-Eintrag entfernt (Discord-Nachricht ${message.id} gelöscht)`); } } catch (error) { console.error('[devlog] Archiv-Löschung fehlgeschlagen:', error); } }); client.on(Events.InteractionCreate, async (interaction) => { // Autocomplete (z. B. /tag) if (interaction.isAutocomplete()) { const command = client.commands.get(interaction.commandName); await command?.autocomplete?.(interaction).catch((e) => console.error(`[bot] Autocomplete /${interaction.commandName}:`, e) ); return; } // 🔔-Button unterm Devlog: Ping-Rolle selbst an-/abmelden if (interaction.isButton() && interaction.customId === 'devlog_ping_toggle') { const roleId = devlogPingRoleId(); try { if (!roleId || !interaction.inGuild()) { throw new Error('Keine Ping-Rolle konfiguriert'); } const member = interaction.member; const hasRole = member.roles.cache.has(roleId); if (hasRole) { await member.roles.remove(roleId); } else { await member.roles.add(roleId); } await interaction.reply({ content: hasRole ? '🔕 Devlog-Benachrichtigungen **abbestellt**.' : '🔔 Devlog-Benachrichtigungen **abonniert** — du wirst ab jetzt gepingt.', flags: MessageFlags.Ephemeral, }); } catch (error) { console.error('[ping-rolle] Toggle fehlgeschlagen:', error); await interaction .reply({ content: '❌ Konnte die Rolle nicht ändern — dem Bot fehlt vermutlich „Rollen verwalten" oder die Rolle steht über seiner.', flags: MessageFlags.Ephemeral, }) .catch(() => {}); } return; } // Rollen-Menü-Buttons: rolemenu:: if (interaction.isButton() && interaction.customId.startsWith('rolemenu:')) { try { await handleRoleMenuButton(interaction); } catch (error) { console.error('[rollen] Toggle fehlgeschlagen:', error); await interaction .reply({ content: '❌ Rolle konnte nicht geändert werden — Bot-Rechte/Rollen-Reihenfolge prüfen.', flags: MessageFlags.Ephemeral }) .catch(() => {}); } return; } // 🎫-Button: Ticket erstellen (privater Thread) / schließen if (interaction.isButton() && interaction.customId === 'ticket_create') { try { const channelId = ticketChannelId(); if (!channelId) throw new Error('Kein Ticket-Kanal konfiguriert'); // Pro User nur ein offenes Ticket const existing = openTicketOf(interaction.user.id); if (existing) { await interaction.reply({ content: `❕ Du hast schon ein offenes Ticket: <#${existing.thread_id}>`, flags: MessageFlags.Ephemeral, }); return; } const channel = await interaction.client.channels.fetch(channelId); const thread = await channel.threads.create({ name: `🎫 ${interaction.user.username}`, type: ChannelType.PrivateThread, invitable: false, autoArchiveDuration: 10080, }); await thread.members.add(interaction.user.id); saveTicket(thread.id, interaction.user.id); await thread.send({ content: `<@${interaction.user.id}>`, embeds: [ new EmbedBuilder() .setColor(0xf5c518) .setDescription( 'Willkommen in deinem Ticket! Beschreib dein Anliegen — das Team meldet sich.\n' + 'Wenn alles geklärt ist, kann das Ticket hier geschlossen werden.' ) .setFooter({ text: 'D4RKST3R // SUPPORT' }), ], components: [ new ActionRowBuilder().addComponents( new ButtonBuilder().setCustomId('ticket_close').setStyle(ButtonStyle.Danger).setLabel('Ticket schließen').setEmoji('🔒') ), ], }); await interaction.reply({ content: `✅ Dein Ticket: <#${thread.id}>`, flags: MessageFlags.Ephemeral, }); } catch (error) { console.error('[ticket] Erstellung fehlgeschlagen:', error); await interaction .reply({ content: '❌ Ticket konnte nicht erstellt werden — dem Bot fehlen vermutlich Thread-Rechte.', flags: MessageFlags.Ephemeral }) .catch(() => {}); } return; } if (interaction.isButton() && interaction.customId === 'ticket_close') { try { const ticket = getTicket(interaction.channelId); if (!ticket) { await interaction.reply({ content: '❌ Das ist kein Ticket-Thread.', flags: MessageFlags.Ephemeral }); return; } closeTicket(interaction.channelId); await interaction.reply(`🔒 Ticket geschlossen von **${interaction.member?.displayName ?? interaction.user.username}**.`); await interaction.channel.setLocked(true).catch(() => {}); await interaction.channel.setArchived(true).catch(() => {}); } catch (error) { console.error('[ticket] Schließen fehlgeschlagen:', error); } return; } // 🎉-Button: Giveaway-Teilnahme togglen if (interaction.isButton() && interaction.customId === 'giveaway_enter') { const g = getGiveaway(interaction.message.id); if (!g || g.ended) { await interaction.reply({ content: '❌ Dieses Giveaway ist schon vorbei.', flags: MessageFlags.Ephemeral }).catch(() => {}); return; } const entered = toggleGiveawayEntry(g.message_id, interaction.user.id); const count = giveawayEntries(g.message_id).length; await interaction.reply({ content: entered ? `🎉 Du bist dabei! (${count} Teilnahmen)` : `🚪 Teilnahme zurückgezogen. (${count} Teilnahmen)`, flags: MessageFlags.Ephemeral, }).catch(() => {}); return; } // 🧪-Button: Playtester-Rolle + Liste togglen if (interaction.isButton() && interaction.customId === 'playtester_toggle') { const roleId = playtesterRoleId(); try { if (!roleId || !interaction.inGuild()) throw new Error('Keine Playtester-Rolle konfiguriert'); const member = interaction.member; const hasRole = member.roles.cache.has(roleId); if (hasRole) { await member.roles.remove(roleId); removePlaytester(interaction.user.id); } else { await member.roles.add(roleId); addPlaytester(interaction.user.id, interaction.user.username); } await interaction.reply({ content: hasRole ? '🚪 Von der Playtester-Liste **abgemeldet**.' : '🧪 **Willkommen im Playtest-Team!** Du stehst auf der Liste — sobald es losgeht, meldet sich der Bot per DM.', flags: MessageFlags.Ephemeral, }); } catch (error) { console.error('[playtester] Toggle fehlgeschlagen:', error); await interaction .reply({ content: '❌ Konnte die Rolle nicht ändern — Rechte/Rollen-Reihenfolge prüfen.', flags: MessageFlags.Ephemeral }) .catch(() => {}); } return; } if (!interaction.isChatInputCommand()) return; const command = client.commands.get(interaction.commandName); if (!command) return; try { await command.execute(interaction); } catch (error) { console.error(`[bot] Fehler bei /${interaction.commandName}:`, error); const reply = { content: '❌ Da ist etwas schiefgelaufen.', ephemeral: true }; if (interaction.replied || interaction.deferred) { await interaction.followUp(reply).catch(() => {}); } else { await interaction.reply(reply).catch(() => {}); } } }); registerCommunityListeners(client); registerModTools(client); registerLevels(client); await client.login(config.discordToken); return client; } /** Slash-Commands bei Discord registrieren (Guild = sofort sichtbar, global = bis zu 1h Delay) */ async function registerCommands() { const rest = new REST().setToken(config.discordToken); const body = commandModules.map((c) => c.data.toJSON()); const route = config.discordGuildId ? Routes.applicationGuildCommands(config.discordClientId, config.discordGuildId) : Routes.applicationCommands(config.discordClientId); await rest.put(route, { body }); console.log( `[bot] ${body.length} Slash-Command(s) registriert (${config.discordGuildId ? 'Guild' : 'global'})` ); }