require('dotenv').config(); const { Client, GatewayIntentBits, Partials, EmbedBuilder, ActionRowBuilder, StringSelectMenuBuilder, SlashCommandBuilder, ChannelType, ThreadAutoArchiveDuration, ModalBuilder, TextInputBuilder, TextInputStyle, PermissionFlagsBits, ButtonBuilder, ButtonStyle, AttachmentBuilder } = require('discord.js'); const sqlite3 = require('sqlite3').verbose(); const { open } = require('sqlite'); const fs = require('fs'); const config = require('./config.json'); // ========================================== // IDs & KONFIGURATION // ========================================== const MODERATOR_ROLE_ID = "1478343762915627108"; const CO_LEADER_ROLE_ID = "1478343618191167638"; const LEADER_ROLE_ID = "1477781833902194811"; const STERNCHEN_ROLE_ID = "1477770276044406937"; const MEMBER_ROLE_ID = "1477776881473294449"; const TEAM_ROLE_ID = "1478336280088674429"; const SUPER_ADMIN_ID = "202779624881651712"; const STATS_CHANNEL_ID = "1478335587093319721"; const RULES_CHANNEL_ID = "1477779136138318117"; const GIVEAWAY_CHANNEL_ID = "1478352923099398316"; const TRANSCRIPT_CHANNEL_ID = "1478354410760310805"; const SANCTION_CHANNEL_ID = "1479950240713805977"; const LEVEL_UP_CHANNEL_ID = "1478422814984900618"; const WELCOME_LEAVE_CHANNEL_ID = "1477779103376478381"; const NO_XP_VOICE_CHANNEL_ID = "1478414540625674272"; const GIVEAWAY_ROLE_NAME = "ɢɪᴠᴇᴀᴡᴀʏ-ᴀᴄᴄᴇꜱꜱ"; const VC_PERMS_ROLE_NAME = "ᴠᴄ-ʀᴏʟᴇ-ᴘᴇʀᴍꜱ"; const VC_ACCESS_ROLE_NAME = "ᴠᴄ-ᴀᴄᴄᴇꜱꜱ"; // --- ZEIT INTERVALLE --- const TIME_48H = 48 * 60 * 60 * 1000; const TIME_12H = 12 * 60 * 60 * 1000; const CHECK_INTERVAL = 5 * 60 * 1000; const GIVEAWAY_INTERVAL = 60 * 1000; const VOICE_XP_INTERVAL = 5 * 60 * 1000; const TEXT_XP_COOLDOWN = 60 * 1000; // ========================================== // ANTI-CRASH SYSTEM // ========================================== process.on('unhandledRejection', (reason) => { console.error('Unhandled Rejection:', reason); }); process.on('uncaughtException', (err) => { console.error('Uncaught Exception:', err); }); process.on('uncaughtExceptionMonitor', (err) => { console.error('Uncaught Exception Monitor:', err); }); // ========================================== // HILFSFUNKTIONEN // ========================================== const COLORS = { user: "#00BFFF", team: "#FF8C00", success: "#32CD32", error: "#DC143C", main: "#2b2d31", info: "#FFA500", giveaway: "#9b59b6", level: "#FFD700" }; function parseDuration(durationStr) { const match = durationStr.toLowerCase().match(/^(\d+)([mhd])$/); if (!match) { return null; } const val = parseInt(match[1]); const unit = match[2]; if (unit === 'm') { return val * 60 * 1000; } if (unit === 'h') { return val * 60 * 60 * 1000; } if (unit === 'd') { return val * 24 * 60 * 60 * 1000; } return null; } function generateTicketId() { return 'TKT-' + Math.random().toString(36).substring(2, 8).toUpperCase(); } function escapeHTML(str) { if (!str) { return ""; } return str.replace(/[&<>'"]/g, tag => ({ '&': '&', '<': '<', '>': '>', "'": ''', '"': '"' }[tag] || tag)); } function getMinotarUrl(mcName) { if (!mcName || mcName.trim() === "") mcName = "Steve"; return `https://minotar.net/helm/${encodeURIComponent(mcName)}/256.png`; } function simpleEmbed(text, color = COLORS.main, title = null) { const embed = new EmbedBuilder() .setDescription(text) .setColor(color); if (title) { embed.setTitle(title); } return embed; } function getForumTags(forumChannel) { let tags = { unbearbeitet: null, inbearbeitung: null, geschlossen: null }; if (!forumChannel || !forumChannel.availableTags) { return tags; } for (const t of forumChannel.availableTags) { const name = t.name.toLowerCase().replace(' ', ''); if (tags[name] !== undefined) { tags[name] = t.id; } } return tags; } // ========================================== // VERBESSERTER TRANSCRIPT GENERATOR // ========================================== async function createTranscript(thread, ticketId, category, closedBy, reason) { let allMessages = []; let lastId; while (true) { const options = { limit: 100 }; if (lastId) { options.before = lastId; } const messages = await thread.messages.fetch(options).catch(() => null); if (!messages || messages.size === 0) { break; } allMessages.push(...messages.values()); lastId = messages.last().id; } allMessages.reverse(); let messagesHtml = ""; for (const msg of allMessages) { let realAuthor = escapeHTML(msg.author.username); let avatar = msg.author.displayAvatarURL({ extension: 'png', size: 64 }); const time = new Date(msg.createdTimestamp).toLocaleString('de-DE'); let content = escapeHTML(msg.content).replace(/\n/g, '
'); let msgClass = "internal-msg"; let badge = "Team (Intern)"; let skipFirstEmbed = false; if (msg.author.bot && msg.embeds.length > 0) { const embed = msg.embeds[0]; const cUser = parseInt(COLORS.user.replace('#', ''), 16); const cTeam = parseInt(COLORS.team.replace('#', ''), 16); const cSys1 = parseInt(COLORS.success.replace('#', ''), 16); const cSys2 = parseInt(COLORS.error.replace('#', ''), 16); const cSys3 = parseInt(COLORS.info.replace('#', ''), 16); if (embed.color === cUser) { msgClass = "user-msg"; badge = "User"; if (embed.author && embed.author.name) realAuthor = escapeHTML(embed.author.name); if (embed.thumbnail && embed.thumbnail.url) avatar = embed.thumbnail.url; else if (embed.author && embed.author.iconURL) avatar = embed.author.iconURL; if (embed.description) content += (content ? "

" : "") + escapeHTML(embed.description).replace(/\n/g, '
'); skipFirstEmbed = true; } else if (embed.color === cTeam) { msgClass = "team-msg"; badge = "Team → User"; if (embed.author && embed.author.name) realAuthor = escapeHTML(embed.author.name.replace('[Team] ', '').replace('Support | ', '')); if (embed.thumbnail && embed.thumbnail.url) avatar = embed.thumbnail.url; else if (embed.author && embed.author.iconURL) avatar = embed.author.iconURL; if (embed.description) { let desc = embed.description.replace('**Nachricht:**\n', ''); content += (content ? "

" : "") + escapeHTML(desc).replace(/\n/g, '
'); } skipFirstEmbed = true; } else if ([cSys1, cSys2, cSys3].includes(embed.color) || embed.title) { msgClass = "system-msg"; badge = "System"; realAuthor = "Support Bot"; avatar = client.user.displayAvatarURL({ extension: 'png', size: 64 }); } } let attachmentsHtml = ""; if (msg.attachments.size > 0) { msg.attachments.forEach(a => { attachmentsHtml += `
📎 [Anhang: ${escapeHTML(a.name)}]`; }); } let embedsHtml = ""; if (msg.embeds.length > 0) { msg.embeds.forEach((embed, index) => { if (index === 0 && skipFirstEmbed) return; embedsHtml += `
`; if (embed.title) embedsHtml += `${escapeHTML(embed.title)}
`; if (embed.description) embedsHtml += `${escapeHTML(embed.description).replace(/\n/g, '
')}`; if (embed.fields && embed.fields.length > 0) { embedsHtml += `

`; embed.fields.forEach(field => { embedsHtml += `${escapeHTML(field.name)}
${escapeHTML(field.value).replace(/\n/g, '
')}

`; }); } embedsHtml += `
`; }); } if (content === "" && attachmentsHtml === "" && embedsHtml === "") continue; messagesHtml += `
Avatar
${realAuthor} ${badge} ${time}
${content}${attachmentsHtml}${embedsHtml}
`; } const htmlContent = ` Transcript: ${ticketId}

Ticket Transcript: ${ticketId}

Kategorie: ${category}

Geschlossen durch: ${closedBy}

Grund: ${reason}

Datum: ${new Date().toLocaleString('de-DE')}

${messagesHtml} `; return new AttachmentBuilder(Buffer.from(htmlContent, 'utf-8'), { name: `transcript-${ticketId}.html` }); } function createProgressBar(currentXP, neededXP, size = 15) { const percentage = currentXP / neededXP; const progress = Math.round(size * percentage); const emptyProgress = size - progress; const progressText = '🟩'.repeat(progress); const emptyProgressText = '⬛'.repeat(emptyProgress); return progressText + emptyProgressText; } // ========================================== // REGELN-DATEI SETUP (regeln.txt) // ========================================== const RULES_FILE = './regeln.txt'; if (!fs.existsSync(RULES_FILE)) { const defaultRules = `**1. Kein unnötiges Markieren (@everyone / @here)** Bitte nutze Pings mit Bedacht und spamme keine Rollen oder User für unwichtige Dinge. **2. Keine Team-Anfragen** Bitte fragt weder hier im Discord noch Ingame nach einem Team-Rang. Wir kommen auf euch zu, wenn wir Verstärkung suchen. **3. Keine Privatnachrichten an das Team** Bitte schreibt Moderatoren oder Entwickler nicht ungefragt privat an. Nutzt für Support-Anfragen ausschließlich unser Ticket-System. **4. Respektvoller Umgang & Kein Spam** Ständiges Pingen oder das unaufgeforderte Nachjoinen in Voice-Channels stört den Ablauf. Bitte respektiert die Privatsphäre aller Mitglieder. **5. Keine Fragen nach Updates oder Uploads** Videos, Streams und Server-Updates kommen, wenn sie fertig sind. Ständiges Nachfragen verzögert die Arbeit eher, als dass es hilft. **6. Fokus auf das Spiel** Wir schätzen gutes Gameplay und einen fairen Wettbewerb. Konzentriert euch auf das Spiel und haltet das Niveau im Chat angemessen. **7. Entscheidungen des Teams** Die Entscheidungen der Admins und Moderatoren sind endgültig und zu respektieren. Endlose Diskussionen darüber sind nicht erwünscht. **8. Sachlicher Support** Wenn du gebannt wurdest oder Hilfe brauchst, öffne ein Ticket und warte geduldig. Bleibe dabei bitte stets sachlich und freundlich. --- **💡 Hinweis:** Unwissenheit schützt nicht vor Strafen. Bitte haltet euch an dieses Regelwerk, damit wir alle zusammen eine gute Zeit auf dem Server haben!`; fs.writeFileSync(RULES_FILE, defaultRules, 'utf8'); } // ========================================== // DATENBANK SETUP (SQLite3) // ========================================== let db; async function initDB() { db = await open({ filename: '/app/data/data.db', driver: sqlite3.Database }); await db.exec(` CREATE TABLE IF NOT EXISTS categories (name TEXT PRIMARY KEY, description TEXT, emoji TEXT, forum_id TEXT); CREATE TABLE IF NOT EXISTS questions (id INTEGER PRIMARY KEY AUTOINCREMENT, category_name TEXT, label TEXT, placeholder TEXT, style INTEGER, required INTEGER); CREATE TABLE IF NOT EXISTS active_tickets (user_id TEXT PRIMARY KEY, thread_id TEXT, category TEXT, last_activity INTEGER, warning_sent INTEGER, claimed_by TEXT DEFAULT NULL); CREATE TABLE IF NOT EXISTS team_emojis (user_id TEXT PRIMARY KEY, emoji TEXT UNIQUE); CREATE TABLE IF NOT EXISTS settings (key TEXT PRIMARY KEY, value TEXT); CREATE TABLE IF NOT EXISTS stats_categories (category_name TEXT PRIMARY KEY, count INTEGER DEFAULT 0); CREATE TABLE IF NOT EXISTS stats_team (user_id TEXT PRIMARY KEY, closed_count INTEGER DEFAULT 0); CREATE TABLE IF NOT EXISTS giveaways (message_id TEXT PRIMARY KEY, channel_id TEXT, prize TEXT, winners_count INTEGER, end_time INTEGER); CREATE TABLE IF NOT EXISTS giveaway_entries (message_id TEXT, user_id TEXT, PRIMARY KEY (message_id, user_id)); CREATE TABLE IF NOT EXISTS users_xp ( user_id TEXT PRIMARY KEY, xp INTEGER DEFAULT 0, level INTEGER DEFAULT 0, last_msg_timestamp INTEGER DEFAULT 0 ); CREATE TABLE IF NOT EXISTS level_rewards ( level INTEGER PRIMARY KEY, reward_text TEXT ); CREATE TABLE IF NOT EXISTS user_claims ( user_id TEXT, level INTEGER, PRIMARY KEY (user_id, level) ); CREATE TABLE IF NOT EXISTS ticket_notifications ( thread_id TEXT, user_id TEXT, PRIMARY KEY (thread_id, user_id) ); CREATE TABLE IF NOT EXISTS sanctions ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id TEXT, mc_name TEXT, reason TEXT, amount TEXT, timestamp INTEGER ); CREATE TABLE IF NOT EXISTS mc_names ( user_id TEXT PRIMARY KEY, mc_name TEXT ); `); const activeTicketsInfo = await db.all("PRAGMA table_info(active_tickets)"); const hasTicketId = activeTicketsInfo.some(col => col.name === 'ticket_id'); if (!hasTicketId) { await db.exec("ALTER TABLE active_tickets ADD COLUMN ticket_id TEXT DEFAULT 'N/A'"); console.log("✅ Datenbank aktualisiert: ticket_id Spalte hinzugefügt."); } const hasMcName = activeTicketsInfo.some(col => col.name === 'mc_name'); if (!hasMcName) { await db.exec("ALTER TABLE active_tickets ADD COLUMN mc_name TEXT DEFAULT 'Steve'"); console.log("✅ Datenbank aktualisiert: mc_name Spalte in Tickets hinzugefügt."); } await db.run("INSERT OR IGNORE INTO settings (key, value) VALUES ('stat_total_opened', '0')"); await db.run("INSERT OR IGNORE INTO settings (key, value) VALUES ('stat_total_closed', '0')"); const catCount = await db.get("SELECT COUNT(*) as count FROM categories"); if (catCount.count === 0) { await db.run("INSERT INTO categories (name, description, emoji, forum_id) VALUES (?, ?, ?, NULL)", ['Allgemein', 'Allgemeiner Support & Fragen', '📩']); await db.run("INSERT INTO questions (category_name, label, placeholder, style, required) VALUES (?, ?, ?, ?, ?)", ['Allgemein', 'Was ist dein Anliegen?', 'Beschreibe dein Anliegen kurz...', 2, 0]); await db.run("INSERT INTO questions (category_name, label, placeholder, style, required) VALUES (?, ?, ?, ?, ?)", ['Allgemein', 'Minecraft-Name', 'Dein genauer Ingame-Name', 1, 1]); await db.run("INSERT INTO categories (name, description, emoji, forum_id) VALUES (?, ?, ?, NULL)", ['Bewerbung', 'Bewerbung für den Chaos Clan', '📝']); await db.run("INSERT INTO questions (category_name, label, placeholder, style, required) VALUES (?, ?, ?, ?, ?)", ['Bewerbung', 'Wieso in den Chaos Clan?', 'Warum möchtest du ein Teil von uns werden?', 2, 1]); await db.run("INSERT INTO questions (category_name, label, placeholder, style, required) VALUES (?, ?, ?, ?, ?)", ['Bewerbung', 'Minecraft-Name', 'Dein genauer Ingame-Name', 1, 1]); await db.run("INSERT INTO questions (category_name, label, placeholder, style, required) VALUES (?, ?, ?, ?, ?)", ['Bewerbung', 'Netto-Vermögen ($)', 'Dein Vermögen (Items + Geld zusammengerechnet)', 1, 1]); await db.run("INSERT INTO questions (category_name, label, placeholder, style, required) VALUES (?, ?, ?, ?, ?)", ['Bewerbung', '3.000.000$ Eintrittsgebühr?', 'Bereit, die 3 Mio. $ Clanbank-Gebühr zu zahlen?', 1, 1]); } } // --- CLIENT SETUP --- const client = new Client({ intents: [ GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent, GatewayIntentBits.DirectMessages, GatewayIntentBits.GuildVoiceStates ], partials: [Partials.Channel, Partials.Message] }); // ========================================== // XP & LEVEL LOGIK // ========================================== async function addXP(member, amount) { if (!member || member.user.bot) { return; } const userId = member.id; let userData = await db.get("SELECT * FROM users_xp WHERE user_id = ?", [userId]); if (!userData) { await db.run("INSERT INTO users_xp (user_id, xp, level, last_msg_timestamp) VALUES (?, ?, ?, ?)", [userId, 0, 0, 0]); userData = { user_id: userId, xp: 0, level: 0, last_msg_timestamp: 0 }; } let currentXP = userData.xp + amount; let oldLevel = userData.level; let currentLevel = oldLevel; let neededXP = 100 * Math.pow(currentLevel + 1, 2); let leveledUp = false; while (currentXP >= neededXP) { currentLevel++; leveledUp = true; neededXP = 100 * Math.pow(currentLevel + 1, 2); } await db.run("UPDATE users_xp SET xp = ?, level = ? WHERE user_id = ?", [currentXP, currentLevel, userId]); if (leveledUp) { let newRoleAssigned = null; if (currentLevel % 5 === 0 && currentLevel > 0) { const roleName = `Level ${currentLevel}`; let role = member.guild.roles.cache.find(r => r.name === roleName); if (!role) { try { role = await member.guild.roles.create({ name: roleName, color: COLORS.level, reason: `Auto-generierte Rolle für Level ${currentLevel}` }); } catch (e) { console.error(`Konnte die Rolle '${roleName}' nicht erstellen. Rechte prüfen!`); } } if (role && !member.roles.cache.has(role.id)) { try { await member.roles.add(role); newRoleAssigned = role.id; } catch (e) { console.error(`Konnte Rolle '${roleName}' nicht an ${member.user.tag} vergeben.`); } } } const unlockedRewards = await db.all("SELECT * FROM level_rewards WHERE level > ? AND level <= ?", [oldLevel, currentLevel]); const levelEmbed = new EmbedBuilder() .setTitle("🎉 LEVEL UP!") .setDescription(`Herzlichen Glückwunsch, ${member}! Du bist auf **Level ${currentLevel}** aufgestiegen!`) .setColor(COLORS.level) .setThumbnail(member.user.displayAvatarURL()); if (newRoleAssigned) { levelEmbed.addFields({ name: "Neue Rolle freigeschaltet:", value: `<@&${newRoleAssigned}>` }); } if (unlockedRewards.length > 0) { let rewardText = unlockedRewards.map(r => `🎁 **Level ${r.level}:** ${r.reward_text}`).join('\n'); levelEmbed.addFields({ name: "Neue Belohnungen freigeschaltet!", value: `${rewardText}\n\n*Öffne ein Ticket, um deine Belohnung(en) einzufordern!*` }); } const levelUpChannel = member.guild.channels.cache.get(LEVEL_UP_CHANNEL_ID); if (levelUpChannel) { await levelUpChannel.send({ content: `${member}`, embeds: [levelEmbed] }).catch(() => {}); } } } // ========================================== // EVENTS // ========================================== client.once('clientReady', async () => { console.log(`🤖 Eingeloggt als ${client.user.tag}`); client.user.setPresence({ activities: [{ name: 'Chaos Clan auf HugoSMP', type: 1, url: 'https://www.twitch.tv/letshugotv' }], status: 'online' }); try { const guild = await client.guilds.fetch(config.guildId).catch(() => null); if (!guild) { return console.error("Guild nicht gefunden. Bitte überprüfe die config.json."); } try { let giveawayRole = guild.roles.cache.find(r => r.name === GIVEAWAY_ROLE_NAME); if (!giveawayRole) { giveawayRole = await guild.roles.create({ name: GIVEAWAY_ROLE_NAME, color: COLORS.giveaway, reason: 'Auto-generierte Rolle für Giveaway Access' }); console.log(`✅ Giveaway-Rolle (${GIVEAWAY_ROLE_NAME}) automatisch erstellt.`); } let vcPermsRole = guild.roles.cache.find(r => r.name === VC_PERMS_ROLE_NAME); if (!vcPermsRole) { vcPermsRole = await guild.roles.create({ name: VC_PERMS_ROLE_NAME, color: "#95a5a6", reason: 'Auto-generierte Rolle für VC Access Vergabe' }); console.log(`✅ VC-Perms-Rolle (${VC_PERMS_ROLE_NAME}) automatisch erstellt.`); } let vcAccessRole = guild.roles.cache.find(r => r.name === VC_ACCESS_ROLE_NAME); if (!vcAccessRole) { vcAccessRole = await guild.roles.create({ name: VC_ACCESS_ROLE_NAME, color: "#2ecc71", reason: 'Auto-generierte Rolle für Voice Channel Access' }); console.log(`✅ VC-Access-Rolle (${VC_ACCESS_ROLE_NAME}) automatisch erstellt.`); } } catch (e) { console.error(`Konnte Rollen nicht überprüfen/erstellen:`, e); } const commands = [ new SlashCommandBuilder().setName('setup').setDescription('Spawnt das Ticket-Panel im Setup-Channel (Nur Super-Admin)'), new SlashCommandBuilder().setName('regeln').setDescription('Aktualisiert die Server-Regeln aus der regeln.txt Datei (Nur Super-Admin)'), new SlashCommandBuilder().setName('addcategory').setDescription('Fügt eine Ticket-Kategorie & ein eigenes Forum hinzu (Nur Super-Admin)') .addStringOption(opt => opt.setName('name').setDescription('Name').setRequired(true)) .addStringOption(opt => opt.setName('beschreibung').setDescription('Beschreibung').setRequired(true)) .addStringOption(opt => opt.setName('emoji').setDescription('Emoji (optional)').setRequired(false)), new SlashCommandBuilder().setName('removecategory').setDescription('Entfernt eine Ticket-Kategorie (Nur Super-Admin)') .addStringOption(opt => opt.setName('name').setDescription('Name').setRequired(true).setAutocomplete(true)), new SlashCommandBuilder().setName('addquestion').setDescription('Fügt eine Frage hinzu (Nur Super-Admin)') .addStringOption(opt => opt.setName('kategorie').setDescription('Kategorie').setRequired(true).setAutocomplete(true)) .addStringOption(opt => opt.setName('frage').setDescription('Sichtbare Frage (Max 45 Zeichen!)').setRequired(true).setMaxLength(45)) .addStringOption(opt => opt.setName('typ').setDescription('Textlänge').setRequired(true).addChoices({name: 'Kurz (Einzeiler)', value: 'short'}, {name: 'Lang (Absatz)', value: 'paragraph'})) .addBooleanOption(opt => opt.setName('pflicht').setDescription('Pflichtfeld?').setRequired(true)), new SlashCommandBuilder().setName('removequestion').setDescription('Entfernt eine Frage aus einer Kategorie (Nur Super-Admin)') .addStringOption(opt => opt.setName('kategorie').setDescription('Kategorie').setRequired(true).setAutocomplete(true)) .addIntegerOption(opt => opt.setName('fragennummer').setDescription('Nummer (1-5)').setRequired(true)), new SlashCommandBuilder().setName('emoji').setDescription('Setze dein persönliches Team-Emoji') .addStringOption(opt => opt.setName('emoji').setDescription('Dein Emoji').setRequired(true).setMaxLength(50)), new SlashCommandBuilder().setName('resetemoji').setDescription('Setzt das Emoji eines Teamlers zurück (Nur Super-Admin)') .addUserOption(opt => opt.setName('user').setDescription('Der Teamler').setRequired(true)), new SlashCommandBuilder().setName('setmcname').setDescription('Setzt den Minecraft-Namen für einen Teamler (Nur Super-Admin)') .addUserOption(opt => opt.setName('user').setDescription('Der Teamler').setRequired(true)) .addStringOption(opt => opt.setName('mc_name').setDescription('Der Minecraft-Name').setRequired(true)), new SlashCommandBuilder().setName('reply').setDescription('Antwortet dem User im Ticket') .addStringOption(opt => opt.setName('nachricht').setDescription('Deine Nachricht').setRequired(true)), new SlashCommandBuilder().setName('close').setDescription('Schließt das aktuelle Ticket') .addStringOption(opt => opt.setName('grund').setDescription('Grund').setRequired(false)), new SlashCommandBuilder().setName('annehmen').setDescription('Nimmt die Bewerbung an (gibt Rolle, ändert Nickname & schließt Ticket)'), new SlashCommandBuilder().setName('ablehnen').setDescription('Lehnt die Bewerbung ab (schließt Ticket)') .addStringOption(opt => opt.setName('grund').setDescription('Begründung').setRequired(false)), new SlashCommandBuilder().setName('giveaway').setDescription('Startet ein neues Giveaway') .addStringOption(opt => opt.setName('preis').setDescription('Was gibt es zu gewinnen?').setRequired(true)) .addIntegerOption(opt => opt.setName('gewinner').setDescription('Anzahl der Gewinner').setRequired(true)) .addStringOption(opt => opt.setName('dauer').setDescription('Dauer (z.B. 10m, 2h, 1d)').setRequired(true)), new SlashCommandBuilder().setName('reroll').setDescription('Zieht einen neuen Gewinner für ein Giveaway') .addStringOption(opt => opt.setName('message_id').setDescription('Die Nachrichten-ID des Giveaways').setRequired(true)), new SlashCommandBuilder().setName('level').setDescription('Zeigt dein aktuelles Level und deine XP an') .addUserOption(opt => opt.setName('user').setDescription('Zeige das Level eines anderen Users an').setRequired(false)), new SlashCommandBuilder().setName('xptop').setDescription('Zeigt das Server-Leaderboard für Level und XP an'), new SlashCommandBuilder().setName('addreward').setDescription('Erstellt eine neue Belohnung für ein bestimmtes Level (Nur Super-Admin)') .addIntegerOption(opt => opt.setName('level').setDescription('Für welches Level?').setRequired(true)) .addStringOption(opt => opt.setName('belohnung').setDescription('Was ist die Belohnung? (z.B. 10.000$ Ingame)').setRequired(true)), new SlashCommandBuilder().setName('removereward').setDescription('Löscht eine Belohnung (Nur Super-Admin)') .addIntegerOption(opt => opt.setName('level').setDescription('Das Level der Belohnung').setRequired(true)), new SlashCommandBuilder().setName('rewards').setDescription('Zeigt alle verfügbaren Belohnungen an') .addUserOption(opt => opt.setName('user').setDescription('Für einen bestimmten User').setRequired(false)), new SlashCommandBuilder().setName('claimreward').setDescription('Markiert eine Belohnung für einen User als ausgegeben (Team)') .addUserOption(opt => opt.setName('user').setDescription('Der User, der die Belohnung erhält').setRequired(true)) .addIntegerOption(opt => opt.setName('level').setDescription('Das Level der Belohnung').setRequired(true)), new SlashCommandBuilder().setName('vcperms').setDescription('Vergibt oder entfernt die VC Access Rolle bei einem User') .addUserOption(opt => opt.setName('user').setDescription('Der User, der die Rolle bekommen/verlieren soll').setRequired(true)), new SlashCommandBuilder().setName('notify').setDescription('Aktiviert oder deaktiviert Benachrichtigungen für dieses Ticket (Team)'), new SlashCommandBuilder().setName('sanktion').setDescription('Verwalte das Sanktions-System (Team)') .addSubcommand(sub => sub.setName('add').setDescription('Füge eine neue Sanktion hinzu') .addUserOption(opt => opt.setName('user').setDescription('Der Discord-User').setRequired(true)) .addStringOption(opt => opt.setName('mc_name').setDescription('Der Minecraft Username').setRequired(true)) .addStringOption(opt => opt.setName('grund').setDescription('Sanktionsgrund (z.B. Verstoß gegen Regel X)').setRequired(true)) .addStringOption(opt => opt.setName('hoehe').setDescription('Sanktionshöhe (z.B. 10.000$ oder 1 Woche Ban)').setRequired(true))) .addSubcommand(sub => sub.setName('remove').setDescription('Entferne eine Sanktion') .addIntegerOption(opt => opt.setName('id').setDescription('Die Sanktions-ID (steht im Info-Channel)').setRequired(true))), // NEUER COMMAND AUS INDEX.JS HIER HINZUGEFÜGT new SlashCommandBuilder().setName('auszahlen').setDescription('Zahlt Geld vom Bank-Account an einen Spieler aus.') .addStringOption(opt => opt.setName('empfänger').setDescription('Der exakte Ingame-Name des Spielers').setRequired(true)) .addNumberOption(opt => opt.setName('betrag').setDescription('Die Summe, die ausgezahlt werden soll').setRequired(true)) ]; await guild.commands.set(commands); console.log("✅ Slash-Commands registriert."); const categories = await db.all("SELECT * FROM categories"); const setupChannel = guild.channels.cache.get(config.ticketCreationChannelId); const parentId = setupChannel ? setupChannel.parentId : null; const allChannels = await guild.channels.fetch(); for (const cat of categories) { let forum = cat.forum_id ? allChannels.get(cat.forum_id) : null; const expectedName = `${cat.emoji ? cat.emoji + '-' : ''}${cat.name}-tickets`.toLowerCase(); if (!forum) { forum = allChannels.find(c => c.type === ChannelType.GuildForum && c.name === expectedName); if (forum) { await db.run("UPDATE categories SET forum_id = ? WHERE name = ?", [forum.id, cat.name]); } } if (!forum) { try { forum = await guild.channels.create({ name: expectedName, type: ChannelType.GuildForum, parent: parentId, availableTags: [ { name: 'Unbearbeitet', moderated: true }, { name: 'In Bearbeitung', moderated: true }, { name: 'Geschlossen', moderated: true } ], permissionOverwrites: [ { id: guild.id, deny: [PermissionFlagsBits.ViewChannel] }, { id: TEAM_ROLE_ID, allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.SendMessages, PermissionFlagsBits.SendMessagesInThreads, PermissionFlagsBits.ManageThreads, PermissionFlagsBits.CreatePublicThreads, PermissionFlagsBits.CreatePrivateThreads] } ] }); await db.run("UPDATE categories SET forum_id = ? WHERE name = ?", [forum.id, cat.name]); } catch (e) { console.error("Fehler bei Forenerstellung: ", e); } } } await updateTicketPanel(client); await updateStatsPanel(client); await updateSanctionPanel(client); setInterval(async () => { try { const now = Date.now(); const tickets = await db.all("SELECT * FROM active_tickets"); for (const ticket of tickets) { try { const thread = await client.channels.fetch(ticket.thread_id).catch(() => null); if (!thread || thread.archived || thread.locked) { await db.run("DELETE FROM active_tickets WHERE thread_id = ?", [ticket.thread_id]); await db.run("DELETE FROM ticket_notifications WHERE thread_id = ?", [ticket.thread_id]); continue; } if (ticket.warning_sent === 0 && (now - ticket.last_activity) > TIME_48H) { await db.run("UPDATE active_tickets SET warning_sent = 1, last_activity = ? WHERE user_id = ?", [now, ticket.user_id]); const warnEmbed = simpleEmbed("⏳ **Inaktivität:** In diesem Ticket gab es seit 48 Stunden keine neue Nachricht. Bitte schreibe eine Nachricht, ansonsten wird das Ticket in 12 Stunden automatisch geschlossen.", COLORS.info, "Ticket Erinnerung"); try { const user = await client.users.fetch(ticket.user_id); await user.send({ embeds: [warnEmbed] }); } catch(e) {} const claimerPing = ticket.claimed_by ? `<@${ticket.claimed_by}>` : ""; let sendPayload = { embeds: [warnEmbed] }; if (claimerPing) sendPayload.content = claimerPing; await thread.send(sendPayload); } else if (ticket.warning_sent === 1 && (now - ticket.last_activity) > TIME_12H) { const reason = "Automatische Schließung wegen Inaktivität."; try { const user = await client.users.fetch(ticket.user_id); const closeEmbed = new EmbedBuilder() .setAuthor({ name: "Chaos Clan Support", iconURL: guild.iconURL() }) .setTitle(`🔒 Dein Ticket wurde geschlossen (${ticket.ticket_id})`) .setDescription(`Dein Ticket wurde automatisch geschlossen.\n\n**Grund:** ${reason}`) .setColor(COLORS.error) .setTimestamp(); await user.send({ embeds: [closeEmbed] }); } catch(e) {} const transcriptChannel = await guild.channels.fetch(TRANSCRIPT_CHANNEL_ID).catch(() => null); if (transcriptChannel) { const file = await createTranscript(thread, ticket.ticket_id, ticket.category, "Bot (Auto-Timeout)", reason); await transcriptChannel.send({ content: `📄 **Neues Transcript:** Ticket \`${ticket.ticket_id}\` (${ticket.category})\nGeschlossen von: Auto-Timeout`, files: [file] }); } const closeThreadEmbed = simpleEmbed(`🔒 Ticket automatisch geschlossen.\n**Grund:** ${reason}`, COLORS.error, "Wegen Inaktivität geschlossen"); await thread.send({ embeds: [closeThreadEmbed] }); const forum = await client.channels.fetch(thread.parentId).catch(() => null); const tags = getForumTags(forum); let appliedTags = thread.appliedTags.filter(id => id !== tags.unbearbeitet && id !== tags.inbearbeitung); if (tags.geschlossen && !appliedTags.includes(tags.geschlossen)) { appliedTags.push(tags.geschlossen); } await thread.setAppliedTags(appliedTags).catch(() => {}); await thread.setLocked(true).catch(() => {}); await thread.setArchived(true).catch(() => {}); await db.run("UPDATE settings SET value = CAST(value AS INTEGER) + 1 WHERE key = 'stat_total_closed'"); if (ticket.claimed_by) { await db.run("INSERT OR IGNORE INTO stats_team (user_id, closed_count) VALUES (?, 0)", [ticket.claimed_by]); await db.run("UPDATE stats_team SET closed_count = closed_count + 1 WHERE user_id = ?", [ticket.claimed_by]); } await db.run("DELETE FROM active_tickets WHERE user_id = ?", [ticket.user_id]); await db.run("DELETE FROM ticket_notifications WHERE thread_id = ?", [ticket.thread_id]); await updateStatsPanel(client); } } catch (ticketError) { console.error(`Fehler bei der Timer-Verarbeitung von Ticket ${ticket.thread_id}:`, ticketError); } } } catch(err) { console.error("Fehler im Timer:", err); } }, CHECK_INTERVAL); setInterval(async () => { try { const endedGiveaways = await db.all("SELECT * FROM giveaways WHERE end_time <= ?", [Date.now()]); for (const ga of endedGiveaways) { try { const channel = await client.channels.fetch(ga.channel_id).catch(() => null); if (channel) { const message = await channel.messages.fetch(ga.message_id).catch(() => null); const entries = await db.all("SELECT user_id FROM giveaway_entries WHERE message_id = ?", [ga.message_id]); let winnerText = ""; if (entries.length === 0) { winnerText = "Niemand hat teilgenommen. 😢"; } else { const shuffled = entries.sort(() => 0.5 - Math.random()); const winners = shuffled.slice(0, ga.winners_count); winnerText = winners.map(w => `<@${w.user_id}>`).join(', '); } let hostString = ""; if (message && message.embeds.length > 0) { const oldDesc = message.embeds[0].description || ""; const hostMatch = oldDesc.match(/👤 \*\*Gestartet von:\*\* .+/); if (hostMatch) hostString = `\n${hostMatch[0]}`; } if (message) { const originalEmbed = EmbedBuilder.from(message.embeds[0]) .setDescription(`**Dieses Giveaway ist beendet!**\n\n🏆 **Gewinner:** ${winnerText}\n👥 **Teilnehmer insgesamt:** ${entries.length}${hostString}`) .setColor("#808080"); const disabledRow = new ActionRowBuilder().addComponents( new ButtonBuilder() .setCustomId('giveaway_ended') .setLabel('Beendet') .setStyle(ButtonStyle.Secondary) .setDisabled(true) ); await message.edit({ embeds: [originalEmbed], components: [disabledRow] }); } const resultEmbed = new EmbedBuilder() .setTitle(`🎉 Giveaway Beendet: ${ga.prize}`) .setDescription(`Herzlichen Glückwunsch an:\n${winnerText}\n\nBitte meldet euch beim Team, um euren Gewinn zu erhalten!${hostString}`) .setColor(COLORS.giveaway) .setTimestamp(); await channel.send({ embeds: [resultEmbed] }); } await db.run("DELETE FROM giveaways WHERE message_id = ?", [ga.message_id]); } catch (gaError) { console.error(gaError); } } } catch(err) { console.error(err); } }, GIVEAWAY_INTERVAL); setInterval(async () => { try { const fetchedGuild = await client.guilds.fetch(config.guildId).catch(() => null); if (!fetchedGuild) return; const voiceChannels = fetchedGuild.channels.cache.filter(c => c.isVoiceBased()); for (const [channelId, vc] of voiceChannels) { if (vc.id === fetchedGuild.afkChannelId) continue; if (vc.id === NO_XP_VOICE_CHANNEL_ID) continue; if (vc.members.size < 2) continue; for (const [memberId, member] of vc.members) { if (member.user.bot) continue; if (member.voice.mute || member.voice.deaf || member.voice.selfMute || member.voice.selfDeaf || member.voice.serverMute || member.voice.serverDeaf) { continue; } const xpToAdd = Math.floor(Math.random() * 16) + 10; await addXP(member, xpToAdd); } } } catch (err) { console.error("Fehler im Voice XP Timer:", err); } }, VOICE_XP_INTERVAL); } catch (err) { console.error("Fehler im Ready-Event:", err); } }); // ========================================== // WELCOME & LEAVE EVENTS // ========================================== client.on('guildMemberAdd', async (member) => { try { const welcomeChannel = member.guild.channels.cache.get(WELCOME_LEAVE_CHANNEL_ID); if (!welcomeChannel) return; const welcomeEmbed = new EmbedBuilder() .setAuthor({ name: "Neues Mitglied im Chaos Clan!", iconURL: member.guild.iconURL() }) .setTitle(`Willkommen, ${member.user.username}! 🎉`) .setDescription(`Hallo ${member}, herzlich willkommen auf dem **Chaos Clan Discord**!\n\nSchön, dass du da bist. Bitte lies dir unsere Regeln durch und hab eine tolle Zeit bei uns!`) .setColor(COLORS.success) .setThumbnail(member.user.displayAvatarURL({ dynamic: true, size: 256 })) .setFooter({ text: `Wir sind jetzt ${member.guild.memberCount} Mitglieder!` }) .setTimestamp(); await welcomeChannel.send({ content: `${member}`, embeds: [welcomeEmbed] }); } catch (error) { console.error("Fehler im Welcome-Event:", error); } }); client.on('guildMemberRemove', async (member) => { try { const leaveChannel = member.guild.channels.cache.get(WELCOME_LEAVE_CHANNEL_ID); if (!leaveChannel) return; const leaveEmbed = new EmbedBuilder() .setAuthor({ name: "Ein Mitglied hat uns verlassen", iconURL: member.guild.iconURL() }) .setDescription(`**${member.user.username}** hat den Server verlassen.\n\nSchade, dass du gehst. Mach's gut! 🍂`) .setColor(COLORS.error) .setThumbnail(member.user.displayAvatarURL({ dynamic: true, size: 256 })) .setFooter({ text: `Wir sind jetzt ${member.guild.memberCount} Mitglieder.` }) .setTimestamp(); await leaveChannel.send({ embeds: [leaveEmbed] }); } catch (error) { console.error("Fehler im Leave-Event:", error); } }); client.on('interactionCreate', async (interaction) => { try { if (interaction.guild && interaction.guild.id !== config.guildId) return; // ========================================== // BUTTON HANDLER (Für Giveaways) // ========================================== if (interaction.isButton() && interaction.customId === 'giveaway_enter') { const msgId = interaction.message.id; const userId = interaction.user.id; const ga = await db.get("SELECT * FROM giveaways WHERE message_id = ?", [msgId]); if (!ga) { return interaction.reply({ embeds: [simpleEmbed("❌ Dieses Giveaway ist bereits beendet oder existiert nicht mehr!", COLORS.error)], ephemeral: true }); } try { await db.run("INSERT INTO giveaway_entries (message_id, user_id) VALUES (?, ?)", [msgId, userId]); const countResult = await db.get("SELECT COUNT(*) as count FROM giveaway_entries WHERE message_id = ?", [msgId]); const currentCount = countResult.count; const message = interaction.message; if (message && message.embeds.length > 0) { const oldEmbed = message.embeds[0]; const newDesc = oldEmbed.description.replace(/👥 \*\*Teilnehmer:\*\* \d+/, `👥 **Teilnehmer:** ${currentCount}`); const updatedEmbed = EmbedBuilder.from(oldEmbed).setDescription(newDesc); await message.edit({ embeds: [updatedEmbed] }); } return interaction.reply({ embeds: [simpleEmbed("✅ Du hast erfolgreich am Giveaway teilgenommen! Viel Glück! 🍀", COLORS.success)], ephemeral: true }); } catch (e) { return interaction.reply({ embeds: [simpleEmbed("❌ Du nimmst bereits an diesem Giveaway teil!", COLORS.error)], ephemeral: true }); } } // ========================================== // AUTOCOMPLETE HANDLER // ========================================== if (interaction.isAutocomplete()) { const focusedValue = interaction.options.getFocused().toLowerCase(); const categories = await db.all("SELECT name FROM categories"); const choices = categories.map(c => c.name); const filtered = choices.filter(choice => choice.toLowerCase().includes(focusedValue)); await interaction.respond( filtered.map(choice => ({ name: choice, value: choice })) ).catch(() => {}); return; } // ========================================== // SLASH COMMANDS HANDLER // ========================================== if (interaction.isChatInputCommand()) { const { commandName } = interaction; // RBAC ABFRAGEN const isSuperAdmin = interaction.user.id === SUPER_ADMIN_ID; const isGuildAdmin = interaction.member.permissions.has(PermissionFlagsBits.Administrator); const isHighAdminRole = interaction.member.roles.cache.has(CO_LEADER_ROLE_ID) || interaction.member.roles.cache.has(LEADER_ROLE_ID) || interaction.member.roles.cache.has(STERNCHEN_ROLE_ID); const isHighAdmin = isSuperAdmin || isGuildAdmin || isHighAdminRole; const isModRole = interaction.member.roles.cache.has(MODERATOR_ROLE_ID) || interaction.member.roles.cache.has(TEAM_ROLE_ID) || isHighAdmin; const adminCommands = ['setup', 'regeln', 'addcategory', 'removecategory', 'addquestion', 'removequestion', 'addreward', 'removereward', 'setmcname']; const teamCommands = ['reply', 'close', 'annehmen', 'ablehnen', 'emoji', 'claimreward', 'notify', 'sanktion']; if (teamCommands.includes(commandName) && !isModRole) { return interaction.reply({ embeds: [simpleEmbed("❌ Du hast keine Rechte dafür.", COLORS.error)], ephemeral: true }); } if (adminCommands.includes(commandName) && !isSuperAdmin) { return interaction.reply({ embeds: [simpleEmbed("❌ Nur der Super-Admin kann diesen Command ausführen.", COLORS.error)], ephemeral: true }); } // --- SETMCNAME COMMAND --- if (commandName === 'setmcname') { await interaction.deferReply({ ephemeral: true }); try { const targetUser = interaction.options.getUser('user'); const mcName = interaction.options.getString('mc_name'); await db.run("INSERT OR REPLACE INTO mc_names (user_id, mc_name) VALUES (?, ?)", [targetUser.id, mcName]); const targetMember = await interaction.guild.members.fetch(targetUser.id).catch(() => null); if (targetMember) { await targetMember.setNickname(mcName).catch(() => {}); } return interaction.editReply({ embeds: [simpleEmbed(`✅ Der Minecraft-Name von ${targetUser} wurde erfolgreich auf **${mcName}** gesetzt und der Nickname wurde aktualisiert!`, COLORS.success)] }); } catch (error) { console.error("Fehler bei /setmcname:", error); return interaction.editReply({ embeds: [simpleEmbed(`❌ Es gab einen Fehler beim Setzen des Namens.`, COLORS.error)] }); } } // --- SANKTION COMMAND --- if (commandName === 'sanktion') { const subcommand = interaction.options.getSubcommand(); if (subcommand === 'add') { const user = interaction.options.getUser('user'); const mcName = interaction.options.getString('mc_name'); const grund = interaction.options.getString('grund'); const hoehe = interaction.options.getString('hoehe'); const timestamp = Math.floor(Date.now() / 1000); await db.run("INSERT INTO sanctions (user_id, mc_name, reason, amount, timestamp) VALUES (?, ?, ?, ?, ?)", [user.id, mcName, grund, hoehe, timestamp]); await updateSanctionPanel(interaction.client); return interaction.reply({ embeds: [simpleEmbed(`✅ Sanktion erfolgreich für ${user} eingetragen!`, COLORS.success)], ephemeral: true }); } if (subcommand === 'remove') { const sanctionId = interaction.options.getInteger('id'); const existing = await db.get("SELECT * FROM sanctions WHERE id = ?", [sanctionId]); if (!existing) { return interaction.reply({ embeds: [simpleEmbed(`❌ Es gibt keine Sanktion mit der ID #${sanctionId}.`, COLORS.error)], ephemeral: true }); } await db.run("DELETE FROM sanctions WHERE id = ?", [sanctionId]); await updateSanctionPanel(interaction.client); return interaction.reply({ embeds: [simpleEmbed(`🗑️ Sanktion #${sanctionId} wurde erfolgreich gelöscht!`, COLORS.success)], ephemeral: true }); } } // --- NOTIFY COMMAND --- if (commandName === 'notify') { const ticket = await db.get("SELECT * FROM active_tickets WHERE thread_id = ?", [interaction.channel.id]); if (!ticket) { return interaction.reply({ embeds: [simpleEmbed("❌ Dieser Command kann nur in einem aktiven Ticket-Kanal ausgeführt werden.", COLORS.error)], ephemeral: true }); } const existing = await db.get("SELECT * FROM ticket_notifications WHERE thread_id = ? AND user_id = ?", [interaction.channel.id, interaction.user.id]); if (existing) { await db.run("DELETE FROM ticket_notifications WHERE thread_id = ? AND user_id = ?", [interaction.channel.id, interaction.user.id]); return interaction.reply({ embeds: [simpleEmbed("🔕 Du hast Benachrichtigungen für dieses Ticket **deaktiviert**.", COLORS.success)], ephemeral: true }); } else { await db.run("INSERT INTO ticket_notifications (thread_id, user_id) VALUES (?, ?)", [interaction.channel.id, interaction.user.id]); return interaction.reply({ embeds: [simpleEmbed("🔔 Du hast Benachrichtigungen für dieses Ticket **aktiviert**. Du wirst nun gepingt, sobald der User antwortet.", COLORS.success)], ephemeral: true }); } } // --- VC PERMS COMMAND --- if (commandName === 'vcperms') { let vcPermsRole = interaction.guild.roles.cache.find(r => r.name === VC_PERMS_ROLE_NAME); const hasVcPermsAccess = interaction.member.roles.cache.has(vcPermsRole?.id); if (!hasVcPermsAccess && !isHighAdmin) { return interaction.reply({ embeds: [simpleEmbed(`❌ Du hast nicht die benötigte Rolle (<@&${vcPermsRole?.id || 'Unbekannt'}>), um diesen Command auszuführen.`, COLORS.error)], ephemeral: true }); } const targetUser = interaction.options.getUser('user'); const targetMember = await interaction.guild.members.fetch(targetUser.id).catch(() => null); if (!targetMember) { return interaction.reply({ embeds: [simpleEmbed(`❌ Der User konnte auf dem Server nicht gefunden werden.`, COLORS.error)], ephemeral: true }); } let vcAccessRole = interaction.guild.roles.cache.find(r => r.name === VC_ACCESS_ROLE_NAME); if (!vcAccessRole) { return interaction.reply({ embeds: [simpleEmbed(`❌ Die VC Access Rolle (${VC_ACCESS_ROLE_NAME}) existiert nicht auf dem Server.`, COLORS.error)], ephemeral: true }); } if (targetMember.roles.cache.has(vcAccessRole.id)) { try { await targetMember.roles.remove(vcAccessRole); return interaction.reply({ embeds: [simpleEmbed(`✅ Du hast ${targetUser} die VC Access Rolle (<@&${vcAccessRole.id}>) erfolgreich **entfernt**!`, COLORS.success)] }); } catch (error) { console.error("Fehler bei /vcperms (Entfernen):", error); return interaction.reply({ embeds: [simpleEmbed(`❌ Es gab einen Fehler beim Entfernen der Rolle. Überprüfe meine Berechtigungen!`, COLORS.error)], ephemeral: true }); } } else { try { await targetMember.roles.add(vcAccessRole); return interaction.reply({ embeds: [simpleEmbed(`✅ Du hast ${targetUser} erfolgreich die VC Access Rolle (<@&${vcAccessRole.id}>) **gegeben**!`, COLORS.success)] }); } catch (error) { console.error("Fehler bei /vcperms (Hinzufügen):", error); return interaction.reply({ embeds: [simpleEmbed(`❌ Es gab einen Fehler beim Vergeben der Rolle. Überprüfe meine Berechtigungen!`, COLORS.error)], ephemeral: true }); } } } if (commandName === 'addreward') { const level = interaction.options.getInteger('level'); const rewardText = interaction.options.getString('belohnung'); await db.run("INSERT OR REPLACE INTO level_rewards (level, reward_text) VALUES (?, ?)", [level, rewardText]); return interaction.reply({ embeds: [simpleEmbed(`✅ Belohnung für **Level ${level}** erfolgreich hinzugefügt/aktualisiert:\n*${rewardText}*`, COLORS.success)], ephemeral: true }); } if (commandName === 'removereward') { const level = interaction.options.getInteger('level'); await db.run("DELETE FROM level_rewards WHERE level = ?", [level]); return interaction.reply({ embeds: [simpleEmbed(`🗑️ Belohnung für **Level ${level}** wurde entfernt.`, COLORS.success)], ephemeral: true }); } if (commandName === 'rewards') { const targetUser = interaction.options.getUser('user') || interaction.user; if (targetUser.bot) { return interaction.reply({ embeds: [simpleEmbed("❌ Bots haben keine Belohnungen.", COLORS.error)], ephemeral: true }); } const userData = await db.get("SELECT * FROM users_xp WHERE user_id = ?", [targetUser.id]) || { level: 0 }; const allRewards = await db.all("SELECT * FROM level_rewards ORDER BY level ASC"); if (allRewards.length === 0) { return interaction.reply({ embeds: [simpleEmbed("ℹ️ Es wurden noch keine Level-Belohnungen auf diesem Server eingerichtet.", COLORS.info)], ephemeral: true }); } let desc = `Aktuelles Level von ${targetUser}: **${userData.level}**\n\n`; for (const r of allRewards) { const claimStatus = await db.get("SELECT * FROM user_claims WHERE user_id = ? AND level = ?", [targetUser.id, r.level]); let statusEmoji = "🔒"; if (userData.level >= r.level) { statusEmoji = claimStatus ? "✅" : "🎁"; } desc += `${statusEmoji} **Level ${r.level}:** ${r.reward_text}\n`; } const embed = new EmbedBuilder() .setAuthor({ name: `Level-Belohnungen von ${targetUser.username}`, iconURL: targetUser.displayAvatarURL() }) .setDescription(desc) .setColor(COLORS.level) .setFooter({ text: "🔒 Gesperrt | 🎁 Abholbereit | ✅ Eingelöst" }); return interaction.reply({ embeds: [embed] }); } if (commandName === 'claimreward') { const targetUser = interaction.options.getUser('user'); const level = interaction.options.getInteger('level'); const reward = await db.get("SELECT * FROM level_rewards WHERE level = ?", [level]); if (!reward) { return interaction.reply({ embeds: [simpleEmbed(`❌ Es gibt keine Belohnung für Level ${level}.`, COLORS.error)], ephemeral: true }); } const userData = await db.get("SELECT * FROM users_xp WHERE user_id = ?", [targetUser.id]) || { level: 0 }; if (userData.level < level) { return interaction.reply({ embeds: [simpleEmbed(`❌ ${targetUser.username} hat Level ${level} noch nicht erreicht!`, COLORS.error)], ephemeral: true }); } const alreadyClaimed = await db.get("SELECT * FROM user_claims WHERE user_id = ? AND level = ?", [targetUser.id, level]); if (alreadyClaimed) { return interaction.reply({ embeds: [simpleEmbed(`❌ ${targetUser.username} hat diese Belohnung bereits eingelöst.`, COLORS.error)], ephemeral: true }); } await db.run("INSERT INTO user_claims (user_id, level) VALUES (?, ?)", [targetUser.id, level]); return interaction.reply({ embeds: [simpleEmbed(`✅ Belohnung für Level ${level} an ${targetUser} ausgegeben und als eingelöst markiert!`, COLORS.success)] }); } if (commandName === 'level') { const targetUser = interaction.options.getUser('user') || interaction.user; if (targetUser.bot) { return interaction.reply({ embeds: [simpleEmbed("❌ Bots haben kein Level.", COLORS.error)], ephemeral: true }); } let userData = await db.get("SELECT * FROM users_xp WHERE user_id = ?", [targetUser.id]); if (!userData) { userData = { xp: 0, level: 0 }; } const currentXP = userData.xp; const currentLevel = userData.level; const neededXP = 100 * Math.pow(currentLevel + 1, 2); const progressBar = createProgressBar(currentXP, neededXP); const embed = new EmbedBuilder() .setAuthor({ name: `Level Profil von ${targetUser.username}`, iconURL: targetUser.displayAvatarURL() }) .setColor(COLORS.level) .addFields( { name: "🏆 Level", value: `**${currentLevel}**`, inline: true }, { name: "✨ Gesamt XP", value: `**${currentXP}**`, inline: true }, { name: "🎯 Nächstes Level in", value: `**${neededXP - currentXP} XP**`, inline: false }, { name: "Fortschritt", value: `${progressBar} (${Math.round((currentXP/neededXP)*100)}%)`, inline: false } ); return interaction.reply({ embeds: [embed] }); } if (commandName === 'xptop') { await interaction.deferReply(); const topUsers = await db.all("SELECT * FROM users_xp ORDER BY xp DESC LIMIT 10"); if (topUsers.length === 0) { return interaction.editReply({ embeds: [simpleEmbed("Es gibt noch keine aktiven Nutzer auf diesem Server.", COLORS.info)] }); } let leaderboardText = ""; for (let i = 0; i < topUsers.length; i++) { const u = topUsers[i]; let rankEmoji = "🏅"; if (i === 0) rankEmoji = "🥇"; else if (i === 1) rankEmoji = "🥈"; else if (i === 2) rankEmoji = "🥉"; else rankEmoji = `**#${i+1}**`; leaderboardText += `${rankEmoji} <@${u.user_id}> — **Level ${u.level}** (${u.xp} XP)\n`; } const embed = new EmbedBuilder() .setTitle("🏆 Server Aktivitäts-Leaderboard") .setDescription(leaderboardText) .setColor(COLORS.level) .setFooter({ text: "Werde im Chat und im Voice aktiv, um im Rang aufzusteigen!" }); return interaction.editReply({ embeds: [embed] }); } if (commandName === 'reroll') { const msgId = interaction.options.getString('message_id'); const entries = await db.all("SELECT user_id FROM giveaway_entries WHERE message_id = ?", [msgId]); if (entries.length === 0) { return interaction.reply({ embeds: [simpleEmbed("❌ Es gibt keine Teilnehmer für diese Nachrichten-ID in der Datenbank.", COLORS.error)], ephemeral: true }); } const randomEntry = entries[Math.floor(Math.random() * entries.length)]; const rerollEmbed = new EmbedBuilder() .setTitle("🎲 Giveaway Reroll!") .setDescription(`Es wurde ein neuer Gewinner für das Giveaway gezogen!\n\n🎉 Neuer Gewinner: <@${randomEntry.user_id}>`) .setColor(COLORS.giveaway); await interaction.reply({ embeds: [rerollEmbed] }); return; } if (commandName === 'giveaway') { let giveawayRole = interaction.guild.roles.cache.find(r => r.name === GIVEAWAY_ROLE_NAME); if (!giveawayRole) { try { giveawayRole = await interaction.guild.roles.create({ name: GIVEAWAY_ROLE_NAME, color: COLORS.giveaway, reason: 'Auto-generierte Rolle für Giveaway Access' }); } catch (e) { console.error("Fehler beim Erstellen der Giveaway-Rolle:", e); } } const hasGiveawayAccess = interaction.member.roles.cache.has(giveawayRole?.id); if (!hasGiveawayAccess && !isHighAdmin) { return interaction.reply({ embeds: [simpleEmbed(`❌ Du hast nicht die benötigte Rolle (<@&${giveawayRole?.id || 'Unbekannt'}>), um ein Giveaway zu starten.`, COLORS.error)], ephemeral: true }); } if (interaction.channel.id !== GIVEAWAY_CHANNEL_ID) { return interaction.reply({ embeds: [simpleEmbed(`❌ Dieser Command kann nur im Channel <#${GIVEAWAY_CHANNEL_ID}> ausgeführt werden.`, COLORS.error)], ephemeral: true }); } const prize = interaction.options.getString('preis'); const winners = interaction.options.getInteger('gewinner'); const durationStr = interaction.options.getString('dauer'); const durationMs = parseDuration(durationStr); if (!durationMs) { return interaction.reply({ embeds: [simpleEmbed("❌ Ungültige Dauer! Nutze `m`, `h` oder `d` (z.B. `24h`).", COLORS.error)], ephemeral: true }); } const endTime = Date.now() + durationMs; const endUnix = Math.floor(endTime / 1000); const embed = new EmbedBuilder() .setTitle(`🎉 GIVEAWAY: ${prize}`) .setDescription(`Klicke auf den Button unter dieser Nachricht, um teilzunehmen!\n\n🏆 **Anzahl Gewinner:** ${winners}\n👥 **Teilnehmer:** 0\n⏳ **Endet:** ()\n👤 **Gestartet von:** ${interaction.user}`) .setColor(COLORS.giveaway) .setFooter({ text: "Jeder kann nur einmal teilnehmen!" }); const row = new ActionRowBuilder().addComponents( new ButtonBuilder().setCustomId('giveaway_enter').setLabel('🎉 Teilnehmen').setStyle(ButtonStyle.Success) ); await interaction.reply({ embeds: [simpleEmbed("⏳ Giveaway wird erstellt...", COLORS.info)], ephemeral: true }); const msg = await interaction.channel.send({ embeds: [embed], components: [row] }); await db.run("INSERT INTO giveaways (message_id, channel_id, prize, winners_count, end_time) VALUES (?, ?, ?, ?, ?)", [msg.id, interaction.channel.id, prize, winners, endTime]); return interaction.editReply({ embeds: [simpleEmbed("✅ Giveaway erfolgreich gestartet!", COLORS.success)] }); } if (commandName === 'regeln') { await interaction.deferReply({ ephemeral: true }); let rulesText = ""; try { rulesText = fs.readFileSync(RULES_FILE, 'utf8'); } catch (e) { return interaction.editReply({ embeds: [simpleEmbed("❌ Die Datei `regeln.txt` konnte nicht gelesen werden.", COLORS.error)] }); } const rulesChannel = interaction.guild.channels.cache.get(RULES_CHANNEL_ID); if (!rulesChannel) { return interaction.editReply({ embeds: [simpleEmbed(`❌ Der Regeln-Channel (${RULES_CHANNEL_ID}) wurde nicht gefunden.`, COLORS.error)] }); } const rulesEmbed = new EmbedBuilder() .setAuthor({ name: "Chaos Clan | HugoSMP", iconURL: interaction.guild.iconURL() }) .setTitle("📜 Das Server-Regelwerk") .setDescription(rulesText) .setColor(COLORS.info) .setFooter({ text: `Zuletzt aktualisiert: ${new Date().toLocaleString('de-DE')} • Mit der Nutzung dieses Servers akzeptierst du diese Regeln.` }); let rulesMsgId = await db.get("SELECT value FROM settings WHERE key = 'rulesMessageId'"); let rulesMsg = null; if (rulesMsgId && rulesMsgId.value) { rulesMsg = await rulesChannel.messages.fetch(rulesMsgId.value).catch(() => null); } if (rulesMsg) { await rulesMsg.edit({ embeds: [rulesEmbed] }); } else { const newMsg = await rulesChannel.send({ embeds: [rulesEmbed] }); await db.run("INSERT OR REPLACE INTO settings (key, value) VALUES ('rulesMessageId', ?)", [newMsg.id]); } return interaction.editReply({ embeds: [simpleEmbed("✅ Die Regeln wurden erfolgreich aktualisiert (Nachricht editiert).", COLORS.success)] }); } if (commandName === 'emoji') { const chosenEmoji = interaction.options.getString('emoji'); const existingUser = await db.get("SELECT * FROM team_emojis WHERE user_id = ?", [interaction.user.id]); if (existingUser) { return interaction.reply({ embeds: [simpleEmbed(`❌ Du hast bereits ein Emoji gesetzt: ${existingUser.emoji}`, COLORS.error)], ephemeral: true }); } const existingEmoji = await db.get("SELECT * FROM team_emojis WHERE emoji = ?", [chosenEmoji]); if (existingEmoji) { return interaction.reply({ embeds: [simpleEmbed(`❌ Das Emoji **${chosenEmoji}** wird bereits genutzt.`, COLORS.error)], ephemeral: true }); } await db.run("INSERT INTO team_emojis (user_id, emoji) VALUES (?, ?)", [interaction.user.id, chosenEmoji]); return interaction.reply({ embeds: [simpleEmbed(`✅ Emoji erfolgreich auf ${chosenEmoji} gesetzt!`, COLORS.success)], ephemeral: true }); } if (commandName === 'resetemoji') { const targetUser = interaction.options.getUser('user'); await db.run("DELETE FROM team_emojis WHERE user_id = ?", [targetUser.id]); return interaction.reply({ embeds: [simpleEmbed(`✅ Emoji von ${targetUser} zurückgesetzt.`, COLORS.success)], ephemeral: true }); } if (commandName === 'setup') { await interaction.deferReply({ ephemeral: true }); await updateTicketPanel(interaction.client); await updateStatsPanel(interaction.client); await updateSanctionPanel(interaction.client); await interaction.editReply({ embeds: [simpleEmbed("✅ Panels wurden aktualisiert.", COLORS.success)] }); } if (commandName === 'addcategory') { const name = interaction.options.getString('name'); const desc = interaction.options.getString('beschreibung'); const emoji = interaction.options.getString('emoji'); const existing = await db.get("SELECT * FROM categories WHERE name = ? COLLATE NOCASE", [name]); if (existing) { return interaction.reply({ embeds: [simpleEmbed("❌ Kategorie existiert bereits.", COLORS.error)], ephemeral: true }); } await interaction.deferReply({ ephemeral: true }); const setupChannel = interaction.guild.channels.cache.get(config.ticketCreationChannelId); let forumId = null; const expectedName = `${emoji ? emoji + '-' : ''}${name}-tickets`.toLowerCase(); try { const forum = await interaction.guild.channels.create({ name: expectedName, type: ChannelType.GuildForum, parent: setupChannel ? setupChannel.parentId : null, availableTags: [ { name: 'Unbearbeitet', moderated: true }, { name: 'In Bearbeitung', moderated: true }, { name: 'Geschlossen', moderated: true } ], permissionOverwrites: [ { id: interaction.guild.id, deny: [PermissionFlagsBits.ViewChannel] }, { id: TEAM_ROLE_ID, allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.SendMessages, PermissionFlagsBits.SendMessagesInThreads, PermissionFlagsBits.ManageThreads, PermissionFlagsBits.CreatePublicThreads, PermissionFlagsBits.CreatePrivateThreads] } ] }); forumId = forum.id; } catch(e) {} await db.run("INSERT INTO categories (name, description, emoji, forum_id) VALUES (?, ?, ?, ?)", [name, desc, emoji || null, forumId]); await updateTicketPanel(interaction.client); await interaction.editReply({ embeds: [simpleEmbed(`✅ Kategorie **${name}** hinzugefügt und Forum erstellt.`, COLORS.success)] }); } if (commandName === 'removecategory') { const name = interaction.options.getString('name'); await db.run("DELETE FROM categories WHERE name = ? COLLATE NOCASE", [name]); await db.run("DELETE FROM questions WHERE category_name = ? COLLATE NOCASE", [name]); await updateTicketPanel(interaction.client); await interaction.reply({ embeds: [simpleEmbed(`🗑️ Kategorie **${name}** entfernt.`, COLORS.success)], ephemeral: true }); } if (commandName === 'addquestion') { const catName = interaction.options.getString('kategorie'); const frage = interaction.options.getString('frage'); const typ = interaction.options.getString('typ'); const pflicht = interaction.options.getBoolean('pflicht'); const category = await db.get("SELECT * FROM categories WHERE name = ? COLLATE NOCASE", [catName]); if (!category) { return interaction.reply({ embeds: [simpleEmbed("❌ Kategorie nicht gefunden.", COLORS.error)], ephemeral: true }); } const qCount = await db.get("SELECT COUNT(*) as count FROM questions WHERE category_name = ? COLLATE NOCASE", [catName]); if (qCount.count >= 5) { return interaction.reply({ embeds: [simpleEmbed("❌ Maximal 5 Fragen erlaubt.", COLORS.error)], ephemeral: true }); } await db.run("INSERT INTO questions (category_name, label, placeholder, style, required) VALUES (?, ?, ?, ?, ?)", [category.name, frage, frage, typ === 'short' ? 1 : 2, pflicht ? 1 : 0]); await interaction.reply({ embeds: [simpleEmbed(`✅ Frage hinzugefügt.`, COLORS.success)], ephemeral: true }); } if (commandName === 'removequestion') { const catName = interaction.options.getString('kategorie'); const index = interaction.options.getInteger('fragennummer') - 1; const questions = await db.all("SELECT * FROM questions WHERE category_name = ? COLLATE NOCASE ORDER BY id ASC", [catName]); if (!questions[index]) { return interaction.reply({ embeds: [simpleEmbed("❌ Frage nicht gefunden.", COLORS.error)], ephemeral: true }); } await db.run("DELETE FROM questions WHERE id = ?", [questions[index].id]); await interaction.reply({ embeds: [simpleEmbed(`🗑️ Frage entfernt.`, COLORS.success)], ephemeral: true }); } // --- TICKET BEARBEITUNG --- if (['reply', 'close', 'annehmen', 'ablehnen'].includes(commandName)) { const ticket = await db.get("SELECT * FROM active_tickets WHERE thread_id = ?", [interaction.channel.id]); if (!ticket) { return interaction.reply({ embeds: [simpleEmbed("❌ Nur in aktiven Tickets möglich.", COLORS.error)], ephemeral: true }); } const userId = ticket.user_id; const category = ticket.category; const ticketId = ticket.ticket_id; const teamEmojiObj = await db.get("SELECT * FROM team_emojis WHERE user_id = ?", [interaction.user.id]); if (!teamEmojiObj) { return interaction.reply({ embeds: [simpleEmbed("❌ Du musst zuerst ein persönliches Emoji mit `/emoji` setzen!", COLORS.error)], ephemeral: true }); } const modDb = await db.get("SELECT mc_name FROM mc_names WHERE user_id = ?", [interaction.user.id]); const modMcName = modDb ? modDb.mc_name : "Steve"; const modMinotar = getMinotarUrl(modMcName); await db.run("UPDATE active_tickets SET last_activity = ?, warning_sent = 0 WHERE user_id = ?", [Date.now(), userId]); const forum = await interaction.guild.channels.fetch(interaction.channel.parentId).catch(() => null); const tags = getForumTags(forum); if (!ticket.claimed_by) { await db.run("UPDATE active_tickets SET claimed_by = ? WHERE thread_id = ?", [interaction.user.id, interaction.channel.id]); const newName = `[${teamEmojiObj.emoji}] ${interaction.channel.name}`.substring(0, 100); await interaction.channel.setName(newName).catch(console.error); } if (commandName === 'reply') { const message = interaction.options.getString('nachricht'); const dmEmbed = new EmbedBuilder() .setAuthor({ name: `Support | ${interaction.user.username}` }) .setThumbnail(modMinotar) .setDescription(`**Nachricht:**\n${message}`) .setColor(COLORS.team) .setTimestamp() .setFooter({ text: `Ticket ID: ${ticketId} • Antworte direkt auf diese Nachricht` }); try { const user = await client.users.fetch(userId); await user.send({ embeds: [dmEmbed] }); const threadEmbed = new EmbedBuilder() .setAuthor({ name: `Support | ${interaction.user.username}` }) .setThumbnail(modMinotar) .setDescription(`**Nachricht:**\n${message}`) .setColor(COLORS.team) .setTimestamp(); await interaction.reply({ embeds: [threadEmbed] }); let appliedTags = interaction.channel.appliedTags.filter(id => id !== tags.unbearbeitet); if (tags.inbearbeitung && !appliedTags.includes(tags.inbearbeitung)) { appliedTags.push(tags.inbearbeitung); } await interaction.channel.setAppliedTags(appliedTags).catch(() => {}); } catch (err) { await interaction.reply({ embeds: [simpleEmbed("❌ Konnte dem User nicht antworten. Er hat DMs deaktiviert.", COLORS.error)], ephemeral: true }); } } if (['close', 'annehmen', 'ablehnen'].includes(commandName)) { await interaction.deferReply(); let reason = interaction.options.getString('grund') || "Kein Grund angegeben"; try { const user = await client.users.fetch(userId); let dmEmbed; if (commandName === 'close') { dmEmbed = new EmbedBuilder() .setAuthor({ name: `Chaos Clan Support | ${interaction.user.username}` }) .setThumbnail(modMinotar) .setTitle(`🔒 Dein Ticket wurde geschlossen (${ticketId})`) .setDescription(`Dein Ticket wurde vom Team geschlossen.\n\n**Grund:** ${reason}`) .setColor(COLORS.error) .setTimestamp(); } else if (commandName === 'annehmen') { dmEmbed = new EmbedBuilder() .setAuthor({ name: `Bewerbungs-Team | ${interaction.user.username}` }) .setThumbnail(modMinotar) .setTitle(`🎉 Bewerbung Angenommen! (${ticketId})`) .setDescription(`Herzlichen Glückwunsch!\n\nDeine Bewerbung wurde soeben von **${interaction.user.username}** angenommen.\nDu hast die **Member**-Rolle erhalten. Dein Ticket wird nun geschlossen.\n\nWillkommen im Team!`) .setColor(COLORS.success) .setTimestamp(); const member = await interaction.guild.members.fetch(userId).catch(() => null); if (member) { await member.roles.add(MEMBER_ROLE_ID).catch(console.error); if (ticket.mc_name && ticket.mc_name.toLowerCase() !== 'steve') { await member.setNickname(ticket.mc_name).catch(e => console.error("Konnte Nickname nicht setzen:", e)); await db.run("INSERT OR REPLACE INTO mc_names (user_id, mc_name) VALUES (?, ?)", [userId, ticket.mc_name]); } } } else if (commandName === 'ablehnen') { dmEmbed = new EmbedBuilder() .setAuthor({ name: `Bewerbungs-Team | ${interaction.user.username}` }) .setThumbnail(modMinotar) .setTitle(`❌ Bewerbung Abgelehnt (${ticketId})`) .setDescription(`Hallo,\n\nleider müssen wir dir mitteilen, dass deine Bewerbung von **${interaction.user.username}** abgelehnt wurde.\n\n**Grund:** ${reason}\n\nDein Ticket wird nun geschlossen.`) .setColor(COLORS.error) .setTimestamp(); } await user.send({ embeds: [dmEmbed] }); } catch (e) {} const transcriptChannel = await interaction.guild.channels.fetch(TRANSCRIPT_CHANNEL_ID).catch(() => null); if (transcriptChannel) { const file = await createTranscript(interaction.channel, ticketId, category, interaction.user.tag, reason); await transcriptChannel.send({ content: `📄 **Neues Transcript:** Ticket \`${ticketId}\` (${category})\nGeschlossen von: ${interaction.user.tag}`, files: [file] }); } let actionText = commandName === 'close' ? `🔒 Geschlossen von ${interaction.user}\n**Grund:** ${reason}` : commandName === 'annehmen' ? `✅ Bewerbung von ${interaction.user} angenommen.` : `❌ Bewerbung von ${interaction.user} abgelehnt.\n**Grund:** ${reason}`; await interaction.editReply({ embeds: [simpleEmbed(actionText, commandName === 'annehmen' ? COLORS.success : COLORS.error, "Ticket Status")] }); if (tags.geschlossen) { await interaction.channel.setAppliedTags([tags.geschlossen]).catch(() => {}); } await interaction.channel.setLocked(true).catch(() => {}); await interaction.channel.setArchived(true).catch(() => {}); await db.run("DELETE FROM active_tickets WHERE user_id = ?", [userId]); await db.run("DELETE FROM ticket_notifications WHERE thread_id = ?", [interaction.channel.id]); await db.run("UPDATE settings SET value = CAST(value AS INTEGER) + 1 WHERE key = 'stat_total_closed'"); await db.run("INSERT OR IGNORE INTO stats_team (user_id, closed_count) VALUES (?, 0)", [interaction.user.id]); await db.run("UPDATE stats_team SET closed_count = closed_count + 1 WHERE user_id = ?", [interaction.user.id]); await updateStatsPanel(interaction.client); } } } // ========================================== // TICKET MODAL HANDLER // ========================================== if (interaction.isStringSelectMenu() && interaction.customId === 'ticket_select') { const userId = interaction.user.id; const hasTicket = await db.get("SELECT * FROM active_tickets WHERE user_id = ?", [userId]); if (hasTicket) { return interaction.reply({ embeds: [simpleEmbed("❌ Du hast bereits ein offenes Ticket!", COLORS.error)], ephemeral: true }); } const categoryName = interaction.values[0]; const category = await db.get("SELECT * FROM categories WHERE name = ?", [categoryName]); if (!category) { return interaction.reply({ embeds: [simpleEmbed("❌ Diese Kategorie existiert nicht mehr.", COLORS.error)], ephemeral: true }); } if (!category.forum_id) { return interaction.reply({ embeds: [simpleEmbed("❌ Diese Kategorie hat keinen zugewiesenen Forum-Kanal.", COLORS.error)], ephemeral: true }); } const modal = new ModalBuilder() .setCustomId(`modal_${categoryName}`) .setTitle(`Ticket: ${categoryName.substring(0, 30)}`); let questions = await db.all("SELECT * FROM questions WHERE category_name = ? ORDER BY id ASC", [categoryName]); if (questions.length === 0) { questions = [{ label: "Bitte beschreibe dein Anliegen", placeholder: "Schreibe hier...", style: 2, required: 1 }]; } questions.forEach((q, index) => { const input = new TextInputBuilder() .setCustomId(`question_${index}`) .setLabel(q.label) .setPlaceholder(q.placeholder || q.label) .setStyle(q.style === 1 ? TextInputStyle.Short : TextInputStyle.Paragraph) .setRequired(q.required === 1); modal.addComponents(new ActionRowBuilder().addComponents(input)); }); await interaction.showModal(modal).catch(console.error); } if (interaction.isModalSubmit() && interaction.customId.startsWith('modal_')) { const categoryName = interaction.customId.replace('modal_', ''); const userId = interaction.user.id; const ticketId = generateTicketId(); await interaction.deferReply({ ephemeral: true }); const category = await db.get("SELECT * FROM categories WHERE name = ?", [categoryName]); const guild = await client.guilds.fetch(config.guildId).catch(() => null); const forumChannel = await guild?.channels.fetch(category.forum_id).catch(() => null); if (!forumChannel) { return interaction.editReply({ embeds: [simpleEmbed("❌ Fehler: Der Forum-Kanal für diese Kategorie wurde nicht gefunden.", COLORS.error)] }); } let questions = await db.all("SELECT * FROM questions WHERE category_name = ? ORDER BY id ASC", [categoryName]); if (questions.length === 0) { questions = [{ label: "Anliegen" }]; } const initialEmbed = new EmbedBuilder() .setAuthor({ name: `Neues Ticket: ${interaction.user.tag}`, iconURL: interaction.user.displayAvatarURL() }) .setTitle(`Kategorie: ${categoryName}`) .setDescription(`**Ticket ID:** \`${ticketId}\``) .setColor(COLORS.success) .setTimestamp(); let mcName = "Steve"; questions.forEach((q, index) => { const answer = interaction.fields.getTextInputValue(`question_${index}`); const fieldName = (q.placeholder || q.label).substring(0, 256); initialEmbed.addFields({ name: fieldName, value: (answer || "*Keine Angabe*").substring(0, 1024) }); if (fieldName.toLowerCase().includes('minecraft-name') || fieldName.toLowerCase().includes('ingame')) { if (answer && answer.trim() !== "") { mcName = answer.trim(); } } }); try { const tags = getForumTags(forumChannel); const thread = await forumChannel.threads.create({ name: `${categoryName} - ${interaction.user.username}`, autoArchiveDuration: ThreadAutoArchiveDuration.OneWeek, message: { content: `Neues Ticket von <@${userId}>`, embeds: [initialEmbed] }, appliedTags: tags.unbearbeitet ? [tags.unbearbeitet] : [] }); await db.run( "INSERT INTO active_tickets (user_id, thread_id, category, last_activity, warning_sent, claimed_by, ticket_id, mc_name) VALUES (?, ?, ?, ?, 0, NULL, ?, ?)", [userId, thread.id, categoryName, Date.now(), ticketId, mcName] ); await db.run("UPDATE settings SET value = CAST(value AS INTEGER) + 1 WHERE key = 'stat_total_opened'"); await db.run("INSERT OR IGNORE INTO stats_categories (category_name, count) VALUES (?, 0)", [categoryName]); await db.run("UPDATE stats_categories SET count = count + 1 WHERE category_name = ?", [categoryName]); await updateStatsPanel(client); if (categoryName.toLowerCase() === 'bewerbung') { const infoEmbed = new EmbedBuilder() .setTitle("📝 Neue Bewerbung!") .setDescription("💡 **Hinweis für das Team:**\n🟢 `/annehmen` (Vergibt Member-Rolle & schließt)\n🔴 `/ablehnen [grund]` (Lehnt ab & schließt)") .setColor(COLORS.info); await thread.send({ embeds: [infoEmbed] }); } const dmEmbed = new EmbedBuilder() .setAuthor({ name: "Chaos Clan Support", iconURL: interaction.guild.iconURL() }) .setTitle(`Ticket erstellt: ${categoryName}`) .setDescription(`Dein Ticket wurde erfolgreich eröffnet!\n**Ticket-ID:** \`${ticketId}\`\n\n**Alle weiteren Nachrichten, die du hier schreibst, werden ans Team weitergeleitet.**`) .setColor(COLORS.success) .setTimestamp(); await interaction.user.send({ embeds: [dmEmbed] }); await interaction.editReply({ embeds: [simpleEmbed(`✅ Ticket erfolgreich erstellt! Bitte schaue in deine Direktnachrichten.\n**ID:** \`${ticketId}\``, COLORS.success)] }); } catch (error) { console.error("Fehler beim Erstellen des Threads:", error); await interaction.editReply({ embeds: [simpleEmbed("❌ Fehler beim Erstellen. Bitte prüfe, ob du DMs von Servermitgliedern erlaubst.", COLORS.error)] }); } } } catch (error) { console.error("Unerwarteter Fehler bei Interaction:", error); const errorEmbed = simpleEmbed("❌ Es ist ein interner Fehler aufgetreten.", COLORS.error); if (interaction.deferred || interaction.replied) { await interaction.editReply({ embeds: [errorEmbed] }).catch(() => {}); } else if (!interaction.isModalSubmit()) { await interaction.reply({ embeds: [errorEmbed], ephemeral: true }).catch(() => {}); } } }); // ========================================== // MESSAGE CREATE HANDLER (DMs & TEXT XP) // ========================================== client.on('messageCreate', async (message) => { try { if (message.author.bot) { return; } // --- DM CATCHER FÜR TICKETS --- if (message.channel.type === ChannelType.DM) { const userId = message.author.id; const ticket = await db.get("SELECT * FROM active_tickets WHERE user_id = ?", [userId]); if (!ticket) { return message.author.send({ embeds: [simpleEmbed("❌ Du hast aktuell kein offenes Ticket beim Chaos Clan.", COLORS.error)] }).catch(() => {}); } try { const guild = await client.guilds.fetch(config.guildId).catch(() => null); const thread = await guild?.channels.fetch(ticket.thread_id).catch(() => null); if (!thread || thread.archived || thread.locked) { await db.run("DELETE FROM active_tickets WHERE user_id = ?", [userId]); await db.run("DELETE FROM ticket_notifications WHERE thread_id = ?", [ticket.thread_id]); return message.author.send({ embeds: [simpleEmbed("🔒 Dein Ticket wurde bereits geschlossen. Bitte erstelle ein neues.", COLORS.error)] }).catch(() => {}); } await db.run("UPDATE active_tickets SET last_activity = ?, warning_sent = 0 WHERE user_id = ?", [Date.now(), userId]); let attachmentLinks = []; let files = []; if (message.attachments.size > 0) { message.attachments.forEach(a => { attachmentLinks.push(`🔗 [Datei / Bild ansehen](${a.url})`); files.push(a.url); }); } let descriptionText = message.content || ""; if (attachmentLinks.length > 0) { descriptionText += `\n\n**Anhänge:**\n${attachmentLinks.join("\n")}`; } if (descriptionText.trim() === "") { descriptionText = "*Hat eine Datei gesendet*"; } const userMinotar = getMinotarUrl(ticket.mc_name); const userEmbed = new EmbedBuilder() .setAuthor({ name: `${message.author.username} (${ticket.mc_name || 'Steve'})` }) .setThumbnail(userMinotar) .setDescription(descriptionText) .setColor(COLORS.user) .setTimestamp(); const notifyUsers = await db.all("SELECT user_id FROM ticket_notifications WHERE thread_id = ?", [ticket.thread_id]); let notifyContent = ""; if (notifyUsers && notifyUsers.length > 0) { notifyContent = notifyUsers.map(n => `<@${n.user_id}>`).join(" "); } let sendPayload = { embeds: [userEmbed], files: files }; if (notifyContent) { sendPayload.content = `🔔 **Neue Nachricht:** ${notifyContent}`; } await thread.send(sendPayload); message.react('✅').catch(() => {}); } catch (error) { console.error("Fehler beim Weiterleiten der DM:", error); message.author.send({ embeds: [simpleEmbed("❌ Fehler beim Weiterleiten deiner Nachricht.", COLORS.error)] }).catch(() => {}); } return; } // --- TICKET AKTIVITÄT DURCH TEAM --- if (message.guild && message.channel.parentId) { const ticket = await db.get("SELECT * FROM active_tickets WHERE thread_id = ?", [message.channel.id]); if (ticket) { await db.run("UPDATE active_tickets SET last_activity = ?, warning_sent = 0 WHERE thread_id = ?", [Date.now(), message.channel.id]); } } // --- TEXT XP LOGIK --- if (message.guild && message.guild.id === config.guildId) { const userId = message.author.id; const now = Date.now(); let userData = await db.get("SELECT * FROM users_xp WHERE user_id = ?", [userId]); if (!userData) { await db.run("INSERT INTO users_xp (user_id, xp, level, last_msg_timestamp) VALUES (?, ?, ?, ?)", [userId, 0, 0, 0]); userData = { user_id: userId, xp: 0, level: 0, last_msg_timestamp: 0 }; } const lastMsgTime = userData.last_msg_timestamp; if (now - lastMsgTime >= TEXT_XP_COOLDOWN) { const xpToAdd = Math.floor(Math.random() * 11) + 10; await db.run("UPDATE users_xp SET last_msg_timestamp = ? WHERE user_id = ?", [now, userId]); await addXP(message.member, xpToAdd); } } } catch (err) { console.error("Fehler im MessageCreate Event:", err); } }); // ========================================== // UPDATE FUNKTIONEN FÜR PANELS // ========================================== async function updateTicketPanel(client) { try { const guild = await client.guilds.fetch(config.guildId).catch(() => null); const setupChannel = await guild?.channels.fetch(config.ticketCreationChannelId).catch(() => null); if (!setupChannel) return; const embed = new EmbedBuilder() .setAuthor({ name: "Chaos Clan Support", iconURL: guild.iconURL() }) .setTitle('🎟️ Ticket eröffnen') .setDescription('Wähle unten im Menü eine Kategorie aus, um ein Support-Ticket zu erstellen.\n\nNach der Erstellung öffnet sich ein Formular. **Die weitere Kommunikation findet danach komplett über deine Direktnachrichten (DMs) statt!**') .setColor(COLORS.main) .setFooter({ text: "Wähle eine Kategorie aus dem Dropdown" }); const categories = await db.all("SELECT * FROM categories"); let options = categories.map(c => { let opt = { label: c.name.substring(0, 100), description: (c.description || "Support").substring(0, 100), value: c.name.substring(0, 100) }; if (c.emoji) opt.emoji = c.emoji; return opt; }); if (options.length === 0) { options = [{ label: 'Allgemein', description: 'Allgemeiner Support', value: 'Allgemein' }]; } const row = new ActionRowBuilder().addComponents( new StringSelectMenuBuilder() .setCustomId('ticket_select') .setPlaceholder('Wähle eine Ticket-Kategorie...') .addOptions(options) ); let msgId = await db.get("SELECT value FROM settings WHERE key = 'panelMessageId'"); let msgToEdit = null; if (msgId && msgId.value) { msgToEdit = await setupChannel.messages.fetch(msgId.value).catch(() => null); } if (!msgToEdit) { const recentMsgs = await setupChannel.messages.fetch({ limit: 50 }); msgToEdit = recentMsgs.find(m => m.author.id === client.user.id && m.embeds.length > 0 && m.embeds[0].title === '🎟️ Ticket eröffnen'); } if (msgToEdit) { await msgToEdit.edit({ embeds: [embed], components: [row] }); await db.run("INSERT OR REPLACE INTO settings (key, value) VALUES ('panelMessageId', ?)", [msgToEdit.id]); } else { const newMessage = await setupChannel.send({ embeds: [embed], components: [row] }); await db.run("INSERT OR REPLACE INTO settings (key, value) VALUES ('panelMessageId', ?)", [newMessage.id]); } } catch (err) { console.error("Fehler beim Panel Update:", err); } } async function updateStatsPanel(client) { try { const guild = await client.guilds.fetch(config.guildId).catch(() => null); const statsChannel = await guild?.channels.fetch(STATS_CHANNEL_ID).catch(() => null); if (!statsChannel) return; const totalOpened = await db.get("SELECT value FROM settings WHERE key = 'stat_total_opened'"); const totalClosed = await db.get("SELECT value FROM settings WHERE key = 'stat_total_closed'"); const activeCountRes = await db.get("SELECT COUNT(*) as count FROM active_tickets"); const catStats = await db.all("SELECT * FROM stats_categories ORDER BY count DESC"); const teamStats = await db.all("SELECT * FROM stats_team ORDER BY closed_count DESC"); let catText = catStats.length > 0 ? catStats.map(c => `**${c.category_name}:** ${c.count}`).join('\n') : "Keine Daten"; const embed1 = new EmbedBuilder() .setAuthor({ name: "Chaos Clan", iconURL: guild.iconURL() }) .setTitle("📊 Globale Ticket Statistiken") .setColor(COLORS.info) .addFields( { name: "Gesamt Erstellt", value: `\`${totalOpened ? totalOpened.value : 0}\``, inline: true }, { name: "Aktuell Offen", value: `\`${activeCountRes.count}\``, inline: true }, { name: "Gesamt Geschlossen", value: `\`${totalClosed ? totalClosed.value : 0}\``, inline: true }, { name: "Nach Kategorien", value: catText, inline: false } ) .setTimestamp(); let teamText = teamStats.length > 0 ? teamStats.map((t, i) => { let rankEmoji = "🏅"; if (i === 0) rankEmoji = "🥇"; else if (i === 1) rankEmoji = "🥈"; else if (i === 2) rankEmoji = "🥉"; else rankEmoji = `**#${i+1}**`; return `${rankEmoji} <@${t.user_id}> - ${t.closed_count} Tickets`; }).join('\n') : "Noch keine Tickets geschlossen."; const embed2 = new EmbedBuilder() .setTitle("🏆 Team Leaderboard") .setDescription(teamText) .setColor(COLORS.success); let msg1Id = await db.get("SELECT value FROM settings WHERE key = 'statsMsg1Id'"); let msg2Id = await db.get("SELECT value FROM settings WHERE key = 'statsMsg2Id'"); let msg1 = null, msg2 = null; if (msg1Id && msg1Id.value) { msg1 = await statsChannel.messages.fetch(msg1Id.value).catch(() => null); } if (msg2Id && msg2Id.value) { msg2 = await statsChannel.messages.fetch(msg2Id.value).catch(() => null); } if (!msg1 || !msg2) { const recentMsgs = await statsChannel.messages.fetch({ limit: 50 }); if (!msg1) { msg1 = recentMsgs.find(m => m.author.id === client.user.id && m.embeds.length > 0 && m.embeds[0].title === '📊 Globale Ticket Statistiken'); } if (!msg2) { msg2 = recentMsgs.find(m => m.author.id === client.user.id && m.embeds.length > 0 && m.embeds[0].title === '🏆 Team Leaderboard'); } } if (msg1) { await msg1.edit({ embeds: [embed1] }); await db.run("INSERT OR REPLACE INTO settings (key, value) VALUES ('statsMsg1Id', ?)", [msg1.id]); } else { const newM = await statsChannel.send({ embeds: [embed1] }); await db.run("INSERT OR REPLACE INTO settings (key, value) VALUES ('statsMsg1Id', ?)", [newM.id]); } if (msg2) { await msg2.edit({ embeds: [embed2] }); await db.run("INSERT OR REPLACE INTO settings (key, value) VALUES ('statsMsg2Id', ?)", [msg2.id]); } else { const newM = await statsChannel.send({ embeds: [embed2] }); await db.run("INSERT OR REPLACE INTO settings (key, value) VALUES ('statsMsg2Id', ?)", [newM.id]); } } catch(e) { console.error("Fehler beim Updaten der Stats:", e); } } async function updateSanctionPanel(client) { try { const guild = await client.guilds.fetch(config.guildId).catch(() => null); const channel = await guild?.channels.fetch(SANCTION_CHANNEL_ID).catch(() => null); if (!channel) return; const sanctions = await db.all("SELECT * FROM sanctions ORDER BY id ASC"); let desc = ""; if (sanctions.length === 0) { desc = "Es gibt aktuell keine aktiven Sanktionen."; } else { for (const s of sanctions) { desc += `**[ID: #${s.id}]** <@${s.user_id}>\n👤 **MC Name:** ${s.mc_name}\n📝 **Grund:** ${s.reason}\n💰 **Höhe:** ${s.amount}\n📅 **Eingetragen am:** \n\n`; } } const embed = new EmbedBuilder() .setAuthor({ name: "Chaos Clan | Sanktionen", iconURL: guild.iconURL() }) .setTitle("🚨 Aktuelle Sanktionsliste") .setDescription(desc.substring(0, 4000)) .setColor(COLORS.error) .setFooter({ text: `Zuletzt aktualisiert: ${new Date().toLocaleString('de-DE')}` }); let msgId = await db.get("SELECT value FROM settings WHERE key = 'sanktionMessageId'"); let msgToEdit = null; if (msgId && msgId.value) { msgToEdit = await channel.messages.fetch(msgId.value).catch(() => null); } if (msgToEdit) { await msgToEdit.edit({ embeds: [embed] }); } else { const newMsg = await channel.send({ embeds: [embed] }); await db.run("INSERT OR REPLACE INTO settings (key, value) VALUES ('sanktionMessageId', ?)", [newMsg.id]); } } catch(err) { console.error("Fehler beim Update des Sanktions-Panels:", err); } } // Bot einloggen (erst nachdem DB initialisiert wurde) initDB().then(() => { client.login(process.env.BOT_TOKEN); });