332 lines
16 KiB
Python
332 lines
16 KiB
Python
import discord
|
|
from discord import app_commands
|
|
from discord.ext import commands
|
|
from utils.gitlab_client import gitlab_client
|
|
from aiohttp import web
|
|
import logging
|
|
import json
|
|
import os
|
|
import asyncio
|
|
|
|
kuby_logger = logging.getLogger("KubyBot")
|
|
# 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(os.path.dirname(REPORTS_FILE), exist_ok=True)
|
|
try:
|
|
if os.path.exists(REPORTS_FILE):
|
|
with open(REPORTS_FILE, "r") as f:
|
|
data = json.load(f)
|
|
else:
|
|
data = {}
|
|
|
|
data[str(issue_iid)] = user_id
|
|
|
|
with open(REPORTS_FILE, "w") as f:
|
|
json.dump(data, f, indent=4)
|
|
except Exception as e:
|
|
kuby_logger.error(f"Error saving report to JSON: {e}")
|
|
|
|
class BugReportModal(discord.ui.Modal, title="Signaler un Bug"):
|
|
def __init__(self, priority_choice):
|
|
super().__init__()
|
|
self.priority_choice = priority_choice
|
|
|
|
bug_title = discord.ui.TextInput(
|
|
label="Titre du bug",
|
|
placeholder="Décrivez brièvement le problème...",
|
|
required=True,
|
|
max_length=100
|
|
)
|
|
|
|
description = discord.ui.TextInput(
|
|
label="Description détaillée",
|
|
style=discord.TextStyle.paragraph,
|
|
placeholder="Que s'est-il passé ? Comment reproduire le bug ?",
|
|
required=True,
|
|
max_length=1000
|
|
)
|
|
|
|
async def on_submit(self, interaction: discord.Interaction):
|
|
await interaction.response.send_message("Envoi de votre rapport de bug à GitLab...", ephemeral=True)
|
|
|
|
description_text = f"**Rapporté par:** {interaction.user} ({interaction.user.id})\n"
|
|
description_text += f"**Priorité:** {self.priority_choice.name}\n\n"
|
|
description_text += f"**Description:**\n{self.description.value}"
|
|
|
|
# Mapping des priorités vers des labels GitLab stylisés (Scoped Labels)
|
|
labels = ["bug", "manual-report", f"Priority::{self.priority_choice.value}"]
|
|
|
|
result = await gitlab_client.create_issue(
|
|
title=f"[MANUAL] {self.bug_title.value}",
|
|
description=description_text,
|
|
labels=labels
|
|
)
|
|
|
|
if result:
|
|
issue_iid = result.get("iid")
|
|
save_report(issue_iid, interaction.user.id)
|
|
await interaction.edit_original_response(content=f"✅ Votre bug a été signalé avec succès ! Gameur a bien été averti et le bug sera réglé dans la prochaine mise à jour. [Voir l'issue]({result.get('web_url')})")
|
|
else:
|
|
await interaction.edit_original_response(content="❌ Une erreur est survenue lors de l'envoi du rapport à GitLab. Veuillez contacter un administrateur.")
|
|
|
|
class FeatureSuggestionModal(discord.ui.Modal, title="Suggérer une fonctionnalité"):
|
|
suggestion_title = discord.ui.TextInput(
|
|
label="Titre de la suggestion",
|
|
placeholder="Que voulez-vous ajouter ?",
|
|
required=True,
|
|
max_length=100
|
|
)
|
|
|
|
description = discord.ui.TextInput(
|
|
label="Détails de la fonctionnalité",
|
|
style=discord.TextStyle.paragraph,
|
|
placeholder="Décrivez comment cela devrait fonctionner...",
|
|
required=True,
|
|
max_length=1000
|
|
)
|
|
|
|
async def on_submit(self, interaction: discord.Interaction):
|
|
await interaction.response.send_message("Envoi de votre suggestion à GitLab...", ephemeral=True)
|
|
|
|
description_text = f"**Suggéré par:** {interaction.user} ({interaction.user.id})\n\n"
|
|
description_text += f"**Description:**\n{self.description.value}"
|
|
|
|
labels = ["enhancement", "manual-suggestion"]
|
|
|
|
result = await gitlab_client.create_issue(
|
|
title=f"[SUGGESTION] {self.suggestion_title.value}",
|
|
description=description_text,
|
|
labels=labels
|
|
)
|
|
|
|
if result:
|
|
issue_iid = result.get("iid")
|
|
save_report(issue_iid, interaction.user.id)
|
|
await interaction.edit_original_response(content=f"✅ Votre suggestion a été envoyée avec succès ! Merci de contribuer à l'amélioration du bot. [Voir l'issue]({result.get('web_url')})")
|
|
else:
|
|
await interaction.edit_original_response(content="❌ Une erreur est survenue lors de l'envoi de la suggestion à GitLab.")
|
|
|
|
class BugReport(commands.Cog):
|
|
def __init__(self, bot):
|
|
self.bot = bot
|
|
|
|
async def cog_load(self):
|
|
self.web_app = web.Application()
|
|
self.web_app.router.add_post('/bot_event', self.handle_bot_event)
|
|
self.runner = web.AppRunner(self.web_app)
|
|
await self.runner.setup()
|
|
|
|
max_retries = 5
|
|
retry_delay = 2
|
|
|
|
for attempt in range(1, max_retries + 1):
|
|
try:
|
|
self.site = web.TCPSite(self.runner, '127.0.0.1', 5001)
|
|
await self.site.start()
|
|
kuby_logger.info(f"Internal Bot Webhook Server started on 127.0.0.1:5001 (Attempt {attempt})")
|
|
return
|
|
except OSError as e:
|
|
if e.errno == 98: # Address already in use
|
|
if attempt < max_retries:
|
|
kuby_logger.warning(f"Port 5001 already in use, retrying in {retry_delay}s... ({attempt}/{max_retries})")
|
|
await asyncio.sleep(retry_delay)
|
|
else:
|
|
kuby_logger.error(f"Failed to bind to port 5001 after {max_retries} attempts.")
|
|
raise e
|
|
else:
|
|
raise e
|
|
|
|
def cog_unload(self):
|
|
if hasattr(self, 'runner'):
|
|
# On utilise create_task car cog_unload est synchrone
|
|
async def cleanup():
|
|
try:
|
|
if hasattr(self, 'site'):
|
|
await self.site.stop()
|
|
await self.runner.cleanup()
|
|
kuby_logger.info("Internal Bot Webhook Server stopped and cleaned up.")
|
|
except Exception as e:
|
|
kuby_logger.error(f"Error during webhook server cleanup: {e}")
|
|
|
|
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")
|
|
if event_type not in ["issue", "note"]:
|
|
return web.json_response({"status": "ignored"}, status=200)
|
|
|
|
if event_type == "issue":
|
|
issue_iid = payload.get("object_attributes", {}).get("iid")
|
|
else:
|
|
issue_iid = payload.get("issue", {}).get("iid")
|
|
|
|
if not issue_iid:
|
|
return web.json_response({"status": "no issue id"}, status=200)
|
|
|
|
if not os.path.exists(REPORTS_FILE):
|
|
return web.json_response({"status": "no reports file"}, status=200)
|
|
|
|
with open(REPORTS_FILE, "r") as f:
|
|
reports = json.load(f)
|
|
|
|
user_id = reports.get(str(issue_iid))
|
|
if not user_id:
|
|
return web.json_response({"status": "untracked issue"}, status=200)
|
|
|
|
user = self.bot.get_user(user_id)
|
|
if not user:
|
|
try:
|
|
user = await self.bot.fetch_user(user_id)
|
|
except:
|
|
pass
|
|
|
|
if not user:
|
|
return web.json_response({"status": "user not found"}, status=200)
|
|
|
|
app_info = await self.bot.application_info()
|
|
dev = app_info.owner
|
|
issue_title = payload.get("object_attributes", {}).get("title", f"#{issue_iid}")
|
|
if event_type == "note":
|
|
issue_title = payload.get("issue", {}).get("title", f"#{issue_iid}")
|
|
|
|
if event_type == "issue":
|
|
action = payload.get("object_attributes", {}).get("action")
|
|
changes = payload.get("changes", {})
|
|
|
|
# Check for label changes
|
|
if "labels" in changes:
|
|
curr_labels = [l.get("title") for l in changes["labels"].get("current", []) if l.get("title")]
|
|
display_labels = [l for l in curr_labels if not l.startswith("Priority::") and l not in ["bug", "manual-report", "manual-suggestion"]]
|
|
|
|
if display_labels:
|
|
current_labels_str = ", ".join(display_labels)
|
|
else:
|
|
current_labels_str = "Mis à jour"
|
|
|
|
try:
|
|
embed = discord.Embed(
|
|
title="🛠️ Mise à jour de votre signalement !",
|
|
description=f"Le statut de votre rapport **{issue_title}** a été mis à jour par l'équipe.",
|
|
color=discord.Color.blue()
|
|
)
|
|
embed.add_field(name="Nouveaux Labels / Statuts", value=current_labels_str)
|
|
|
|
try:
|
|
await user.send(embed=embed)
|
|
if dev and dev.id != user.id:
|
|
await dev.send(f"✅ [FIABLE] L'utilisateur {user} ({user.id}) a bien reçu la notification de statut **{current_labels_str}** pour l'issue #{issue_iid} ({issue_title}).")
|
|
except discord.Forbidden:
|
|
kuby_logger.warning(f"Unable to DM user {user} ({user.id}) - DMs are disabled or bot is blocked.")
|
|
if dev and dev.id != user.id:
|
|
try:
|
|
await dev.send(f"⚠️ [DM BLOQUÉ] Impossible de notifier l'utilisateur {user} ({user.id}) pour l'issue #{issue_iid} (DMs désactivés).")
|
|
except discord.Forbidden:
|
|
pass
|
|
except discord.HTTPException as e:
|
|
kuby_logger.error(f"HTTP error sending DM to {user}: {e}")
|
|
except Exception as e:
|
|
kuby_logger.error(f"Unexpected error in label update notification: {e}")
|
|
|
|
# Check if it was closed
|
|
if action == "close" or payload.get("object_attributes", {}).get("state") == "closed":
|
|
try:
|
|
embed = discord.Embed(
|
|
title="🛠️ Bug / Suggestion Terminé(e) !",
|
|
description=f"Bonne nouvelle ! Votre signalement **{issue_title}** a été marqué comme terminé ou résolu.",
|
|
color=discord.Color.green()
|
|
)
|
|
embed.add_field(name="Prochaine étape", value="La mise à jour arrive prochainement (ou est déjà là) !")
|
|
embed.set_footer(text="Merci pour votre aide !")
|
|
|
|
try:
|
|
await user.send(embed=embed)
|
|
if dev and dev.id != user.id:
|
|
await dev.send(f"✅ [FIABLE] L'utilisateur {user} ({user.id}) a bien reçu la notification de CLÔTURE pour l'issue #{issue_iid}.")
|
|
except discord.Forbidden:
|
|
kuby_logger.warning(f"Unable to DM user {user} ({user.id}) - DMs are disabled or bot is blocked during closure.")
|
|
if dev and dev.id != user.id:
|
|
try:
|
|
await dev.send(f"⚠️ [DM BLOQUÉ] Impossible d'annoncer la clôture à {user} ({user.id}) pour l'issue #{issue_iid} (DMs désactivés).")
|
|
except discord.Forbidden:
|
|
pass
|
|
except discord.HTTPException as e:
|
|
kuby_logger.error(f"HTTP error sending closure DM to {user}: {e}")
|
|
except Exception as e:
|
|
kuby_logger.error(f"Unexpected error in closure notification: {e}")
|
|
|
|
elif event_type == "note":
|
|
note_attr = payload.get("object_attributes", {})
|
|
is_system = note_attr.get("system", False)
|
|
|
|
if not is_system:
|
|
note_body = note_attr.get("note", "")
|
|
author_name = payload.get("user", {}).get("name", "Développeur")
|
|
|
|
try:
|
|
embed = discord.Embed(
|
|
title="💬 Nouveau commentaire sur votre signalement",
|
|
description=f"Le développeur a répondu à votre rapport **{issue_title}** :\n\n>>> {note_body}",
|
|
color=discord.Color.orange()
|
|
)
|
|
embed.set_footer(text=f"Par {author_name}")
|
|
|
|
try:
|
|
await user.send(embed=embed)
|
|
if dev and dev.id != user.id:
|
|
await dev.send(f"✅ [FIABLE] L'utilisateur {user} ({user.id}) a bien reçu votre commentaire pour l'issue #{issue_iid}.")
|
|
except discord.Forbidden:
|
|
kuby_logger.warning(f"Unable to DM user {user} ({user.id}) - DMs are disabled or bot is blocked during comment notification.")
|
|
if dev and dev.id != user.id:
|
|
try:
|
|
await dev.send(f"⚠️ [DM BLOQUÉ] Impossible d'envoyer votre commentaire à {user} ({user.id}) pour l'issue #{issue_iid} (DMs désactivés).")
|
|
except discord.Forbidden:
|
|
pass
|
|
except discord.HTTPException as e:
|
|
kuby_logger.error(f"HTTP error sending comment DM to {user}: {e}")
|
|
except Exception as e:
|
|
kuby_logger.error(f"Unexpected error in comment notification: {e}")
|
|
|
|
return web.json_response({"status": "success"}, status=200)
|
|
|
|
except Exception as e:
|
|
kuby_logger.error(f"Error handling bot event: {e}")
|
|
return web.json_response({"status": "error", "message": str(e)}, status=500)
|
|
|
|
@app_commands.command(name="signaler_bug", description="Signaler un bug aux développeurs")
|
|
@app_commands.choices(priority=[
|
|
app_commands.Choice(name="Basse", value="Low"),
|
|
app_commands.Choice(name="Normale", value="Normal"),
|
|
app_commands.Choice(name="Haute", value="High"),
|
|
app_commands.Choice(name="Urgente", value="Urgent"),
|
|
])
|
|
async def signaler_bug(self, interaction: discord.Interaction, priority: app_commands.Choice[str]):
|
|
await interaction.response.send_modal(BugReportModal(priority))
|
|
|
|
@app_commands.command(name="suggerer_fonctionnalite", description="Proposer une nouvelle fonctionnalité pour le bot")
|
|
async def suggerer_fonctionnalite(self, interaction: discord.Interaction):
|
|
await interaction.response.send_modal(FeatureSuggestionModal())
|
|
|
|
async def setup(bot):
|
|
await bot.add_cog(BugReport(bot))
|