Kuby/commandes/bug_report.py

644 lines
28 KiB
Python
Raw Normal View History

2026-05-06 17:07:09 +02:00
import disnake
from disnake.ext import commands
2026-07-11 23:11:48 +02:00
from utils.forgejo_client import gitlab_client
from aiohttp import web
import logging
import json
import os
import asyncio
from typing import Dict, Any, Optional
2026-07-06 16:08:14 +02:00
from utils.premium import check_premium_tier
kuby_logger = logging.getLogger("KubyBot")
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# ⚡ NOUVEAU: Utilise le même fichier que gitlab_integration.py
TRACKING_FILE = os.path.join(BASE_DIR, "commandes", "gitlab_issues.json")
LEGACY_REPORTS_FILE = os.path.join(BASE_DIR, "data", "gitlab_reports.json")
2026-05-19 18:50:16 +02:00
2026-07-11 23:50:19 +02:00
def classify_webhook_event(payload: Dict[str, Any]) -> Optional[str]:
"""Détermine si un webhook correspond à une issue ou à un commentaire."""
object_kind = payload.get("object_kind")
if object_kind in {"issue", "note"}:
return object_kind
action = str(payload.get("action") or payload.get("event") or "").lower()
if action in {"comment_created", "comment_updated", "commented", "issue_comment_created", "issue_comment_updated"}:
2026-07-11 23:50:19 +02:00
return "note"
if "comment" in payload or payload.get("comment") or "comment" in action:
return "note"
2026-07-12 15:01:47 +02:00
if payload.get("issue") or payload.get("object_attributes") or action in {"open", "reopen", "close", "closed", "updated", "created", "edited", "label_updated", "label_added", "label_removed", "label_changed"}:
2026-07-11 23:50:19 +02:00
return "issue"
return None
def extract_issue_iid(payload: Dict[str, Any]) -> Optional[str]:
"""Extrait lidentifiant dissue depuis plusieurs formats Forgejo/GitLab."""
for container in (payload.get("object_attributes", {}), payload.get("issue", {}), payload.get("comment", {})):
if not isinstance(container, dict):
continue
for key in ("iid", "number", "id"):
value = container.get(key)
if value not in (None, ""):
return str(value)
return None
def extract_issue_identifier(issue_payload: Optional[Dict[str, Any]]) -> Optional[str]:
"""Récupère un identifiant stable à stocker dans le tracking."""
if not isinstance(issue_payload, dict):
return None
for key in ("iid", "number", "id"):
value = issue_payload.get(key)
if value not in (None, ""):
return str(value)
return None
2026-07-12 12:26:01 +02:00
def normalize_labels(labels: Any) -> list[str]:
"""Normalise une liste de labels provenant de Forgejo/GitLab."""
if not labels:
return []
if isinstance(labels, str):
return [labels]
if isinstance(labels, list):
normalized = []
for item in labels:
if isinstance(item, dict):
name = item.get("name") or item.get("title") or item.get("label") or item.get("id")
else:
name = item
if name is None:
continue
name = str(name).strip()
if name and name not in normalized:
normalized.append(name)
return normalized
return []
def extract_label_change(payload: Dict[str, Any]) -> Optional[Dict[str, Any]]:
"""Extrait le changement de labels dun payload dissue."""
2026-07-12 15:01:47 +02:00
action = str(payload.get("action") or payload.get("event") or "").lower()
if not action.startswith("label"):
2026-07-12 12:26:01 +02:00
return None
2026-07-12 15:01:47 +02:00
issue = payload.get("issue") or {}
current_labels = normalize_labels(issue.get("labels") or payload.get("labels"))
if not current_labels:
2026-07-12 12:26:01 +02:00
return None
2026-07-12 15:01:47 +02:00
label_payload = payload.get("label")
label_name = None
if isinstance(label_payload, dict):
label_name = label_payload.get("name") or label_payload.get("title") or label_payload.get("label")
elif isinstance(label_payload, str):
label_name = label_payload
previous_labels = []
if isinstance(payload.get("changes"), dict):
prev = payload["changes"].get("labels") or payload["changes"].get("label")
if isinstance(prev, dict):
previous_labels = normalize_labels(prev.get("previous"))
if not previous_labels and issue.get("labels"):
previous_labels = []
if action in {"label_removed", "label_deleted"}:
return {
"previous": previous_labels or ([label_name] if label_name else []),
"current": current_labels,
"added": [],
"removed": [label_name] if label_name else [],
}
added = [label for label in current_labels if label not in previous_labels]
removed = [label for label in previous_labels if label not in current_labels]
if not added and not removed and label_name:
added = [label_name]
2026-07-12 12:26:01 +02:00
return {
2026-07-12 15:01:47 +02:00
"previous": previous_labels,
"current": current_labels,
2026-07-12 12:26:01 +02:00
"added": added,
"removed": removed,
}
def calculate_progress_from_labels(labels: list[str]) -> float:
"""Estime une progression visuelle à partir des labels présents."""
if not labels:
return 0.1
normalized = [label.lower() for label in labels]
if any(token in normalized for token in ["done", "resolved", "fixed", "closed", "terminé", "résolu", "ok", "completed"]):
2026-07-12 12:26:01 +02:00
return 1.0
if any(token in normalized for token in ["review", "ready", "qa", "testing", "test", "validated", "validation", "verif", "vérif"]):
2026-07-12 12:26:01 +02:00
return 0.8
if any(token in normalized for token in ["in progress", "working", "wip", "develop", "dev", "investigating", "progress", "travail"]):
2026-07-12 12:26:01 +02:00
return 0.6
if any(token in normalized for token in ["confirmed", "accepted", "triaged", "priority", "urgent", "prioritaire"]):
2026-07-12 12:26:01 +02:00
return 0.4
if any(token in normalized for token in ["bug", "enhancement", "feature", "manual", "new", "suggestion"]):
2026-07-12 12:26:01 +02:00
return 0.2
return 0.3
def build_status_message_from_labels(labels: list[str]) -> str:
"""Traduit les labels en un message utilisateur lisible."""
if not labels:
return "⏳ La demande évolue et l'équipe suit son avancement."
normalized = [str(label).lower() for label in labels if str(label).strip()]
if any(token in normalized for token in ["done", "resolved", "fixed", "closed", "terminé", "résolu", "completed", "ok"]):
return "✅ La demande a été traitée et est prête à être déployée."
if any(token in normalized for token in ["review", "qa", "testing", "test", "validated", "validation", "verif", "vérif"]):
return "🔎 La demande est en cours de vérification par l'équipe."
if any(token in normalized for token in ["in progress", "working", "wip", "develop", "dev", "investigating", "progress", "travail"]):
return "🛠️ L'équipe est en train de travailler dessus."
if any(token in normalized for token in ["confirmed", "accepted", "triaged", "priority", "urgent", "prioritaire"]):
return "🗂️ La demande a bien été prise en compte et est en cours d'analyse."
if any(token in normalized for token in ["bug", "enhancement", "feature", "manual", "new", "suggestion"]):
return "📝 La demande a été reçue et sera étudiée prochainement."
return "⏳ La demande évolue et l'équipe suit son avancement."
2026-07-12 12:26:01 +02:00
def build_progress_bar(progress: float, width: int = 18) -> str:
"""Construit une barre de progression stylée en Unicode."""
progress = max(0.0, min(1.0, progress))
filled = int(round(progress * width))
filled = max(0, min(width, filled))
empty = width - filled
percent = int(progress * 100)
return f"{'' * filled}{'' * empty} {percent}%"
def color_from_progress(progress: float) -> disnake.Color:
"""Retourne une couleur violet→bleu selon la progression."""
progress = max(0.0, min(1.0, progress))
purple = (117, 82, 255)
blue = (74, 144, 255)
r = int(purple[0] + (blue[0] - purple[0]) * progress)
g = int(purple[1] + (blue[1] - purple[1]) * progress)
b = int(purple[2] + (blue[2] - purple[2]) * progress)
return disnake.Color.from_rgb(r, g, b)
def load_tracking():
"""Charge le tracking depuis gitlab_issues.json (avec migration automatique)"""
tracking = {}
if os.path.exists(TRACKING_FILE):
try:
with open(TRACKING_FILE, "r", encoding="utf-8") as f:
tracking = json.load(f)
except Exception as e:
kuby_logger.error(f"Erreur chargement gitlab_issues.json: {e}")
# Migration automatique depuis l'ancien fichier
if os.path.exists(LEGACY_REPORTS_FILE):
try:
with open(LEGACY_REPORTS_FILE, "r", encoding="utf-8") as f:
legacy_data = json.load(f)
for issue_iid, user_id in legacy_data.items():
if issue_iid not in tracking:
tracking[issue_iid] = {
"requester_id": user_id,
"last_note_id": 0,
"type": "legacy_report"
}
kuby_logger.info(f"Migration automatique: {len(legacy_data)} issues fusionnées")
except Exception as e:
kuby_logger.error(f"Erreur migration gitlab_reports.json: {e}")
return tracking
2026-05-19 18:50:16 +02:00
def save_tracking(data: Dict[str, Any]) -> None:
"""Sauvegarde dans gitlab_issues.json"""
os.makedirs(os.path.dirname(TRACKING_FILE), exist_ok=True)
temp_file = f"{TRACKING_FILE}.tmp"
with open(temp_file, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2)
os.replace(temp_file, TRACKING_FILE)
2026-05-19 18:50:16 +02:00
2026-05-06 17:07:09 +02:00
class BugReportModal(disnake.ui.Modal):
def __init__(self, cog):
self.cog = cog # ⚡ NOUVEAU: Reçoit le cog pour tracking
components = [
disnake.ui.TextInput(
label="Titre du bug",
placeholder="Décrivez brièvement le problème...",
custom_id="bug_title",
required=True,
max_length=100,
),
disnake.ui.TextInput(
label="Description détaillée",
style=disnake.TextInputStyle.paragraph,
placeholder="Que s'est-il passé ?",
custom_id="bug_description",
required=True,
max_length=1000,
),
]
super().__init__(
title="Signaler un Bug",
custom_id="bug_modal",
components=components,
)
async def callback(self, interaction: disnake.ModalInteraction):
await interaction.response.send_message("Envoi de votre rapport de bug à GitLab...", ephemeral=True)
2026-05-19 18:50:16 +02:00
bug_title = (interaction.text_values.get("bug_title") or "").strip()
2026-05-19 18:50:16 +02:00
if not bug_title:
bug_title = "Sans titre"
priority_name = "Normale"
priority_value = "Normal"
description_value = interaction.text_values.get("bug_description") or ""
2026-05-19 18:50:16 +02:00
description_text = f"**Rapporté par:** {interaction.user} ({interaction.user.id})\n"
2026-05-19 18:50:16 +02:00
description_text += f"**Priorité:** {priority_name}\n\n"
description_text += f"**Description:**\n{description_value}"
labels = ["bug", "manual-report", f"Priority::{priority_value}"]
result = await gitlab_client.create_issue(
2026-05-19 18:50:16 +02:00
title=f"[MANUAL] {bug_title}",
description=description_text,
2026-05-19 18:50:16 +02:00
labels=labels,
)
2026-05-19 18:50:16 +02:00
if result:
issue_iid = extract_issue_identifier(result)
if not issue_iid:
kuby_logger.warning("Impossible de déterminer lID de lissue créée depuis le retour Forgejo/GitLab")
await interaction.edit_original_response(content="✅ Votre rapport a été envoyé, mais lidentifiant de lissue na pas pu être récupéré automatiquement.")
return
self.cog.issue_data[issue_iid] = {
"requester_id": interaction.user.id,
"last_note_id": 0,
"type": "manual-report"
}
save_tracking(self.cog.issue_data)
2026-05-19 18:50:16 +02:00
await interaction.edit_original_response(
content=(
"✅ Votre bug a été signalé avec succès ! "
2026-05-19 21:19:43 +02:00
"Nous avons bien été averti et le bug sera réglé dans la prochaine mise à jour. "
f"[Voir l'issue]({result.get('web_url') or result.get('html_url')})"
2026-05-19 18:50:16 +02:00
)
)
else:
2026-05-19 18:50:16 +02:00
await interaction.edit_original_response(
content="❌ Une erreur est survenue lors de l'envoi du rapport à GitLab. Veuillez contacter un administrateur."
)
2026-05-06 17:07:09 +02:00
class FeatureSuggestionModal(disnake.ui.Modal):
def __init__(self, cog):
self.cog = cog # ⚡ NOUVEAU: Reçoit le cog pour tracking
components = [
disnake.ui.TextInput(
label="Titre de la suggestion",
placeholder="Que voulez-vous ajouter ?",
custom_id="suggestion_title",
required=True,
max_length=100,
),
disnake.ui.TextInput(
label="Détails de la fonctionnalité",
style=disnake.TextInputStyle.paragraph,
placeholder="Décrivez comment cela devrait fonctionner...",
custom_id="suggestion_description",
required=True,
max_length=1000,
),
]
super().__init__(
title="Suggérer une fonctionnalité",
custom_id="suggestion_modal",
components=components,
)
async def callback(self, interaction: disnake.ModalInteraction):
await interaction.response.send_message("Envoi de votre suggestion à GitLab...", ephemeral=True)
2026-05-19 18:50:16 +02:00
suggestion_title = (interaction.text_values.get("suggestion_title") or "").strip() or "Sans titre"
description_value = interaction.text_values.get("suggestion_description") or ""
2026-05-19 18:50:16 +02:00
description_text = f"**Sugéré par:** {interaction.user} ({interaction.user.id})\n\n"
2026-05-19 18:50:16 +02:00
description_text += f"**Description:**\n{description_value}"
labels = ["enhancement", "manual-suggestion"]
2026-05-19 18:50:16 +02:00
result = await gitlab_client.create_issue(
2026-05-19 18:50:16 +02:00
title=f"[SUGGESTION] {suggestion_title}",
description=description_text,
2026-05-19 18:50:16 +02:00
labels=labels,
)
2026-05-19 18:50:16 +02:00
if result:
issue_iid = extract_issue_identifier(result)
if not issue_iid:
kuby_logger.warning("Impossible de déterminer lID de lissue créée depuis le retour Forgejo/GitLab")
await interaction.edit_original_response(content="✅ Votre suggestion a été envoyée, mais lidentifiant de lissue na pas pu être récupéré automatiquement.")
return
self.cog.issue_data[issue_iid] = {
"requester_id": interaction.user.id,
"last_note_id": 0,
"type": "manual-suggestion"
}
save_tracking(self.cog.issue_data)
2026-05-19 18:50:16 +02:00
await interaction.edit_original_response(
content=(
"✅ Votre suggestion a été envoyée avec succès ! "
"Merci de contribuer à l'amélioration du bot. "
f"[Voir l'issue]({result.get('web_url') or result.get('html_url')})"
2026-05-19 18:50:16 +02:00
)
)
else:
2026-05-19 18:50:16 +02:00
await interaction.edit_original_response(
content="❌ Une erreur est survenue lors de l'envoi de la suggestion à GitLab."
)
class BugReportView(disnake.ui.View):
def __init__(self, cog):
self.cog = cog # ⚡ NOUVEAU: Reçoit le cog
super().__init__(timeout=None)
@disnake.ui.button(label="🎫 Remplir le formulaire", style=disnake.ButtonStyle.primary, custom_id="open_bug_form")
async def open_bug_form(self, button: disnake.ui.Button, interaction: disnake.MessageInteraction):
modal = BugReportModal(self.cog)
await interaction.response.send_modal(modal)
class FeatureSuggestionView(disnake.ui.View):
def __init__(self, cog):
self.cog = cog # ⚡ NOUVEAU: Reçoit le cog
super().__init__(timeout=None)
@disnake.ui.button(label="🎫 Remplir le formulaire", style=disnake.ButtonStyle.green, custom_id="open_suggestion_form")
async def open_suggestion_form(self, button: disnake.ui.Button, interaction: disnake.MessageInteraction):
modal = FeatureSuggestionModal(self.cog)
await interaction.response.send_modal(modal)
class BugReport(commands.Cog):
def __init__(self, bot):
self.bot = bot
self.issue_data = load_tracking() # ⚡ NOUVEAU: Charge le tracking unifié
2026-05-20 15:49:40 +02:00
async def cog_load(self):
kuby_logger.info("BugReport cog chargé. Démarrage du serveur de relais interne (port 5001)...")
REPORTS_DIR = os.path.dirname(LEGACY_REPORTS_FILE)
2026-05-25 19:08:45 +02:00
os.makedirs(REPORTS_DIR, exist_ok=True)
await self._start_webhook_server()
2026-05-20 15:23:09 +02:00
# ⚡ Enregistrement des vues persistantes (boutons panel)
# Sans ça, après un redémarrage les boutons "Remplir le formulaire"
# dans les anciens messages de panel ne fonctionnent plus.
self.bot.add_view(BugReportView(self))
self.bot.add_view(FeatureSuggestionView(self))
kuby_logger.info("✅ Vues persistantes BugReport enregistrées (open_bug_form, open_suggestion_form)")
2026-05-20 15:23:09 +02:00
async def _start_webhook_server(self):
try:
app = web.Application()
app.router.add_post('/bot_event', self.handle_bot_event)
app.router.add_post('/deployment_complete', self.handle_deployment_complete)
self.runner = web.AppRunner(app)
await self.runner.setup()
2026-06-07 16:40:46 +02:00
self.site = web.TCPSite(self.runner, '127.0.0.1', 5001, reuse_address=True)
await self.site.start()
kuby_logger.info("Internal Bot Webhook Server started on 127.0.0.1:5001")
except Exception as e:
kuby_logger.error(f"Failed to start Internal Bot Webhook Server: {e}")
def cog_unload(self):
if hasattr(self, 'runner'):
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_deployment_complete(self, request):
"""Notifie les demandeurs dont la fonctionnalité a été intégrée et attend le prochain déploiement."""
try:
2026-07-12 16:44:47 +02:00
try:
await request.json()
except Exception:
try:
raw_body = await request.text()
if raw_body and raw_body.strip():
json.loads(raw_body)
except Exception:
pass
for issue_iid, issue_data in list(self.issue_data.items()):
if not issue_data.get("pending_deployment_notification"):
continue
requester_id = issue_data.get("requester_id")
if not requester_id:
continue
user = self.bot.get_user(requester_id)
if not user:
try:
user = await self.bot.fetch_user(requester_id)
except Exception:
continue
try:
embed = disnake.Embed(
title="🚀 Fonctionnalité disponible !",
description=(
"✅ Votre demande est désormais fonctionnelle sur le bot.\n\n"
"Merci encore pour votre contribution et votre patience."
),
color=disnake.Color.blurple()
)
if self.bot.user.display_avatar:
embed.set_thumbnail(url=self.bot.user.display_avatar.url)
await user.send(embed=embed)
except Exception as e:
kuby_logger.warning(f"Impossible d'envoyer la notification de déploiement à {user}: {e}")
issue_data["pending_deployment_notification"] = False
save_tracking(self.issue_data)
return web.json_response({"status": "notified"}, status=200)
except Exception as e:
kuby_logger.error(f"Error handling deployment complete: {e}")
return web.json_response({"status": "error", "message": str(e)}, status=500)
async def handle_bot_event(self, request):
2026-07-11 23:11:48 +02:00
"""Gère les webhooks (adapté pour GitLab ET Forgejo)"""
try:
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:
2026-07-11 23:11:48 +02:00
kuby_logger.warning("Bot still not ready after 10s. Proceeding anyway.")
payload = await request.json()
event_type = classify_webhook_event(payload)
if event_type not in ["issue", "note"]:
kuby_logger.info(
"Webhook ignoré : type dévénement non reconnu (keys=%s action=%s object_kind=%s)",
list(payload.keys()),
payload.get("action") or payload.get("event"),
payload.get("object_kind"),
)
return web.json_response({"status": "ignored"}, status=200)
2026-07-11 23:31:34 +02:00
# Récupère l'IID de l'issue
issue_iid = extract_issue_iid(payload)
if not issue_iid:
2026-07-11 23:11:48 +02:00
kuby_logger.warning("Webhook ignoré : aucun identifiant dissue trouvé dans le payload")
return web.json_response({"status": "no issue id"}, status=200)
issue_data = self.issue_data.get(issue_iid)
if not issue_data:
return web.json_response({"status": "untracked issue"}, status=200)
user_id = issue_data.get("requester_id")
if not user_id:
return web.json_response({"status": "no requester"}, status=200)
user = self.bot.get_user(user_id)
if not user:
try:
user = await self.bot.fetch_user(user_id)
except:
return web.json_response({"status": "user not found"}, status=200)
2026-07-11 23:11:48 +02:00
# 🕵️ 3. RÉCUPÉRATION DU TITRE
issue_title = (
payload.get("object_attributes", {}).get("title") or
payload.get("issue", {}).get("title") or
payload.get("issue", {}).get("name") or
f"#{issue_iid}"
2026-07-11 23:11:48 +02:00
)
if event_type == "note":
# ⚡ NOUVEAU: Notifie les commentaires via le webhook (instantané)
comment_payload = payload.get("comment")
if isinstance(comment_payload, dict):
note_body = comment_payload.get("body") or comment_payload.get("content") or ""
author_name = (
payload.get("sender", {}).get("username")
or payload.get("sender", {}).get("login")
or payload.get("user", {}).get("name")
or "Développeur"
)
is_system = False
else:
note_attr = payload.get("object_attributes", {})
is_system = note_attr.get("system", False)
note_body = (
note_attr.get("note") or
note_attr.get("body") or
2026-07-11 23:11:48 +02:00
payload.get("note", "") or ""
)
author_name = payload.get("user", {}).get("name", "Développeur")
if not is_system and note_body:
try:
embed = disnake.Embed(
title="💬 Nouveau commentaire sur votre signalement",
description=f"Le développeur a répondu à votre rapport **{issue_title}** :\n\n>>> {note_body}\n\n✍️ **Par :** {author_name}",
color=disnake.Color.orange()
)
embed.set_thumbnail(url=self.bot.user.display_avatar.url)
await user.send(embed=embed)
kuby_logger.info(f"Commentaire envoyé à {user} pour l'issue #{issue_iid}")
except (disnake.Forbidden, disnake.HTTPException) as e:
kuby_logger.warning(f"Impossible d'envoyer le DM à {user}: {e}")
if dev and dev.id != user.id:
try:
await dev.send(f"⚠️ [DM BLOQUÉ] Impossible d'envoyer le commentaire à {user} pour l'issue #{issue_iid}")
except:
pass
2026-07-11 23:11:48 +02:00
elif event_type == "issue":
action = payload.get("object_attributes", {}).get("action") or payload.get("action")
state = payload.get("object_attributes", {}).get("state") or payload.get("issue", {}).get("state")
2026-07-12 12:26:01 +02:00
label_change = extract_label_change(payload)
if label_change:
current_labels = label_change["current"]
progress = calculate_progress_from_labels(current_labels)
bar = build_progress_bar(progress)
status_message = build_status_message_from_labels(current_labels)
2026-07-12 12:26:01 +02:00
try:
embed = disnake.Embed(
title="📈 Évolution du ticket",
description=f"Le suivi de **{issue_title}** a été mis à jour.\n\n{status_message}",
2026-07-12 12:26:01 +02:00
color=color_from_progress(progress),
)
embed.add_field(name="Progression", value=bar, inline=False)
embed.set_thumbnail(url=self.bot.user.display_avatar.url)
await user.send(embed=embed)
kuby_logger.info(f"Notification de progression envoyée à {user} pour l'issue #{issue_iid}")
except Exception as e:
kuby_logger.error(f"Erreur notification progression: {e}")
2026-07-11 23:11:48 +02:00
is_closing_now = action in ["close", "closed"] or state == "closed"
2026-05-01 16:16:33 +02:00
if is_closing_now:
self.issue_data[issue_iid]["pending_deployment_notification"] = True
save_tracking(self.issue_data)
try:
2026-05-20 15:49:40 +02:00
embed = disnake.Embed(
title="🛠️ Demande traitée !",
description=(
f"Bonne nouvelle ! Votre demande **{issue_title}** a été intégrée au code du bot.\n\n"
"✅ Elle n'est pas encore disponible au public, mais elle est maintenant prise en charge côté développement.\n\n"
"🔜 Lors du prochain déploiement, Kuby vous informera qu'elle est désormais fonctionnelle."
),
2026-05-20 15:49:40 +02:00
color=disnake.Color.green()
)
2026-07-11 23:11:48 +02:00
if self.bot.user.display_avatar:
embed.set_thumbnail(url=self.bot.user.display_avatar.url)
await user.send(embed=embed)
except Exception as e:
kuby_logger.error(f"Erreur notification clôture: {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)
@commands.slash_command(name="signaler_bug", description="Signaler un bug aux développeurs")
2026-07-06 16:08:14 +02:00
@check_premium_tier()
async def signifier_bug(self, interaction: disnake.ApplicationCommandInteraction):
modal = BugReportModal(self)
await interaction.response.send_modal(modal)
@commands.slash_command(name="suggerer_fonctionnalite", description="Proposer une nouvelle fonctionnalité")
2026-07-06 16:08:14 +02:00
@check_premium_tier()
2026-05-06 17:07:09 +02:00
async def suggerer_fonctionnalite(self, interaction: disnake.ApplicationCommandInteraction):
modal = FeatureSuggestionModal(self)
await interaction.response.send_modal(modal)
2026-05-19 18:50:16 +02:00
def setup(bot):
bot.add_cog(BugReport(bot))