Add guild_member_stats table and update permissions for Discord members

- Created a new table `guild_member_stats` to track message counts and voice activity for users in each guild.
- Updated the database schema and models to include the new table.
- Added a new permission entry for `discord_members` in the `webapp_page_permission` table.
- Enhanced the webapp to include links and settings for managing Discord member statistics.
This commit is contained in:
2026-03-25 22:52:32 +01:00
parent bb92c3dd92
commit 855c13c183
10 changed files with 535 additions and 2 deletions
+157
View File
@@ -0,0 +1,157 @@
from collections import defaultdict
from datetime import datetime
from flask import render_template, request
from sqlalchemy import text
from webapp import webapp
from webapp.auth import require_page
from database import db
from database.models import GuildMemberStats, ModerationEvent
from discordbot import bot
from discordbot.member_stats import get_discord_members_snapshot_sync
def _format_voice_seconds(sec: int) -> str:
if sec <= 0:
return "0 min"
h, sec = divmod(sec, 3600)
m, sec = divmod(sec, 60)
if h:
return f"{h}h {m}min"
if m:
return f"{m}min"
return f"{sec}s"
def _event_to_row(e: ModerationEvent) -> dict:
return {
"type": e.type or "",
"created_at": e.created_at.strftime("%d/%m/%Y %H:%M") if e.created_at else "",
"reason": (e.reason or "")[:500],
"staff_name": e.staff_name or "",
"duration": e.duration,
}
def _load_latest_invites(guild_id_str: str) -> dict[str, dict]:
rows = db.session.execute(
text("""
SELECT user_id, invite_code, inviter_name, join_date FROM (
SELECT user_id, invite_code, inviter_name, join_date,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY join_date DESC, id DESC) AS rn
FROM member_invites WHERE guild_id = :gid
) WHERE rn = 1
"""),
{"gid": guild_id_str},
).mappings().all()
out = {}
for r in rows:
jd = r["join_date"]
out[str(r["user_id"])] = {
"invite_code": r["invite_code"] or "",
"inviter_name": r["inviter_name"] or "",
"join_date": jd.strftime("%d/%m/%Y %H:%M") if jd else "",
}
return out
def _load_sanctions_by_user(user_ids: list[str]) -> dict[str, list]:
by_user: dict[str, list] = defaultdict(list)
chunk = 400
for i in range(0, len(user_ids), chunk):
part = user_ids[i : i + chunk]
if not part:
continue
q = ModerationEvent.query.filter(ModerationEvent.discord_id.in_(part))
for e in q.all():
by_user[e.discord_id].append(e)
for uid in by_user:
by_user[uid].sort(key=lambda x: x.created_at or datetime.min, reverse=True)
return by_user
@webapp.route("/discord-membres")
@require_page("discord_members")
def discord_members():
status = webapp.config.get("BOT_STATUS", {})
bot_connected = bool(status.get("discord_connected"))
guild_param = request.args.get("guild_id", type=int)
ok, err, payload = get_discord_members_snapshot_sync(bot, guild_id=guild_param)
guild_choices = payload.get("guilds") if payload else None
if not ok and guild_choices:
return render_template(
"discord_members.html",
bot_connected=bot_connected,
load_error=err,
guild_choices=guild_choices,
guild_name=None,
members=[],
)
if not ok:
return render_template(
"discord_members.html",
bot_connected=bot_connected,
load_error=err or "Impossible de charger les membres.",
guild_choices=None,
guild_name=None,
members=[],
)
guild_id_str = payload["guild_id"]
guild_name = payload["guild_name"]
raw_members = payload["members"]
user_ids = [m["id"] for m in raw_members]
stats_map = {
r.user_id: r
for r in GuildMemberStats.query.filter_by(guild_id=guild_id_str).all()
}
invites_map = _load_latest_invites(guild_id_str)
sanctions_by_user = _load_sanctions_by_user(user_ids)
members = []
for m in raw_members:
uid = m["id"]
joined_raw = m.get("joined_at")
joined_display = ""
if joined_raw:
try:
dt = datetime.fromisoformat(joined_raw.replace("Z", "+00:00"))
joined_display = dt.strftime("%d/%m/%Y %H:%M") + " UTC"
except (ValueError, TypeError):
joined_display = joined_raw
st = stats_map.get(uid)
msg_c = st.message_count if st else 0
voice_s = st.voice_seconds if st else 0
inv = invites_map.get(uid, {"invite_code": "", "inviter_name": "", "join_date": ""})
events = sanctions_by_user.get(uid, [])
sanction_rows = [_event_to_row(e) for e in events]
search_blob = f"{m.get('display_name') or ''} {m.get('name') or ''} {uid}".lower()
members.append({
**m,
"joined_display": joined_display,
"search_blob": search_blob,
"message_count": msg_c,
"voice_label": _format_voice_seconds(voice_s),
"voice_seconds": voice_s,
"invite_code": inv["invite_code"],
"inviter_name": inv["inviter_name"],
"invite_join_date": inv["join_date"],
"sanction_count": len(sanction_rows),
"sanctions": sanction_rows,
})
return render_template(
"discord_members.html",
bot_connected=bot_connected,
load_error=None,
guild_choices=None,
guild_name=guild_name,
guild_id=guild_id_str,
members=members,
)