Füge umfassende Dokumentation hinzu: Erstelle README.md mit Funktionen, Installationsanweisungen, Befehlen und Bot-Berechtigungen.
This commit is contained in:
165
README.md
Normal file
165
README.md
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
# MuteCounter — Discord Bot
|
||||||
|
|
||||||
|
Ein Discord-Bot der Self-Mutes in Voice-Channels zählt, ein Leaderboard führt und Meilensteine feiert.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
| Feature | Beschreibung |
|
||||||
|
|---|---|
|
||||||
|
| **Mute-Tracking** | Zählt Mikrofon-Mutes (🎙️) und Deaf-Mutes (🔇) getrennt |
|
||||||
|
| **Leaderboard** | All-Time, wöchentlich und monatlich per `/mutescore` |
|
||||||
|
| **Nutzer-Fokus** | Rang eines bestimmten Nutzers mit Kontext anzeigen |
|
||||||
|
| **Persönliche Stats** | Rekordtag, Mute-Ratio, Tages-/Wochen-/Monatswerte |
|
||||||
|
| **Meilensteine** | Automatische Channel-Nachricht bei 10 / 25 / 50 / 100 / 250 / 500 / 1000 Mutes |
|
||||||
|
| **Rang-Rollen** | Automatisch zugewiesene Rollen je nach Mute-Anzahl |
|
||||||
|
| **Overtake-Alert** | Benachrichtigung wenn jemand auf dem Leaderboard überholt wird |
|
||||||
|
| **Weekly-Posting** | Automatische Wochenzusammenfassung jeden Montag |
|
||||||
|
| **Mute-Ratio** | Mutes pro Stunde Voice-Zeit (AFK-Channel ausgeschlossen) |
|
||||||
|
| **Persistenz** | Alle Daten werden in `data.json` gespeichert |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Voraussetzungen
|
||||||
|
|
||||||
|
- Python 3.10 oder neuer
|
||||||
|
- Ein Discord-Bot-Token ([Discord Developer Portal](https://discord.com/developers/applications))
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
**1. Abhängigkeiten installieren**
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. Umgebungsvariablen konfigurieren**
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
Dann `.env` öffnen und die Werte eintragen:
|
||||||
|
```env
|
||||||
|
DISCORD_TOKEN=dein_bot_token_hier
|
||||||
|
MILESTONE_CHANNEL_ID=1234567890123456789 # optional
|
||||||
|
```
|
||||||
|
|
||||||
|
**3. Bot starten**
|
||||||
|
```bash
|
||||||
|
python bot.py
|
||||||
|
```
|
||||||
|
|
||||||
|
> Slash-Commands werden beim ersten Start synchronisiert. Es kann bis zu einer Stunde dauern, bis sie Discord-weit erscheinen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Konfiguration
|
||||||
|
|
||||||
|
| Variable | Pflicht | Beschreibung |
|
||||||
|
|---|---|---|
|
||||||
|
| `DISCORD_TOKEN` | ✅ | Bot-Token aus dem Developer Portal |
|
||||||
|
| `MILESTONE_CHANNEL_ID` | ❌ | Channel-ID für Meilensteine, Overtakes & Weekly-Posting. Fallback: Server-Systemchannel |
|
||||||
|
|
||||||
|
**Channel-ID kopieren:** Rechtsklick auf Channel → *ID kopieren* (Entwicklermodus muss aktiviert sein: Einstellungen → Erweitert → Entwicklermodus)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Befehle
|
||||||
|
|
||||||
|
### `/mutescore`
|
||||||
|
Zeigt das Mute-Leaderboard.
|
||||||
|
|
||||||
|
| Parameter | Typ | Standard | Beschreibung |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `period` | Auswahl | All-Time | `Gesamt`, `Diese Woche` oder `Diesen Monat` |
|
||||||
|
| `limit` | Zahl | 10 | Anzahl der angezeigten Plätze (1–25) |
|
||||||
|
| `user` | @Mention | — | Fokus-Ansicht für einen bestimmten Nutzer |
|
||||||
|
|
||||||
|
**Beispiele:**
|
||||||
|
```
|
||||||
|
/mutescore
|
||||||
|
/mutescore period:Diese Woche limit:5
|
||||||
|
/mutescore user:@Max
|
||||||
|
/mutescore user:@Max period:Diesen Monat
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `/mymutes`
|
||||||
|
Zeigt die eigenen Mute-Stats (nur für den Nutzer selbst sichtbar).
|
||||||
|
|
||||||
|
```
|
||||||
|
📊 All-Time 🎙️ 12x Mikro | 🔇 5x Deaf | 17x gesamt
|
||||||
|
📅 Diese Woche 4x
|
||||||
|
🗓️ Diesen Monat 11x
|
||||||
|
☀️ Heute 2x
|
||||||
|
🏆 Rekordtag 15.01.2026 mit 6x
|
||||||
|
⚡ Mute-Ratio 2.4 Mutes/Stunde (Voice-Zeit: 7h 5min)
|
||||||
|
🏷️ Rang 💯 Mute-Veteran
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Rang-Rollen
|
||||||
|
|
||||||
|
Rollen werden automatisch erstellt und zugewiesen. Bei einem Aufstieg wird die alte Rolle entfernt.
|
||||||
|
|
||||||
|
| Mutes | Rolle |
|
||||||
|
|---|---|
|
||||||
|
| 10 | 🔇 Stummer Gast |
|
||||||
|
| 25 | 😶 Gelegentlicher Muter |
|
||||||
|
| 50 | 🤫 Fortgeschrittener Schweiger |
|
||||||
|
| 100 | 💯 Mute-Veteran |
|
||||||
|
| 250 | 🏅 Schweige-Meister |
|
||||||
|
| 500 | 🎖️ Elite-Muter |
|
||||||
|
| 1000 | 👑 Mute-König |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bot-Berechtigungen
|
||||||
|
|
||||||
|
Folgende Berechtigungen werden benötigt:
|
||||||
|
|
||||||
|
| Berechtigung | Wozu |
|
||||||
|
|---|---|
|
||||||
|
| `View Channels` | Channels sehen |
|
||||||
|
| `Send Messages` | Nachrichten & Embeds senden |
|
||||||
|
| `Embed Links` | Embeds posten |
|
||||||
|
| `Manage Roles` | Rang-Rollen erstellen & vergeben |
|
||||||
|
| `Connect` | Voice-Channels sehen (für AFK-Erkennung) |
|
||||||
|
|
||||||
|
**Privileged Gateway Intents** (im Developer Portal aktivieren):
|
||||||
|
- `Server Members Intent`
|
||||||
|
- `Voice State Intent` (standardmäßig aktiv)
|
||||||
|
|
||||||
|
**Einlade-Scopes:** `bot` + `applications.commands`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Datenspeicherung
|
||||||
|
|
||||||
|
Alle Daten werden lokal in `data.json` gespeichert.
|
||||||
|
|
||||||
|
```
|
||||||
|
MuteCounter/
|
||||||
|
├── bot.py
|
||||||
|
├── data.json # automatisch erstellt
|
||||||
|
├── .env # nicht in Git einchecken!
|
||||||
|
├── .env.example
|
||||||
|
├── requirements.txt
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
> `data.json` und `.env` sollten **nicht** in ein öffentliches Repository eingecheckt werden.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Mute-Typen
|
||||||
|
|
||||||
|
| Aktion in Discord | Typ | Symbol |
|
||||||
|
|---|---|---|
|
||||||
|
| Mikrofon-Button drücken | `mic` | 🎙️ |
|
||||||
|
| Deaf-Button drücken (taubstellen) | `deaf` | 🔇 |
|
||||||
|
|
||||||
|
Wenn sich jemand taubstellt, wird nur der Deaf-Counter erhöht (nicht doppelt gezählt).
|
||||||
499
bot.py
499
bot.py
@@ -1,41 +1,23 @@
|
|||||||
import discord
|
import discord
|
||||||
from discord import app_commands
|
from discord import app_commands
|
||||||
from discord.ext import commands
|
from discord.ext import commands, tasks
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
from datetime import date, datetime, timedelta
|
from datetime import date, datetime, timedelta
|
||||||
|
from typing import Optional
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
TOKEN = os.getenv("DISCORD_TOKEN")
|
TOKEN = os.getenv("DISCORD_TOKEN")
|
||||||
MILESTONE_CHANNEL_ID = int(os.getenv("MILESTONE_CHANNEL_ID", 0))
|
MILESTONE_CHANNEL_ID = int(os.getenv("MILESTONE_CHANNEL_ID", 0))
|
||||||
DATA_FILE = "data.json"
|
DATA_FILE = "data.json"
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Datenformat pro Nutzer:
|
# Konfiguration
|
||||||
# {
|
|
||||||
# "mic": int, # All-Time Mikrofon-Mutes
|
|
||||||
# "deaf": int, # All-Time Deaf-Mutes
|
|
||||||
# "w_mic": int, # Wöchentlich Mikrofon-Mutes
|
|
||||||
# "w_deaf": int, # Wöchentlich Deaf-Mutes
|
|
||||||
# "m_mic": int, # Monatlich Mikrofon-Mutes
|
|
||||||
# "m_deaf": int, # Monatlich Deaf-Mutes
|
|
||||||
# "best_day": str, # Datum des Rekordtages "YYYY-MM-DD"
|
|
||||||
# "best_day_count": int,
|
|
||||||
# "today": str, # Heutiges Datum "YYYY-MM-DD"
|
|
||||||
# "today_count": int,
|
|
||||||
# "voice_sec": float, # Gesamte Voice-Zeit in Sekunden (ohne AFK)
|
|
||||||
# }
|
|
||||||
#
|
|
||||||
# Pro Guild zusätzlich ein "_meta"-Eintrag:
|
|
||||||
# { "weekly_start": "YYYY-MM-DD", "monthly_key": "YYYY-MM" }
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
MILESTONES: dict[int, tuple[str, str]] = {
|
||||||
# Meilenstein-Konfiguration
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
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**!"),
|
||||||
@@ -45,10 +27,8 @@ MILESTONES = {
|
|||||||
1000: ("👑", "**{name}** ist der unbestrittene **MUTE-KÖNIG** mit **1000 Mutes**!"),
|
1000: ("👑", "**{name}** ist der unbestrittene **MUTE-KÖNIG** mit **1000 Mutes**!"),
|
||||||
}
|
}
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# Absteigend nach Schwellenwert sortiert
|
||||||
# Rang-Rollen-Konfiguration (absteigend nach Schwellenwert)
|
RANK_ROLES: list[tuple[int, str]] = [
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
RANK_ROLES = [
|
|
||||||
(1000, "👑 Mute-König"),
|
(1000, "👑 Mute-König"),
|
||||||
(500, "🎖️ Elite-Muter"),
|
(500, "🎖️ Elite-Muter"),
|
||||||
(250, "🏅 Schweige-Meister"),
|
(250, "🏅 Schweige-Meister"),
|
||||||
@@ -57,33 +37,89 @@ RANK_ROLES = [
|
|||||||
(25, "😶 Gelegentlicher Muter"),
|
(25, "😶 Gelegentlicher Muter"),
|
||||||
(10, "🔇 Stummer Gast"),
|
(10, "🔇 Stummer Gast"),
|
||||||
]
|
]
|
||||||
RANK_ROLE_NAMES = {name for _, name in RANK_ROLES}
|
RANK_ROLE_NAMES: set[str] = {name for _, name in RANK_ROLES}
|
||||||
|
|
||||||
|
PERIOD_LABELS: dict[str, str] = {
|
||||||
|
"alltime": "All-Time",
|
||||||
|
"weekly": "Diese Woche",
|
||||||
|
"monthly": "Diesen Monat",
|
||||||
|
}
|
||||||
|
|
||||||
|
MEDALS: dict[int, str] = {1: "🥇", 2: "🥈", 3: "🥉"}
|
||||||
|
|
||||||
|
|
||||||
def get_target_role_name(total: int):
|
# ---------------------------------------------------------------------------
|
||||||
|
# Hilfsfunktionen
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def get_target_role_name(total: int) -> Optional[str]:
|
||||||
for threshold, name in RANK_ROLES:
|
for threshold, name in RANK_ROLES:
|
||||||
if total >= threshold:
|
if total >= threshold:
|
||||||
return name
|
return name
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Zeitraum-Hilfsfunktionen
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
def get_week_start() -> str:
|
def get_week_start() -> str:
|
||||||
"""Gibt den Montag der aktuellen Woche als 'YYYY-MM-DD' zurück."""
|
|
||||||
today = date.today()
|
today = date.today()
|
||||||
return (today - timedelta(days=today.weekday())).isoformat()
|
return (today - timedelta(days=today.weekday())).isoformat()
|
||||||
|
|
||||||
|
|
||||||
def get_month_key() -> str:
|
def get_month_key() -> str:
|
||||||
"""Gibt den aktuellen Monat als 'YYYY-MM' zurück."""
|
|
||||||
return date.today().strftime("%Y-%m")
|
return date.today().strftime("%Y-%m")
|
||||||
|
|
||||||
|
|
||||||
|
def format_date(date_str: str) -> str:
|
||||||
|
return datetime.fromisoformat(date_str).strftime("%d.%m.%Y")
|
||||||
|
|
||||||
|
|
||||||
|
def format_voice_time(seconds: float) -> str:
|
||||||
|
h = int(seconds // 3600)
|
||||||
|
m = int((seconds % 3600) // 60)
|
||||||
|
return f"{h}h {m}min" if h > 0 else f"{m}min"
|
||||||
|
|
||||||
|
|
||||||
|
def is_regular_voice(channel, afk_channel) -> bool:
|
||||||
|
if channel is None:
|
||||||
|
return False
|
||||||
|
if afk_channel is not None and channel.id == afk_channel.id:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def find_announce_channel(guild: discord.Guild) -> Optional[discord.TextChannel]:
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def build_lb_line(guild: discord.Guild, rank: int, uid: str, counts: dict,
|
||||||
|
period: str, highlight: bool = False) -> str:
|
||||||
|
member = guild.get_member(int(uid))
|
||||||
|
name = member.display_name if member else f"Unbekannt ({uid})"
|
||||||
|
badge = MEDALS.get(rank, f"#{rank}")
|
||||||
|
|
||||||
|
if period == "weekly":
|
||||||
|
mic, deaf = counts.get("w_mic", 0), counts.get("w_deaf", 0)
|
||||||
|
elif period == "monthly":
|
||||||
|
mic, deaf = counts.get("m_mic", 0), counts.get("m_deaf", 0)
|
||||||
|
else:
|
||||||
|
mic, deaf = counts.get("mic", 0), counts.get("deaf", 0)
|
||||||
|
|
||||||
|
line = f"{badge} {name} — **{mic + deaf}x** *(🎙️ {mic}x / 🔇 {deaf}x)*"
|
||||||
|
return f"**▶ {line} ◀**" if highlight else line
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Datenverwaltung
|
# Datenverwaltung
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
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:
|
||||||
@@ -91,7 +127,7 @@ def load_data() -> dict:
|
|||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
def save_data(data: dict):
|
def save_data(data: dict) -> None:
|
||||||
with open(DATA_FILE, "w", encoding="utf-8") as f:
|
with open(DATA_FILE, "w", encoding="utf-8") as f:
|
||||||
json.dump(data, f, indent=2, ensure_ascii=False)
|
json.dump(data, f, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
@@ -108,26 +144,47 @@ def ensure_meta(data: dict, guild_id: int) -> dict:
|
|||||||
return data[gid]["_meta"]
|
return data[gid]["_meta"]
|
||||||
|
|
||||||
|
|
||||||
def reset_periods_if_needed(data: dict, guild_id: int):
|
def reset_periods_if_needed(data: dict, guild_id: int) -> bool:
|
||||||
"""Setzt Wochen-/Monatszähler zurück falls der Zeitraum gewechselt hat."""
|
"""
|
||||||
meta = ensure_meta(data, guild_id)
|
Setzt Perioden-Zähler zurück wenn Woche oder Monat gewechselt hat.
|
||||||
gid = str(guild_id)
|
Bei einem Wochen-Reset wird ein Snapshot für den weekly_post_task gespeichert.
|
||||||
|
Gibt True zurück wenn Daten verändert wurden (Save empfohlen).
|
||||||
|
"""
|
||||||
|
meta = ensure_meta(data, guild_id)
|
||||||
|
gid = str(guild_id)
|
||||||
|
changed = False
|
||||||
|
|
||||||
current_week = get_week_start()
|
current_week = get_week_start()
|
||||||
if meta["weekly_start"] != current_week:
|
if meta["weekly_start"] != current_week:
|
||||||
for uid, entry in data[gid].items():
|
snapshot = [
|
||||||
if uid != "_meta" and isinstance(entry, dict):
|
{"uid": uid, "w_mic": e.get("w_mic", 0), "w_deaf": e.get("w_deaf", 0)}
|
||||||
entry["w_mic"] = 0
|
for uid, e in data[gid].items()
|
||||||
entry["w_deaf"] = 0
|
if uid != "_meta" and isinstance(e, dict)
|
||||||
|
and e.get("w_mic", 0) + e.get("w_deaf", 0) > 0
|
||||||
|
]
|
||||||
|
if snapshot:
|
||||||
|
snapshot.sort(key=lambda x: x["w_mic"] + x["w_deaf"], reverse=True)
|
||||||
|
meta["pending_weekly_post"] = {
|
||||||
|
"week_start": meta["weekly_start"],
|
||||||
|
"entries": snapshot[:10],
|
||||||
|
}
|
||||||
|
for uid, e in data[gid].items():
|
||||||
|
if uid != "_meta" and isinstance(e, dict):
|
||||||
|
e["w_mic"] = 0
|
||||||
|
e["w_deaf"] = 0
|
||||||
meta["weekly_start"] = current_week
|
meta["weekly_start"] = current_week
|
||||||
|
changed = True
|
||||||
|
|
||||||
current_month = get_month_key()
|
current_month = get_month_key()
|
||||||
if meta["monthly_key"] != current_month:
|
if meta["monthly_key"] != current_month:
|
||||||
for uid, entry in data[gid].items():
|
for uid, e in data[gid].items():
|
||||||
if uid != "_meta" and isinstance(entry, dict):
|
if uid != "_meta" and isinstance(e, dict):
|
||||||
entry["m_mic"] = 0
|
e["m_mic"] = 0
|
||||||
entry["m_deaf"] = 0
|
e["m_deaf"] = 0
|
||||||
meta["monthly_key"] = current_month
|
meta["monthly_key"] = current_month
|
||||||
|
changed = True
|
||||||
|
|
||||||
|
return changed
|
||||||
|
|
||||||
|
|
||||||
def default_user_entry() -> dict:
|
def default_user_entry() -> dict:
|
||||||
@@ -149,87 +206,46 @@ def get_user_entry(data: dict, guild_id: int, user_id: int) -> dict:
|
|||||||
return data[gid][uid]
|
return data[gid][uid]
|
||||||
|
|
||||||
|
|
||||||
def update_daily(entry: dict, delta: int = 1):
|
def update_daily(entry: dict) -> None:
|
||||||
"""Aktualisiert den heutigen Tageszähler und ggf. den Rekordtag."""
|
|
||||||
today_str = date.today().isoformat()
|
today_str = date.today().isoformat()
|
||||||
if entry.get("today") != today_str:
|
if entry.get("today") != today_str:
|
||||||
entry["today"] = today_str
|
entry["today"] = today_str
|
||||||
entry["today_count"] = 0
|
entry["today_count"] = 0
|
||||||
entry["today_count"] += delta
|
entry["today_count"] += 1
|
||||||
if entry["today_count"] > entry.get("best_day_count", 0):
|
if entry["today_count"] > entry.get("best_day_count", 0):
|
||||||
entry["best_day"] = today_str
|
entry["best_day"] = today_str
|
||||||
entry["best_day_count"] = entry["today_count"]
|
entry["best_day_count"] = entry["today_count"]
|
||||||
|
|
||||||
|
|
||||||
def entry_total(entry, period: str = "alltime") -> int:
|
def increment(data: dict, guild_id: int, user_id: int, kind: str) -> int:
|
||||||
if not isinstance(entry, dict):
|
"""Erhöht alle Zähler und gibt den neuen All-Time-Gesamtwert zurück."""
|
||||||
return int(entry)
|
reset_periods_if_needed(data, guild_id)
|
||||||
if period == "weekly":
|
entry = get_user_entry(data, guild_id, user_id)
|
||||||
return entry.get("w_mic", 0) + entry.get("w_deaf", 0)
|
entry[kind] += 1
|
||||||
if period == "monthly":
|
entry[f"w_{kind}"] += 1
|
||||||
return entry.get("m_mic", 0) + entry.get("m_deaf", 0)
|
entry[f"m_{kind}"] += 1
|
||||||
|
update_daily(entry)
|
||||||
return entry.get("mic", 0) + entry.get("deaf", 0)
|
return entry.get("mic", 0) + entry.get("deaf", 0)
|
||||||
|
|
||||||
|
|
||||||
def increment(data: dict, guild_id: int, user_id: int, kind: str) -> int:
|
|
||||||
"""Erhöht alle relevanten Zähler und gibt den neuen All-Time-Gesamtwert zurück."""
|
|
||||||
reset_periods_if_needed(data, guild_id)
|
|
||||||
entry = get_user_entry(data, guild_id, user_id)
|
|
||||||
entry[kind] += 1
|
|
||||||
entry[f"w_{kind}"] += 1
|
|
||||||
entry[f"m_{kind}"] += 1
|
|
||||||
update_daily(entry, 1)
|
|
||||||
return entry_total(entry)
|
|
||||||
|
|
||||||
|
|
||||||
def get_sorted_lb(data: dict, guild_id: int, period: str = "alltime") -> list:
|
def get_sorted_lb(data: dict, guild_id: int, period: str = "alltime") -> list:
|
||||||
|
def total(e) -> int:
|
||||||
|
if period == "weekly": return e.get("w_mic", 0) + e.get("w_deaf", 0)
|
||||||
|
if period == "monthly": return e.get("m_mic", 0) + e.get("m_deaf", 0)
|
||||||
|
return e.get("mic", 0) + e.get("deaf", 0)
|
||||||
|
|
||||||
guild_data = {k: v for k, v in data.get(str(guild_id), {}).items() if k != "_meta"}
|
guild_data = {k: v for k, v in data.get(str(guild_id), {}).items() if k != "_meta"}
|
||||||
return sorted(guild_data.items(), key=lambda x: entry_total(x[1], period), reverse=True)
|
return sorted(guild_data.items(), key=lambda x: total(x[1]), reverse=True)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Hilfsfunktionen
|
# Bot-Events
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
def is_regular_voice(channel, afk_channel) -> bool:
|
|
||||||
if channel is None:
|
|
||||||
return False
|
|
||||||
if afk_channel is not None and channel.id == afk_channel.id:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
async def check_milestone(guild: discord.Guild, member: discord.Member, total: int) -> None:
|
||||||
def find_announce_channel(guild: discord.Guild):
|
if total not in MILESTONES:
|
||||||
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,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def format_date(date_str: str) -> str:
|
|
||||||
return datetime.fromisoformat(date_str).strftime("%d.%m.%Y")
|
|
||||||
|
|
||||||
|
|
||||||
def format_voice_time(seconds: float) -> str:
|
|
||||||
h = int(seconds // 3600)
|
|
||||||
m = int((seconds % 3600) // 60)
|
|
||||||
if h > 0:
|
|
||||||
return f"{h}h {m}min"
|
|
||||||
return f"{m}min"
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Feature: Meilenstein-Nachrichten
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
async def check_milestone(guild: discord.Guild, member: discord.Member, new_total: int):
|
|
||||||
if new_total not in MILESTONES:
|
|
||||||
return
|
return
|
||||||
emoji, template = MILESTONES[new_total]
|
emoji, template = MILESTONES[total]
|
||||||
channel = find_announce_channel(guild)
|
channel = find_announce_channel(guild)
|
||||||
if not channel:
|
if not channel:
|
||||||
return
|
return
|
||||||
@@ -239,16 +255,13 @@ async def check_milestone(guild: discord.Guild, member: discord.Member, new_tota
|
|||||||
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: {total} Mutes")
|
||||||
await channel.send(embed=embed)
|
await channel.send(embed=embed)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
async def update_rank_role(guild: discord.Guild, member: discord.Member, total: int) -> None:
|
||||||
# Feature: Rang-Rollen
|
target_name = get_target_role_name(total)
|
||||||
# ---------------------------------------------------------------------------
|
to_remove = [r for r in member.roles if r.name in RANK_ROLE_NAMES and r.name != target_name]
|
||||||
async def update_rank_role(guild: discord.Guild, member: discord.Member, new_total: int):
|
|
||||||
target_name = get_target_role_name(new_total)
|
|
||||||
to_remove = [r for r in member.roles if r.name in RANK_ROLE_NAMES and r.name != target_name]
|
|
||||||
if to_remove:
|
if to_remove:
|
||||||
try:
|
try:
|
||||||
await member.remove_roles(*to_remove, reason="MuteCounter Rang-Update")
|
await member.remove_roles(*to_remove, reason="MuteCounter Rang-Update")
|
||||||
@@ -263,36 +276,28 @@ async def update_rank_role(guild: discord.Guild, member: discord.Member, new_tot
|
|||||||
try:
|
try:
|
||||||
role = await guild.create_role(name=target_name, reason="MuteCounter Rang-System")
|
role = await guild.create_role(name=target_name, reason="MuteCounter Rang-System")
|
||||||
except discord.Forbidden:
|
except discord.Forbidden:
|
||||||
print(f"[RANG] Fehlende Berechtigung zum Erstellen der Rolle '{target_name}'.")
|
print(f"[RANG] Fehlende Berechtigung beim Erstellen von '{target_name}'.")
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
await member.add_roles(role, reason="MuteCounter Rang-Update")
|
await member.add_roles(role, reason="MuteCounter Rang-Update")
|
||||||
except discord.Forbidden:
|
except discord.Forbidden:
|
||||||
print(f"[RANG] Fehlende Berechtigung zum Zuweisen der Rolle '{target_name}'.")
|
print(f"[RANG] Fehlende Berechtigung beim Zuweisen von '{target_name}'.")
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
async def check_overtake(guild: discord.Guild, member: discord.Member,
|
||||||
# Feature: Overtake-Alert
|
old_lb: list, new_lb: list) -> None:
|
||||||
# ---------------------------------------------------------------------------
|
uid = str(member.id)
|
||||||
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)
|
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)
|
new_rank = next((i + 1 for i, (u, _) in enumerate(new_lb) if u == uid), None)
|
||||||
|
|
||||||
if old_rank is None or new_rank is None or new_rank >= old_rank:
|
if old_rank is None or new_rank is None or new_rank >= old_rank:
|
||||||
return
|
return
|
||||||
|
|
||||||
if new_rank - 1 >= len(old_lb):
|
if new_rank - 1 >= len(old_lb):
|
||||||
return
|
return
|
||||||
|
|
||||||
overtaken_uid, _ = old_lb[new_rank - 1]
|
overtaken_uid, _ = old_lb[new_rank - 1]
|
||||||
overtaken = guild.get_member(int(overtaken_uid))
|
overtaken = guild.get_member(int(overtaken_uid))
|
||||||
overtaken_name = overtaken.display_name if overtaken else f"User {overtaken_uid}"
|
overtaken_name = overtaken.display_name if overtaken else f"User {overtaken_uid}"
|
||||||
|
|
||||||
channel = find_announce_channel(guild)
|
channel = find_announce_channel(guild)
|
||||||
if not channel:
|
if not channel:
|
||||||
@@ -310,23 +315,70 @@ async def check_overtake(
|
|||||||
await channel.send(embed=embed)
|
await channel.send(embed=embed)
|
||||||
|
|
||||||
|
|
||||||
|
async def post_weekly_summary(guild: discord.Guild, post_data: dict) -> None:
|
||||||
|
channel = find_announce_channel(guild)
|
||||||
|
if not channel:
|
||||||
|
return
|
||||||
|
|
||||||
|
week_start_dt = datetime.fromisoformat(post_data["week_start"])
|
||||||
|
week_end_dt = week_start_dt + timedelta(days=6)
|
||||||
|
entries = post_data.get("entries", [])
|
||||||
|
|
||||||
|
lines = []
|
||||||
|
for rank, e in enumerate(entries, start=1):
|
||||||
|
member = guild.get_member(int(e["uid"]))
|
||||||
|
name = member.display_name if member else f"Unbekannt ({e['uid']})"
|
||||||
|
mic, deaf = e["w_mic"], e["w_deaf"]
|
||||||
|
lines.append(f"{MEDALS.get(rank, f'**#{rank}**')} {name} — **{mic + deaf}x** *(🎙️ {mic}x / 🔇 {deaf}x)*")
|
||||||
|
|
||||||
|
embed = discord.Embed(
|
||||||
|
title="📋 Wöchentliches Leaderboard",
|
||||||
|
description=(
|
||||||
|
f"*Woche vom {week_start_dt.strftime('%d.%m.')} "
|
||||||
|
f"bis {week_end_dt.strftime('%d.%m.%Y')}*\n\n"
|
||||||
|
+ ("\n".join(lines) if lines else "Keine Mutes diese Woche.")
|
||||||
|
),
|
||||||
|
color=discord.Color.green(),
|
||||||
|
)
|
||||||
|
embed.set_footer(text=f"Top {len(entries)} Nutzer dieser Woche")
|
||||||
|
await channel.send(embed=embed)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Bot-Setup
|
# Bot-Setup
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
intents = discord.Intents.default()
|
|
||||||
intents.voice_states = True
|
|
||||||
intents.members = True
|
|
||||||
|
|
||||||
bot = commands.Bot(command_prefix="!", intents=intents)
|
intents = discord.Intents.default()
|
||||||
mute_data = load_data()
|
intents.voice_states = True
|
||||||
voice_sessions: dict = {} # (guild_id, user_id) -> Unix-Timestamp des Channel-Beitritts
|
intents.members = True
|
||||||
|
|
||||||
|
bot = commands.Bot(command_prefix="!", intents=intents)
|
||||||
|
mute_data = load_data()
|
||||||
|
voice_sessions: dict[tuple[int, int], float] = {}
|
||||||
|
|
||||||
|
|
||||||
|
@tasks.loop(hours=1)
|
||||||
|
async def weekly_post_task() -> None:
|
||||||
|
for guild in bot.guilds:
|
||||||
|
gid = str(guild.id)
|
||||||
|
meta = mute_data.get(gid, {}).get("_meta", {})
|
||||||
|
if "pending_weekly_post" not in meta:
|
||||||
|
continue
|
||||||
|
post_data = meta.pop("pending_weekly_post")
|
||||||
|
save_data(mute_data)
|
||||||
|
await post_weekly_summary(guild, post_data)
|
||||||
|
print(f"[WEEKLY] Zusammenfassung für '{guild.name}' gepostet.")
|
||||||
|
|
||||||
|
|
||||||
|
@weekly_post_task.before_loop
|
||||||
|
async def before_weekly_task() -> None:
|
||||||
|
await bot.wait_until_ready()
|
||||||
|
|
||||||
|
|
||||||
@bot.event
|
@bot.event
|
||||||
async def on_ready():
|
async def on_ready() -> None:
|
||||||
print(f"Bot gestartet als {bot.user} (ID: {bot.user.id})")
|
print(f"Bot gestartet als {bot.user} (ID: {bot.user.id})")
|
||||||
|
|
||||||
# Bereits verbundene Mitglieder in voice_sessions eintragen (Bot-Neustart)
|
|
||||||
for guild in bot.guilds:
|
for guild in bot.guilds:
|
||||||
afk = guild.afk_channel
|
afk = guild.afk_channel
|
||||||
for vc in guild.voice_channels:
|
for vc in guild.voice_channels:
|
||||||
@@ -335,56 +387,48 @@ async def on_ready():
|
|||||||
for member in vc.members:
|
for member in vc.members:
|
||||||
if not member.bot:
|
if not member.bot:
|
||||||
voice_sessions[(guild.id, member.id)] = time.time()
|
voice_sessions[(guild.id, member.id)] = time.time()
|
||||||
print(f"{sum(1 for _ in voice_sessions)} Voice-Sessions wiederhergestellt.")
|
print(f"{len(voice_sessions)} Voice-Session(s) wiederhergestellt.")
|
||||||
|
|
||||||
|
if not weekly_post_task.is_running():
|
||||||
|
weekly_post_task.start()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
synced = await bot.tree.sync()
|
synced = await bot.tree.sync()
|
||||||
print(f"{len(synced)} Slash-Commands synchronisiert.")
|
print(f"{len(synced)} Slash-Command(s) synchronisiert.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
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,
|
before: discord.VoiceState,
|
||||||
before: discord.VoiceState,
|
after: discord.VoiceState) -> None:
|
||||||
after: discord.VoiceState,
|
|
||||||
):
|
|
||||||
if member.bot:
|
if member.bot:
|
||||||
return
|
return
|
||||||
|
|
||||||
guild = member.guild
|
guild = member.guild
|
||||||
afk_channel = guild.afk_channel
|
afk_channel = guild.afk_channel
|
||||||
|
|
||||||
was_regular = is_regular_voice(before.channel, afk_channel)
|
was_regular = is_regular_voice(before.channel, afk_channel)
|
||||||
now_regular = is_regular_voice(after.channel, afk_channel)
|
now_regular = is_regular_voice(after.channel, afk_channel)
|
||||||
|
|
||||||
# --- Voice-Zeit tracken ---
|
|
||||||
if not was_regular and now_regular:
|
if not was_regular and now_regular:
|
||||||
voice_sessions[(guild.id, member.id)] = time.time()
|
voice_sessions[(guild.id, member.id)] = time.time()
|
||||||
elif was_regular and not now_regular:
|
elif was_regular and not now_regular:
|
||||||
join_time = voice_sessions.pop((guild.id, member.id), None)
|
join_time = voice_sessions.pop((guild.id, member.id), None)
|
||||||
if join_time is not None:
|
if join_time is not None:
|
||||||
duration = time.time() - join_time
|
|
||||||
entry = get_user_entry(mute_data, guild.id, member.id)
|
entry = get_user_entry(mute_data, guild.id, member.id)
|
||||||
entry["voice_sec"] = entry.get("voice_sec", 0.0) + duration
|
entry["voice_sec"] = entry.get("voice_sec", 0.0) + (time.time() - join_time)
|
||||||
save_data(mute_data)
|
save_data(mute_data)
|
||||||
|
|
||||||
# --- Mute-Erkennung (nur in regulären Channels) ---
|
|
||||||
if not now_regular:
|
if not now_regular:
|
||||||
return
|
return
|
||||||
|
|
||||||
just_deafened = (not before.self_deaf) and after.self_deaf
|
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)
|
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:
|
if not just_deafened and not just_mic_muted:
|
||||||
return
|
return
|
||||||
|
|
||||||
kind = "deaf" if just_deafened else "mic"
|
kind = "deaf" if just_deafened else "mic"
|
||||||
label = "DEAF" if just_deafened else "MIC"
|
|
||||||
|
|
||||||
old_lb = get_sorted_lb(mute_data, guild.id)
|
old_lb = get_sorted_lb(mute_data, guild.id)
|
||||||
new_total = increment(mute_data, guild.id, member.id, kind)
|
new_total = increment(mute_data, guild.id, member.id, kind)
|
||||||
@@ -392,7 +436,8 @@ async def on_voice_state_update(
|
|||||||
new_lb = get_sorted_lb(mute_data, guild.id)
|
new_lb = get_sorted_lb(mute_data, guild.id)
|
||||||
|
|
||||||
entry = get_user_entry(mute_data, guild.id, member.id)
|
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}]")
|
print(f"[{'DEAF' if kind == 'deaf' else 'MIC '}] {member.display_name}: "
|
||||||
|
f"mic={entry['mic']}x deaf={entry['deaf']}x [{guild.name}]")
|
||||||
|
|
||||||
await check_milestone(guild, member, new_total)
|
await check_milestone(guild, member, new_total)
|
||||||
await update_rank_role(guild, member, new_total)
|
await update_rank_role(guild, member, new_total)
|
||||||
@@ -402,60 +447,75 @@ async def on_voice_state_update(
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Slash-Commands
|
# Slash-Commands
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
PERIOD_LABELS = {
|
|
||||||
"alltime": "All-Time",
|
|
||||||
"weekly": "Diese Woche",
|
|
||||||
"monthly": "Diesen Monat",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@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(
|
@app_commands.describe(
|
||||||
limit="Wie viele Plätze anzeigen? (Standard: 10)",
|
limit="Wie viele Plätze anzeigen? (Standard: 10, Max: 25)",
|
||||||
period="Zeitraum (Standard: Gesamt)",
|
period="Zeitraum (Standard: Gesamt)",
|
||||||
|
user="Fokus-Ansicht für einen bestimmten Nutzer.",
|
||||||
)
|
)
|
||||||
@app_commands.choices(period=[
|
@app_commands.choices(period=[
|
||||||
app_commands.Choice(name="Gesamt (All-Time)", value="alltime"),
|
app_commands.Choice(name="Gesamt (All-Time)", value="alltime"),
|
||||||
app_commands.Choice(name="Diese Woche", value="weekly"),
|
app_commands.Choice(name="Diese Woche", value="weekly"),
|
||||||
app_commands.Choice(name="Diesen Monat", value="monthly"),
|
app_commands.Choice(name="Diesen Monat", value="monthly"),
|
||||||
])
|
])
|
||||||
async def mutescore(interaction: discord.Interaction, limit: int = 10, period: str = "alltime"):
|
async def mutescore(
|
||||||
limit = max(1, min(limit, 25))
|
interaction: discord.Interaction,
|
||||||
|
limit: int = 10,
|
||||||
|
period: str = "alltime",
|
||||||
|
user: Optional[discord.Member] = None,
|
||||||
|
) -> None:
|
||||||
guild = interaction.guild
|
guild = interaction.guild
|
||||||
|
if reset_periods_if_needed(mute_data, guild.id):
|
||||||
|
save_data(mute_data)
|
||||||
|
|
||||||
# Perioden-Reset prüfen bevor angezeigt wird
|
lb = get_sorted_lb(mute_data, guild.id, period)
|
||||||
reset_periods_if_needed(mute_data, guild.id)
|
lb_filtered = [(uid, e) for uid, e in lb if
|
||||||
|
(e.get("w_mic", 0) + e.get("w_deaf", 0) if period == "weekly" else
|
||||||
|
e.get("m_mic", 0) + e.get("m_deaf", 0) if period == "monthly" else
|
||||||
|
e.get("mic", 0) + e.get("deaf", 0)) > 0]
|
||||||
|
|
||||||
lb = get_sorted_lb(mute_data, guild.id, period)
|
if user is not None:
|
||||||
lb_filtered = [(uid, e) for uid, e in lb if entry_total(e, period) > 0]
|
uid = str(user.id)
|
||||||
|
user_rank = next((i + 1 for i, (u, _) in enumerate(lb_filtered) if u == uid), None)
|
||||||
|
|
||||||
|
if user_rank is None:
|
||||||
|
await interaction.response.send_message(
|
||||||
|
f"**{user.display_name}** hat noch keine Mutes im Zeitraum "
|
||||||
|
f"**{PERIOD_LABELS[period]}**.",
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
ctx_start = max(0, user_rank - 3)
|
||||||
|
ctx_end = min(len(lb_filtered), user_rank + 2)
|
||||||
|
lines = [
|
||||||
|
build_lb_line(guild, ctx_start + i + 1, u, counts, period, highlight=(u == uid))
|
||||||
|
for i, (u, counts) in enumerate(lb_filtered[ctx_start:ctx_end])
|
||||||
|
]
|
||||||
|
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=f"🔍 Mute-Leaderboard — Fokus: {user.display_name}",
|
||||||
|
color=discord.Color.blurple(),
|
||||||
|
)
|
||||||
|
embed.description = "\n".join(lines)
|
||||||
|
embed.set_thumbnail(url=user.display_avatar.url)
|
||||||
|
embed.set_footer(
|
||||||
|
text=f"Platz #{user_rank} von {len(lb_filtered)} Nutzern | {PERIOD_LABELS[period]}"
|
||||||
|
)
|
||||||
|
await interaction.response.send_message(embed=embed)
|
||||||
|
return
|
||||||
|
|
||||||
if not lb_filtered:
|
if not lb_filtered:
|
||||||
await interaction.response.send_message(
|
await interaction.response.send_message(
|
||||||
f"Für den Zeitraum **{PERIOD_LABELS[period]}** gibt es noch keine Mutes.",
|
f"Für **{PERIOD_LABELS[period]}** gibt es noch keine Mutes.",
|
||||||
ephemeral=True,
|
ephemeral=True,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
top = lb_filtered[:limit]
|
top = lb_filtered[:max(1, min(limit, 25))]
|
||||||
medals = {1: "🥇", 2: "🥈", 3: "🥉"}
|
lines = [build_lb_line(guild, rank, uid, counts, period)
|
||||||
lines = []
|
for rank, (uid, counts) in enumerate(top, start=1)]
|
||||||
|
|
||||||
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}**")
|
|
||||||
|
|
||||||
if period == "weekly":
|
|
||||||
mic, deaf = counts.get("w_mic", 0), counts.get("w_deaf", 0)
|
|
||||||
elif period == "monthly":
|
|
||||||
mic, deaf = counts.get("m_mic", 0), counts.get("m_deaf", 0)
|
|
||||||
else:
|
|
||||||
mic, deaf = counts.get("mic", 0), counts.get("deaf", 0)
|
|
||||||
|
|
||||||
lines.append(
|
|
||||||
f"{medal} {name} — **{mic + deaf}x** gesamt "
|
|
||||||
f"*(🎙️ {mic}x Mikro / 🔇 {deaf}x Deaf)*"
|
|
||||||
)
|
|
||||||
|
|
||||||
embed = discord.Embed(
|
embed = discord.Embed(
|
||||||
title=f"Mute-Leaderboard — {PERIOD_LABELS[period]}",
|
title=f"Mute-Leaderboard — {PERIOD_LABELS[period]}",
|
||||||
@@ -469,51 +529,46 @@ async def mutescore(interaction: discord.Interaction, limit: int = 10, period: s
|
|||||||
|
|
||||||
|
|
||||||
@bot.tree.command(name="mymutes", description="Zeigt deine persönlichen Mute-Stats.")
|
@bot.tree.command(name="mymutes", description="Zeigt deine persönlichen Mute-Stats.")
|
||||||
async def mymutes(interaction: discord.Interaction):
|
async def mymutes(interaction: discord.Interaction) -> None:
|
||||||
reset_periods_if_needed(mute_data, interaction.guild.id)
|
if reset_periods_if_needed(mute_data, interaction.guild.id):
|
||||||
entry = get_user_entry(mute_data, interaction.guild.id, interaction.user.id)
|
save_data(mute_data)
|
||||||
|
|
||||||
|
entry = get_user_entry(mute_data, interaction.guild.id, interaction.user.id)
|
||||||
mic, deaf = entry.get("mic", 0), entry.get("deaf", 0)
|
mic, deaf = entry.get("mic", 0), entry.get("deaf", 0)
|
||||||
w_mic, w_deaf = entry.get("w_mic", 0), entry.get("w_deaf", 0)
|
w_mic, w_deaf = entry.get("w_mic", 0), entry.get("w_deaf", 0)
|
||||||
m_mic, m_deaf = entry.get("m_mic", 0), entry.get("m_deaf", 0)
|
m_mic, m_deaf = entry.get("m_mic", 0), entry.get("m_deaf", 0)
|
||||||
total = mic + deaf
|
total = mic + deaf
|
||||||
|
|
||||||
# Mute-Ratio
|
|
||||||
voice_sec = entry.get("voice_sec", 0.0)
|
voice_sec = entry.get("voice_sec", 0.0)
|
||||||
voice_h = voice_sec / 3600
|
voice_h = voice_sec / 3600
|
||||||
if voice_h >= 0.5:
|
ratio_str = (
|
||||||
ratio_str = f"**{total / voice_h:.1f}** Mutes/Stunde *(Voice-Zeit: {format_voice_time(voice_sec)})*"
|
f"**{total / voice_h:.1f}** Mutes/Stunde *(Voice-Zeit: {format_voice_time(voice_sec)})*"
|
||||||
else:
|
if voice_h >= 0.5 else
|
||||||
ratio_str = f"Noch nicht genug Voice-Zeit *(bisher: {format_voice_time(voice_sec)})*"
|
f"Noch nicht genug Voice-Zeit *(bisher: {format_voice_time(voice_sec)})*"
|
||||||
|
)
|
||||||
|
|
||||||
# Rekordtag
|
|
||||||
best_day = entry.get("best_day")
|
best_day = entry.get("best_day")
|
||||||
best_count = entry.get("best_day_count", 0)
|
best_count = entry.get("best_day_count", 0)
|
||||||
best_str = f"{format_date(best_day)} mit **{best_count}x**" if best_day else "Noch kein Rekord"
|
best_str = f"{format_date(best_day)} mit **{best_count}x**" if best_day else "Noch kein Rekord"
|
||||||
|
|
||||||
# Heutiger Tag
|
today_str = date.today().isoformat()
|
||||||
today_str = date.today().isoformat()
|
|
||||||
today_count = entry.get("today_count", 0) if entry.get("today") == today_str else 0
|
today_count = entry.get("today_count", 0) if entry.get("today") == today_str else 0
|
||||||
|
|
||||||
rank_name = get_target_role_name(total) or "Noch kein Rang"
|
|
||||||
|
|
||||||
embed = discord.Embed(
|
embed = discord.Embed(
|
||||||
title=f"Mute-Stats von {interaction.user.display_name}",
|
title=f"Mute-Stats von {interaction.user.display_name}",
|
||||||
color=discord.Color.blurple(),
|
color=discord.Color.blurple(),
|
||||||
)
|
)
|
||||||
embed.set_thumbnail(url=interaction.user.display_avatar.url)
|
embed.set_thumbnail(url=interaction.user.display_avatar.url)
|
||||||
|
embed.add_field(name="📊 All-Time",
|
||||||
embed.add_field(
|
value=f"🎙️ {mic}x Mikro | 🔇 {deaf}x Deaf | **{total}x gesamt**",
|
||||||
name="📊 All-Time",
|
inline=False)
|
||||||
value=f"🎙️ {mic}x Mikro | 🔇 {deaf}x Deaf | **{total}x gesamt**",
|
embed.add_field(name="📅 Diese Woche", value=f"**{w_mic + w_deaf}x**", inline=True)
|
||||||
inline=False,
|
embed.add_field(name="🗓️ Diesen Monat", value=f"**{m_mic + m_deaf}x**", inline=True)
|
||||||
)
|
embed.add_field(name="☀️ Heute", value=f"**{today_count}x**", inline=True)
|
||||||
embed.add_field(name="📅 Diese Woche", value=f"**{w_mic + w_deaf}x**", inline=True)
|
embed.add_field(name="🏆 Rekordtag", value=best_str, inline=False)
|
||||||
embed.add_field(name="🗓️ Diesen Monat", value=f"**{m_mic + m_deaf}x**", inline=True)
|
embed.add_field(name="⚡ Mute-Ratio", value=ratio_str, inline=False)
|
||||||
embed.add_field(name="☀️ Heute", value=f"**{today_count}x**", inline=True)
|
embed.add_field(name="🏷️ Rang", value=get_target_role_name(total) or "Noch kein Rang",
|
||||||
embed.add_field(name="🏆 Rekordtag", value=best_str, inline=False)
|
inline=False)
|
||||||
embed.add_field(name="⚡ Mute-Ratio", value=ratio_str, inline=False)
|
|
||||||
embed.add_field(name="🏷️ Rang", value=rank_name, inline=False)
|
|
||||||
|
|
||||||
await interaction.response.send_message(embed=embed, ephemeral=True)
|
await interaction.response.send_message(embed=embed, ephemeral=True)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user