feat: add /drama-leaderboard command with historical composite scoring
Queries Messages, AnalysisResults, and Actions tables to rank users by a composite drama score (weighted avg toxicity, peak toxicity, and action rate). Public command with configurable time period (7d/30d/90d/all-time). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -713,6 +713,94 @@ class Database:
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Drama Leaderboard (historical stats from Messages + AnalysisResults + Actions)
|
||||
# ------------------------------------------------------------------
|
||||
async def get_drama_leaderboard(self, guild_id: int, days: int | None = None) -> list[dict]:
|
||||
"""Get per-user drama stats for the leaderboard.
|
||||
days=None means all-time. Returns list of dicts sorted by user_id."""
|
||||
if not self._available:
|
||||
return []
|
||||
try:
|
||||
return await asyncio.to_thread(self._get_drama_leaderboard_sync, guild_id, days)
|
||||
except Exception:
|
||||
logger.exception("Failed to get drama leaderboard")
|
||||
return []
|
||||
|
||||
def _get_drama_leaderboard_sync(self, guild_id: int, days: int | None) -> list[dict]:
|
||||
conn = self._connect()
|
||||
try:
|
||||
cursor = conn.cursor()
|
||||
|
||||
date_filter = ""
|
||||
params: list = [guild_id]
|
||||
if days is not None:
|
||||
date_filter = "AND m.CreatedAt >= DATEADD(DAY, ?, SYSUTCDATETIME())"
|
||||
params.append(-days)
|
||||
|
||||
# Analysis stats from Messages + AnalysisResults
|
||||
cursor.execute(f"""
|
||||
SELECT
|
||||
m.UserId,
|
||||
MAX(m.Username) AS Username,
|
||||
AVG(ar.ToxicityScore) AS AvgToxicity,
|
||||
MAX(ar.ToxicityScore) AS MaxToxicity,
|
||||
COUNT(*) AS MessagesAnalyzed
|
||||
FROM Messages m
|
||||
INNER JOIN AnalysisResults ar ON ar.MessageId = m.Id
|
||||
WHERE m.GuildId = ? {date_filter}
|
||||
GROUP BY m.UserId
|
||||
""", *params)
|
||||
|
||||
analysis_rows = cursor.fetchall()
|
||||
|
||||
# Action counts
|
||||
action_date_filter = ""
|
||||
action_params: list = [guild_id]
|
||||
if days is not None:
|
||||
action_date_filter = "AND CreatedAt >= DATEADD(DAY, ?, SYSUTCDATETIME())"
|
||||
action_params.append(-days)
|
||||
|
||||
cursor.execute(f"""
|
||||
SELECT
|
||||
UserId,
|
||||
SUM(CASE WHEN ActionType = 'warning' THEN 1 ELSE 0 END) AS Warnings,
|
||||
SUM(CASE WHEN ActionType = 'mute' THEN 1 ELSE 0 END) AS Mutes,
|
||||
SUM(CASE WHEN ActionType IN ('topic_remind', 'topic_nudge') THEN 1 ELSE 0 END) AS OffTopic
|
||||
FROM Actions
|
||||
WHERE GuildId = ? {action_date_filter}
|
||||
GROUP BY UserId
|
||||
""", *action_params)
|
||||
|
||||
action_map = {}
|
||||
for row in cursor.fetchall():
|
||||
action_map[row[0]] = {
|
||||
"warnings": row[1],
|
||||
"mutes": row[2],
|
||||
"off_topic": row[3],
|
||||
}
|
||||
|
||||
cursor.close()
|
||||
|
||||
results = []
|
||||
for row in analysis_rows:
|
||||
user_id = row[0]
|
||||
actions = action_map.get(user_id, {"warnings": 0, "mutes": 0, "off_topic": 0})
|
||||
results.append({
|
||||
"user_id": user_id,
|
||||
"username": row[1],
|
||||
"avg_toxicity": float(row[2]),
|
||||
"max_toxicity": float(row[3]),
|
||||
"messages_analyzed": row[4],
|
||||
"warnings": actions["warnings"],
|
||||
"mutes": actions["mutes"],
|
||||
"off_topic": actions["off_topic"],
|
||||
})
|
||||
|
||||
return results
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
async def close(self):
|
||||
"""No persistent connection to close (connections are per-operation)."""
|
||||
pass
|
||||
|
||||
Reference in New Issue
Block a user