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:
2026-02-27 16:08:39 -05:00
parent 0ff962c95e
commit 1d653ec216
3 changed files with 217 additions and 0 deletions

View File

@@ -669,6 +669,78 @@ class CommandsCog(commands.Cog):
old_mode, mode, interaction.user.display_name,
)
@app_commands.command(
name="drama-leaderboard",
description="Show the all-time drama leaderboard for the server.",
)
@app_commands.describe(period="Time period to rank (default: 30d)")
@app_commands.choices(period=[
app_commands.Choice(name="Last 7 days", value="7d"),
app_commands.Choice(name="Last 30 days", value="30d"),
app_commands.Choice(name="Last 90 days", value="90d"),
app_commands.Choice(name="All time", value="all"),
])
async def drama_leaderboard(
self, interaction: discord.Interaction, period: app_commands.Choice[str] | None = None,
):
await interaction.response.defer()
period_val = period.value if period else "30d"
if period_val == "all":
days = None
period_label = "All Time"
else:
days = int(period_val.rstrip("d"))
period_label = f"Last {days} Days"
rows = await self.bot.db.get_drama_leaderboard(interaction.guild.id, days)
if not rows:
await interaction.followup.send(
f"No drama data for **{period_label}**. Everyone's been suspiciously well-behaved."
)
return
# Compute composite score for each user
scored = []
for r in rows:
avg_tox = r["avg_toxicity"]
max_tox = r["max_toxicity"]
msg_count = r["messages_analyzed"]
action_weight = r["warnings"] + r["mutes"] * 2 + r["off_topic"] * 0.5
action_rate = min(1.0, action_weight / msg_count * 10) if msg_count > 0 else 0.0
composite = avg_tox * 0.4 + max_tox * 0.2 + action_rate * 0.4
scored.append({**r, "composite": composite, "action_rate": action_rate})
scored.sort(key=lambda x: x["composite"], reverse=True)
top = scored[:10]
medals = ["🥇", "🥈", "🥉"]
lines = []
for i, entry in enumerate(top):
rank = medals[i] if i < 3 else f"`{i + 1}.`"
# Resolve display name from guild if possible
member = interaction.guild.get_member(entry["user_id"])
name = member.display_name if member else entry["username"]
lines.append(
f"{rank} **{entry['composite']:.2f}** — {name}\n"
f" Avg: {entry['avg_toxicity']:.2f} | "
f"Peak: {entry['max_toxicity']:.2f} | "
f"⚠️ {entry['warnings']} | "
f"🔇 {entry['mutes']} | "
f"📢 {entry['off_topic']}"
)
embed = discord.Embed(
title=f"Drama Leaderboard — {period_label}",
description="\n".join(lines),
color=discord.Color.orange(),
)
embed.set_footer(text=f"{len(rows)} users tracked | {sum(r['messages_analyzed'] for r in rows)} messages analyzed")
await interaction.followup.send(embed=embed)
@bcs_mode.autocomplete("mode")
async def _mode_autocomplete(
self, interaction: discord.Interaction, current: str,