chaosdc/bot.js

2284 lines
114 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 => ({
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
"'": '&#39;',
'"': '&quot;'
}[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, '<br>');
let msgClass = "internal-msg";
let badge = "<span class='badge badge-internal'>Team (Intern)</span>";
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 = "<span class='badge badge-user'>User</span>";
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 ? "<br><br>" : "") + escapeHTML(embed.description).replace(/\n/g, '<br>');
skipFirstEmbed = true;
} else if (embed.color === cTeam) {
msgClass = "team-msg";
badge = "<span class='badge badge-team'>Team &rarr; User</span>";
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 ? "<br><br>" : "") + escapeHTML(desc).replace(/\n/g, '<br>');
}
skipFirstEmbed = true;
} else if ([cSys1, cSys2, cSys3].includes(embed.color) || embed.title) {
msgClass = "system-msg";
badge = "<span class='badge badge-system'>System</span>";
realAuthor = "Support Bot";
avatar = client.user.displayAvatarURL({ extension: 'png', size: 64 });
}
}
let attachmentsHtml = "";
if (msg.attachments.size > 0) {
msg.attachments.forEach(a => {
attachmentsHtml += `<br><a href="${a.url}" class="attachment" target="_blank">📎 [Anhang: ${escapeHTML(a.name)}]</a>`;
});
}
let embedsHtml = "";
if (msg.embeds.length > 0) {
msg.embeds.forEach((embed, index) => {
if (index === 0 && skipFirstEmbed) return;
embedsHtml += `<div class="embed">`;
if (embed.title) embedsHtml += `<strong style="color: #ffffff;">${escapeHTML(embed.title)}</strong><br>`;
if (embed.description) embedsHtml += `${escapeHTML(embed.description).replace(/\n/g, '<br>')}`;
if (embed.fields && embed.fields.length > 0) {
embedsHtml += `<br><br>`;
embed.fields.forEach(field => {
embedsHtml += `<strong>${escapeHTML(field.name)}</strong><br>${escapeHTML(field.value).replace(/\n/g, '<br>')}<br><br>`;
});
}
embedsHtml += `</div>`;
});
}
if (content === "" && attachmentsHtml === "" && embedsHtml === "") continue;
messagesHtml += `
<div class="msg ${msgClass}">
<img src="${avatar}" class="avatar" alt="Avatar">
<div class="content">
<div class="name-row">
<span class="name">${realAuthor}</span>
${badge}
<span class="time">${time}</span>
</div>
<div class="text">${content}${attachmentsHtml}${embedsHtml}</div>
</div>
</div>`;
}
const htmlContent = `
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<title>Transcript: ${ticketId}</title>
<style>
body { background-color: #1e1f22; color: #dbdee1; font-family: 'gg sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; padding: 30px; margin: 0; }
.header { background-color: #2b2d31; padding: 25px; border-radius: 8px; margin-bottom: 30px; border-left: 4px solid #5865F2; box-shadow: 0 2px 10px rgba(0,0,0,0.2);}
.header h1 { margin: 0 0 10px 0; color: #ffffff; font-size: 24px; }
.header p { margin: 5px 0; font-size: 14px; }
.msg { display: flex; margin-bottom: 15px; padding: 15px; border-radius: 8px; box-shadow: 0 1px 4px rgba(0,0,0,0.1); }
.user-msg { background-color: #2b2d31; border-left: 4px solid #00BFFF; }
.team-msg { background-color: #2b2d31; border-left: 4px solid #FF8C00; }
.internal-msg { background-color: #2b2d31; border-left: 4px solid #95a5a6; opacity: 0.9; }
.system-msg { background-color: #232428; border-left: 4px solid #FFA500; font-style: normal; }
.avatar { width: 45px; height: 45px; border-radius: 50%; margin-right: 15px; flex-shrink: 0; object-fit: cover; }
.content { flex-grow: 1; min-width: 0; }
.name-row { display: flex; align-items: center; margin-bottom: 5px; flex-wrap: wrap; }
.name { font-weight: 600; color: #ffffff; font-size: 15px; }
.time { color: #949ba4; font-size: 0.75em; margin-left: 10px; }
.badge { font-size: 0.65em; padding: 3px 6px; border-radius: 4px; margin-left: 8px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px;}
.badge-user { background-color: rgba(0, 191, 255, 0.15); color: #00BFFF; }
.badge-team { background-color: rgba(255, 140, 0, 0.15); color: #FF8C00; }
.badge-internal { background-color: rgba(149, 165, 166, 0.15); color: #95a5a6; }
.badge-system { background-color: rgba(255, 165, 0, 0.15); color: #FFA500; }
.text { line-height: 1.5; word-wrap: break-word; font-size: 15px; }
.embed { background-color: #1e1f22; border-left: 4px solid #2ecc71; padding: 12px 15px; margin-top: 10px; border-radius: 4px; font-size: 14px; }
.attachment { color: #00A8FC; text-decoration: none; font-weight: 500; display: inline-block; margin-top: 5px; }
.attachment:hover { text-decoration: underline; }
</style>
</head>
<body>
<div class="header">
<h1>Ticket Transcript: ${ticketId}</h1>
<p><strong style="color: #ffffff;">Kategorie:</strong> ${category}</p>
<p><strong style="color: #ffffff;">Geschlossen durch:</strong> ${closedBy}</p>
<p><strong style="color: #ffffff;">Grund:</strong> ${reason}</p>
<p><strong style="color: #ffffff;">Datum:</strong> ${new Date().toLocaleString('de-DE')}</p>
</div>
${messagesHtml}
</body>
</html>`;
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: './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:** <t:${endUnix}:R> (<t:${endUnix}:f>)\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:** <t:${s.timestamp}:d>\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);
});