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 += `
${realAuthor}
${badge}
${time}
${content}${attachmentsHtml}${embedsHtml}
`;
}
const htmlContent = `
Transcript: ${ticketId}
${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);
});