import discord from discord import app_commands from discord.ext import commands import json import os from dotenv import load_dotenv load_dotenv() TOKEN = os.getenv("DISCORD_TOKEN") MILESTONE_CHANNEL_ID = int(os.getenv("MILESTONE_CHANNEL_ID", 0)) DATA_FILE = "data.json" # Datenformat: # { "guild_id": { "user_id": { "mic": , "deaf": } } } # --------------------------------------------------------------------------- # Meilenstein-Konfiguration # --------------------------------------------------------------------------- MILESTONES = { 10: ("🔇", "Erste Schritte in die Stille — **{name}** hat sich **10x** gemutet!"), 25: ("😶", "**{name}** hält langsam die Klappe... **25 Mutes** erreicht!"), 50: ("🤫", "**{name}** ist auf dem Weg zur Legende — **50 Mutes**!"), 100: ("💯", "**{name}** hat die **100 Mutes** geknackt! Ein wahrer Schweiger."), 250: ("🏅", "**{name}** erreicht **250 Mutes** — Professionelles Schweigen!"), 500: ("🎖️", "**{name}** mit satten **500 Mutes**. Respekt."), 1000: ("👑", "**{name}** ist der unbestrittene **MUTE-KÖNIG** mit **1000 Mutes**!"), } # --------------------------------------------------------------------------- # Rang-Rollen-Konfiguration (absteigend nach Schwellenwert) # --------------------------------------------------------------------------- RANK_ROLES = [ (1000, "👑 Mute-König"), (500, "🎖️ Elite-Muter"), (250, "🏅 Schweige-Meister"), (100, "💯 Mute-Veteran"), (50, "🤫 Fortgeschrittener Schweiger"), (25, "😶 Gelegentlicher Muter"), (10, "🔇 Stummer Gast"), ] RANK_ROLE_NAMES = {name for _, name in RANK_ROLES} def get_target_role_name(total: int): for threshold, name in RANK_ROLES: if total >= threshold: return name return None # --------------------------------------------------------------------------- # Datenhilfsfunktionen # --------------------------------------------------------------------------- def load_data() -> dict: if os.path.exists(DATA_FILE): with open(DATA_FILE, "r", encoding="utf-8") as f: return json.load(f) return {} def save_data(data: dict): with open(DATA_FILE, "w", encoding="utf-8") as f: json.dump(data, f, indent=2, ensure_ascii=False) def get_user_entry(data: dict, guild_id: int, user_id: int) -> dict: return data.get(str(guild_id), {}).get(str(user_id), {"mic": 0, "deaf": 0}) def entry_total(entry) -> int: if isinstance(entry, dict): return entry.get("mic", 0) + entry.get("deaf", 0) return int(entry) def increment(data: dict, guild_id: int, user_id: int, kind: str) -> int: """Zählt hoch und gibt den neuen Gesamtwert zurück.""" gid, uid = str(guild_id), str(user_id) if gid not in data: data[gid] = {} if uid not in data[gid]: data[gid][uid] = {"mic": 0, "deaf": 0} data[gid][uid][kind] += 1 return entry_total(data[gid][uid]) def get_sorted_leaderboard(data: dict, guild_id: int) -> list: guild_data = data.get(str(guild_id), {}) return sorted(guild_data.items(), key=lambda x: entry_total(x[1]), reverse=True) def get_user_rank(leaderboard: list, user_id: int) -> int: uid = str(user_id) for i, (u, _) in enumerate(leaderboard): if u == uid: return i + 1 return len(leaderboard) + 1 # --------------------------------------------------------------------------- # Hilfsfunktion: Ziel-Channel ermitteln # --------------------------------------------------------------------------- def find_announce_channel(guild: discord.Guild): if MILESTONE_CHANNEL_ID: ch = guild.get_channel(MILESTONE_CHANNEL_ID) if ch: return ch if guild.system_channel: return guild.system_channel return next( (c for c in guild.text_channels if c.permissions_for(guild.me).send_messages), None, ) # --------------------------------------------------------------------------- # Feature: Meilenstein-Nachrichten # --------------------------------------------------------------------------- async def check_milestone(guild: discord.Guild, member: discord.Member, new_total: int): if new_total not in MILESTONES: return emoji, template = MILESTONES[new_total] channel = find_announce_channel(guild) if channel is None: return embed = discord.Embed( title=f"{emoji} Meilenstein erreicht!", description=template.format(name=member.display_name), color=discord.Color.gold(), ) embed.set_thumbnail(url=member.display_avatar.url) embed.set_footer(text=f"Gesamt: {new_total} Mutes") await channel.send(embed=embed) # --------------------------------------------------------------------------- # Feature: Rang-Rollen # --------------------------------------------------------------------------- async def update_rank_role(guild: discord.Guild, member: discord.Member, new_total: int): """Weist die passende Rang-Rolle zu und entfernt veraltete Rang-Rollen.""" target_name = get_target_role_name(new_total) # Alte Rang-Rollen entfernen (außer der Ziel-Rolle) to_remove = [r for r in member.roles if r.name in RANK_ROLE_NAMES and r.name != target_name] if to_remove: try: await member.remove_roles(*to_remove, reason="MuteCounter Rang-Update") except discord.Forbidden: print("[RANG] Fehlende Berechtigung zum Entfernen von Rollen.") if target_name is None: return None # Prüfen ob Nutzer die Rolle schon hat if any(r.name == target_name for r in member.roles): return None # Bereits zugewiesen, kein Update nötig # Rolle holen oder erstellen role = discord.utils.get(guild.roles, name=target_name) if role is None: try: role = await guild.create_role(name=target_name, reason="MuteCounter Rang-System") except discord.Forbidden: print(f"[RANG] Fehlende Berechtigung zum Erstellen der Rolle '{target_name}'.") return None try: await member.add_roles(role, reason="MuteCounter Rang-Update") return target_name # Neu zugewiesener Rang except discord.Forbidden: print(f"[RANG] Fehlende Berechtigung zum Zuweisen der Rolle '{target_name}'.") return None # --------------------------------------------------------------------------- # Feature: Overtake-Alert # --------------------------------------------------------------------------- async def check_overtake( guild: discord.Guild, member: discord.Member, old_lb: list, new_lb: list, ): uid = str(member.id) old_rank = next((i + 1 for i, (u, _) in enumerate(old_lb) if u == uid), None) new_rank = next((i + 1 for i, (u, _) in enumerate(new_lb) if u == uid), None) # Kein Aufstieg oder neuer Nutzer im Leaderboard if old_rank is None or new_rank is None or new_rank >= old_rank: return # Wer stand vor dem Update auf dem Platz, den der Nutzer jetzt einnimmt? if new_rank - 1 >= len(old_lb): return overtaken_uid, _ = old_lb[new_rank - 1] overtaken = guild.get_member(int(overtaken_uid)) overtaken_name = overtaken.display_name if overtaken else f"User {overtaken_uid}" channel = find_announce_channel(guild) if channel is None: return embed = discord.Embed( title="⚡ Überholung!", description=( f"**{member.display_name}** hat **{overtaken_name}** " f"überholt und ist jetzt auf Platz **#{new_rank}**!" ), color=discord.Color.orange(), ) embed.set_footer(text=f"{member.display_name}: #{old_rank} → #{new_rank}") await channel.send(embed=embed) # --------------------------------------------------------------------------- # Bot-Setup # --------------------------------------------------------------------------- intents = discord.Intents.default() intents.voice_states = True intents.members = True bot = commands.Bot(command_prefix="!", intents=intents) mute_data = load_data() @bot.event async def on_ready(): print(f"Bot gestartet als {bot.user} (ID: {bot.user.id})") try: synced = await bot.tree.sync() print(f"{len(synced)} Slash-Commands synchronisiert.") except Exception as e: print(f"Fehler beim Sync: {e}") # --------------------------------------------------------------------------- # Voice-State-Tracking # --------------------------------------------------------------------------- @bot.event async def on_voice_state_update( member: discord.Member, before: discord.VoiceState, after: discord.VoiceState, ): if member.bot: return guild = member.guild afk_channel = guild.afk_channel if afk_channel is not None: if (after.channel is not None and after.channel.id == afk_channel.id) or \ (before.channel is not None and before.channel.id == afk_channel.id): return just_deafened = (not before.self_deaf) and after.self_deaf just_mic_muted = (not before.self_mute) and after.self_mute and (not after.self_deaf) if not just_deafened and not just_mic_muted: return kind = "deaf" if just_deafened else "mic" label = "DEAF" if just_deafened else "MIC" # Leaderboard vor dem Increment für Overtake-Vergleich old_lb = get_sorted_leaderboard(mute_data, guild.id) new_total = increment(mute_data, guild.id, member.id, kind) save_data(mute_data) entry = get_user_entry(mute_data, guild.id, member.id) print(f"[{label}] {member.display_name}: mic={entry['mic']}x, deaf={entry['deaf']}x [{guild.name}]") new_lb = get_sorted_leaderboard(mute_data, guild.id) # Features parallel ausführen await check_milestone(guild, member, new_total) await update_rank_role(guild, member, new_total) await check_overtake(guild, member, old_lb, new_lb) # --------------------------------------------------------------------------- # Slash-Commands # --------------------------------------------------------------------------- @bot.tree.command(name="mutescore", description="Zeigt das Mute-Leaderboard des Servers.") @app_commands.describe(limit="Wie viele Plätze anzeigen? (Standard: 10)") async def mutescore(interaction: discord.Interaction, limit: int = 10): limit = max(1, min(limit, 25)) guild = interaction.guild guild_data: dict = mute_data.get(str(guild.id), {}) if not guild_data: await interaction.response.send_message("Noch keine Mutes aufgezeichnet.", ephemeral=True) return sorted_entries = get_sorted_leaderboard(mute_data, guild.id) top = sorted_entries[:limit] medals = {1: "🥇", 2: "🥈", 3: "🥉"} lines = [] for rank, (uid, counts) in enumerate(top, start=1): m = guild.get_member(int(uid)) name = m.display_name if m else f"Unbekannt ({uid})" medal = medals.get(rank, f"**#{rank}**") mic = counts.get("mic", 0) if isinstance(counts, dict) else counts deaf = counts.get("deaf", 0) if isinstance(counts, dict) else 0 lines.append( f"{medal} {name} — **{mic + deaf}x** gesamt " f"*(🎙️ {mic}x Mikro / 🔇 {deaf}x Deaf)*" ) embed = discord.Embed(title="Mute-Leaderboard", color=discord.Color.blurple()) embed.description = "\n".join(lines) embed.set_footer(text=f"Top {len(top)} von {len(sorted_entries)} Nutzern | 🎙️ = nur Mikro | 🔇 = Deaf") await interaction.response.send_message(embed=embed) @bot.tree.command(name="mymutes", description="Zeigt deine eigenen Mute-Stats.") async def mymutes(interaction: discord.Interaction): entry = get_user_entry(mute_data, interaction.guild.id, interaction.user.id) mic, deaf = entry.get("mic", 0), entry.get("deaf", 0) rank_name = get_target_role_name(mic + deaf) or "Noch kein Rang" await interaction.response.send_message( f"Deine Mute-Stats:\n" f"🎙️ Nur Mikrofon gemutet: **{mic}x**\n" f"🔇 Taubgestellt (Deaf): **{deaf}x**\n" f"📊 Gesamt: **{mic + deaf}x**\n" f"🏷️ Aktueller Rang: **{rank_name}**", ephemeral=True, ) bot.run(TOKEN)