Fix(Ticket Whitelist): Ajout systeme async et tracking pour le deblocage des validation rate limitees (10 min) + infos MP

This commit is contained in:
Mathis 2026-04-02 19:06:50 +02:00
parent 97f69b4cff
commit 5fe70d754b
35 changed files with 111 additions and 3194 deletions

View file

@ -7,6 +7,34 @@ import os
import asyncio
from typing import Optional
from src.logger import kuby_logger
import time
# Dictionnaire global pour suivre les renommages de salons
# Format: {channel_id: [timestamp1, timestamp2, ...]}
channel_rename_timestamps = {}
def check_channel_rename_rate_limit(channel_id: int) -> int:
"""Vérifie si le renommage va atteindre la limite de Discord (2 edits / 10 min)."""
now = time.time()
if channel_id not in channel_rename_timestamps:
channel_rename_timestamps[channel_id] = []
timestamps = [ts for ts in channel_rename_timestamps[channel_id] if now - ts < 600]
channel_rename_timestamps[channel_id] = timestamps
if len(timestamps) >= 2:
return max(0, int(600 - (now - timestamps[0])))
return 0
def log_channel_rename(channel_id: int):
"""Enregistre un renommage de salon."""
now = time.time()
if channel_id not in channel_rename_timestamps:
channel_rename_timestamps[channel_id] = []
timestamps = [ts for ts in channel_rename_timestamps[channel_id] if now - ts < 600]
timestamps.append(now)
channel_rename_timestamps[channel_id] = timestamps
TICKET_WHITELIST_SETTINGS_FILE = "data/ticket_whitelist_settings.json"
@ -35,6 +63,31 @@ def save_settings(guild_id: int, settings: dict) -> None:
with open(TICKET_WHITELIST_SETTINGS_FILE, "w", encoding='utf-8') as f:
json.dump(data, f, indent=4, ensure_ascii=False)
async def get_ticket_user_id(channel: discord.TextChannel) -> Optional[int]:
"""Récupère l'ID du propriétaire du ticket de manière robuste (gère le lag de cache)"""
import re
topic = getattr(channel, "topic", None)
# Tentative d'utilisation du cache d.py (plus rapide)
if topic:
match = re.search(r"(\d{17,20})", topic)
if match:
return int(match.group(1))
# Si le topic est vide ou ID introuvable, on tente de forcer un refresh via l'API
try:
# On utilise fetch_channel pour avoir un objet tout neuf avec le topic à jour
refreshed_channel = await channel.guild.fetch_channel(channel.id)
topic = getattr(refreshed_channel, "topic", None)
if topic:
match = re.search(r"(\d{17,20})", topic)
if match:
return int(match.group(1))
except:
pass
return None
class TicketWhitelistConfigView(ui.View):
def __init__(self, guild_id: int):
super().__init__(timeout=60)
@ -321,15 +374,9 @@ class TicketManagementView(discord.ui.View):
super().__init__(timeout=None)
async def get_ticket_owner(self, interaction: discord.Interaction):
"""Récupère le propriétaire du ticket via le topic du salon (robuste via regex)"""
import re
topic = interaction.channel.topic
if not topic:
return None
match = re.search(r"(\d{17,20})", topic)
if match:
owner_id = int(match.group(1))
"""Récupère le propriétaire du ticket via le helper robuste"""
owner_id = await get_ticket_user_id(interaction.channel)
if owner_id:
try:
member = interaction.guild.get_member(owner_id)
if not member:
@ -357,7 +404,8 @@ class TicketManagementView(discord.ui.View):
try:
await interaction.channel.set_permissions(owner, read_messages=False, send_messages=False)
await interaction.channel.edit(name=f"fermé-{owner.display_name}".replace(" ", "-").lower()[:100])
log_channel_rename(interaction.channel.id)
asyncio.create_task(interaction.channel.edit(name=f"fermé-{owner.display_name}".replace(" ", "-").lower()[:100]))
# Envoi du message de fermeture dans le salon
new_view = ClosedTicketView()
@ -400,15 +448,9 @@ class ClosedTicketView(discord.ui.View):
super().__init__(timeout=None)
async def get_ticket_owner(self, interaction: discord.Interaction):
"""Récupère le propriétaire du ticket via le topic du salon (robuste via regex)"""
import re
topic = interaction.channel.topic
if not topic:
return None
match = re.search(r"(\d{17,20})", topic)
if match:
owner_id = int(match.group(1))
"""Récupère le propriétaire du ticket via le helper robuste"""
owner_id = await get_ticket_user_id(interaction.channel)
if owner_id:
try:
member = interaction.guild.get_member(owner_id)
if not member:
@ -502,7 +544,8 @@ class ReopenStatusView(discord.ui.View):
# Mise à jour du SALON
await interaction.channel.set_permissions(self.owner, read_messages=True, send_messages=True)
new_name = f"{prefix}-{self.owner.display_name}".replace(" ", "-").lower()[:100]
await interaction.channel.edit(name=new_name, topic=f"Ticket de {self.owner.id}")
log_channel_rename(interaction.channel.id)
asyncio.create_task(interaction.channel.edit(name=new_name, topic=f"Ticket de {self.owner.id}"))
new_view = TicketManagementView()
await interaction.channel.send(f"✅ Ticket réouvert par {interaction.user.mention} (Statut: **{prefix}**).", view=new_view)
@ -602,22 +645,16 @@ class Ticket(commands.Cog):
if message.guild is None or message.author.bot:
return
# Détection du format du topic du ticket via regex (robuste)
if not message.channel.topic:
if "validé" in message.content.lower():
await message.channel.send("❌ Debug: `message.channel.topic` est vide ou None.")
return
match = re.search(r"(\d{17,20})", message.channel.topic)
if not match:
if "validé" in message.content.lower():
await message.channel.send(f"❌ Debug: Topic trouvé mais pas d'ID matché. Content: `{message.channel.topic}`")
return
# On ne traite que si le message contient "validé"
if "validé" not in message.content.lower():
return
# Détection du format du topic du ticket via helper (robuste)
user_id = await get_ticket_user_id(message.channel)
if not user_id:
await message.channel.send("❌ Debug: Impossible de récupérer l'ID utilisateur via le topic (même après refresh API).")
return
settings = load_settings(message.guild.id)
staff_role_id = settings.get("staff_role_id")
@ -632,8 +669,8 @@ class Ticket(commands.Cog):
return
try:
# L'ID est déjà trouvé par le regex ci-dessus
user_id = int(match.group(1))
# L'ID est déjà trouvé par le helper ci-dessus
user_id = user_id
try:
user = await message.guild.fetch_member(user_id)
except (discord.NotFound, discord.HTTPException):
@ -659,7 +696,9 @@ class Ticket(commands.Cog):
# Mise à jour du nom du salon
new_name = f"accepté-{user.display_name}".replace(" ", "-").lower()[:100]
await message.channel.edit(name=new_name)
wait_time = check_channel_rename_rate_limit(message.channel.id)
log_channel_rename(message.channel.id)
asyncio.create_task(message.channel.edit(name=new_name))
# Confirmation dans le salon (Feedback visuel)
embed_val = discord.Embed(
@ -679,6 +718,9 @@ class Ticket(commands.Cog):
embed_dm.add_field(name="🏙️ Nouveau Statut", value="Citoyen", inline=True)
embed_dm.add_field(name="📍 Serveur", value=message.guild.name, inline=True)
embed_dm.add_field(name="📋 Prochaine étape", value="Tu peux désormais te connecter au serveur et commencer ton aventure RP. N'oublie pas de consulter les salons d'informations !", inline=False)
if wait_time > 0:
m, s = divmod(wait_time, 60)
embed_dm.add_field(name="⏳ Nom du ticket", value=f"Le nom de ton ticket sera actualisé d'ici {m}m et {s}s environ (limite de sécurité Discord).", inline=False)
embed_dm.add_field(name="🔗 Lien vers ton ticket", value=f"[Clique ici pour voir ton ticket]({message.channel.jump_url})", inline=False)
embed_dm.set_footer(text=f"L'équipe {message.guild.name}")
if message.guild.icon: embed_dm.set_thumbnail(url=message.guild.icon.url)
@ -693,7 +735,9 @@ class Ticket(commands.Cog):
# Mise à jour du nom du salon
new_name = f"oral-à-faire-{user.display_name}".replace(" ", "-").lower()[:100]
await message.channel.edit(name=new_name)
wait_time = check_channel_rename_rate_limit(message.channel.id)
log_channel_rename(message.channel.id)
asyncio.create_task(message.channel.edit(name=new_name))
# Confirmation dans le salon
embed_val = discord.Embed(
@ -712,6 +756,9 @@ class Ticket(commands.Cog):
)
embed_dm.add_field(name="🎤 Statut Actuel", value="Attente Oral", inline=True)
embed_dm.add_field(name="🛠️ Action Requise", value="Merci de te rendre dans le salon vocal 'Attente Oral' dès que tu es disponible pour passer ton entretien.", inline=False)
if wait_time > 0:
m, s = divmod(wait_time, 60)
embed_dm.add_field(name="⏳ Nom du ticket", value=f"Le nom de ton ticket sera actualisé d'ici {m}m et {s}s environ (limite de sécurité Discord).", inline=False)
embed_dm.add_field(name="🔗 Lien vers ton ticket", value=f"[Clique ici pour voir ton ticket]({message.channel.jump_url})", inline=False)
embed_dm.set_footer(text=f"L'équipe {message.guild.name}")
if message.guild.icon: embed_dm.set_thumbnail(url=message.guild.icon.url)