Initial: Pixelpöbel Discord Bot
Some checks failed
Auto Build and Push Docker Image / build (push) Failing after 8s

- Modular Discord Bot mit TypeScript und discord.js
- Grouped Commands (/admin, /owner subcommands)
- SQLite Datenbank mit Persistenz
- Twitch Monitoring mit Stream-ID Tracking
- Konfigurierbares Warn-System (Auto-Kick/Mute/Ban)
- Mute mit verschiedenen Zeiteinheiten (sec, min, hour, day, week, month, year, perm)
- Auto-Deploy beim Start (Option 3)
- Docker Support

Features:
- /ping, /help, /twitch (online, add, remove, list)
- /admin (kick, warn, unwarn, mute, unmute, ban, unban, config)
- /owner (deploy, stats, servers)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-22 16:29:30 +01:00
commit 40be1e181c
26 changed files with 3042 additions and 0 deletions

View File

@@ -0,0 +1,419 @@
import { SlashCommandBuilder, PermissionFlagsBits, EmbedBuilder, GuildMember } from 'discord.js';
import { Command } from '../../structures/Command.js';
const command: Command = {
data: new SlashCommandBuilder()
.setName('admin')
.setDescription('Admin-Commands')
.addSubcommand(subcommand =>
subcommand
.setName('kick')
.setDescription('Kickt einen Nutzer')
.addUserOption(option =>
option.setName('target')
.setDescription('Der zu kickende Nutzer')
.setRequired(true))
.addStringOption(option =>
option.setName('reason')
.setDescription('Grund für den Kick')))
.addSubcommand(subcommand =>
subcommand
.setName('warn')
.setDescription('Verwarnt einen Nutzer')
.addUserOption(option =>
option.setName('target')
.setDescription('Der zu verwarnende Nutzer')
.setRequired(true))
.addStringOption(option =>
option.setName('reason')
.setDescription('Grund für die Verwarnung')
.setRequired(true)))
.addSubcommand(subcommand =>
subcommand
.setName('unwarn')
.setDescription('Entfernt die letzte Warnung')
.addUserOption(option =>
option.setName('target')
.setDescription('Der Nutzer')
.setRequired(true)))
.addSubcommand(subcommand =>
subcommand
.setName('mute')
.setDescription('Muted einen Nutzer')
.addUserOption(option =>
option.setName('target')
.setDescription('Der zu mutende Nutzer')
.setRequired(true))
.addIntegerOption(option =>
option.setName('duration')
.setDescription('Dauer (Zahl)')
.setMinValue(1))
.addStringOption(option =>
option.setName('unit')
.setDescription('Zeiteinheit')
.addChoices(
{ name: 'Sekunden', value: 'sec' },
{ name: 'Minuten', value: 'min' },
{ name: 'Stunden', value: 'hour' },
{ name: 'Tage', value: 'day' },
{ name: 'Wochen', value: 'week' },
{ name: 'Monate', value: 'month' },
{ name: 'Jahre', value: 'year' },
{ name: 'Permanent', value: 'perm' }
))
.addStringOption(option =>
option.setName('reason')
.setDescription('Grund für den Mute')))
.addSubcommand(subcommand =>
subcommand
.setName('unmute')
.setDescription('Entmutet einen Nutzer')
.addUserOption(option =>
option.setName('target')
.setDescription('Der zu entmutende Nutzer')
.setRequired(true)))
.addSubcommand(subcommand =>
subcommand
.setName('ban')
.setDescription('Bannt einen Nutzer')
.addUserOption(option =>
option.setName('target')
.setDescription('Der zu bannende Nutzer')
.setRequired(true))
.addStringOption(option =>
option.setName('reason')
.setDescription('Grund für den Ban'))
.addIntegerOption(option =>
option.setName('delete_days')
.setDescription('Nachrichten der letzten X Tage löschen')
.setMinValue(0)
.setMaxValue(7)))
.addSubcommand(subcommand =>
subcommand
.setName('unban')
.setDescription('Entbannt einen Nutzer')
.addStringOption(option =>
option.setName('target')
.setDescription('Nutzer-ID oder @username')
.setRequired(true)))
.addSubcommand(subcommand =>
subcommand
.setName('config')
.setDescription('Konfiguriert das Warn-System')
.addStringOption(option =>
option.setName('setting')
.setDescription('Welche Einstellung ändern?')
.setRequired(true)
.addChoices(
{ name: 'warn_threshold', value: 'warn_threshold' },
{ name: 'warn_action', value: 'warn_action' },
{ name: 'warn_mute_duration', value: 'warn_mute_duration' },
{ name: 'show', value: 'show' }))
.addStringOption(option =>
option.setName('value')
.setDescription('Neuer Wert'))
.addIntegerOption(option =>
option.setName('duration_value')
.setDescription('Dauer in Sekunden')
.setMinValue(0)))
.setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers),
category: 'Admin',
async execute(interaction: any) {
const subcommand = interaction.options.getSubcommand();
const DB = interaction.client.DB;
const guild = interaction.guild;
const guildId = interaction.guildId;
// Get or create guild settings
let settings: any = DB.get('SELECT * FROM guild_settings WHERE guild_id = ?', guildId);
if (!settings) {
DB.run('INSERT INTO guild_settings (guild_id) VALUES (?)', guildId);
settings = DB.get('SELECT * FROM guild_settings WHERE guild_id = ?', guildId);
}
if (subcommand === 'kick') {
const target = interaction.options.getMember('target') as GuildMember;
const reason = interaction.options.getString('reason') ?? 'Kein Grund';
if (!target) {
await interaction.reply({ content: 'Nutzer nicht gefunden.', ephemeral: true });
return;
}
if (!target.kickable) {
await interaction.reply({ content: 'Ich kann diesen Nutzer nicht kicken.', ephemeral: true });
return;
}
try {
await target.kick(reason);
await interaction.reply(`✅ Gekickt: ${target.user.tag}\nGrund: ${reason}`);
} catch (error) {
console.error(error);
await interaction.reply({ content: '❌ Fehler beim Kicken.', ephemeral: true });
}
return;
}
if (subcommand === 'warn') {
const target = interaction.options.getMember('target') as GuildMember;
const reason = interaction.options.getString('reason')!;
if (!target) {
await interaction.reply({ content: 'Nutzer nicht gefunden.', ephemeral: true });
return;
}
DB.run(
'INSERT INTO mod_logs (guild_id, user_id, moderator_id, action, reason) VALUES (?, ?, ?, ?, ?)',
guildId,
target.id,
interaction.user.id,
'WARN',
reason
);
const countResult: any = DB.get(
'SELECT COUNT(*) as count FROM mod_logs WHERE guild_id = ? AND user_id = ? AND action = ?',
guildId,
target.id,
'WARN'
);
const warnCount = countResult?.count ?? 0;
const warnThreshold = settings?.warn_threshold ?? 3;
const warnAction = settings?.warn_action ?? 'ban';
let message = `✅ Verwarnt: ${target.user.tag}\nGrund: ${reason}\nDas ist die ${warnCount}. Verwarnung.`;
// Check if threshold reached
if (warnCount >= warnThreshold) {
message += `\n\n⚠ **Warn-Limit erreicht (${warnThreshold}/${warnThreshold})!**`;
try {
if (warnAction === 'kick') {
await target.kick(`Auto-Kick: ${warnThreshold} Warnungen`);
message += `\n👢 **Automatisch gekickt!**`;
} else if (warnAction === 'mute') {
const muteDuration = settings?.warn_mute_duration ?? 1800; // 30 min default
await target.timeout(muteDuration * 1000, `Auto-Mute: ${warnThreshold} Warnungen`);
const durationMins = Math.floor(muteDuration / 60);
message += `\n🔇 **Automatisch gemuted (${durationMins} Min)!**`;
} else if (warnAction === 'ban') {
await target.ban({ reason: `Auto-Ban: ${warnThreshold} Warnungen` });
message += `\n🔨 **Automatisch gebannt!**`;
}
} catch (error) {
console.error('[ADMIN] Auto-action failed:', error);
message += `\n❌ Auto-Aktion fehlgeschlagen.`;
}
}
await interaction.reply({ content: message });
return;
}
if (subcommand === 'unwarn') {
const target = interaction.options.getMember('target') as GuildMember;
if (!target) {
await interaction.reply({ content: 'Nutzer nicht gefunden.', ephemeral: true });
return;
}
const latestWarn: any = DB.get(
'SELECT * FROM mod_logs WHERE guild_id = ? AND user_id = ? AND action = ? ORDER BY id DESC LIMIT 1',
guildId,
target.id,
'WARN'
);
if (!latestWarn) {
await interaction.reply({ content: 'Keine Warnung für diesen Nutzer gefunden.', ephemeral: true });
return;
}
DB.run('DELETE FROM mod_logs WHERE id = ?', latestWarn.id);
await interaction.reply(`✅ Letzte Warnung für ${target.user.tag} entfernt.`);
return;
}
if (subcommand === 'mute') {
const target = interaction.options.getMember('target') as GuildMember;
const duration = interaction.options.getInteger('duration');
const unit = interaction.options.getString('unit');
const reason = interaction.options.getString('reason') ?? 'Kein Grund';
if (!target) {
await interaction.reply({ content: 'Nutzer nicht gefunden.', ephemeral: true });
return;
}
// Parse duration
let durationMs: number | null = null;
let durationText = '';
if (unit === 'perm') {
durationMs = null; // Permanent = null
durationText = 'Permanent';
} else if (duration) {
const unitSeconds: Record<string, number> = {
sec: 1,
min: 60,
hour: 60 * 60,
day: 60 * 60 * 24,
week: 60 * 60 * 24 * 7,
month: 60 * 60 * 24 * 30,
year: 60 * 60 * 24 * 365
};
const multiplier = unitSeconds[unit!];
if (!multiplier) {
await interaction.reply({ content: '❌ Ungültige Zeiteinheit.', ephemeral: true });
return;
}
durationMs = duration * multiplier * 1000;
// Human-readable duration
const unitNames: Record<string, string> = {
sec: 'Sekunden',
min: 'Minuten',
hour: 'Stunden',
day: 'Tage',
week: 'Wochen',
month: 'Monate',
year: 'Jahre'
};
durationText = `${duration} ${unitNames[unit!]}`;
} else {
await interaction.reply({ content: '❌ Dauer oder Zeiteinheit angeben.', ephemeral: true });
return;
}
try {
await target.timeout(durationMs, reason);
await interaction.reply(`✅ Gemuted: ${target.user.tag}\nDauer: ${durationText}\nGrund: ${reason}`);
} catch (error) {
console.error(error);
await interaction.reply({ content: '❌ Fehler beim Muten.', ephemeral: true });
}
return;
}
if (subcommand === 'unmute') {
const target = interaction.options.getMember('target') as GuildMember;
if (!target) {
await interaction.reply({ content: 'Nutzer nicht gefunden.', ephemeral: true });
return;
}
try {
await target.timeout(null, 'Unmute');
await interaction.reply(`✅ Entmutet: ${target.user.tag}`);
} catch (error) {
console.error(error);
await interaction.reply({ content: '❌ Fehler beim Entmuten.', ephemeral: true });
}
return;
}
if (subcommand === 'ban') {
const target = interaction.options.getMember('target') as GuildMember;
const reason = interaction.options.getString('reason') ?? 'Kein Grund';
const deleteDays = interaction.options.getInteger('delete_days') ?? 0;
if (!target) {
await interaction.reply({ content: 'Nutzer nicht gefunden.', ephemeral: true });
return;
}
if (!target.bannable) {
await interaction.reply({ content: 'Ich kann diesen Nutzer nicht bannen.', ephemeral: true });
return;
}
try {
await target.ban({ deleteMessageSeconds: deleteDays * 86400, reason });
await interaction.reply(`✅ Gebannt: ${target.user.tag}\nGrund: ${reason}`);
} catch (error) {
console.error(error);
await interaction.reply({ content: '❌ Fehler beim Bannen.', ephemeral: true });
}
return;
}
if (subcommand === 'unban') {
let targetId = interaction.options.getString('target')!;
if (targetId.startsWith('<@') && targetId.endsWith('>')) {
targetId = targetId.replace(/[<@!>]/g, '');
}
try {
await guild.bans.remove(targetId, 'Unban durch Admin');
await interaction.reply(`✅ Entbannt: ${targetId}`);
} catch (error: any) {
console.error(error);
await interaction.reply({ content: `❌ Fehler beim Entbannen: ${error.message}`, ephemeral: true });
}
return;
}
if (subcommand === 'config') {
const setting = interaction.options.getString('setting')!;
const value = interaction.options.getString('value');
const durationValue = interaction.options.getInteger('duration_value');
if (setting === 'show') {
const embed = new EmbedBuilder()
.setTitle('⚙️ Warn-System Konfiguration')
.setColor(0x3498db)
.addFields(
{ name: 'Warn-Limit', value: `${settings?.warn_threshold ?? 3} Warnungen`, inline: true },
{ name: 'Auto-Aktion', value: settings?.warn_action ?? 'ban', inline: true },
{ name: 'Mute-Dauer', value: `${(settings?.warn_mute_duration ?? 1800) / 60} Min`, inline: true }
);
await interaction.reply({ embeds: [embed], ephemeral: true });
return;
}
if (setting === 'warn_threshold') {
const threshold = parseInt(value!);
if (isNaN(threshold) || threshold < 1) {
await interaction.reply({ content: '❌ Ungültiger Wert. Mindestens 1.', ephemeral: true });
return;
}
DB.run('UPDATE guild_settings SET warn_threshold = ? WHERE guild_id = ?', threshold, guildId);
await interaction.reply(`✅ Warn-Limit auf ${threshold} gesetzt.`);
return;
}
if (setting === 'warn_action') {
const validActions = ['none', 'kick', 'mute', 'ban'];
if (!validActions.includes(value!)) {
await interaction.reply({ content: `❌ Ungültige Aktion. Möglich: ${validActions.join(', ')}`, ephemeral: true });
return;
}
DB.run('UPDATE guild_settings SET warn_action = ? WHERE guild_id = ?', value, guildId);
await interaction.reply(`✅ Auto-Aktion auf ${value} gesetzt.`);
return;
}
if (setting === 'warn_mute_duration') {
const duration = durationValue ?? 1800;
if (duration < 0) {
await interaction.reply({ content: '❌ Ungültige Dauer.', ephemeral: true });
return;
}
DB.run('UPDATE guild_settings SET warn_mute_duration = ? WHERE guild_id = ?', duration, guildId);
await interaction.reply(`✅ Mute-Dauer auf ${duration} Sekunden (${Math.floor(duration/60)} Min) gesetzt.`);
return;
}
}
},
};
export default command;

View File

@@ -0,0 +1,59 @@
import { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits } from 'discord.js';
import { Command } from '../../structures/Command.js';
const command: Command = {
data: new SlashCommandBuilder()
.setName('help')
.setDescription('Zeigt alle verfügbaren Befehle an.')
.setDMPermission(true),
category: 'Public',
async execute(interaction: any, client: any) {
if (!client.application?.owner) await client.application?.fetch();
const isOwner = interaction.user.id === client.application?.owner?.id;
const isAdmin = interaction.memberPermissions?.has(PermissionFlagsBits.Administrator);
const embed = new EmbedBuilder()
.setTitle('📖 Hilfe-Menü')
.setDescription('Hier sind die Befehle:')
.setColor(0x2ecc71)
.setTimestamp();
const publicCmds: string[] = [];
const adminCmds: string[] = [];
const ownerCmds: string[] = [];
client.commands.forEach((cmd: Command) => {
const desc = 'description' in cmd.data ? cmd.data.description : 'Kontextmenü';
const entry = `• **/${cmd.data.name}**: ${desc}`;
if (cmd.category === 'Public') {
publicCmds.push(entry);
} else if (cmd.category === 'Admin') {
adminCmds.push(entry);
} else if (cmd.category === 'Owner') {
ownerCmds.push(entry);
}
});
if (publicCmds.length > 0) {
embed.addFields({ name: '🌍 Öffentliche Befehle', value: publicCmds.join('\n') });
}
if (isAdmin || isOwner) {
if (adminCmds.length > 0) {
embed.addFields({ name: '🛡️ Admin-Befehle', value: adminCmds.join('\n') });
}
}
if (isOwner) {
if (ownerCmds.length > 0) {
embed.addFields({ name: '🔑 Owner-Befehle', value: ownerCmds.join('\n') });
}
}
await interaction.reply({ embeds: [embed], ephemeral: true });
},
};
export default command;

View File

@@ -0,0 +1,107 @@
import { SlashCommandBuilder, EmbedBuilder } from 'discord.js';
import { Command } from '../../structures/Command.js';
import { Deployer } from '../../structures/Deployer.js';
const command: Command = {
data: new SlashCommandBuilder()
.setName('owner')
.setDescription('Bot-Owner Commands')
.addSubcommand(subcommand =>
subcommand
.setName('deploy')
.setDescription('Aktualisiert Commands'))
.addSubcommand(subcommand =>
subcommand
.setName('stats')
.setDescription('Zeigt Bot-Statistiken'))
.addSubcommand(subcommand =>
subcommand
.setName('servers')
.setDescription('Listet alle Server'))
.setDMPermission(true),
category: 'Owner',
async execute(interaction: any, client: any) {
if (!client.application?.owner) await client.application?.fetch();
if (interaction.user.id !== client.application?.owner?.id) {
await interaction.reply({ content: 'Keine Berechtigung.', ephemeral: true });
return;
}
const subcommand = interaction.options.getSubcommand();
if (subcommand === 'deploy') {
await interaction.deferReply({ ephemeral: true });
try {
const count = await Deployer.deploy(client.user!.id, client.token!);
await interaction.editReply(`${count} Befehle aktualisiert!`);
} catch (error) {
console.error(error);
await interaction.editReply('❌ Fehler beim Deployment.');
}
return;
}
if (subcommand === 'stats') {
await interaction.deferReply({ ephemeral: true });
const uptime = process.uptime();
const days = Math.floor(uptime / 86400);
const hours = Math.floor(uptime / 3600) % 24;
const minutes = Math.floor(uptime / 60) % 60;
const uptimeStr = `${days}d ${hours}h ${minutes}m`;
const totalServers = client.guilds.cache.size;
const totalUsers = client.guilds.cache.reduce((acc: number, guild: any) => acc + guild.memberCount, 0);
const embed = new EmbedBuilder()
.setTitle('📊 Bot-Statistiken')
.setColor(0x3498db)
.addFields(
{ name: '🤖 Bot', value: `**Uptime:** ${uptimeStr}\n**Latency:** ${client.ws.ping}ms`, inline: true },
{ name: '🌍 Netzwerk', value: `**Server:** ${totalServers}\n**Nutzer:** ${totalUsers}`, inline: true }
)
.setTimestamp();
await interaction.editReply({ embeds: [embed] });
return;
}
if (subcommand === 'servers') {
await interaction.deferReply({ ephemeral: true });
const guilds = client.guilds.cache.map((guild: any) =>
`• **${guild.name}** \`(${guild.id})\` - ${guild.memberCount} Mitglieder`
);
const chunks = [];
for (let i = 0; i < guilds.length; i += 10) {
chunks.push(guilds.slice(i, i + 10).join('\n'));
}
if (chunks.length === 0) {
await interaction.editReply('Keine Server.');
return;
}
const embed = new EmbedBuilder()
.setTitle(`🌍 Server (${client.guilds.cache.size})`)
.setColor(0xf1c40f)
.setDescription(chunks[0].substring(0, 4000))
.setTimestamp();
await interaction.editReply({ embeds: [embed] });
for (let i = 1; i < chunks.length; i++) {
const nextEmbed = new EmbedBuilder()
.setColor(0xf1c40f)
.setDescription(chunks[i].substring(0, 4000));
await interaction.followUp({ embeds: [nextEmbed], ephemeral: true });
}
return;
}
},
};
export default command;

View File

@@ -0,0 +1,14 @@
import { SlashCommandBuilder } from 'discord.js';
import { Command } from '../../structures/Command.js';
const command: Command = {
data: new SlashCommandBuilder()
.setName('ping')
.setDescription('Testet die Latenz.'),
category: 'Public',
async execute(interaction: any) {
await interaction.reply(`🏓 Pong! Latenz: ${interaction.client.ws.ping}ms`);
},
};
export default command;

View File

@@ -0,0 +1,121 @@
import { SlashCommandBuilder, PermissionFlagsBits, EmbedBuilder } from 'discord.js';
import { Command } from '../../structures/Command.js';
import { TwitchManager } from '../../structures/TwitchManager.js';
const command: Command = {
data: new SlashCommandBuilder()
.setName('twitch')
.setDescription('Twitch-Befehle.')
.addSubcommand(subcommand =>
subcommand
.setName('online')
.setDescription('Prüft, ob ein Kanal online ist.')
.addStringOption(option =>
option.setName('channel')
.setDescription('Der Name des Twitch-Kanals')
.setRequired(true)))
.addSubcommand(subcommand =>
subcommand
.setName('add')
.setDescription('Kanal überwachen (Admin).')
.addStringOption(option =>
option.setName('channel')
.setDescription('Der Name des Twitch-Kanals')
.setRequired(true))
.addStringOption(option =>
option.setName('message')
.setDescription('Optionale Nachricht')))
.addSubcommand(subcommand =>
subcommand
.setName('remove')
.setDescription('Kanal aus Überwachung entfernen (Admin).')
.addStringOption(option =>
option.setName('channel')
.setDescription('Der Name des Twitch-Kanals')
.setRequired(true)))
.addSubcommand(subcommand =>
subcommand
.setName('list')
.setDescription('Listet überwachte Kanäle.')),
category: 'Public',
async execute(interaction: any) {
const subcommand = interaction.options.getSubcommand();
const guildId = interaction.guildId;
const isAdmin = interaction.memberPermissions?.has(PermissionFlagsBits.Administrator);
const DB = interaction.client.DB;
if (subcommand === 'online') {
const channelName = interaction.options.getString('channel')!.toLowerCase();
await interaction.deferReply();
const stream = await TwitchManager.fetchStreamData(channelName);
if (!stream) {
await interaction.editReply(`🔴 Der Kanal **${channelName}** ist offline.`);
return;
}
const thumbnailUrl = stream.thumbnail_url
.replace('{width}', '1280')
.replace('{height}', '720') + `?t=${Date.now()}`;
const embed = new EmbedBuilder()
.setTitle(`🟢 ${stream.user_name} ist ONLINE!`)
.setURL(`https://twitch.tv/${stream.user_login}`)
.setColor(0x00FF00)
.setImage(thumbnailUrl)
.addFields(
{ name: 'Titel', value: stream.title || 'Kein Titel', inline: false },
{ name: 'Kategorie', value: stream.game_name || 'Unbekannt', inline: true },
{ name: 'Zuschauer', value: stream.viewer_count.toString(), inline: true }
)
.setTimestamp();
await interaction.editReply({ embeds: [embed] });
return;
}
if (subcommand === 'list') {
const monitors: any[] = DB.all('SELECT * FROM twitch_monitors WHERE guild_id = ? AND discord_channel_id = ?', guildId, interaction.channelId);
if (monitors.length === 0) {
await interaction.reply('Keine Twitch-Kanäle überwacht.');
return;
}
const list = monitors.map((m: any) => `• **${m.channel_name}**`).join('\n');
const embed = new EmbedBuilder()
.setTitle('📺 Überwachte Twitch-Kanäle')
.setColor(0x6441a5)
.setDescription(list);
await interaction.reply({ embeds: [embed] });
return;
}
// Admin only
if (!isAdmin) {
await interaction.reply({ content: '❌ Keine Berechtigung.', ephemeral: true });
return;
}
if (subcommand === 'add') {
const channelName = interaction.options.getString('channel')!.toLowerCase();
const message = interaction.options.getString('message');
const discordChannelId = interaction.channelId;
DB.run(
'INSERT INTO twitch_monitors (guild_id, channel_name, discord_channel_id, custom_message) VALUES (?, ?, ?, ?) ON CONFLICT(guild_id, channel_name) DO UPDATE SET discord_channel_id = ?, custom_message = ?',
guildId, channelName, discordChannelId, message, discordChannelId, message
);
await interaction.reply(`✅ Kanal **${channelName}** wird überwacht.`);
}
if (subcommand === 'remove') {
const channelName = interaction.options.getString('channel')!.toLowerCase();
DB.run('DELETE FROM twitch_monitors WHERE guild_id = ? AND channel_name = ?', guildId, channelName);
await interaction.reply(`✅ Kanal **${channelName}** entfernt.`);
}
},
};
export default command;