2284 lines
114 KiB
JavaScript
2284 lines
114 KiB
JavaScript
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, '<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 → 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);
|
||
}); |