Verbessere Meilenstein- und Rang-Rollen-Konfiguration: Strukturierung der Meilensteine, Einführung von Rang-Rollen und Optimierung der Benutzerstatistiken
This commit is contained in:
276
bot.py
276
bot.py
@@ -13,9 +13,10 @@ DATA_FILE = "data.json"
|
|||||||
# Datenformat:
|
# Datenformat:
|
||||||
# { "guild_id": { "user_id": { "mic": <int>, "deaf": <int> } } }
|
# { "guild_id": { "user_id": { "mic": <int>, "deaf": <int> } } }
|
||||||
|
|
||||||
MILESTONES = [10, 25, 50, 100, 250, 500, 1000]
|
# ---------------------------------------------------------------------------
|
||||||
|
# Meilenstein-Konfiguration
|
||||||
MILESTONE_MESSAGES = {
|
# ---------------------------------------------------------------------------
|
||||||
|
MILESTONES = {
|
||||||
10: ("🔇", "Erste Schritte in die Stille — **{name}** hat sich **10x** gemutet!"),
|
10: ("🔇", "Erste Schritte in die Stille — **{name}** hat sich **10x** gemutet!"),
|
||||||
25: ("😶", "**{name}** hält langsam die Klappe... **25 Mutes** erreicht!"),
|
25: ("😶", "**{name}** hält langsam die Klappe... **25 Mutes** erreicht!"),
|
||||||
50: ("🤫", "**{name}** ist auf dem Weg zur Legende — **50 Mutes**!"),
|
50: ("🤫", "**{name}** ist auf dem Weg zur Legende — **50 Mutes**!"),
|
||||||
@@ -25,7 +26,31 @@ MILESTONE_MESSAGES = {
|
|||||||
1000: ("👑", "**{name}** ist der unbestrittene **MUTE-KÖNIG** mit **1000 Mutes**!"),
|
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:
|
def load_data() -> dict:
|
||||||
if os.path.exists(DATA_FILE):
|
if os.path.exists(DATA_FILE):
|
||||||
with open(DATA_FILE, "r", encoding="utf-8") as f:
|
with open(DATA_FILE, "r", encoding="utf-8") as f:
|
||||||
@@ -42,53 +67,155 @@ 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})
|
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:
|
def increment(data: dict, guild_id: int, user_id: int, kind: str) -> int:
|
||||||
"""Zählt hoch und gibt den neuen Gesamtwert (mic + deaf) zurück."""
|
"""Zählt hoch und gibt den neuen Gesamtwert zurück."""
|
||||||
gid = str(guild_id)
|
gid, uid = str(guild_id), str(user_id)
|
||||||
uid = str(user_id)
|
|
||||||
if gid not in data:
|
if gid not in data:
|
||||||
data[gid] = {}
|
data[gid] = {}
|
||||||
if uid not in data[gid]:
|
if uid not in data[gid]:
|
||||||
data[gid][uid] = {"mic": 0, "deaf": 0}
|
data[gid][uid] = {"mic": 0, "deaf": 0}
|
||||||
data[gid][uid][kind] += 1
|
data[gid][uid][kind] += 1
|
||||||
return data[gid][uid]["mic"] + data[gid][uid]["deaf"]
|
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):
|
async def check_milestone(guild: discord.Guild, member: discord.Member, new_total: int):
|
||||||
"""Prüft ob ein Meilenstein erreicht wurde und postet ggf. eine Nachricht."""
|
|
||||||
if new_total not in MILESTONES:
|
if new_total not in MILESTONES:
|
||||||
return
|
return
|
||||||
|
emoji, template = MILESTONES[new_total]
|
||||||
emoji, template = MILESTONE_MESSAGES[new_total]
|
channel = find_announce_channel(guild)
|
||||||
message = template.format(name=member.display_name)
|
|
||||||
|
|
||||||
# Ziel-Channel: konfigurierter Channel → System-Channel → erstes Text-Channel
|
|
||||||
channel = None
|
|
||||||
if MILESTONE_CHANNEL_ID:
|
|
||||||
channel = guild.get_channel(MILESTONE_CHANNEL_ID)
|
|
||||||
if channel is None:
|
if channel is None:
|
||||||
channel = guild.system_channel
|
|
||||||
if channel is None:
|
|
||||||
channel = next(
|
|
||||||
(c for c in guild.text_channels if c.permissions_for(guild.me).send_messages),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
|
|
||||||
if channel is None:
|
|
||||||
print(f"[MILESTONE] Kein Channel gefunden für: {message}")
|
|
||||||
return
|
return
|
||||||
|
|
||||||
embed = discord.Embed(
|
embed = discord.Embed(
|
||||||
title=f"{emoji} Meilenstein erreicht!",
|
title=f"{emoji} Meilenstein erreicht!",
|
||||||
description=message,
|
description=template.format(name=member.display_name),
|
||||||
color=discord.Color.gold(),
|
color=discord.Color.gold(),
|
||||||
)
|
)
|
||||||
embed.set_thumbnail(url=member.display_avatar.url)
|
embed.set_thumbnail(url=member.display_avatar.url)
|
||||||
embed.set_footer(text=f"Gesamt: {new_total} Mutes")
|
embed.set_footer(text=f"Gesamt: {new_total} Mutes")
|
||||||
|
|
||||||
await channel.send(embed=embed)
|
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 = discord.Intents.default()
|
||||||
intents.voice_states = True
|
intents.voice_states = True
|
||||||
intents.members = True
|
intents.members = True
|
||||||
@@ -107,6 +234,9 @@ async def on_ready():
|
|||||||
print(f"Fehler beim Sync: {e}")
|
print(f"Fehler beim Sync: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Voice-State-Tracking
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
@bot.event
|
@bot.event
|
||||||
async def on_voice_state_update(
|
async def on_voice_state_update(
|
||||||
member: discord.Member,
|
member: discord.Member,
|
||||||
@@ -119,39 +249,40 @@ async def on_voice_state_update(
|
|||||||
guild = member.guild
|
guild = member.guild
|
||||||
afk_channel = guild.afk_channel
|
afk_channel = guild.afk_channel
|
||||||
|
|
||||||
# AFK-Channel ausschließen
|
|
||||||
if afk_channel is not None:
|
if afk_channel is not None:
|
||||||
if (after.channel is not None and after.channel.id == afk_channel.id) or \
|
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):
|
(before.channel is not None and before.channel.id == afk_channel.id):
|
||||||
return
|
return
|
||||||
|
|
||||||
# Full Mute (Deafen): Nutzer hat sich gerade taubgestellt
|
just_deafened = (not before.self_deaf) and after.self_deaf
|
||||||
just_deafened = (not before.self_deaf) and after.self_deaf
|
|
||||||
|
|
||||||
# Nur Mikrofon gemutet: self_mute wurde aktiviert, aber NICHT deafen
|
|
||||||
just_mic_muted = (not before.self_mute) and after.self_mute and (not after.self_deaf)
|
just_mic_muted = (not before.self_mute) and after.self_mute and (not after.self_deaf)
|
||||||
|
|
||||||
if just_deafened:
|
if not just_deafened and not just_mic_muted:
|
||||||
new_total = increment(mute_data, guild.id, member.id, "deaf")
|
return
|
||||||
save_data(mute_data)
|
|
||||||
entry = get_user_entry(mute_data, guild.id, member.id)
|
|
||||||
print(
|
|
||||||
f"[DEAF] {member.display_name} hat sich taubgestellt "
|
|
||||||
f"(deaf={entry['deaf']}x, mic={entry['mic']}x) [{guild.name}]"
|
|
||||||
)
|
|
||||||
await check_milestone(guild, member, new_total)
|
|
||||||
|
|
||||||
elif just_mic_muted:
|
kind = "deaf" if just_deafened else "mic"
|
||||||
new_total = increment(mute_data, guild.id, member.id, "mic")
|
label = "DEAF" if just_deafened else "MIC"
|
||||||
save_data(mute_data)
|
|
||||||
entry = get_user_entry(mute_data, guild.id, member.id)
|
# Leaderboard vor dem Increment für Overtake-Vergleich
|
||||||
print(
|
old_lb = get_sorted_leaderboard(mute_data, guild.id)
|
||||||
f"[MIC] {member.display_name} hat nur sein Mikrofon gemutet "
|
|
||||||
f"(mic={entry['mic']}x, deaf={entry['deaf']}x) [{guild.name}]"
|
new_total = increment(mute_data, guild.id, member.id, kind)
|
||||||
)
|
save_data(mute_data)
|
||||||
await check_milestone(guild, member, new_total)
|
|
||||||
|
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.")
|
@bot.tree.command(name="mutescore", description="Zeigt das Mute-Leaderboard des Servers.")
|
||||||
@app_commands.describe(limit="Wie viele Plätze anzeigen? (Standard: 10)")
|
@app_commands.describe(limit="Wie viele Plätze anzeigen? (Standard: 10)")
|
||||||
async def mutescore(interaction: discord.Interaction, limit: int = 10):
|
async def mutescore(interaction: discord.Interaction, limit: int = 10):
|
||||||
@@ -160,58 +291,43 @@ async def mutescore(interaction: discord.Interaction, limit: int = 10):
|
|||||||
guild_data: dict = mute_data.get(str(guild.id), {})
|
guild_data: dict = mute_data.get(str(guild.id), {})
|
||||||
|
|
||||||
if not guild_data:
|
if not guild_data:
|
||||||
await interaction.response.send_message(
|
await interaction.response.send_message("Noch keine Mutes aufgezeichnet.", ephemeral=True)
|
||||||
"Noch keine Mutes aufgezeichnet.", ephemeral=True
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
def total(entry):
|
sorted_entries = get_sorted_leaderboard(mute_data, guild.id)
|
||||||
if isinstance(entry, dict):
|
|
||||||
return entry.get("mic", 0) + entry.get("deaf", 0)
|
|
||||||
return entry
|
|
||||||
|
|
||||||
sorted_entries = sorted(guild_data.items(), key=lambda x: total(x[1]), reverse=True)
|
|
||||||
top = sorted_entries[:limit]
|
top = sorted_entries[:limit]
|
||||||
|
|
||||||
medals = {1: "🥇", 2: "🥈", 3: "🥉"}
|
medals = {1: "🥇", 2: "🥈", 3: "🥉"}
|
||||||
lines = []
|
lines = []
|
||||||
|
|
||||||
for rank, (uid, counts) in enumerate(top, start=1):
|
for rank, (uid, counts) in enumerate(top, start=1):
|
||||||
member = guild.get_member(int(uid))
|
m = guild.get_member(int(uid))
|
||||||
name = member.display_name if member else f"Unbekannt ({uid})"
|
name = m.display_name if m else f"Unbekannt ({uid})"
|
||||||
medal = medals.get(rank, f"**#{rank}**")
|
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)*"
|
||||||
|
)
|
||||||
|
|
||||||
if isinstance(counts, dict):
|
embed = discord.Embed(title="Mute-Leaderboard", color=discord.Color.blurple())
|
||||||
mic = counts.get("mic", 0)
|
|
||||||
deaf = counts.get("deaf", 0)
|
|
||||||
lines.append(
|
|
||||||
f"{medal} {name} — "
|
|
||||||
f"**{mic + deaf}x** gesamt "
|
|
||||||
f"*(🎙️ {mic}x Mikro / 🔇 {deaf}x Deaf)*"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
lines.append(f"{medal} {name} — **{counts}x** gemutet")
|
|
||||||
|
|
||||||
embed = discord.Embed(
|
|
||||||
title="Mute-Leaderboard",
|
|
||||||
color=discord.Color.blurple(),
|
|
||||||
)
|
|
||||||
embed.description = "\n".join(lines)
|
embed.description = "\n".join(lines)
|
||||||
embed.set_footer(text=f"Top {len(top)} von {len(sorted_entries)} Nutzern | 🎙️ = nur Mikro | 🔇 = Deaf")
|
embed.set_footer(text=f"Top {len(top)} von {len(sorted_entries)} Nutzern | 🎙️ = nur Mikro | 🔇 = Deaf")
|
||||||
|
|
||||||
await interaction.response.send_message(embed=embed)
|
await interaction.response.send_message(embed=embed)
|
||||||
|
|
||||||
|
|
||||||
@bot.tree.command(name="mymutes", description="Zeigt deine eigenen Mute-Stats.")
|
@bot.tree.command(name="mymutes", description="Zeigt deine eigenen Mute-Stats.")
|
||||||
async def mymutes(interaction: discord.Interaction):
|
async def mymutes(interaction: discord.Interaction):
|
||||||
entry = get_user_entry(mute_data, interaction.guild.id, interaction.user.id)
|
entry = get_user_entry(mute_data, interaction.guild.id, interaction.user.id)
|
||||||
mic = entry.get("mic", 0)
|
mic, deaf = entry.get("mic", 0), entry.get("deaf", 0)
|
||||||
deaf = entry.get("deaf", 0)
|
rank_name = get_target_role_name(mic + deaf) or "Noch kein Rang"
|
||||||
await interaction.response.send_message(
|
await interaction.response.send_message(
|
||||||
f"Deine Mute-Stats:\n"
|
f"Deine Mute-Stats:\n"
|
||||||
f"🎙️ Nur Mikrofon gemutet: **{mic}x**\n"
|
f"🎙️ Nur Mikrofon gemutet: **{mic}x**\n"
|
||||||
f"🔇 Taubgestellt (Deaf): **{deaf}x**\n"
|
f"🔇 Taubgestellt (Deaf): **{deaf}x**\n"
|
||||||
f"📊 Gesamt: **{mic + deaf}x**",
|
f"📊 Gesamt: **{mic + deaf}x**\n"
|
||||||
|
f"🏷️ Aktueller Rang: **{rank_name}**",
|
||||||
ephemeral=True,
|
ephemeral=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user