Mise à jour du système de whitelist, sécurité et logging
This commit is contained in:
parent
d82f704c75
commit
1e384032be
7 changed files with 199 additions and 145 deletions
15
commandes/PENDING_TASKS.md
Normal file
15
commandes/PENDING_TASKS.md
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# À RÉGLER - Session Suivante (Ticket Whitelist)
|
||||
|
||||
### Bug Identifié
|
||||
La commande **"validé"** par message (on_message) n'est pas détectée ou ne réagit pas si le ticket a été refermé puis réouvert avec le statut "Ouvert".
|
||||
|
||||
### État Actuel
|
||||
- Le bouton "Fermer" fonctionne et détecte l'ID via Regex.
|
||||
- Le bouton "Réouvrir" synchronise maintenant les rôles (Écrit/Oral/Accepté).
|
||||
- Le design des MPs (Orange/Vert) est restauré conforme aux captures.
|
||||
- **Reste** : Vérifier pourquoi `on_message` ignore parfois le message "validé" (probablement un problème de sujet (topic) ou de permissions au moment de la lecture).
|
||||
|
||||
### Prochaine Étape
|
||||
1. Lancer le bot.
|
||||
2. Ouvrir/Réouvrir un ticket.
|
||||
3. Taper "validé" et observer les messages d'erreur explicites (❌) que j'ai ajoutés.
|
||||
|
|
@ -9,10 +9,12 @@ import os
|
|||
import asyncio
|
||||
|
||||
kuby_logger = logging.getLogger("KubyBot")
|
||||
REPORTS_FILE = "data/gitlab_reports.json"
|
||||
# Utilisation de chemins absolus pour la persistance après reboot
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
REPORTS_FILE = os.path.join(BASE_DIR, "data", "gitlab_reports.json")
|
||||
|
||||
def save_report(issue_iid, user_id):
|
||||
os.makedirs("data", exist_ok=True)
|
||||
os.makedirs(os.path.dirname(REPORTS_FILE), exist_ok=True)
|
||||
try:
|
||||
if os.path.exists(REPORTS_FILE):
|
||||
with open(REPORTS_FILE, "r") as f:
|
||||
|
|
@ -152,7 +154,23 @@ class BugReport(commands.Cog):
|
|||
self.bot.loop.create_task(cleanup())
|
||||
|
||||
async def handle_bot_event(self, request):
|
||||
# Vérification de la sécurité (Secret Token GitLab)
|
||||
webhook_secret = os.getenv("GITLAB_WEBHOOK_SECRET")
|
||||
if webhook_secret:
|
||||
provided_token = request.headers.get("X-Gitlab-Token")
|
||||
if provided_token != webhook_secret:
|
||||
kuby_logger.warning("Unauthorised webhook attempt detected (invalid X-Gitlab-Token).")
|
||||
return web.json_response({"status": "unauthorised"}, status=401)
|
||||
|
||||
try:
|
||||
# On attend que le bot soit prêt si on vient de rebooter
|
||||
if not self.bot.is_ready():
|
||||
kuby_logger.info("Webhook received but bot is not ready yet. Waiting up to 10s...")
|
||||
try:
|
||||
await asyncio.wait_for(self.bot.wait_until_ready(), timeout=10.0)
|
||||
except asyncio.TimeoutError:
|
||||
kuby_logger.warning("Bot still not ready after 10s. Proceeding anyway (best effort).")
|
||||
|
||||
payload = await request.json()
|
||||
|
||||
event_type = payload.get("object_kind")
|
||||
|
|
|
|||
|
|
@ -960,53 +960,49 @@ class Security(commands.Cog):
|
|||
Commande principale pour ouvrir l'interface de sécurité.
|
||||
Accessible uniquement aux administrateurs et propriétaires whitelistés.
|
||||
"""
|
||||
# On diffère la réponse immédiatement car le chargement des réglages peut prendre du temps
|
||||
await interaction.response.defer(ephemeral=True)
|
||||
|
||||
# Vérification des permissions
|
||||
if not is_immune(interaction.guild, interaction.user):
|
||||
if interaction.response.is_done():
|
||||
return await interaction.followup.send(
|
||||
"❌ **Accès refusé**\n\nVous devez être administrateur, propriétaire du serveur, ou être dans la whitelist pour accéder à ce panneau.",
|
||||
ephemeral=True
|
||||
)
|
||||
return await interaction.response.send_message(
|
||||
return await interaction.followup.send(
|
||||
"❌ **Accès refusé**\n\nVous devez être administrateur, propriétaire du serveur, ou être dans la whitelist pour accéder à ce panneau.",
|
||||
ephemeral=True
|
||||
)
|
||||
|
||||
# Chargement des paramètres
|
||||
settings = load_settings(interaction.guild.id)
|
||||
try:
|
||||
# Chargement des paramètres
|
||||
settings = load_settings(interaction.guild.id)
|
||||
|
||||
# Création de la vue
|
||||
view = SecurityView(interaction, settings, bot=self.bot)
|
||||
embed = view.create_main_embed(interaction.user)
|
||||
# Création de la vue
|
||||
view = SecurityView(interaction, settings, bot=self.bot)
|
||||
embed = view.create_main_embed(interaction.user)
|
||||
|
||||
if interaction.response.is_done():
|
||||
await interaction.followup.send(
|
||||
embed=embed,
|
||||
view=view,
|
||||
ephemeral=True
|
||||
)
|
||||
else:
|
||||
await interaction.response.send_message(
|
||||
embed=embed,
|
||||
view=view,
|
||||
except Exception as e:
|
||||
await interaction.followup.send(
|
||||
f"❌ Une erreur est survenue lors de l'ouverture du panneau : {e}",
|
||||
ephemeral=True
|
||||
)
|
||||
|
||||
@security.error
|
||||
async def security_error(self, interaction: discord.Interaction, error: app_commands.AppCommandError):
|
||||
"""Gestion des erreurs de la commande security"""
|
||||
# Ne pas envoyer de message si l'interaction a déjà été traitée
|
||||
if interaction.response.is_done():
|
||||
return
|
||||
# Utilisation de followup si l'interaction est déjà différée ou répondue
|
||||
send_method = interaction.followup.send if interaction.response.is_done() else interaction.response.send_message
|
||||
|
||||
if isinstance(error, app_commands.MissingPermissions):
|
||||
await interaction.response.send_message(
|
||||
await send_method(
|
||||
"❌ Permissions insuffisantes.",
|
||||
ephemeral=True
|
||||
)
|
||||
else:
|
||||
await interaction.response.send_message(
|
||||
f"❌ Une erreur est survenue.",
|
||||
await send_method(
|
||||
f"❌ Une erreur est survenue lors de l'exécution de la commande : {error}",
|
||||
ephemeral=True
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import discord
|
||||
import re
|
||||
from discord.ext import commands
|
||||
from discord import app_commands, ui
|
||||
import json
|
||||
|
|
@ -313,26 +314,28 @@ class TicketStep3(discord.ui.Modal, title="📋 Développement du Personnage"):
|
|||
except discord.errors.NotFound:
|
||||
await interaction.followup.send(f"✅ Ticket ouvert : {ticket_channel.mention}", ephemeral=True)
|
||||
|
||||
class TicketManagementView(discord.ui.view.View):
|
||||
class TicketManagementView(discord.ui.View):
|
||||
"""Vue pour gérer le ticket avec les boutons Fermer, Réouvrir, Claim et Supprimer"""
|
||||
|
||||
def __init__(self):
|
||||
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)"""
|
||||
if interaction.channel.topic and "Ticket de" in interaction.channel.topic:
|
||||
"""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))
|
||||
try:
|
||||
user_id_str = interaction.channel.topic.replace("Ticket de ", "").strip()
|
||||
owner_id = int(user_id_str)
|
||||
member = interaction.guild.get_member(owner_id)
|
||||
if not member:
|
||||
try:
|
||||
member = await interaction.guild.fetch_member(owner_id)
|
||||
except (discord.NotFound, discord.HTTPException):
|
||||
return None
|
||||
member = await interaction.guild.fetch_member(owner_id)
|
||||
return member
|
||||
except (ValueError, IndexError):
|
||||
except:
|
||||
return None
|
||||
return None
|
||||
|
||||
|
|
@ -390,26 +393,28 @@ class TicketManagementView(discord.ui.view.View):
|
|||
kuby_logger.error(f"Erreur lors du claim du ticket: {e}")
|
||||
await interaction.followup.send(f"❌ Erreur lors du claim : {e}")
|
||||
|
||||
class ClosedTicketView(discord.ui.view.View):
|
||||
class ClosedTicketView(discord.ui.View):
|
||||
"""Vue pour les tickets fermés avec les boutons Réouvrir et Supprimer"""
|
||||
|
||||
def __init__(self):
|
||||
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)"""
|
||||
if interaction.channel.topic and "Ticket de" in interaction.channel.topic:
|
||||
"""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))
|
||||
try:
|
||||
user_id_str = interaction.channel.topic.replace("Ticket de ", "").strip()
|
||||
owner_id = int(user_id_str)
|
||||
member = interaction.guild.get_member(owner_id)
|
||||
if not member:
|
||||
try:
|
||||
member = await interaction.guild.fetch_member(owner_id)
|
||||
except (discord.NotFound, discord.HTTPException):
|
||||
return None
|
||||
member = await interaction.guild.fetch_member(owner_id)
|
||||
return member
|
||||
except (ValueError, IndexError):
|
||||
except:
|
||||
return None
|
||||
return None
|
||||
|
||||
|
|
@ -443,7 +448,7 @@ class ClosedTicketView(discord.ui.view.View):
|
|||
kuby_logger.error(f"Erreur lors de la préparation de réouverture: {e}")
|
||||
await interaction.followup.send(f"❌ Erreur : {e}", ephemeral=True)
|
||||
|
||||
class ReopenStatusView(discord.ui.view.View):
|
||||
class ReopenStatusView(discord.ui.View):
|
||||
"""Vue pour choisir le statut lors d'une réouverture"""
|
||||
def __init__(self, owner: discord.Member):
|
||||
super().__init__(timeout=180)
|
||||
|
|
@ -464,16 +469,48 @@ class ReopenStatusView(discord.ui.view.View):
|
|||
async def apply_status(self, interaction: discord.Interaction, prefix: str):
|
||||
await interaction.response.defer(ephemeral=True)
|
||||
try:
|
||||
settings = load_settings(interaction.guild.id)
|
||||
candidature_a_faire_role_id = settings.get("candidature_a_faire_role_id")
|
||||
attente_oral_role_id = settings.get("attente_oral_role_id")
|
||||
citoyen_role_id = settings.get("citoyen_role_id")
|
||||
|
||||
# On récupère les objets rôles
|
||||
role_écrit = interaction.guild.get_role(int(candidature_a_faire_role_id)) if candidature_a_faire_role_id else None
|
||||
role_oral = interaction.guild.get_role(int(attente_oral_role_id)) if attente_oral_role_id else None
|
||||
role_citoyen = interaction.guild.get_role(int(citoyen_role_id)) if citoyen_role_id else None
|
||||
|
||||
# Synchronisation des RÔLES en fonction du statut choisi
|
||||
roles_to_remove = []
|
||||
role_to_add = None
|
||||
|
||||
if prefix == "ouvert":
|
||||
role_to_add = role_écrit
|
||||
roles_to_remove = [r for r in [role_oral, role_citoyen] if r]
|
||||
elif prefix == "oral-à-faire":
|
||||
role_to_add = role_oral
|
||||
roles_to_remove = [r for r in [role_écrit, role_citoyen] if r]
|
||||
elif prefix == "accepté":
|
||||
role_to_add = role_citoyen
|
||||
roles_to_remove = [r for r in [role_écrit, role_oral] if r]
|
||||
|
||||
# Mise à jour des rôles sur le membre
|
||||
if roles_to_remove:
|
||||
await self.owner.remove_roles(*roles_to_remove, reason="Réouverture du ticket - Maj Statut")
|
||||
if role_to_add:
|
||||
await self.owner.add_roles(role_to_add, reason="Réouverture du ticket - Maj Statut")
|
||||
|
||||
# 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)
|
||||
await 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)
|
||||
await interaction.followup.send(f"✅ Statut {prefix} appliqué.", ephemeral=True)
|
||||
await interaction.channel.send(f"✅ Ticket réouvert par {interaction.user.mention} (Statut: **{prefix}**).", view=new_view)
|
||||
await interaction.followup.send(f"✅ Statut **{prefix}** appliqué et rôles synchronisés.", ephemeral=True)
|
||||
self.stop()
|
||||
except Exception as e:
|
||||
await interaction.followup.send(f"❌ Erreur : {e}", ephemeral=True)
|
||||
kuby_logger.error(f"Erreur lors de la réouverture/synchro: {e}")
|
||||
await interaction.followup.send(f"❌ Erreur système : {e}", ephemeral=True)
|
||||
|
||||
@discord.ui.button(label="❌ Supprimer le Ticket", style=discord.ButtonStyle.danger, custom_id="delete_ticket")
|
||||
async def delete_ticket(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
|
|
@ -565,8 +602,12 @@ class Ticket(commands.Cog):
|
|||
if message.guild is None or message.author.bot:
|
||||
return
|
||||
|
||||
# Détection du format du topic du ticket
|
||||
if not message.channel.topic or not message.channel.topic.startswith("Ticket de"):
|
||||
# Détection du format du topic du ticket via regex (robuste)
|
||||
if not message.channel.topic:
|
||||
return
|
||||
|
||||
match = re.search(r"(\d{17,20})", message.channel.topic)
|
||||
if not match:
|
||||
return
|
||||
|
||||
# On ne traite que si le message contient "validé"
|
||||
|
|
@ -585,12 +626,12 @@ class Ticket(commands.Cog):
|
|||
return
|
||||
|
||||
try:
|
||||
# Extraction propre de l'ID utilisateur depuis le topic
|
||||
# L'ID est déjà trouvé par le regex ci-dessus
|
||||
user_id = int(match.group(1))
|
||||
try:
|
||||
user_id_str = message.channel.topic.replace("Ticket de ", "").strip()
|
||||
user = await message.guild.fetch_member(int(user_id_str))
|
||||
except (ValueError, IndexError, discord.NotFound, discord.HTTPException):
|
||||
kuby_logger.warning(f"Impossible de trouver l'utilisateur du ticket via le topic: {message.channel.topic}")
|
||||
user = await message.guild.fetch_member(user_id)
|
||||
except (discord.NotFound, discord.HTTPException):
|
||||
await message.channel.send("❌ Utilisateur introuvable. A-t-il quitté le serveur ?", delete_after=10)
|
||||
return
|
||||
|
||||
attente_oral_role_id = settings.get("attente_oral_role_id")
|
||||
|
|
@ -605,7 +646,7 @@ class Ticket(commands.Cog):
|
|||
candidature_a_faire_role = message.guild.get_role(int(candidature_a_faire_role_id))
|
||||
citoyen_role = message.guild.get_role(int(citoyen_role_id))
|
||||
|
||||
if attente_oral_role in user.roles:
|
||||
if (attente_oral_role and attente_oral_role in user.roles) or "oral-à-faire" in message.channel.name.lower():
|
||||
# DEUXIÈME VALIDATION -> ACCEPTÉ / CITOYEN
|
||||
if attente_oral_role: await user.remove_roles(attente_oral_role)
|
||||
if citoyen_role: await user.add_roles(citoyen_role)
|
||||
|
|
|
|||
|
|
@ -64,11 +64,11 @@ class WebsiteSync(commands.Cog, name="Website Sync"):
|
|||
def cog_unload(self):
|
||||
self.daily_export.cancel()
|
||||
|
||||
# ── Tâche de fond : génération (30s pour test) ────────────────────────────
|
||||
@tasks.loop(seconds=30)
|
||||
# ── Tâche de fond : génération (1 heure) ────────────────────────────
|
||||
@tasks.loop(hours=1)
|
||||
async def daily_export(self):
|
||||
"""Génère les JSON toutes les 30 secondes (Mode Test)."""
|
||||
logger.info("[JSON Export] Génération automatique (toutes les 30s) déclenchée.")
|
||||
"""Génère les JSON toutes les heures."""
|
||||
logger.info("[JSON Export] Génération automatique déclenchée.")
|
||||
await generate_all(self.bot)
|
||||
|
||||
@daily_export.before_loop
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue