Refactor Discord member statistics handling and remove unused components
- Removed the `guild_member_stats` table and related logic from the database and models, as well as the associated webapp routes and templates. - Cleaned up the Discord bot initialization by eliminating unused imports and functions related to member statistics. - Updated logging configuration in `run-web.py` to streamline the startup process and improve thread management.
This commit is contained in:
@@ -29,13 +29,11 @@ from discordbot.welcome import sendWelcomeMessage, sendLeaveMessage, updateInvit
|
||||
from discordbot.patreon import checkPatreonPosts
|
||||
from discordbot.youtube import checkYouTubeVideos
|
||||
from discordbot.auto_rooms import on_voice_state_update_auto_rooms, on_raw_reaction_add_auto_rooms, on_message_auto_rooms, cleanup_orphaned_auto_rooms
|
||||
from discordbot.member_stats import record_message, on_voice_state_update_track_voice
|
||||
from discordbot.rate_limit_logging import build_discord_http_trace_config
|
||||
from protondb import searhProtonDb
|
||||
|
||||
class DiscordBot(discord.Client):
|
||||
def __init__(self, *, intents: discord.Intents):
|
||||
super().__init__(intents=intents, http_trace=build_discord_http_trace_config())
|
||||
super().__init__(intents=intents)
|
||||
self.tree = app_commands.CommandTree(self)
|
||||
self.synced = False
|
||||
|
||||
@@ -169,10 +167,7 @@ async def on_message(message: Message):
|
||||
|
||||
# Gestion des messages dans les auto rooms (avant le check des commandes !)
|
||||
await on_message_auto_rooms(bot, message)
|
||||
|
||||
if message.guild and not message.author.bot:
|
||||
record_message(message.guild.id, message.author.id)
|
||||
|
||||
|
||||
if not message.content.startswith('!'):
|
||||
return
|
||||
command_name = message.content.split()[0]
|
||||
@@ -336,7 +331,6 @@ async def on_message(message: Message):
|
||||
@bot.event
|
||||
async def on_voice_state_update(member: Member, before, after):
|
||||
await on_voice_state_update_auto_rooms(bot, member, before, after)
|
||||
on_voice_state_update_track_voice(member, before, after)
|
||||
|
||||
@bot.event
|
||||
async def on_raw_reaction_add(payload: discord.RawReactionActionEvent):
|
||||
|
||||
@@ -1,183 +0,0 @@
|
||||
"""Compteurs messages / vocal par membre pour la webapp (table guild_member_stats)."""
|
||||
import asyncio
|
||||
import concurrent.futures
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import discord
|
||||
from sqlalchemy import text
|
||||
|
||||
from webapp import webapp
|
||||
from database import db
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# (guild_id, user_id) -> datetime début session vocale (UTC)
|
||||
_voice_join_at: dict[tuple[int, int], datetime] = {}
|
||||
|
||||
|
||||
def _now_utc() -> datetime:
|
||||
return datetime.now(timezone.utc)
|
||||
|
||||
|
||||
def record_message(guild_id: int, user_id: int) -> None:
|
||||
"""Incrémente message_count (hors bots)."""
|
||||
try:
|
||||
with webapp.app_context():
|
||||
db.session.execute(
|
||||
text("""
|
||||
INSERT INTO guild_member_stats (guild_id, user_id, message_count, voice_seconds, updated_at)
|
||||
VALUES (:gid, :uid, 1, 0, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT(guild_id, user_id) DO UPDATE SET
|
||||
message_count = guild_member_stats.message_count + 1,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
"""),
|
||||
{"gid": str(guild_id), "uid": str(user_id)},
|
||||
)
|
||||
db.session.commit()
|
||||
except Exception as e:
|
||||
logger.warning("record_message: %s", e)
|
||||
try:
|
||||
db.session.rollback()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def add_voice_seconds(guild_id: int, user_id: int, seconds: int) -> None:
|
||||
if seconds <= 0:
|
||||
return
|
||||
try:
|
||||
with webapp.app_context():
|
||||
db.session.execute(
|
||||
text("""
|
||||
INSERT INTO guild_member_stats (guild_id, user_id, message_count, voice_seconds, updated_at)
|
||||
VALUES (:gid, :uid, 0, :sec, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT(guild_id, user_id) DO UPDATE SET
|
||||
voice_seconds = guild_member_stats.voice_seconds + :sec,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
"""),
|
||||
{"gid": str(guild_id), "uid": str(user_id), "sec": seconds},
|
||||
)
|
||||
db.session.commit()
|
||||
except Exception as e:
|
||||
logger.warning("add_voice_seconds: %s", e)
|
||||
try:
|
||||
db.session.rollback()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _finalize_voice_session(guild_id: int, user_id: int, end: datetime) -> None:
|
||||
key = (guild_id, user_id)
|
||||
started = _voice_join_at.pop(key, None)
|
||||
if started is None:
|
||||
return
|
||||
delta = (end - started).total_seconds()
|
||||
add_voice_seconds(guild_id, user_id, int(delta))
|
||||
|
||||
|
||||
def on_voice_state_update_track_voice(member: discord.Member, before: discord.VoiceState, after: discord.VoiceState) -> None:
|
||||
if member.bot:
|
||||
return
|
||||
guild_id = member.guild.id
|
||||
uid = member.id
|
||||
bc = before.channel
|
||||
ac = after.channel
|
||||
if bc == ac:
|
||||
return
|
||||
now = _now_utc()
|
||||
if bc is not None:
|
||||
_finalize_voice_session(guild_id, uid, now)
|
||||
if ac is not None:
|
||||
_voice_join_at[(guild_id, uid)] = now
|
||||
|
||||
|
||||
async def fetch_guild_members_snapshot(bot: discord.Client, guild_id: int | None) -> tuple[bool, str | None, dict]:
|
||||
"""
|
||||
Retourne (ok, erreur, payload) avec payload =
|
||||
{ guild_id, guild_name, members: [ { id, display_name, name, avatar_url, joined_at, nick, roles } ] }
|
||||
"""
|
||||
guilds = list(bot.guilds)
|
||||
if not guilds:
|
||||
return False, "Le bot n'est sur aucun serveur.", {}
|
||||
chosen: discord.Guild | None = None
|
||||
if guild_id is not None:
|
||||
chosen = discord.utils.get(guilds, id=guild_id)
|
||||
if chosen is None:
|
||||
return False, "Serveur Discord introuvable pour ce bot.", {}
|
||||
else:
|
||||
if len(guilds) == 1:
|
||||
chosen = guilds[0]
|
||||
else:
|
||||
return (
|
||||
False,
|
||||
"Plusieurs serveurs : précisez ?guild_id=… dans l'URL.",
|
||||
{"guilds": [{"id": g.id, "name": g.name} for g in guilds]},
|
||||
)
|
||||
try:
|
||||
await asyncio.wait_for(chosen.chunk(cache=True), timeout=120.0)
|
||||
except asyncio.TimeoutError:
|
||||
logger.warning(
|
||||
"guild.chunk timeout (%s), suite avec le cache membres partiel",
|
||||
chosen.name,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning("guild.chunk: %s", e, exc_info=True)
|
||||
members_out = []
|
||||
for m in chosen.members:
|
||||
if m.bot:
|
||||
continue
|
||||
role_list = [r for r in m.roles if r.name != "@everyone"]
|
||||
role_list.sort(key=lambda r: r.position, reverse=True)
|
||||
roles = ", ".join(r.name for r in role_list[:8])
|
||||
if len(role_list) > 8:
|
||||
roles += f" (+{len(role_list) - 8})"
|
||||
joined = m.joined_at.isoformat() if m.joined_at else None
|
||||
members_out.append({
|
||||
"id": str(m.id),
|
||||
"display_name": m.display_name,
|
||||
"name": m.name,
|
||||
"avatar_url": m.display_avatar.url if m.display_avatar else "",
|
||||
"joined_at": joined,
|
||||
"nick": m.nick,
|
||||
"roles": roles or "—",
|
||||
})
|
||||
members_out.sort(key=lambda x: (x["display_name"] or x["name"]).lower())
|
||||
return True, None, {
|
||||
"guild_id": str(chosen.id),
|
||||
"guild_name": chosen.name,
|
||||
"members": members_out,
|
||||
}
|
||||
|
||||
|
||||
def get_discord_members_snapshot_sync(bot: discord.Client, guild_id: int | None = None, timeout: float = 180.0) -> tuple[bool, str | None, dict]:
|
||||
"""Appel thread-safe depuis Flask (run_coroutine_threadsafe sur la boucle du bot)."""
|
||||
loop = getattr(bot, "loop", None)
|
||||
if loop is None:
|
||||
logger.error("get_discord_members_snapshot_sync: bot.loop est None (bot non démarré ?)")
|
||||
return False, "Bot Discord non démarré (aucune boucle événements).", {}
|
||||
if not bot.is_ready():
|
||||
return False, "Bot Discord pas encore prêt (connexion en cours). Réessayez dans quelques secondes.", {}
|
||||
try:
|
||||
future = asyncio.run_coroutine_threadsafe(
|
||||
fetch_guild_members_snapshot(bot, guild_id),
|
||||
loop,
|
||||
)
|
||||
ok, err, payload = future.result(timeout=timeout)
|
||||
return ok, err, payload
|
||||
except (concurrent.futures.TimeoutError, TimeoutError) as e:
|
||||
logger.error(
|
||||
"get_discord_members_snapshot_sync: délai dépassé après %.0fs (%s)",
|
||||
timeout,
|
||||
type(e).__name__,
|
||||
exc_info=True,
|
||||
)
|
||||
return (
|
||||
False,
|
||||
"Délai dépassé pendant le chargement des membres (serveur volumineux ou bot occupé). Réessayez dans un instant.",
|
||||
{},
|
||||
)
|
||||
except Exception as e:
|
||||
logger.exception("get_discord_members_snapshot_sync: %s", type(e).__name__)
|
||||
msg = str(e) if str(e) else repr(e)
|
||||
return False, msg or "Erreur lors de l’appel au bot Discord.", {}
|
||||
@@ -1,67 +0,0 @@
|
||||
"""
|
||||
Journalisation des en-têtes HTTP sur les réponses 429 (rate limit Discord).
|
||||
|
||||
Réf. Discord : https://support-dev.discord.com/hc/en-us/articles/6223003921559-My-Bot-is-Being-Rate-Limited
|
||||
|
||||
discord.py transmet ce TraceConfig aiohttp via l’option Client(http_trace=...).
|
||||
Le corps JSON (retry_after, global) est toujours loggé par le logger discord.http.
|
||||
"""
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import aiohttp
|
||||
|
||||
logger = logging.getLogger('discord.ratelimit_headers')
|
||||
|
||||
# En-têtes utiles pour identifier le type de limite (global / user / shared, etc.)
|
||||
_HEADER_KEYS = (
|
||||
'X-RateLimit-Limit',
|
||||
'X-RateLimit-Remaining',
|
||||
'X-RateLimit-Reset',
|
||||
'X-RateLimit-Reset-After',
|
||||
'X-RateLimit-Scope',
|
||||
'X-Ratelimit-Bucket',
|
||||
'X-Ratelimit-Limit',
|
||||
'X-Ratelimit-Remaining',
|
||||
'X-Ratelimit-Reset',
|
||||
'Retry-After',
|
||||
'Via',
|
||||
)
|
||||
|
||||
|
||||
def _collect_headers(resp: aiohttp.ClientResponse) -> dict[str, str]:
|
||||
h = resp.headers
|
||||
out: dict[str, str] = {}
|
||||
for key in _HEADER_KEYS:
|
||||
val = h.get(key)
|
||||
if val is not None:
|
||||
out[key] = val
|
||||
return out
|
||||
|
||||
|
||||
def build_discord_http_trace_config() -> aiohttp.TraceConfig:
|
||||
trace = aiohttp.TraceConfig()
|
||||
|
||||
async def on_request_end(
|
||||
session: aiohttp.ClientSession,
|
||||
trace_config_ctx: Any,
|
||||
params: Any,
|
||||
) -> None:
|
||||
try:
|
||||
resp = getattr(params, 'response', None)
|
||||
if resp is None or getattr(resp, 'status', None) != 429:
|
||||
return
|
||||
method = getattr(params, 'method', '?')
|
||||
url = getattr(params, 'url', '?')
|
||||
hdr = _collect_headers(resp)
|
||||
logger.warning(
|
||||
'Discord API 429 %s %s | rate_limit_headers=%s',
|
||||
method,
|
||||
url,
|
||||
hdr,
|
||||
)
|
||||
except Exception:
|
||||
logger.debug('rate_limit trace callback failed', exc_info=True)
|
||||
|
||||
trace.on_request_end.append(on_request_end)
|
||||
return trace
|
||||
Reference in New Issue
Block a user