commit f0c8fe734aa2950fa282c5947dbbfce41d9e2a94 Author: Robin Date: Mon Oct 6 14:20:28 2025 +0200 + Upload diff --git a/.env b/.env new file mode 100644 index 0000000..445ed02 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +DISCORD_TOKEN="DEIN_SUPER_GEHEIMER_BOT_TOKEN" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2c469d8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ + + +#Ignore vscode AI rules +.github\instructions\codacy.instructions.md diff --git a/cogs/announcement.py b/cogs/announcement.py new file mode 100644 index 0000000..e6ea412 --- /dev/null +++ b/cogs/announcement.py @@ -0,0 +1,54 @@ +import discord +from discord.ext import commands +from discord import app_commands + +class Announcement(commands.Cog): + def __init__(self, bot: commands.Bot): + self.bot = bot + + @app_commands.command(name="announce", description="Sendet eine formatierte Ankündigung in einen Kanal.") + @app_commands.describe( + channel="Der Kanal, in den die Ankündigung gesendet werden soll.", + title="Der Titel der Ankündigung.", + message="Die Nachricht. Benutze '\\n' für Zeilenumbrüche.", + color="Die Farbe des Embeds als HEX-Code (z.B. #FF5733).", + mention="Eine Rolle, die mit der Ankündigung erwähnt werden soll." + ) + @app_commands.checks.has_permissions(manage_messages=True) + async def announce(self, interaction: discord.Interaction, channel: discord.TextChannel, title: str, message: str, color: str = None, mention: discord.Role = None): + """Erstellt und sendet eine Ankündigung.""" + + # Ersetze \n durch echte Zeilenumbrüche + message = message.replace("\\n", "\n") + + # Verarbeite die Farbe + embed_color = discord.Color.blue() # Standardfarbe + if color: + try: + # Entferne '#' und konvertiere HEX zu Integer + embed_color = discord.Color(int(color.lstrip('#'), 16)) + except ValueError: + await interaction.response.send_message("Ungültiger HEX-Farbcode. Bitte benutze ein Format wie `#FF5733`.", ephemeral=True) + return + + embed = discord.Embed( + title=title, + description=message, + color=embed_color + ) + embed.set_footer(text=f"Ankündigung von {interaction.user.display_name}") + + # Erstelle den Inhalt der Nachricht (für den Ping) + content = mention.mention if mention else None + + try: + await channel.send(content=content, embed=embed) + await interaction.response.send_message(f"✅ Ankündigung wurde erfolgreich in {channel.mention} gesendet.", ephemeral=True) + except discord.Forbidden: + await interaction.response.send_message("❌ Ich habe keine Berechtigung, in diesem Kanal zu schreiben.", ephemeral=True) + except Exception as e: + await interaction.response.send_message(f"Ein Fehler ist aufgetreten: {e}", ephemeral=True) + + +async def setup(bot: commands.Bot): + await bot.add_cog(Announcement(bot)) \ No newline at end of file diff --git a/cogs/audit_log.py b/cogs/audit_log.py new file mode 100644 index 0000000..f87db7c --- /dev/null +++ b/cogs/audit_log.py @@ -0,0 +1,118 @@ +import discord +from discord.ext import commands +from discord import app_commands +import sqlite3 +import datetime + +class AuditLog(commands.Cog): + def __init__(self, bot: commands.Bot): + self.bot = bot + self.db_connection = sqlite3.connect("xalos_data.db") + self.db_cursor = self.db_connection.cursor() + # Tabelle für server-spezifische Einstellungen + self.db_cursor.execute(""" + CREATE TABLE IF NOT EXISTS server_configs ( + guild_id INTEGER PRIMARY KEY, + log_channel_id INTEGER + ) + """) + + async def get_log_channel(self, guild_id: int) -> discord.TextChannel | None: + """Holt den Log-Kanal für einen Server aus der DB.""" + self.db_cursor.execute("SELECT log_channel_id FROM server_configs WHERE guild_id = ?", (guild_id,)) + result = self.db_cursor.fetchone() + if result and result[0]: + return self.bot.get_channel(result[0]) + return None + + @app_commands.command(name="setlogchannel", description="Setzt den Kanal für die Audit-Logs.") + @app_commands.describe(channel="Der Kanal, in den die Logs gesendet werden sollen.") + @app_commands.checks.has_permissions(manage_guild=True) + async def set_log_channel(self, interaction: discord.Interaction, channel: discord.TextChannel): + """Speichert den Log-Kanal in der Datenbank.""" + self.db_cursor.execute( + "INSERT OR REPLACE INTO server_configs (guild_id, log_channel_id) VALUES (?, ?)", + (interaction.guild.id, channel.id) + ) + self.db_connection.commit() + await interaction.response.send_message(f"✅ Der Log-Kanal wurde auf {channel.mention} gesetzt.", ephemeral=True) + + @commands.Cog.listener() + async def on_message_delete(self, message: discord.Message): + if message.author.bot or not message.guild: + return + + log_channel = await self.get_log_channel(message.guild.id) + if log_channel: + embed = discord.Embed( + title="🗑️ Nachricht gelöscht", + description=f"**Autor:** {message.author.mention}\n" + f"**Kanal:** {message.channel.mention}", + color=discord.Color.orange(), + timestamp=datetime.datetime.now(datetime.timezone.utc) + ) + # Füge den Inhalt hinzu, wenn er nicht zu lang ist + if message.content: + embed.add_field(name="Inhalt", value=message.content[:1024], inline=False) + + await log_channel.send(embed=embed) + + @commands.Cog.listener() + async def on_message_edit(self, before: discord.Message, after: discord.Message): + if before.author.bot or not before.guild or before.content == after.content: + return + + log_channel = await self.get_log_channel(before.guild.id) + if log_channel: + embed = discord.Embed( + title="✏️ Nachricht bearbeitet", + description=f"**Autor:** {before.author.mention}\n" + f"**Kanal:** {before.channel.mention}\n" + f"[Zur Nachricht springen]({after.jump_url})", + color=discord.Color.blue(), + timestamp=datetime.datetime.now(datetime.timezone.utc) + ) + # Füge den alten und neuen Inhalt hinzu, wenn er nicht zu lang ist + embed.add_field(name="Vorher", value=before.content[:1024], inline=False) + embed.add_field(name="Nachher", value=after.content[:1024], inline=False) + + await log_channel.send(embed=embed) + + @commands.Cog.listener() + async def on_member_ban(self, guild: discord.Guild, user: discord.User | discord.Member): + log_channel = await self.get_log_channel(guild.id) + if log_channel: + embed = discord.Embed( + title="🔨 Mitglied gebannt", + description=f"**Mitglied:** {user.mention} (`{user.id}`)", + color=discord.Color.dark_red(), + timestamp=datetime.datetime.now(datetime.timezone.utc) + ) + await log_channel.send(embed=embed) + + @commands.Cog.listener() + async def on_member_remove(self, member: discord.Member): + # Dieser Event wird auch bei einem Kick oder Bann ausgelöst. Wir warten kurz, + # um zu sehen, ob es ein Bann war, um doppelte Logs zu vermeiden. + await asyncio.sleep(2) + try: + # Wenn der Nutzer gebannt wurde, existiert ein Ban-Eintrag + await member.guild.fetch_ban(member) + return # Es war ein Bann, on_member_ban kümmert sich darum + except discord.NotFound: + # Es war kein Bann (also ein Kick oder freiwilliger Leave) + log_channel = await self.get_log_channel(member.guild.id) + if log_channel: + embed = discord.Embed( + title="🚪 Mitglied hat den Server verlassen", + description=f"**Mitglied:** {member.mention} (`{member.id}`)", + color=discord.Color.greyple(), + timestamp=datetime.datetime.now(datetime.timezone.utc) + ) + await log_channel.send(embed=embed) + + +async def setup(bot: commands.Bot): + # Wir brauchen asyncio für das Warten im on_member_remove Event + import asyncio + await bot.add_cog(AuditLog(bot)) \ No newline at end of file diff --git a/cogs/custom_commands.py b/cogs/custom_commands.py new file mode 100644 index 0000000..a45196b --- /dev/null +++ b/cogs/custom_commands.py @@ -0,0 +1,90 @@ +import discord +from discord.ext import commands +from discord import app_commands +import sqlite3 + +class CustomCommands(commands.Cog): + def __init__(self, bot: commands.Bot): + self.bot = bot + self.db_connection = sqlite3.connect("xalos_data.db") + self.db_cursor = self.db_connection.cursor() + self.db_cursor.execute(""" + CREATE TABLE IF NOT EXISTS custom_commands ( + guild_id INTEGER, + command_name TEXT, + response_text TEXT, + PRIMARY KEY (guild_id, command_name) + ) + """) + self.db_connection.commit() + # We define a static prefix for custom commands, as they are text-based. + self.prefix = "!" + + @commands.Cog.listener() + async def on_message(self, message: discord.Message): + # Ignore bots, DMs, and messages that don't start with the prefix + if message.author.bot or not message.guild or not message.content.startswith(self.prefix): + return + + # Extract command name (e.g., "!hello" -> "hello") + command_name = message.content[len(self.prefix):].split(' ')[0].lower() + if not command_name: + return + + # Look up the command in the database + self.db_cursor.execute( + "SELECT response_text FROM custom_commands WHERE guild_id = ? AND command_name = ?", + (message.guild.id, command_name) + ) + result = self.db_cursor.fetchone() + + if result: + response_text = result[0] + await message.channel.send(response_text) + + # --- Management Commands --- + + customcmd_group = app_commands.Group(name="customcommand", description="Verwaltet benutzerdefinierte Befehle.", default_permissions=discord.Permissions(manage_guild=True)) + + @customcmd_group.command(name="add", description="Erstellt einen neuen benutzerdefinierten Befehl.") + @app_commands.describe(name="Der Name des Befehls (ohne '!').", response="Die Antwort, die der Bot senden soll.") + async def cc_add(self, interaction: discord.Interaction, name: str, response: str): + command_name = name.lower().split(' ')[0] # Ensure single word and lowercase + try: + self.db_cursor.execute("INSERT INTO custom_commands (guild_id, command_name, response_text) VALUES (?, ?, ?)", (interaction.guild.id, command_name, response)) + self.db_connection.commit() + await interaction.response.send_message(f"✅ Der Befehl `{self.prefix}{command_name}` wurde erstellt.", ephemeral=True) + except sqlite3.IntegrityError: + await interaction.response.send_message(f"⚠️ Ein Befehl mit dem Namen `{command_name}` existiert bereits. Bitte entferne ihn zuerst.", ephemeral=True) + + @customcmd_group.command(name="remove", description="Löscht einen benutzerdefinierten Befehl.") + @app_commands.describe(name="Der Name des Befehls, der gelöscht werden soll (ohne '!').") + async def cc_remove(self, interaction: discord.Interaction, name: str): + command_name = name.lower() + self.db_cursor.execute("DELETE FROM custom_commands WHERE guild_id = ? AND command_name = ?", (interaction.guild.id, command_name)) + if self.db_cursor.rowcount > 0: + self.db_connection.commit() + await interaction.response.send_message(f"✅ Der Befehl `{self.prefix}{command_name}` wurde gelöscht.", ephemeral=True) + else: + await interaction.response.send_message(f"❌ Kein Befehl mit dem Namen `{command_name}` gefunden.", ephemeral=True) + + @customcmd_group.command(name="list", description="Zeigt alle benutzerdefinierten Befehle an.") + async def cc_list(self, interaction: discord.Interaction): + self.db_cursor.execute("SELECT command_name FROM custom_commands WHERE guild_id = ?", (interaction.guild.id,)) + commands_list = [row[0] for row in self.db_cursor.fetchall()] + + if not commands_list: + await interaction.response.send_message("Es gibt keine benutzerdefinierten Befehle auf diesem Server.", ephemeral=True) + return + + embed = discord.Embed( + title="Benutzerdefinierte Befehle", + description=", ".join(f"`{self.prefix}{cmd}`" for cmd in commands_list), + color=discord.Color.green() + ) + await interaction.response.send_message(embed=embed, ephemeral=True) + +async def setup(bot: commands.Bot): + cog = CustomCommands(bot) + bot.tree.add_command(cog.customcmd_group) + await bot.add_cog(cog) \ No newline at end of file diff --git a/cogs/giveaways.py b/cogs/giveaways.py new file mode 100644 index 0000000..b4ab2c9 --- /dev/null +++ b/cogs/giveaways.py @@ -0,0 +1,103 @@ +import discord +from discord.ext import commands +from discord import app_commands +import datetime +import asyncio +import random +import re + +def parse_duration(duration_str: str) -> int: + """ + Wandelt einen Dauer-String (z.B. "1d", "12h", "30m", "5s") in Sekunden um. + Gibt 0 zurück, wenn das Format ungültig ist. + """ + match = re.match(r"(\d+)([dhms])", duration_str.lower()) + if not match: + return 0 + + value, unit = int(match.group(1)), match.group(2) + + if unit == 'd': + return value * 86400 + elif unit == 'h': + return value * 3600 + elif unit == 'm': + return value * 60 + elif unit == 's': + return value + return 0 + +class Giveaways(commands.Cog): + def __init__(self, bot: commands.Bot): + self.bot = bot + + @app_commands.command(name="giveaway", description="Startet eine Verlosung.") + @app_commands.describe( + duration="Dauer (z.B. 10m, 1h, 2d).", + winners="Anzahl der Gewinner.", + prize="Der Preis, der verlost wird." + ) + @app_commands.checks.has_permissions(manage_guild=True) + async def giveaway(self, interaction: discord.Interaction, duration: str, winners: int, prize: str): + """Startet eine Verlosung.""" + + seconds = parse_duration(duration) + if seconds <= 0: + await interaction.response.send_message("Ungültiges Zeitformat! Bitte benutze `d`, `h`, `m` oder `s` (z.B. `10m` oder `2h`).", ephemeral=True) + return + + if winners < 1: + await interaction.response.send_message("Die Anzahl der Gewinner muss mindestens 1 sein.", ephemeral=True) + return + + end_time = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(seconds=seconds) + + embed = discord.Embed( + title=f"🎉 Verlosung: {prize}", + description=f"Reagiere mit 🎉, um teilzunehmen!\n" + f"Endet: \n" + f"Veranstalter: {interaction.user.mention}", + color=discord.Color.gold() + ) + embed.set_footer(text=f"{winners} Gewinner") + + # Sende die Nachricht und reagiere, um die Teilnahme zu starten + await interaction.response.send_message("Verlosung wird gestartet...", ephemeral=True) + giveaway_message = await interaction.channel.send(embed=embed) + await giveaway_message.add_reaction("🎉") + + # Warte bis zum Ende der Verlosung + await asyncio.sleep(seconds) + + # Hole die Nachricht erneut, um die aktuellen Reaktionen zu bekommen + try: + updated_message = await interaction.channel.fetch_message(giveaway_message.id) + except discord.NotFound: + # Nachricht wurde gelöscht, Verlosung abbrechen + return + + # Finde alle Teilnehmer (ohne Bots) + reaction = discord.utils.get(updated_message.reactions, emoji="🎉") + participants = [user async for user in reaction.users() if not user.bot] + + # Wähle den/die Gewinner + if not participants: + winner_text = "Niemand hat teilgenommen. 😢" + new_embed = embed.copy() + new_embed.description = f"Verlosung beendet.\n{winner_text}" + new_embed.color = discord.Color.dark_red() + await updated_message.edit(embed=new_embed) + return + + chosen_winners = random.sample(participants, k=min(winners, len(participants))) + winner_mentions = ", ".join(w.mention for w in chosen_winners) + + # Bearbeite die ursprüngliche Nachricht und verkünde die Gewinner + new_embed = embed.copy() + new_embed.description = f"Verlosung beendet!\n**Gewinner:** {winner_mentions}" + new_embed.color = discord.Color.green() + await updated_message.edit(embed=new_embed) + await interaction.channel.send(f"Herzlichen Glückwunsch {winner_mentions}! Ihr habt **{prize}** gewonnen!") + +async def setup(bot: commands.Bot): + await bot.add_cog(Giveaways(bot)) \ No newline at end of file diff --git a/cogs/leveling.py b/cogs/leveling.py new file mode 100644 index 0000000..41bf45a --- /dev/null +++ b/cogs/leveling.py @@ -0,0 +1,205 @@ +import discord +from discord.ext import commands +from discord import app_commands +import sqlite3 +import random +import time + +class Leveling(commands.Cog): + def __init__(self, bot: commands.Bot): + self.bot = bot + self.db_connection = sqlite3.connect("xalos_data.db") + self.db_cursor = self.db_connection.cursor() + self.db_cursor.execute(""" + CREATE TABLE IF NOT EXISTS levels ( + guild_id INTEGER, + user_id INTEGER, + level INTEGER DEFAULT 1, + xp INTEGER DEFAULT 0, + last_message_timestamp INTEGER DEFAULT 0, + PRIMARY KEY (guild_id, user_id) + ) + """) + self.db_cursor.execute(""" + CREATE TABLE IF NOT EXISTS level_rewards ( + guild_id INTEGER, + level INTEGER, + role_id INTEGER, + PRIMARY KEY (guild_id, level) + ) + """) + + def cog_unload(self): + """Wird aufgerufen, wenn der Cog entladen wird, um die DB-Verbindung zu schließen.""" + self.db_connection.close() + + def _calculate_xp_for_next_level(self, level: int) -> int: + """Berechnet die benötigten XP für das nächste Level.""" + return 5 * (level ** 2) + 50 * level + 100 + + @commands.Cog.listener() + async def on_message(self, message: discord.Message): + # Ignoriere Bots und Nachrichten in DMs + if message.author.bot or not message.guild: + return + + # Cooldown-System (z.B. alle 60 Sekunden XP) + user_id = message.author.id + guild_id = message.guild.id + current_time = time.time() + + if self.cooldowns.get(user_id, 0) + 60 > current_time: + return # User ist noch im Cooldown + + self.cooldowns[user_id] = current_time + + # XP vergeben + xp_to_add = random.randint(15, 25) + + # User in DB suchen/erstellen + self.db_cursor.execute("SELECT * FROM levels WHERE guild_id = ? AND user_id = ?", (guild_id, user_id)) + user_data = self.db_cursor.fetchone() + + if user_data is None: + self.db_cursor.execute("INSERT INTO levels (guild_id, user_id, xp) VALUES (?, ?, ?)", (guild_id, user_id, xp_to_add)) + current_level, current_xp = 1, xp_to_add + else: + current_level = user_data[2] + current_xp = user_data[3] + xp_to_add + self.db_cursor.execute("UPDATE levels SET xp = ? WHERE guild_id = ? AND user_id = ?", (current_xp, guild_id, user_id)) + + # Level-Up-Check + xp_for_next_level = 5 * (current_level ** 2) + 50 * current_level + 100 + if current_xp >= xp_for_next_level: + new_level = current_level + 1 + remaining_xp = current_xp - xp_for_next_level + self.db_cursor.execute("UPDATE levels SET level = ?, xp = ? WHERE guild_id = ? AND user_id = ?", (new_level, remaining_xp, guild_id, user_id)) + await message.channel.send(f"🎉 Herzlichen Glückwunsch, {message.author.mention}! Du hast Level **{new_level}** erreicht!") + + # Check for role rewards + self.db_cursor.execute("SELECT role_id FROM level_rewards WHERE guild_id = ? AND level = ?", (guild_id, new_level)) + reward = self.db_cursor.fetchone() + if reward: + role_id = reward[0] + role = message.guild.get_role(role_id) + if role: + await message.author.add_roles(role, reason=f"Level {new_level} erreicht") + await message.channel.send(f"Als Belohnung hast du die Rolle **{role.name}** erhalten!") + + self.db_connection.commit() + + @app_commands.command(name="rank", description="Zeigt dein Level und deine XP an.") + @app_commands.describe(member="Das Mitglied, dessen Rang du sehen möchtest.") + async def rank(self, interaction: discord.Interaction, member: discord.Member = None): + target = member or interaction.user + + self.db_cursor.execute("SELECT level, xp FROM levels WHERE guild_id = ? AND user_id = ?", (interaction.guild.id, target.id)) + user_data = self.db_cursor.fetchone() + + if user_data is None: + await interaction.response.send_message(f"{target.display_name} hat noch keine XP gesammelt.", ephemeral=True) + return + + level, xp = user_data + xp_for_next_level = self._calculate_xp_for_next_level(level) + + # Finde den Rang des Nutzers (effiziente SQL-Abfrage) + self.db_cursor.execute( + "SELECT COUNT(*) + 1 FROM levels WHERE guild_id = ? AND xp > ?", + (interaction.guild.id, xp) + ) + rank_result = self.db_cursor.fetchone() + rank = rank_result[0] if rank_result else -1 + + embed = discord.Embed( + title=f"Rang von {target.display_name}", + color=target.color + ) + embed.set_thumbnail(url=target.display_avatar.url) + embed.add_field(name="Level", value=f"`{level}`", inline=True) + embed.add_field(name="XP", value=f"`{xp} / {xp_for_next_level}`", inline=True) + if rank > 0: + embed.add_field(name="Rang", value=f"`#{rank}`", inline=True) + + await interaction.response.send_message(embed=embed) + + @app_commands.command(name="leaderboard", description="Zeigt die Top 10 Nutzer des Servers an.") + async def leaderboard(self, interaction: discord.Interaction): + self.db_cursor.execute("SELECT user_id, level, xp FROM levels WHERE guild_id = ? ORDER BY xp DESC LIMIT 10", (interaction.guild.id,)) + top_users = self.db_cursor.fetchall() + + if not top_users: + await interaction.response.send_message("Es gibt noch keine Rangliste für diesen Server.", ephemeral=True) + return + + embed = discord.Embed( + title=f"🏆 Rangliste für {interaction.guild.name}", + color=discord.Color.gold() + ) + + description = "" + for i, (user_id, level, xp, *_) in enumerate(top_users): # Ignoriere zusätzliche Spalten + # Versuche, das Mitgliedsobjekt zu bekommen. Wenn es den Server verlassen hat, wird es übersprungen. + member = interaction.guild.get_member(user_id) + if member: + rank_emoji = ["🥇", "🥈", "🥉"] + if i < 3: + description += f"{rank_emoji[i]} **{member.display_name}** - Level {level} ({xp} XP)\n" + else: + description += f"`#{i+1}` **{member.display_name}** - Level {level} ({xp} XP)\n" + + if not description: + description = "Konnte keine aktuellen Mitglieder für die Rangliste finden." + + embed.description = description + await interaction.response.send_message(embed=embed) + + # --- Role Reward Management --- + + leveling_group = app_commands.Group(name="leveling", description="Verwaltet das Leveling-System.", default_permissions=discord.Permissions(manage_guild=True)) + + @leveling_group.command(name="addreward", description="Fügt eine Rollen-Belohnung für ein bestimmtes Level hinzu.") + @app_commands.describe(level="Das Level, für das die Belohnung vergeben wird.", role="Die Rolle, die vergeben werden soll.") + async def add_reward(self, interaction: discord.Interaction, level: int, role: discord.Role): + if level <= 0: + await interaction.response.send_message("Das Level muss größer als 0 sein.", ephemeral=True) + return + + try: + self.db_cursor.execute("INSERT OR REPLACE INTO level_rewards (guild_id, level, role_id) VALUES (?, ?, ?)", (interaction.guild.id, level, role.id)) + self.db_connection.commit() + await interaction.response.send_message(f"✅ Rolle `{role.name}` wird nun auf Level `{level}` vergeben.", ephemeral=True) + except Exception as e: + await interaction.response.send_message(f"Ein Fehler ist aufgetreten: {e}", ephemeral=True) + + @leveling_group.command(name="removereward", description="Entfernt eine Rollen-Belohnung von einem Level.") + @app_commands.describe(level="Das Level, von dem die Belohnung entfernt werden soll.") + async def remove_reward(self, interaction: discord.Interaction, level: int): + self.db_cursor.execute("DELETE FROM level_rewards WHERE guild_id = ? AND level = ?", (interaction.guild.id, level)) + if self.db_cursor.rowcount > 0: + self.db_connection.commit() + await interaction.response.send_message(f"✅ Die Belohnung für Level `{level}` wurde entfernt.", ephemeral=True) + else: + await interaction.response.send_message(f"❌ Für Level `{level}` war keine Belohnung konfiguriert.", ephemeral=True) + + @leveling_group.command(name="listrewards", description="Zeigt alle konfigurierten Rollen-Belohnungen an.") + async def list_rewards(self, interaction: discord.Interaction): + self.db_cursor.execute("SELECT level, role_id FROM level_rewards WHERE guild_id = ? ORDER BY level ASC", (interaction.guild.id,)) + rewards = self.db_cursor.fetchall() + + if not rewards: + await interaction.response.send_message("Es sind keine Rollen-Belohnungen für diesen Server konfiguriert.", ephemeral=True) + return + + description = "" + for level, role_id in rewards: + role = interaction.guild.get_role(role_id) + description += f"**Level {level}** → {role.mention if role else f'`Rolle nicht gefunden (ID: {role_id})`'}\n" + + embed = discord.Embed(title="Rollen-Belohnungen", description=description, color=discord.Color.purple()) + await interaction.response.send_message(embed=embed, ephemeral=True) + +async def setup(bot: commands.Bot): + cog = Leveling(bot) + bot.tree.add_command(cog.leveling_group) + await bot.add_cog(cog) \ No newline at end of file diff --git a/cogs/moderation.py b/cogs/moderation.py new file mode 100644 index 0000000..7acec5a --- /dev/null +++ b/cogs/moderation.py @@ -0,0 +1,401 @@ +import discord +from discord.ext import commands +from discord import app_commands +import datetime +import sqlite3 +import time + +class Moderation(commands.Cog): + def __init__(self, bot: commands.Bot): + self.bot = bot + self.db_connection = sqlite3.connect("xalos_data.db") + self.db_cursor = self.db_connection.cursor() + self.db_cursor.execute(""" + CREATE TABLE IF NOT EXISTS warnings ( + warning_id INTEGER PRIMARY KEY AUTOINCREMENT, + guild_id INTEGER, + user_id INTEGER, + moderator_id INTEGER, + reason TEXT, + timestamp INTEGER + ) + """) + self.db_cursor.execute(""" + CREATE TABLE IF NOT EXISTS automod_configs ( + guild_id INTEGER PRIMARY KEY, + badwords_enabled INTEGER DEFAULT 0 + ) + """) + self.db_cursor.execute(""" + CREATE TABLE IF NOT EXISTS forbidden_words ( + guild_id INTEGER, + word TEXT, + PRIMARY KEY (guild_id, word) + ) + """) + self.db_connection.commit() + + @app_commands.command(name="kick", description="Kickt ein Mitglied vom Server.") + @app_commands.describe( + member="Das Mitglied, das gekickt werden soll.", + reason="Der Grund fuer den Kick." + ) + @app_commands.checks.has_permissions(kick_members=True) + async def kick(self, interaction: discord.Interaction, member: discord.Member, reason: str = "Kein Grund angegeben"): + if member == interaction.user: + await interaction.response.send_message("Du kannst dich nicht selbst kicken.", ephemeral=True) + return + + if member == self.bot.user or member == interaction.guild.owner: + await interaction.response.send_message("Diese Person kann nicht gekickt werden.", ephemeral=True) + return + + try: + await member.send(f"Du wurdest vom Server '{interaction.guild.name}' gekickt. Grund: {reason}") + except discord.Forbidden: + pass + + await member.kick(reason=reason) + + embed = discord.Embed( + title="Mitglied gekickt", + description=f"{member.mention} wurde erfolgreich gekickt.", + color=discord.Color.orange() + ) + embed.add_field(name="Grund", value=reason, inline=False) + embed.add_field(name="Moderator", value=interaction.user.mention, inline=False) + + await interaction.response.send_message(embed=embed) + + @kick.error + async def kick_error(self, interaction: discord.Interaction, error: app_commands.AppCommandError): + if isinstance(error, app_commands.MissingPermissions): + await interaction.response.send_message("Dir fehlt die Berechtigung (`kick_members`), um diesen Befehl auszufuehren!", ephemeral=True) + else: + await interaction.response.send_message("Ein unerwarteter Fehler ist aufgetreten.", ephemeral=True) + print(error) + + @app_commands.command(name="ban", description="Verbannt ein Mitglied permanent vom Server.") + @app_commands.describe( + member="Das Mitglied, das verbannt werden soll.", + reason="Der Grund für den Bann." + ) + @app_commands.checks.has_permissions(ban_members=True) + async def ban(self, interaction: discord.Interaction, member: discord.Member, reason: str = "Kein Grund angegeben"): + """Verbannt ein Mitglied und sendet eine Bestätigung.""" + if member == interaction.user: + await interaction.response.send_message("Du kannst dich nicht selbst bannen.", ephemeral=True) + return + + if member == self.bot.user or member == interaction.guild.owner: + await interaction.response.send_message("Diese Person kann nicht gebannt werden.", ephemeral=True) + return + + try: + await member.send(f"Du wurdest vom Server '{interaction.guild.name}' permanent gebannt. Grund: {reason}") + except discord.Forbidden: + pass + + await member.ban(reason=reason) + + embed = discord.Embed( + title="Mitglied gebannt", + description=f"{member.mention} wurde erfolgreich gebannt.", + color=discord.Color.red() + ) + embed.add_field(name="Grund", value=reason, inline=False) + embed.add_field(name="Moderator", value=interaction.user.mention, inline=False) + + await interaction.response.send_message(embed=embed) + + @ban.error + async def ban_error(self, interaction: discord.Interaction, error: app_commands.AppCommandError): + if isinstance(error, app_commands.MissingPermissions): + await interaction.response.send_message("Dir fehlt die Berechtigung (`ban_members`), um diesen Befehl auszuführen!", ephemeral=True) + else: + await interaction.response.send_message("Ein unerwarteter Fehler ist aufgetreten.", ephemeral=True) + print(error) + + @app_commands.command(name="mute", description="Schaltet ein Mitglied für eine bestimmte Zeit stumm (Timeout).") + @app_commands.describe( + member="Das Mitglied, das stummgeschaltet werden soll.", + minutes="Die Dauer des Mutes in Minuten.", + reason="Der Grund für den Mute." + ) + @app_commands.checks.has_permissions(moderate_members=True) + async def mute(self, interaction: discord.Interaction, member: discord.Member, minutes: int, reason: str = "Kein Grund angegeben"): + """Versetzt ein Mitglied in einen Timeout.""" + duration = datetime.timedelta(minutes=minutes) + await member.timeout(duration, reason=reason) + + embed = discord.Embed( + title="Mitglied stummgeschaltet", + description=f"{member.mention} wurde für {minutes} Minute(n) stummgeschaltet.", + color=discord.Color.blue() + ) + embed.add_field(name="Grund", value=reason, inline=False) + embed.add_field(name="Moderator", value=interaction.user.mention, inline=False) + + await interaction.response.send_message(embed=embed) + + @mute.error + async def mute_error(self, interaction: discord.Interaction, error: app_commands.AppCommandError): + if isinstance(error, app_commands.MissingPermissions): + await interaction.response.send_message("Dir fehlt die Berechtigung (`moderate_members`), um diesen Befehl auszuführen!", ephemeral=True) + else: + await interaction.response.send_message("Ein unerwarteter Fehler ist aufgetreten.", ephemeral=True) + print(error) + + @app_commands.command(name="warn", description="Verwarnt ein Mitglied.") + @app_commands.describe( + member="Das Mitglied, das verwarnt werden soll.", + reason="Der Grund für die Verwarnung." + ) + @app_commands.checks.has_permissions(moderate_members=True) + async def warn(self, interaction: discord.Interaction, member: discord.Member, reason: str): + """Verwarnt ein Mitglied und speichert es in der Datenbank.""" + guild_id = interaction.guild.id + user_id = member.id + moderator_id = interaction.user.id + timestamp = int(time.time()) + + self.db_cursor.execute( + "INSERT INTO warnings (guild_id, user_id, moderator_id, reason, timestamp) VALUES (?, ?, ?, ?, ?)", + (guild_id, user_id, moderator_id, reason, timestamp) + ) + self.db_connection.commit() + warning_id = self.db_cursor.lastrowid + + embed = discord.Embed( + title="⚠️ Mitglied verwarnt", + description=f"{member.mention} wurde verwarnt.", + color=discord.Color.yellow() + ) + embed.add_field(name="Moderator", value=interaction.user.mention, inline=True) + embed.add_field(name="Grund", value=reason, inline=True) + embed.set_footer(text=f"Warnungs-ID: {warning_id}") + + await interaction.response.send_message(embed=embed) + + @app_commands.command(name="warnings", description="Zeigt alle Verwarnungen eines Mitglieds an.") + @app_commands.describe(member="Das Mitglied, dessen Verwarnungen du sehen möchtest.") + @app_commands.checks.has_permissions(moderate_members=True) + async def warnings(self, interaction: discord.Interaction, member: discord.Member): + """Listet alle Verwarnungen eines Mitglieds auf.""" + guild_id = interaction.guild.id + user_id = member.id + + self.db_cursor.execute("SELECT warning_id, moderator_id, reason, timestamp FROM warnings WHERE guild_id = ? AND user_id = ?", (guild_id, user_id)) + user_warnings = self.db_cursor.fetchall() + + if not user_warnings: + await interaction.response.send_message(f"{member.display_name} hat keine Verwarnungen.", ephemeral=True) + return + + embed = discord.Embed( + title=f"Verwarnungen für {member.display_name}", + color=member.color + ) + + for warn_id, mod_id, reason, ts in user_warnings: + moderator = interaction.guild.get_member(mod_id) or f"ID: {mod_id}" + embed.add_field( + name=f"Warnung #{warn_id} - ", + value=f"**Grund:** {reason}\n**Moderator:** {moderator}", + inline=False + ) + + await interaction.response.send_message(embed=embed) + + @app_commands.command(name="delwarn", description="Löscht eine spezifische Verwarnung.") + @app_commands.describe(warning_id="Die ID der Verwarnung, die gelöscht werden soll.") + @app_commands.checks.has_permissions(moderate_members=True) + async def delwarn(self, interaction: discord.Interaction, warning_id: int): + """Löscht eine Verwarnung anhand ihrer ID.""" + self.db_cursor.execute("DELETE FROM warnings WHERE warning_id = ? AND guild_id = ?", (warning_id, interaction.guild.id)) + changes = self.db_connection.total_changes + self.db_connection.commit() + + if changes > 0: + await interaction.response.send_message(f"✅ Verwarnung mit der ID `{warning_id}` wurde erfolgreich gelöscht.", ephemeral=True) + else: + await interaction.response.send_message(f"❌ Keine Verwarnung mit der ID `{warning_id}` auf diesem Server gefunden.", ephemeral=True) + + @app_commands.command(name="lock", description="Sperrt einen Kanal, sodass niemand mehr schreiben kann.") + @app_commands.describe(channel="Der Kanal, der gesperrt werden soll (standardmäßig der aktuelle).") + @app_commands.checks.has_permissions(manage_channels=True) + async def lock(self, interaction: discord.Interaction, channel: discord.TextChannel = None): + """Sperrt einen Kanal für die @everyone Rolle.""" + target_channel = channel or interaction.channel + + try: + # Bearbeite die Berechtigungen für die @everyone Rolle + await target_channel.set_permissions( + interaction.guild.default_role, + send_messages=False, + reason=f"Kanal gesperrt von {interaction.user}" + ) + except discord.Forbidden: + await interaction.response.send_message("Ich habe nicht die nötigen Berechtigungen, um diesen Kanal zu sperren.", ephemeral=True) + return + + embed = discord.Embed( + title="🔒 Kanal gesperrt", + description=f"Der Kanal {target_channel.mention} wurde von {interaction.user.mention} gesperrt.", + color=discord.Color.dark_grey() + ) + await interaction.response.send_message(embed=embed) + + @app_commands.command(name="unlock", description="Entsperrt einen Kanal, sodass jeder wieder schreiben kann.") + @app_commands.describe(channel="Der Kanal, der entsperrt werden soll (standardmäßig der aktuelle).") + @app_commands.checks.has_permissions(manage_channels=True) + async def unlock(self, interaction: discord.Interaction, channel: discord.TextChannel = None): + """Entsperrt einen Kanal für die @everyone Rolle.""" + target_channel = channel or interaction.channel + + try: + # Setzt die Berechtigung für send_messages auf den Standard zurück + await target_channel.set_permissions( + interaction.guild.default_role, + send_messages=None, + reason=f"Kanal entsperrt von {interaction.user}" + ) + except discord.Forbidden: + await interaction.response.send_message("Ich habe nicht die nötigen Berechtigungen, um diesen Kanal zu entsperren.", ephemeral=True) + return + + embed = discord.Embed( + title="🔓 Kanal entsperrt", + description=f"Der Kanal {target_channel.mention} wurde von {interaction.user.mention} entsperrt.", + color=discord.Color.light_grey() + ) + await interaction.response.send_message(embed=embed) + + @app_commands.command(name="slowmode", description="Setzt den langsamen Modus für einen Kanal.") + @app_commands.describe( + seconds="Die Verzögerung in Sekunden (0 zum Deaktivieren).", + channel="Der Kanal, für den der Slowmode gesetzt werden soll (standardmäßig der aktuelle)." + ) + @app_commands.checks.has_permissions(manage_channels=True) + async def slowmode(self, interaction: discord.Interaction, seconds: int, channel: discord.TextChannel = None): + """Setzt den Slowmode für einen Kanal.""" + target_channel = channel or interaction.channel + + await target_channel.edit(slowmode_delay=seconds, reason=f"Slowmode gesetzt von {interaction.user}") + + embed = discord.Embed( + title="⏳ Slowmode aktualisiert", + description=f"Der Slowmode für {target_channel.mention} wurde auf **{seconds} Sekunden** gesetzt.", + color=discord.Color.blue() + ) + await interaction.response.send_message(embed=embed) + + # --- Auto-Moderator --- + + async def get_log_channel(self, guild_id: int) -> discord.TextChannel | None: + """Helper to get the log channel from the server_configs table.""" + # This table is managed by the audit_log cog + self.db_cursor.execute("SELECT log_channel_id FROM server_configs WHERE guild_id = ?", (guild_id,)) + result = self.db_cursor.fetchone() + if result and result[0]: + return self.bot.get_channel(result[0]) + return None + + @commands.Cog.listener() + async def on_message(self, message: discord.Message): + if not message.guild or message.author.bot: + return + + # Check if user is an admin/moderator - they should be immune + if message.author.guild_permissions.manage_messages: + return + + # --- Bad Word Filter --- + self.db_cursor.execute("SELECT badwords_enabled FROM automod_configs WHERE guild_id = ?", (message.guild.id,)) + config = self.db_cursor.fetchone() + if config and config[0] == 1: + self.db_cursor.execute("SELECT word FROM forbidden_words WHERE guild_id = ?", (message.guild.id,)) + forbidden_words = [row[0] for row in self.db_cursor.fetchall()] + + if any(word in message.content.lower() for word in forbidden_words): + try: + await message.delete() + await message.channel.send(f"{message.author.mention}, deine Nachricht wurde gelöscht, da sie ein verbotenes Wort enthielt.", delete_after=10) + + # Log the action + log_channel = await self.get_log_channel(message.guild.id) + if log_channel: + embed = discord.Embed( + title="🛡️ Auto-Mod Aktion", + description=f"**Aktion:** Nachricht mit verbotenem Wort gelöscht.", + color=discord.Color.red() + ) + embed.add_field(name="Mitglied", value=message.author.mention, inline=True) + embed.add_field(name="Kanal", value=message.channel.mention, inline=True) + embed.add_field(name="Inhalt", value=f"||{message.content[:1000]}||", inline=False) + await log_channel.send(embed=embed) + + except discord.Forbidden: + # Bot has no permissions to delete messages + pass + except Exception as e: + print(f"Error in automod: {e}") + + # --- Auto-Mod Commands --- + + automod_group = app_commands.Group(name="automod", description="Konfiguriert den Auto-Moderator.", default_permissions=discord.Permissions(manage_guild=True)) + + @automod_group.command(name="togglewords", description="Aktiviert oder deaktiviert den Filter für verbotene Wörter.") + @app_commands.describe(enabled="Soll der Filter aktiviert werden?") + async def automod_toggle_words(self, interaction: discord.Interaction, enabled: bool): + self.db_cursor.execute("INSERT OR REPLACE INTO automod_configs (guild_id, badwords_enabled) VALUES (?, ?)", (interaction.guild.id, int(enabled))) + self.db_connection.commit() + status = "aktiviert" if enabled else "deaktiviert" + await interaction.response.send_message(f"✅ Der Filter für verbotene Wörter wurde **{status}**.", ephemeral=True) + + badwords_group = app_commands.Group(name="badwords", description="Verwaltet die Liste der verbotenen Wörter.", parent=automod_group) + + @badwords_group.command(name="add", description="Fügt ein verbotenes Wort hinzu.") + @app_commands.describe(word="Das Wort, das hinzugefügt werden soll.") + async def badwords_add(self, interaction: discord.Interaction, word: str): + word = word.lower() + try: + self.db_cursor.execute("INSERT INTO forbidden_words (guild_id, word) VALUES (?, ?)", (interaction.guild.id, word)) + self.db_connection.commit() + await interaction.response.send_message(f"✅ Das Wort `{word}` wurde zur Liste hinzugefügt.", ephemeral=True) + except sqlite3.IntegrityError: + await interaction.response.send_message(f"⚠️ Das Wort `{word}` ist bereits auf der Liste.", ephemeral=True) + + @badwords_group.command(name="remove", description="Entfernt ein verbotenes Wort.") + @app_commands.describe(word="Das Wort, das entfernt werden soll.") + async def badwords_remove(self, interaction: discord.Interaction, word: str): + word = word.lower() + self.db_cursor.execute("DELETE FROM forbidden_words WHERE guild_id = ? AND word = ?", (interaction.guild.id, word)) + if self.db_cursor.rowcount > 0: + self.db_connection.commit() + await interaction.response.send_message(f"✅ Das Wort `{word}` wurde von der Liste entfernt.", ephemeral=True) + else: + await interaction.response.send_message(f"❌ Das Wort `{word}` wurde nicht auf der Liste gefunden.", ephemeral=True) + + @badwords_group.command(name="list", description="Zeigt alle verbotenen Wörter an.") + async def badwords_list(self, interaction: discord.Interaction): + self.db_cursor.execute("SELECT word FROM forbidden_words WHERE guild_id = ?", (interaction.guild.id,)) + words = [row[0] for row in self.db_cursor.fetchall()] + + if not words: + await interaction.response.send_message("Die Liste der verbotenen Wörter ist leer.", ephemeral=True) + return + + embed = discord.Embed( + title="🚫 Liste der verbotenen Wörter", + description=", ".join(f"`{w}`" for w in words), + color=discord.Color.red() + ) + await interaction.response.send_message(embed=embed, ephemeral=True) + + +async def setup(bot: commands.Bot): + cog = Moderation(bot) + # Add the command groups to the bot's command tree + bot.tree.add_command(cog.automod_group) + await bot.add_cog(cog) diff --git a/cogs/polls.py b/cogs/polls.py new file mode 100644 index 0000000..006ac95 --- /dev/null +++ b/cogs/polls.py @@ -0,0 +1,54 @@ +import discord +from discord.ext import commands +from discord import app_commands + +class Polls(commands.Cog): + def __init__(self, bot: commands.Bot): + self.bot = bot + # Emojis für bis zu 20 Optionen + self.poll_emojis = [ + "🇦", "🇧", "🇨", "🇩", "🇪", "🇫", "🇬", "🇭", "🇮", "🇯", + "🇰", "🇱", "🇲", "🇳", "🇴", "🇵", "🇶", "🇷", "🇸", "🇹" + ] + + @app_commands.command(name="poll", description="Erstellt eine einfache Umfrage.") + @app_commands.describe( + question="Die Frage der Umfrage.", + options="Die Antwortmöglichkeiten, getrennt durch ein Semikolon (;)." + ) + async def poll(self, interaction: discord.Interaction, question: str, options: str): + """Erstellt eine Umfrage mit Reaktionen.""" + + # Trenne die Optionen am Semikolon + split_options = [opt.strip() for opt in options.split(';')] + + # Überprüfe die Anzahl der Optionen + if len(split_options) < 2: + await interaction.response.send_message("Du musst mindestens 2 Optionen angeben.", ephemeral=True) + return + if len(split_options) > 20: + await interaction.response.send_message("Du kannst maximal 20 Optionen angeben.", ephemeral=True) + return + + # Erstelle die Beschreibung für das Embed + description = [] + for i, option in enumerate(split_options): + description.append(f"{self.poll_emojis[i]} {option}") + + embed = discord.Embed( + title=f"📊 Umfrage: {question}", + description="\n".join(description), + color=discord.Color.dark_purple() + ) + embed.set_footer(text=f"Umfrage gestartet von {interaction.user.display_name}") + + # Sende die Nachricht und speichere sie, um Reaktionen hinzuzufügen + await interaction.response.send_message(embed=embed) + poll_message = await interaction.original_response() + + # Füge die Reaktionen hinzu + for i in range(len(split_options)): + await poll_message.add_reaction(self.poll_emojis[i]) + +async def setup(bot: commands.Bot): + await bot.add_cog(Polls(bot)) \ No newline at end of file diff --git a/cogs/reaction_roles.py b/cogs/reaction_roles.py new file mode 100644 index 0000000..0d75afd --- /dev/null +++ b/cogs/reaction_roles.py @@ -0,0 +1,104 @@ +import discord +from discord.ext import commands +from discord import app_commands +import sqlite3 + +class ReactionRoles(commands.Cog): + def __init__(self, bot: commands.Bot): + self.bot = bot + self.db_connection = sqlite3.connect("xalos_data.db") + self.db_cursor = self.db_connection.cursor() + self.db_cursor.execute(""" + CREATE TABLE IF NOT EXISTS reaction_roles ( + message_id INTEGER, + guild_id INTEGER, + role_id INTEGER, + emoji TEXT, + PRIMARY KEY (message_id, emoji) + ) + """) + + @app_commands.group(name="reactionrole", description="Verwaltet Reaktions-Rollen.") + @app_commands.checks.has_permissions(manage_roles=True) + async def reactionrole(self, interaction: discord.Interaction): + # This base command is not invoked directly + pass + + @reactionrole.command(name="add", description="Fügt eine neue Reaktions-Rolle zu einer Nachricht hinzu.") + @app_commands.describe( + message_id="Die ID der Nachricht, zu der die Rolle hinzugefügt werden soll.", + emoji="Das Emoji, das als Reaktion verwendet wird.", + role="Die Rolle, die vergeben werden soll." + ) + async def add_reaction_role(self, interaction: discord.Interaction, message_id: str, emoji: str, role: discord.Role): + """Fügt eine neue Reaktions-Rolle hinzu.""" + try: + # Check if the message exists + message = await interaction.channel.fetch_message(int(message_id)) + except (discord.NotFound, ValueError): + await interaction.response.send_message("Ungültige Nachrichten-ID. Bitte stelle sicher, dass die ID korrekt ist und der Befehl im selben Kanal wie die Nachricht ausgeführt wird.", ephemeral=True) + return + + # Add the reaction to the message so users can see it + try: + await message.add_reaction(emoji) + except discord.HTTPException: + await interaction.response.send_message("Ungültiges Emoji. Ich konnte es nicht zur Nachricht hinzufügen.", ephemeral=True) + return + + # Save to database + try: + self.db_cursor.execute( + "INSERT INTO reaction_roles (message_id, guild_id, role_id, emoji) VALUES (?, ?, ?, ?)", + (message.id, interaction.guild.id, role.id, emoji) + ) + self.db_connection.commit() + except sqlite3.IntegrityError: + await interaction.response.send_message("Diese Emoji-Rollen-Kombination existiert bereits für diese Nachricht.", ephemeral=True) + return + + await interaction.response.send_message(f"✅ Reaktions-Rolle hinzugefügt: Wenn jemand mit {emoji} auf die Nachricht reagiert, erhält er/sie die Rolle '{role.name}'.", ephemeral=True) + + @commands.Cog.listener() + async def on_raw_reaction_add(self, payload: discord.RawReactionActionEvent): + # Ignore reactions from bots + if payload.member.bot: + return + + # Check if this reaction is for a configured reaction role + self.db_cursor.execute("SELECT role_id FROM reaction_roles WHERE message_id = ? AND emoji = ?", (payload.message_id, str(payload.emoji))) + result = self.db_cursor.fetchone() + + if result: + role_id = result[0] + guild = self.bot.get_guild(payload.guild_id) + role = guild.get_role(role_id) + + if role: + try: + await payload.member.add_roles(role, reason="Reaction Role") + except discord.Forbidden: + # Bot has insufficient permissions + pass + + @commands.Cog.listener() + async def on_raw_reaction_remove(self, payload: discord.RawReactionActionEvent): + # Check if this reaction is for a configured reaction role + self.db_cursor.execute("SELECT role_id FROM reaction_roles WHERE message_id = ? AND emoji = ?", (payload.message_id, str(payload.emoji))) + result = self.db_cursor.fetchone() + + if result: + role_id = result[0] + guild = self.bot.get_guild(payload.guild_id) + member = guild.get_member(payload.user_id) + role = guild.get_role(role_id) + + if guild and member and role: + try: + await member.remove_roles(role, reason="Reaction Role") + except discord.Forbidden: + # Bot has insufficient permissions + pass + +async def setup(bot: commands.Bot): + await bot.add_cog(ReactionRoles(bot)) \ No newline at end of file diff --git a/cogs/search.py b/cogs/search.py new file mode 100644 index 0000000..bc7667e --- /dev/null +++ b/cogs/search.py @@ -0,0 +1,65 @@ +import discord +from discord.ext import commands +from discord import app_commands +import wikipedia +from googlesearch import search + +class Search(commands.Cog): + def __init__(self, bot: commands.Bot): + self.bot = bot + # Set Wikipedia language to German + wikipedia.set_lang("de") + + @app_commands.command(name="google", description="Sucht bei Google und zeigt die Top-Ergebnisse an.") + @app_commands.describe(query="Wonach möchtest du suchen?") + async def google_search(self, interaction: discord.Interaction, query: str): + await interaction.response.defer(ephemeral=True) + try: + results = [] + # We use a generator and stop after 5 results + for j in search(query, num=5, stop=5, pause=1.0): + results.append(j) + + if not results: + await interaction.followup.send("Ich konnte keine Ergebnisse für deine Suche finden.") + return + + description = "" + for i, result in enumerate(results): + description += f"{i+1}. [{result.split('//')[1].split('/')[0]}]({result})\n" + + embed = discord.Embed( + title=f"Google-Ergebnisse für: `{query}`", + description=description, + color=discord.Color.from_rgb(66, 133, 244) # Google Blue + ) + await interaction.followup.send(embed=embed) + + except Exception as e: + await interaction.followup.send(f"Ein Fehler ist bei der Google-Suche aufgetreten: {e}") + + @app_commands.command(name="wikipedia", description="Durchsucht Wikipedia nach einem Artikel.") + @app_commands.describe(query="Wonach möchtest du suchen?") + async def wikipedia_search(self, interaction: discord.Interaction, query: str): + await interaction.response.defer() + try: + # Get the summary of the first result, auto-suggesting corrections + page = wikipedia.page(query, auto_suggest=True, redirect=True) + summary = wikipedia.summary(query, sentences=3, auto_suggest=True, redirect=True) + + embed = discord.Embed( + title=f"Wikipedia: {page.title}", + url=page.url, + description=summary, + color=discord.Color.light_grey() + ) + await interaction.followup.send(embed=embed) + + except wikipedia.exceptions.PageError: + await interaction.followup.send(f"Ich konnte keinen Wikipedia-Artikel für `{query}` finden.", ephemeral=True) + except wikipedia.exceptions.DisambiguationError as e: + options_list = "\n".join(e.options[:5]) + await interaction.followup.send(f"Deine Suche ist mehrdeutig. Meintest du vielleicht:\n`{options_list}`", ephemeral=True) + +async def setup(bot: commands.Bot): + await bot.add_cog(Search(bot)) \ No newline at end of file diff --git a/cogs/utility.py b/cogs/utility.py new file mode 100644 index 0000000..84a559d --- /dev/null +++ b/cogs/utility.py @@ -0,0 +1,55 @@ +import discord +from discord.ext import commands +from discord import app_commands +import time + +class Utility(commands.Cog): + def __init__(self, bot: commands.Bot): + self.bot = bot + + @app_commands.command(name="ping", description="Zeigt die Latenz des Bots an.") + async def ping(self, interaction: discord.Interaction): + """Zeigt die Latenz des Bots an.""" + # Die Latenz der Discord-API (Websocket) + api_latency = round(self.bot.latency * 1000) + + # Sende eine Bestätigungsnachricht und messe die Zeit dafür + start_time = time.time() + await interaction.response.send_message("Pinging...", ephemeral=True) + end_time = time.time() + + # Berechne die Antwortzeit + response_latency = round((end_time - start_time) * 1000) + + embed = discord.Embed(title="Pong! 🏓", color=discord.Color.blurple()) + embed.add_field(name="API Latenz", value=f"{api_latency}ms", inline=True) + embed.add_field(name="Antwortzeit", value=f"{response_latency}ms", inline=True) + + # Bearbeite die ursprüngliche Nachricht mit dem Ergebnis + await interaction.edit_original_response(content=None, embed=embed) + + @app_commands.command(name="userinfo", description="Zeigt Informationen über ein Mitglied an.") + @app_commands.describe(member="Das Mitglied, dessen Infos du sehen möchtest (optional).") + async def userinfo(self, interaction: discord.Interaction, member: discord.Member = None): + """Zeigt Informationen über dich oder ein anderes Mitglied an.""" + # Wenn kein Mitglied angegeben wird, nimm den Autor des Befehls + target = member or interaction.user + + embed = discord.Embed( + title=f"Informationen über {target.display_name}", + color=target.color + ) + embed.set_thumbnail(url=target.avatar.url if target.avatar else target.default_avatar.url) + + embed.add_field(name="Name", value=f"{target.name}#{target.discriminator}", inline=True) + embed.add_field(name="ID", value=target.id, inline=True) + embed.add_field(name="Status", value=str(target.status).title(), inline=True) + + embed.add_field(name="Server beigetreten am", value=target.joined_at.strftime("%d.%m.%Y, %H:%M:%S"), inline=False) + embed.add_field(name="Account erstellt am", value=target.created_at.strftime("%d.%m.%Y, %H:%M:%S"), inline=False) + + await interaction.response.send_message(embed=embed) + + +async def setup(bot: commands.Bot): + await bot.add_cog(Utility(bot)) \ No newline at end of file diff --git a/cogs/welcome.py b/cogs/welcome.py new file mode 100644 index 0000000..37db746 --- /dev/null +++ b/cogs/welcome.py @@ -0,0 +1,26 @@ +import discord +from discord.ext import commands +from discord import app_commands + +class Welcome(commands.Cog): + def __init__(self, bot: commands.Bot): + self.bot = bot + + @commands.Cog.listener() + async def on_member_join(self, member: discord.Member): + """Wird ausgelöst, wenn ein neues Mitglied dem Server beitritt.""" + guild = member.guild + welcome_channel = guild.system_channel + + if welcome_channel is not None: + embed = discord.Embed( + title=f"Willkommen auf {guild.name}!", + description=f"Hallo {member.mention}, wir freuen uns, dich hier zu sehen! Schau dich doch mal um.", + color=discord.Color.green() + ) + embed.set_thumbnail(url=member.avatar.url if member.avatar else member.default_avatar.url) + embed.set_footer(text=f"Du bist das {guild.member_count}. Mitglied!") + await welcome_channel.send(embed=embed) + +async def setup(bot: commands.Bot): + await bot.add_cog(Welcome(bot)) diff --git a/features.txt b/features.txt new file mode 100644 index 0000000..4928443 --- /dev/null +++ b/features.txt @@ -0,0 +1,28 @@ +# Funktionssammlung für einen Multifunktions-Discord-Bot (inspiriert von MEE6 & Dyno) + +## 1. Moderation (Kernfunktionen) +- **Auto-Moderator:** Automatisches Löschen/Bestrafen bei Spam, Schimpfwörtern, zu vielen Großbuchstaben, exzessiven Erwähnungen etc. +- **Audit-Log:** Protokollierung aller wichtigen Aktionen (gelöschte Nachrichten, bearbeitete Nachrichten, gebannte Mitglieder etc.) in einem bestimmten Kanal. +- **Warn-System:** Verwarnen von Mitgliedern mit Befehlen. Bei X Verwarnungen erfolgt eine automatische Aktion (Mute, Kick, Ban). +- **Mute/Timeout:** Mitglieder für eine bestimmte Zeit stummschalten. +- **Kick/Ban:** Mitglieder vom Server entfernen. +- **Slowmode:** Verlangsamt die Schreibgeschwindigkeit in einem Kanal. +- **Lock/Unlock:** Sperrt oder entsperrt einen Kanal für normale Mitglieder. + +## 2. Community-Management & Engagement +- **Willkommensnachrichten:** Automatische Begrüßung neuer Mitglieder (mit Bannern, Texten, Rollen). +- **Leveling-System:** Mitglieder erhalten XP für Aktivität und steigen im Level auf. Belohnungen (Rollen) bei bestimmten Leveln. +- **Leaderboard:** Eine Rangliste der aktivsten Mitglieder. +- **Reaktions-Rollen (Reaction Roles):** Mitglieder können sich durch Reagieren auf eine Nachricht selbst Rollen zuweisen. +- **Custom Commands:** Server-Admins können eigene, einfache Text-Befehle erstellen. +- **Umfragen (Polls):** Einfaches Erstellen von Abstimmungen. + +## 3. Benachrichtigungen & Ankündigungen +- **Social Media Alerts:** Benachrichtigungen, wenn ein verknüpfter Kanal (YouTube, Twitch, Twitter/X) live geht oder neuen Inhalt postet. +- **Ankündigungs-Befehl:** Ein formatierter Befehl, um wichtige Nachrichten an den ganzen Server zu senden. + +## 4. Nützliches & Unterhaltung +- **Musik-Player:** Abspielen von Musik aus Quellen wie YouTube oder Spotify (Achtung: YouTube erschwert dies zunehmend). +- **Such-Befehle:** Suchen nach Informationen auf Google, Wikipedia, Urban Dictionary etc. +- **Statistiken:** Anzeigen von Server- oder Nutzerstatistiken. +- **Giveaways:** Erstellen und Verwalten von Verlosungen. diff --git a/main.py b/main.py new file mode 100644 index 0000000..998a1e9 --- /dev/null +++ b/main.py @@ -0,0 +1,48 @@ +import os +import discord +from discord.ext import commands +from dotenv import load_dotenv + +# Lade die Umgebungsvariablen aus der .env-Datei +load_dotenv() +TOKEN = os.getenv('DISCORD_TOKEN') + +# Definiere die Intents (Berechtigungen), die dein Bot benötigt. +intents = discord.Intents.default() +intents.members = True # Notwendig für Willkommensnachrichten +intents.message_content = True # Notwendig, um Nachrichten zu lesen + +# Erstelle die Bot-Instanz +bot = commands.Bot(command_prefix=None, intents=intents) + +# Event, das beim Starten des Bots ausgeführt wird +@bot.event +async def on_ready(): + """Wird aufgerufen, wenn der Bot erfolgreich mit Discord verbunden ist.""" + print(f'Erfolgreich eingeloggt als {bot.user.name} (ID: {bot.user.id})') + print('--------------------------------------------------') + await load_cogs() + # Synchronisiere die Slash-Commands mit Discord + try: + synced = await bot.tree.sync() + print(f'{len(synced)} Slash-Command(s) wurden synchronisiert.') + except Exception as e: + print(f'Fehler beim Synchronisieren der Commands: {e}') + +async def load_cogs(): + """Sucht im 'cogs'-Ordner nach Python-Dateien und lädt sie als Erweiterungen.""" + print("Lade Cogs...") + for filename in os.listdir('./cogs'): + if filename.endswith('.py'): + try: + await bot.load_extension(f'cogs.{filename[:-3]}') + print(f'-> Cog "{filename[:-3]}" wurde geladen.') + except Exception as e: + print(f'Fehler beim Laden von Cog "{filename[:-3]}": {e}') + +# Starte den Bot mit dem Token +if __name__ == "__main__": + if TOKEN is None: + print("FEHLER: DISCORD_TOKEN wurde nicht in der .env-Datei gefunden!") + else: + bot.run(TOKEN) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..7944c54 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +discord.py>=2.0.0 +python-dotenv +wikipedia-api +googlesearch-python