Merge branch 'dev' into 'main'
Fix: récupération des issues_gitlab après incident See merge request Omega_Kube/kuby!28
This commit is contained in:
commit
28a67c8d8c
8 changed files with 682 additions and 308 deletions
|
|
@ -8,36 +8,54 @@ import os
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
kuby_logger = logging.getLogger("KubyBot")
|
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__)))
|
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
REPORTS_FILE = os.path.join(BASE_DIR, "data", "gitlab_reports.json")
|
|
||||||
|
# ⚡ 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")
|
||||||
|
|
||||||
|
|
||||||
def save_report(issue_iid, user_id):
|
def load_tracking():
|
||||||
os.makedirs(os.path.dirname(REPORTS_FILE), exist_ok=True)
|
"""Charge le tracking depuis gitlab_issues.json (avec migration automatique)"""
|
||||||
|
tracking = {}
|
||||||
|
if os.path.exists(TRACKING_FILE):
|
||||||
try:
|
try:
|
||||||
data = {}
|
with open(TRACKING_FILE, "r", encoding="utf-8") as f:
|
||||||
if os.path.exists(REPORTS_FILE):
|
tracking = json.load(f)
|
||||||
try:
|
|
||||||
with open(REPORTS_FILE, "r", encoding="utf-8") as f:
|
|
||||||
data = json.load(f)
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
kuby_logger.warning("REPORTS_FILE corrupted, re-initializing to avoid tracking loss.")
|
|
||||||
|
|
||||||
data[str(issue_iid)] = user_id
|
|
||||||
|
|
||||||
temp_file = f"{REPORTS_FILE}.tmp"
|
|
||||||
with open(temp_file, "w", encoding="utf-8") as f:
|
|
||||||
json.dump(data, f, indent=4)
|
|
||||||
os.replace(temp_file, REPORTS_FILE)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
kuby_logger.error(f"Error saving report to JSON: {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
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
class BugReportModal(disnake.ui.Modal):
|
class BugReportModal(disnake.ui.Modal):
|
||||||
def __init__(self):
|
def __init__(self, cog):
|
||||||
super().__init__(
|
self.cog = cog # ⚡ NOUVEAU: Reçoit le cog pour tracking
|
||||||
title="Signaler un Bug",
|
|
||||||
components = [
|
components = [
|
||||||
disnake.ui.TextInput(
|
disnake.ui.TextInput(
|
||||||
label="Titre du bug",
|
label="Titre du bug",
|
||||||
|
|
@ -55,6 +73,10 @@ class BugReportModal(disnake.ui.Modal):
|
||||||
max_length=1000,
|
max_length=1000,
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
super().__init__(
|
||||||
|
title="Signaler un Bug",
|
||||||
|
custom_id="bug_modal",
|
||||||
|
components=components,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def callback(self, interaction: disnake.ModalInteraction):
|
async def callback(self, interaction: disnake.ModalInteraction):
|
||||||
|
|
@ -66,7 +88,6 @@ class BugReportModal(disnake.ui.Modal):
|
||||||
|
|
||||||
priority_name = "Normale"
|
priority_name = "Normale"
|
||||||
priority_value = "Normal"
|
priority_value = "Normal"
|
||||||
|
|
||||||
description_value = interaction.text_values.get("bug_description") or ""
|
description_value = interaction.text_values.get("bug_description") or ""
|
||||||
|
|
||||||
description_text = f"**Rapporté par:** {interaction.user} ({interaction.user.id})\n"
|
description_text = f"**Rapporté par:** {interaction.user} ({interaction.user.id})\n"
|
||||||
|
|
@ -82,8 +103,14 @@ class BugReportModal(disnake.ui.Modal):
|
||||||
)
|
)
|
||||||
|
|
||||||
if result:
|
if result:
|
||||||
issue_iid = result.get("iid")
|
issue_iid = str(result.get("iid"))
|
||||||
save_report(issue_iid, interaction.user.id)
|
# ⚡ NOUVEAU: Utilise le tracking unifié
|
||||||
|
self.cog.issue_data[issue_iid] = {
|
||||||
|
"requester_id": interaction.user.id,
|
||||||
|
"last_note_id": 0,
|
||||||
|
"type": "manual-report"
|
||||||
|
}
|
||||||
|
save_tracking(self.cog.issue_data)
|
||||||
await interaction.edit_original_response(
|
await interaction.edit_original_response(
|
||||||
content=(
|
content=(
|
||||||
"✅ Votre bug a été signalé avec succès ! "
|
"✅ Votre bug a été signalé avec succès ! "
|
||||||
|
|
@ -97,20 +124,9 @@ class BugReportModal(disnake.ui.Modal):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class BugReportView(disnake.ui.View):
|
|
||||||
def __init__(self):
|
|
||||||
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()
|
|
||||||
await interaction.response.send_modal(modal)
|
|
||||||
|
|
||||||
|
|
||||||
class FeatureSuggestionModal(disnake.ui.Modal):
|
class FeatureSuggestionModal(disnake.ui.Modal):
|
||||||
def __init__(self):
|
def __init__(self, cog):
|
||||||
super().__init__(
|
self.cog = cog # ⚡ NOUVEAU: Reçoit le cog pour tracking
|
||||||
title="Suggérer une fonctionnalité",
|
|
||||||
components = [
|
components = [
|
||||||
disnake.ui.TextInput(
|
disnake.ui.TextInput(
|
||||||
label="Titre de la suggestion",
|
label="Titre de la suggestion",
|
||||||
|
|
@ -126,8 +142,12 @@ class FeatureSuggestionModal(disnake.ui.Modal):
|
||||||
custom_id="suggestion_description",
|
custom_id="suggestion_description",
|
||||||
required=True,
|
required=True,
|
||||||
max_length=1000,
|
max_length=1000,
|
||||||
)
|
),
|
||||||
]
|
]
|
||||||
|
super().__init__(
|
||||||
|
title="Suggérer une fonctionnalité",
|
||||||
|
custom_id="suggestion_modal",
|
||||||
|
components=components,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def callback(self, interaction: disnake.ModalInteraction):
|
async def callback(self, interaction: disnake.ModalInteraction):
|
||||||
|
|
@ -148,8 +168,14 @@ class FeatureSuggestionModal(disnake.ui.Modal):
|
||||||
)
|
)
|
||||||
|
|
||||||
if result:
|
if result:
|
||||||
issue_iid = result.get("iid")
|
issue_iid = str(result.get("iid"))
|
||||||
save_report(issue_iid, interaction.user.id)
|
# ⚡ NOUVEAU: Utilise le tracking unifié
|
||||||
|
self.cog.issue_data[issue_iid] = {
|
||||||
|
"requester_id": interaction.user.id,
|
||||||
|
"last_note_id": 0,
|
||||||
|
"type": "manual-suggestion"
|
||||||
|
}
|
||||||
|
save_tracking(self.cog.issue_data)
|
||||||
await interaction.edit_original_response(
|
await interaction.edit_original_response(
|
||||||
content=(
|
content=(
|
||||||
"✅ Votre suggestion a été envoyée avec succès ! "
|
"✅ Votre suggestion a été envoyée avec succès ! "
|
||||||
|
|
@ -163,23 +189,36 @@ class FeatureSuggestionModal(disnake.ui.Modal):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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):
|
class FeatureSuggestionView(disnake.ui.View):
|
||||||
def __init__(self):
|
def __init__(self, cog):
|
||||||
|
self.cog = cog # ⚡ NOUVEAU: Reçoit le cog
|
||||||
super().__init__(timeout=None)
|
super().__init__(timeout=None)
|
||||||
|
|
||||||
@disnake.ui.button(label="🎫 Remplir le formulaire", style=disnake.ButtonStyle.green, custom_id="open_suggestion_form")
|
@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):
|
async def open_suggestion_form(self, button: disnake.ui.Button, interaction: disnake.MessageInteraction):
|
||||||
modal = FeatureSuggestionModal()
|
modal = FeatureSuggestionModal(self.cog)
|
||||||
await interaction.response.send_modal(modal)
|
await interaction.response.send_modal(modal)
|
||||||
|
|
||||||
|
|
||||||
class BugReport(commands.Cog):
|
class BugReport(commands.Cog):
|
||||||
def __init__(self, bot):
|
def __init__(self, bot):
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
|
self.issue_data = load_tracking() # ⚡ NOUVEAU: Charge le tracking unifié
|
||||||
|
|
||||||
async def cog_load(self):
|
async def cog_load(self):
|
||||||
kuby_logger.info("BugReport cog chargé. Démarrage du serveur de relais interne (port 5001)...")
|
kuby_logger.info("BugReport cog chargé. Démarrage du serveur de relais interne (port 5001)...")
|
||||||
REPORTS_DIR = os.path.dirname(REPORTS_FILE)
|
REPORTS_DIR = os.path.dirname(LEGACY_REPORTS_FILE)
|
||||||
os.makedirs(REPORTS_DIR, exist_ok=True)
|
os.makedirs(REPORTS_DIR, exist_ok=True)
|
||||||
await self._start_webhook_server()
|
await self._start_webhook_server()
|
||||||
|
|
||||||
|
|
@ -205,13 +244,10 @@ class BugReport(commands.Cog):
|
||||||
kuby_logger.info("Internal Bot Webhook Server stopped and cleaned up.")
|
kuby_logger.info("Internal Bot Webhook Server stopped and cleaned up.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
kuby_logger.error(f"Error during webhook server cleanup: {e}")
|
kuby_logger.error(f"Error during webhook server cleanup: {e}")
|
||||||
|
|
||||||
self.bot.loop.create_task(cleanup())
|
self.bot.loop.create_task(cleanup())
|
||||||
|
|
||||||
async def handle_bot_event(self, request):
|
async def handle_bot_event(self, request):
|
||||||
provided_token = request.headers.get("X-Gitlab-Token")
|
"""Gère les webhooks GitLab (notifications instantanées)"""
|
||||||
# On ignore la vérification du token pour faciliter le relais local
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if not self.bot.is_ready():
|
if not self.bot.is_ready():
|
||||||
kuby_logger.info("Webhook received but bot is not ready yet. Waiting up to 10s...")
|
kuby_logger.info("Webhook received but bot is not ready yet. Waiting up to 10s...")
|
||||||
|
|
@ -221,206 +257,106 @@ class BugReport(commands.Cog):
|
||||||
kuby_logger.warning("Bot still not ready after 10s. Proceeding anyway (best effort).")
|
kuby_logger.warning("Bot still not ready after 10s. Proceeding anyway (best effort).")
|
||||||
|
|
||||||
payload = await request.json()
|
payload = await request.json()
|
||||||
|
|
||||||
event_type = payload.get("object_kind")
|
event_type = payload.get("object_kind")
|
||||||
|
|
||||||
if event_type not in ["issue", "note"]:
|
if event_type not in ["issue", "note"]:
|
||||||
return web.json_response({"status": "ignored"}, status=200)
|
return web.json_response({"status": "ignored"}, status=200)
|
||||||
|
|
||||||
if event_type == "issue":
|
# Récupère l'IID de l'issue
|
||||||
issue_iid = payload.get("object_attributes", {}).get("iid")
|
issue_iid = str(
|
||||||
else:
|
payload.get("object_attributes", {}).get("iid") or
|
||||||
issue_iid = payload.get("issue", {}).get("iid")
|
payload.get("issue", {}).get("iid")
|
||||||
|
)
|
||||||
|
|
||||||
if not issue_iid:
|
if not issue_iid:
|
||||||
return web.json_response({"status": "no issue id"}, status=200)
|
return web.json_response({"status": "no issue id"}, status=200)
|
||||||
|
|
||||||
if not os.path.exists(REPORTS_FILE):
|
# Vérifie si l'issue est trackée
|
||||||
return web.json_response({"status": "no reports file"}, status=200)
|
issue_data = self.issue_data.get(issue_iid)
|
||||||
|
if not issue_data:
|
||||||
try:
|
|
||||||
with open(REPORTS_FILE, "r", encoding="utf-8") as f:
|
|
||||||
reports = json.load(f)
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
kuby_logger.error("REPORTS_FILE corrupted during read!")
|
|
||||||
return web.json_response({"status": "corrupted file"}, status=500)
|
|
||||||
|
|
||||||
user_id = reports.get(str(issue_iid))
|
|
||||||
if not user_id:
|
|
||||||
return web.json_response({"status": "untracked issue"}, status=200)
|
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)
|
user = self.bot.get_user(user_id)
|
||||||
if not user:
|
if not user:
|
||||||
try:
|
try:
|
||||||
user = await self.bot.fetch_user(user_id)
|
user = await self.bot.fetch_user(user_id)
|
||||||
except:
|
except:
|
||||||
pass
|
|
||||||
|
|
||||||
if not user:
|
|
||||||
return web.json_response({"status": "user not found"}, status=200)
|
return web.json_response({"status": "user not found"}, status=200)
|
||||||
|
|
||||||
# DEBUG: log brute payload minimal pour comprendre pourquoi aucun DM n'est envoyé
|
|
||||||
kuby_logger.info(
|
|
||||||
f"[GitLab webhook] event_type={event_type} issue_iid={issue_iid} "
|
|
||||||
f"tracked_user_id={user_id} payload_keys={list(payload.keys())[:20]}"
|
|
||||||
)
|
|
||||||
|
|
||||||
if event_type == "note":
|
|
||||||
note_obj_attrs = payload.get("object_attributes") or {}
|
|
||||||
kuby_logger.info(
|
|
||||||
f"[GitLab note] system={note_obj_attrs.get('system', False)} "
|
|
||||||
f"note_len={len(str(note_obj_attrs.get('note') or note_obj_attrs.get('body') or payload.get('note') or ''))}"
|
|
||||||
)
|
|
||||||
|
|
||||||
app_info = await self.bot.application_info()
|
app_info = await self.bot.application_info()
|
||||||
dev = app_info.owner
|
dev = app_info.owner
|
||||||
issue_title = payload.get("object_attributes", {}).get("title", f"#{issue_iid}")
|
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":
|
if event_type == "note":
|
||||||
|
# ⚡ NOUVEAU: Notifie les commentaires via le webhook (instantané)
|
||||||
|
note_attr = payload.get("object_attributes", {})
|
||||||
|
is_system = note_attr.get("system", False)
|
||||||
|
|
||||||
|
if not is_system:
|
||||||
|
note_body = (
|
||||||
|
note_attr.get("note") or
|
||||||
|
note_attr.get("body") or
|
||||||
|
payload.get("note", "") or
|
||||||
|
""
|
||||||
|
)
|
||||||
|
author_name = payload.get("user", {}).get("name", "Développeur")
|
||||||
|
|
||||||
|
if 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 GitLab 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
|
||||||
|
|
||||||
|
elif event_type == "issue":
|
||||||
|
# Notifie les changements d'état/label
|
||||||
action = payload.get("object_attributes", {}).get("action")
|
action = payload.get("object_attributes", {}).get("action")
|
||||||
changes = payload.get("changes", {})
|
changes = payload.get("changes", {})
|
||||||
|
state = payload.get("object_attributes", {}).get("state")
|
||||||
|
|
||||||
if "labels" in changes:
|
if "labels" in changes:
|
||||||
curr_labels = [l.get("title") for l in changes["labels"].get("current", []) if l.get("title")]
|
curr_labels = [l.get("title") for l in changes["labels"].get("current", []) if l.get("title")]
|
||||||
display_labels = [
|
display_labels = [l for l in curr_labels if not l.startswith("Priority::") and l not in ["bug", "manual-report", "manual-suggestion"]]
|
||||||
l
|
current_labels_str = ", ".join(display_labels) if display_labels else "Mis à jour"
|
||||||
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:
|
try:
|
||||||
embed = disnake.Embed(
|
embed = disnake.Embed(
|
||||||
title="🛠️ Mise à jour de votre signalement !",
|
title="🛠️ Mise à jour de votre signalement !",
|
||||||
description=(
|
description=f"Le statut de votre rapport **{issue_title}** a été mis à jour par l'équipe.\n\n📌 **Nouveaux Labels / Statuts :** {current_labels_str}",
|
||||||
f"Le statut de votre rapport **{issue_title}** a été mis à jour par l'équipe.\n\n"
|
|
||||||
f"📌 **Nouveaux Labels / Statuts :** {current_labels_str}"
|
|
||||||
),
|
|
||||||
color=disnake.Color.blue()
|
color=disnake.Color.blue()
|
||||||
)
|
)
|
||||||
embed.set_thumbnail(url=self.bot.user.display_avatar.url)
|
embed.set_thumbnail(url=self.bot.user.display_avatar.url)
|
||||||
|
|
||||||
try:
|
|
||||||
await user.send(embed=embed)
|
await user.send(embed=embed)
|
||||||
kuby_logger.info(
|
|
||||||
f"Notification de label envoyée à {user} ({user.id}) pour l'issue #{issue_iid}"
|
|
||||||
)
|
|
||||||
if dev and dev.id != user.id:
|
|
||||||
await dev.send(
|
|
||||||
f"✅ [SUIVI] L'utilisateur {user} ({user.id}) a bien reçu la notification de statut "
|
|
||||||
f"**{current_labels_str}** pour l'issue #{issue_iid} ({issue_title})."
|
|
||||||
)
|
|
||||||
except (disnake.Forbidden, disnake.HTTPException) as e:
|
|
||||||
error_code = getattr(e, 'code', None)
|
|
||||||
if isinstance(e, disnake.Forbidden) or error_code == 50007:
|
|
||||||
kuby_logger.warning(
|
|
||||||
f"Impossible d'envoyer un MP à {user} ({user.id}) - MPs désactivés (Code: {error_code})"
|
|
||||||
)
|
|
||||||
if dev and dev.id != user.id:
|
|
||||||
await dev.send(
|
|
||||||
f"⚠️ [DM BLOQUÉ] {user} ({user.id}) a ses MPs fermés. Impossible de notifier pour l'issue #{issue_iid}."
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
kuby_logger.error(f"Erreur HTTP lors de l'envoi du MP de statut à {user}: {e}")
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
kuby_logger.error(f"Erreur inattendue dans la notification de label : {e}")
|
kuby_logger.error(f"Erreur notification label: {e}")
|
||||||
|
|
||||||
state = payload.get("object_attributes", {}).get("state")
|
|
||||||
is_closing_now = action == "close" or (state == "closed" and "state_id" in changes)
|
is_closing_now = action == "close" or (state == "closed" and "state_id" in changes)
|
||||||
|
|
||||||
if is_closing_now:
|
if is_closing_now:
|
||||||
try:
|
try:
|
||||||
embed = disnake.Embed(
|
embed = disnake.Embed(
|
||||||
title="🛠️ Bug / Suggestion Terminé(e) !",
|
title="🛠️ Bug / Suggestion Terminé(e) !",
|
||||||
description=(
|
description=f"Bonne nouvelle ! Votre signalement **{issue_title}** a été marqué comme terminé ou résolu.\n\n🚀 **Prochaine étape :** La mise à jour arrive prochainement !\n✨ Merci pour votre aide !",
|
||||||
f"Bonne nouvelle ! Votre signalement **{issue_title}** a été marqué comme terminé ou résolu.\n\n"
|
|
||||||
f"🚀 **Prochaine étape :** La mise à jour arrive prochainement (ou est déjà là) !\n"
|
|
||||||
f"✨ Merci pour votre aide !"
|
|
||||||
),
|
|
||||||
color=disnake.Color.green()
|
color=disnake.Color.green()
|
||||||
)
|
)
|
||||||
embed.set_thumbnail(url=self.bot.user.display_avatar.url)
|
embed.set_thumbnail(url=self.bot.user.display_avatar.url)
|
||||||
|
|
||||||
try:
|
|
||||||
await user.send(embed=embed)
|
await user.send(embed=embed)
|
||||||
kuby_logger.info(
|
|
||||||
f"Notification de clôture envoyée à {user} ({user.id}) pour l'issue #{issue_iid}"
|
|
||||||
)
|
|
||||||
if dev and dev.id != user.id:
|
|
||||||
await dev.send(
|
|
||||||
f"✅ [SUIVI] L'utilisateur {user} ({user.id}) a bien reçu la notification de CLÔTURE pour l'issue #{issue_iid}."
|
|
||||||
)
|
|
||||||
except (disnake.Forbidden, disnake.HTTPException) as e:
|
|
||||||
error_code = getattr(e, 'code', None)
|
|
||||||
if isinstance(e, disnake.Forbidden) or error_code == 50007:
|
|
||||||
kuby_logger.warning(f"Impossible d'envoyer le MP de clôture à {user} ({user.id}) - MPs désactivés.")
|
|
||||||
if dev and dev.id != user.id:
|
|
||||||
await dev.send(
|
|
||||||
f"⚠️ [DM BLOQUÉ] Impossible d'annoncer la clôture à {user} ({user.id}) pour l'issue #{issue_iid}."
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
kuby_logger.error(f"Erreur HTTP lors de l'envoi du MP de clôture à {user}: {e}")
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
kuby_logger.error(f"Erreur inattendue dans la notification de clôture : {e}")
|
kuby_logger.error(f"Erreur notification clôture: {e}")
|
||||||
|
|
||||||
elif event_type == "note":
|
|
||||||
note_attr = payload.get("object_attributes", {})
|
|
||||||
is_system = note_attr.get("system", False)
|
|
||||||
|
|
||||||
kuby_logger.info(
|
|
||||||
f"[GitLab note] dispatch started issue_iid={issue_iid} is_system={is_system} "
|
|
||||||
f"note_field_present={bool(note_attr.get('note') or note_attr.get('body'))}"
|
|
||||||
)
|
|
||||||
|
|
||||||
if not is_system:
|
|
||||||
note_body = (
|
|
||||||
note_attr.get("note")
|
|
||||||
or note_attr.get("body")
|
|
||||||
or payload.get("note", "")
|
|
||||||
or payload.get("object_attributes", {}).get("note", "")
|
|
||||||
or ""
|
|
||||||
)
|
|
||||||
author_name = payload.get("user", {}).get("name", "Développeur")
|
|
||||||
|
|
||||||
try:
|
|
||||||
embed = disnake.Embed(
|
|
||||||
title="💬 Nouveau commentaire sur votre signalement",
|
|
||||||
description=(
|
|
||||||
f"Le développeur a répondu à votre rapport **{issue_title}** :\n\n"
|
|
||||||
f">>> {note_body}\n\n"
|
|
||||||
f"✍️ **Par :** {author_name}"
|
|
||||||
),
|
|
||||||
color=disnake.Color.orange()
|
|
||||||
)
|
|
||||||
embed.set_thumbnail(url=self.bot.user.display_avatar.url)
|
|
||||||
|
|
||||||
try:
|
|
||||||
await user.send(embed=embed)
|
|
||||||
kuby_logger.info(
|
|
||||||
f"Commentaire GitLab envoyé à {user} ({user.id}) pour l'issue #{issue_iid}"
|
|
||||||
)
|
|
||||||
if dev and dev.id != user.id:
|
|
||||||
await dev.send(
|
|
||||||
f"✅ [SUIVI] L'utilisateur {user} ({user.id}) a bien reçu votre commentaire pour l'issue #{issue_iid}."
|
|
||||||
)
|
|
||||||
except (disnake.Forbidden, disnake.HTTPException):
|
|
||||||
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 (disnake.Forbidden, disnake.HTTPException):
|
|
||||||
pass # On ne peut pas envoyer au dev non plus, on abandonne
|
|
||||||
except Exception as e:
|
|
||||||
kuby_logger.error(
|
|
||||||
f"Erreur inattendue dans la notification de commentaire : {e}",
|
|
||||||
exc_info=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
return web.json_response({"status": "success"}, status=200)
|
return web.json_response({"status": "success"}, status=200)
|
||||||
|
|
||||||
|
|
@ -428,22 +364,14 @@ class BugReport(commands.Cog):
|
||||||
kuby_logger.error(f"Error handling bot event: {e}")
|
kuby_logger.error(f"Error handling bot event: {e}")
|
||||||
return web.json_response({"status": "error", "message": str(e)}, status=500)
|
return web.json_response({"status": "error", "message": str(e)}, status=500)
|
||||||
|
|
||||||
@commands.slash_command(
|
@commands.slash_command(name="signaler_bug", description="Signaler un bug aux développeurs")
|
||||||
name="signaler_bug",
|
|
||||||
description="Signaler un bug aux développeurs",
|
|
||||||
)
|
|
||||||
async def signifier_bug(self, interaction: disnake.ApplicationCommandInteraction):
|
async def signifier_bug(self, interaction: disnake.ApplicationCommandInteraction):
|
||||||
"""Ouvre un formulaire pour signaler un bug."""
|
modal = BugReportModal(self)
|
||||||
modal = BugReportModal()
|
|
||||||
await interaction.response.send_modal(modal)
|
await interaction.response.send_modal(modal)
|
||||||
|
|
||||||
@commands.slash_command(
|
@commands.slash_command(name="suggerer_fonctionnalite", description="Proposer une nouvelle fonctionnalité")
|
||||||
name="suggerer_fonctionnalite",
|
|
||||||
description="Proposer une nouvelle fonctionnalité pour le bot",
|
|
||||||
)
|
|
||||||
async def suggerer_fonctionnalite(self, interaction: disnake.ApplicationCommandInteraction):
|
async def suggerer_fonctionnalite(self, interaction: disnake.ApplicationCommandInteraction):
|
||||||
"""Ouvre un formulaire pour suggérer une fonctionnalité."""
|
modal = FeatureSuggestionModal(self)
|
||||||
modal = FeatureSuggestionModal()
|
|
||||||
await interaction.response.send_modal(modal)
|
await interaction.response.send_modal(modal)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import os
|
||||||
import json
|
import json
|
||||||
import asyncio
|
import asyncio
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import Dict, Any, List
|
from typing import Dict, Any, List, Optional
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
import disnake
|
import disnake
|
||||||
|
|
@ -15,14 +15,39 @@ GITLAB_PROJECT_ID = os.getenv("GITLAB_PROJECT_ID") # numeric ID or URL-encoded
|
||||||
if not GITLAB_TOKEN or not GITLAB_PROJECT_ID:
|
if not GITLAB_TOKEN or not GITLAB_PROJECT_ID:
|
||||||
raise RuntimeError("GitLab token and project ID must be set in environment variables GITLAB_TOKEN and GITLAB_PROJECT_ID")
|
raise RuntimeError("GitLab token and project ID must be set in environment variables GITLAB_TOKEN and GITLAB_PROJECT_ID")
|
||||||
|
|
||||||
# File to persist issue tracking information
|
# Files to persist issue tracking information
|
||||||
ISSUE_TRACK_FILE = os.path.join(os.path.dirname(__file__), "gitlab_issues.json")
|
ISSUE_TRACK_FILE = os.path.join(os.path.dirname(__file__), "gitlab_issues.json")
|
||||||
|
LEGACY_REPORTS_FILE = os.path.join(os.path.dirname(__file__), "..", "data", "gitlab_reports.json")
|
||||||
|
|
||||||
def load_issue_tracking() -> Dict[str, Any]:
|
def load_issue_tracking() -> Dict[str, Any]:
|
||||||
|
"""Charge le tracking et fusionne automatiquement avec gitlab_reports.json (migration)"""
|
||||||
|
tracking = {}
|
||||||
|
|
||||||
|
# Charge le fichier principal
|
||||||
if os.path.exists(ISSUE_TRACK_FILE):
|
if os.path.exists(ISSUE_TRACK_FILE):
|
||||||
|
try:
|
||||||
with open(ISSUE_TRACK_FILE, "r", encoding="utf-8") as f:
|
with open(ISSUE_TRACK_FILE, "r", encoding="utf-8") as f:
|
||||||
return json.load(f)
|
tracking = json.load(f)
|
||||||
return {}
|
except Exception as e:
|
||||||
|
print(f"⚠️ Erreur chargement gitlab_issues.json: {e}")
|
||||||
|
|
||||||
|
# Migration automatique depuis l'ancien système
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
print(f"🔄 Migration automatique: {len(legacy_data)} issues fusionnées depuis gitlab_reports.json")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ Erreur migration gitlab_reports.json: {e}")
|
||||||
|
|
||||||
|
return tracking
|
||||||
|
|
||||||
def save_issue_tracking(data: Dict[str, Any]) -> None:
|
def save_issue_tracking(data: Dict[str, Any]) -> None:
|
||||||
with open(ISSUE_TRACK_FILE, "w", encoding="utf-8") as f:
|
with open(ISSUE_TRACK_FILE, "w", encoding="utf-8") as f:
|
||||||
|
|
@ -39,8 +64,8 @@ class GitLabIntegration(commands.Cog):
|
||||||
def cog_unload(self):
|
def cog_unload(self):
|
||||||
self.check_new_notes.cancel()
|
self.check_new_notes.cancel()
|
||||||
|
|
||||||
async def _create_gitlab_issue(self, title: str, description: str, labels: List[str]) -> Dict[str, Any]:
|
async def _create_gitlab_issue(self, title: str, description: str, labels: List[str], requester_id: int = None) -> Optional[Dict[str, Any]]:
|
||||||
"""Create an issue on GitLab and return the JSON response."""
|
"""Crée une issue sur GitLab et la track automatiquement."""
|
||||||
url = f"https://gitlab.com/api/v4/projects/{GITLAB_PROJECT_ID}/issues"
|
url = f"https://gitlab.com/api/v4/projects/{GITLAB_PROJECT_ID}/issues"
|
||||||
headers = {
|
headers = {
|
||||||
"PRIVATE-TOKEN": GITLAB_TOKEN,
|
"PRIVATE-TOKEN": GITLAB_TOKEN,
|
||||||
|
|
@ -51,10 +76,24 @@ class GitLabIntegration(commands.Cog):
|
||||||
"description": description,
|
"description": description,
|
||||||
"labels": ",".join(labels),
|
"labels": ",".join(labels),
|
||||||
}
|
}
|
||||||
|
try:
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
async with session.post(url, headers=headers, json=payload) as resp:
|
async with session.post(url, headers=headers, json=payload) as resp:
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
return await resp.json()
|
result = await resp.json()
|
||||||
|
# Track automatiquement la nouvelle issue
|
||||||
|
if requester_id:
|
||||||
|
issue_iid = str(result["iid"])
|
||||||
|
self.issue_data[issue_iid] = {
|
||||||
|
"requester_id": requester_id,
|
||||||
|
"last_note_id": 0,
|
||||||
|
"type": "discord-command"
|
||||||
|
}
|
||||||
|
save_issue_tracking(self.issue_data)
|
||||||
|
return result
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Erreur création issue GitLab: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
async def _get_issue_notes(self, issue_iid: int) -> List[Dict[str, Any]]:
|
async def _get_issue_notes(self, issue_iid: int) -> List[Dict[str, Any]]:
|
||||||
"""Fetch all notes (comments) for a given issue."""
|
"""Fetch all notes (comments) for a given issue."""
|
||||||
|
|
@ -85,41 +124,61 @@ class GitLabIntegration(commands.Cog):
|
||||||
|
|
||||||
@tasks.loop(minutes=1)
|
@tasks.loop(minutes=1)
|
||||||
async def check_new_notes(self):
|
async def check_new_notes(self):
|
||||||
"""Periodic task that checks for new notes on tracked issues and notifies requesters."""
|
"""Vérifie TOUS les nouveaux commentaires (pas seulement le dernier) et notifie."""
|
||||||
|
for issue_iid_str, info in list(self.issue_data.items()):
|
||||||
|
issue_iid = int(issue_iid_str)
|
||||||
|
last_known_id = info.get("last_note_id", 0)
|
||||||
|
requester_id = info.get("requester_id")
|
||||||
|
|
||||||
|
try:
|
||||||
|
notes = await self._get_issue_notes(issue_iid)
|
||||||
|
if not notes:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# ⚡ NOUVEAU: Notifie TOUS les commentaires manquants, pas seulement le dernier
|
||||||
|
missing_notes = [n for n in notes if n["id"] > last_known_id]
|
||||||
|
if missing_notes:
|
||||||
|
for note in missing_notes:
|
||||||
|
await self._notify_requester(requester_id, issue_iid, note)
|
||||||
|
# Met à jour avec le dernier ID vu
|
||||||
|
new_last_id = max(n["id"] for n in notes)
|
||||||
|
self.issue_data[issue_iid_str]["last_note_id"] = new_last_id
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ Erreur vérification issue #{issue_iid}: {e}")
|
||||||
|
continue
|
||||||
|
save_issue_tracking(self.issue_data)
|
||||||
|
|
||||||
|
@commands.slash_command(name="recuperer_suivi_issues", description="Récupère le suivi des commentaires après un incident")
|
||||||
|
async def recuperer_suivi(self, inter: disnake.ApplicationCommandInteraction):
|
||||||
|
"""Reconstruit gitlab_issues.json avec les derniers commentaires."""
|
||||||
|
await inter.response.defer(ephemeral=True)
|
||||||
|
updated_count = 0
|
||||||
|
|
||||||
for issue_iid_str, info in list(self.issue_data.items()):
|
for issue_iid_str, info in list(self.issue_data.items()):
|
||||||
issue_iid = int(issue_iid_str)
|
issue_iid = int(issue_iid_str)
|
||||||
try:
|
try:
|
||||||
notes = await self._get_issue_notes(issue_iid)
|
notes = await self._get_issue_notes(issue_iid)
|
||||||
except Exception as exc:
|
if notes:
|
||||||
# Skip this issue if we cannot fetch notes.
|
latest_id = max(n["id"] for n in notes)
|
||||||
|
if latest_id > info.get("last_note_id", 0):
|
||||||
|
self.issue_data[issue_iid_str]["last_note_id"] = latest_id
|
||||||
|
updated_count += 1
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ Erreur récupération issue #{issue_iid}: {e}")
|
||||||
continue
|
continue
|
||||||
# Find the newest note ID
|
|
||||||
if not notes:
|
|
||||||
continue
|
|
||||||
latest_note = max(notes, key=lambda n: n["id"])
|
|
||||||
last_known_id = info.get("last_note_id", 0)
|
|
||||||
if latest_note["id"] > last_known_id:
|
|
||||||
# New comment detected
|
|
||||||
await self._notify_requester(info["requester_id"], issue_iid, latest_note)
|
|
||||||
# Update stored last_note_id
|
|
||||||
self.issue_data[issue_iid_str]["last_note_id"] = latest_note["id"]
|
|
||||||
save_issue_tracking(self.issue_data)
|
|
||||||
|
|
||||||
# ---------- Slash command: /suggerer_fonctionnalite ----------
|
save_issue_tracking(self.issue_data)
|
||||||
|
await inter.edit_original_message(
|
||||||
|
content=f"✅ Suivi récupéré pour **{updated_count}/{len(self.issue_data)}** issues. "
|
||||||
|
f"Fichier `{ISSUE_TRACK_FILE}` mis à jour."
|
||||||
|
)
|
||||||
|
|
||||||
|
# ---------- Slash commands ----------
|
||||||
@commands.slash_command(name="suggerer_fonctionnalite", description="Suggérer une fonctionnalité pour le projet")
|
@commands.slash_command(name="suggerer_fonctionnalite", description="Suggérer une fonctionnalité pour le projet")
|
||||||
async def suggere_fonctionnalite(self, inter: disnake.ApplicationCommandInteraction):
|
async def suggere_fonctionnalite(self, inter: disnake.ApplicationCommandInteraction):
|
||||||
await inter.response.send_modal(modal=FeatureModal(self))
|
await inter.response.send_modal(modal=FeatureModal(self))
|
||||||
|
|
||||||
# ---------- Slash command: /signaler_bug ----------
|
@commands.slash_command(name="signaler_bug", description="Signaler un bug")
|
||||||
@commands.slash_command(name="notify_deploy", description="Envoyer un DM de confirmation du déploiement")
|
|
||||||
async def notify_deploy(self, inter: disnake.ApplicationCommandInteraction):
|
|
||||||
"""DM the user that the deployment is done."""
|
|
||||||
try:
|
|
||||||
await inter.author.send("✅ C'est fait ! Le code a été déployé sur la branche `main`.")
|
|
||||||
await inter.response.send_message("✅ DM de confirmation envoyé.", ephemeral=True)
|
|
||||||
except disnake.HTTPException:
|
|
||||||
await inter.response.send_message("⚠️ Impossible d'envoyer le DM (DMs désactivés).", ephemeral=True)
|
|
||||||
|
|
||||||
async def signaler_bug(self, inter: disnake.ApplicationCommandInteraction):
|
async def signaler_bug(self, inter: disnake.ApplicationCommandInteraction):
|
||||||
await inter.response.send_modal(modal=BugModal(self))
|
await inter.response.send_modal(modal=BugModal(self))
|
||||||
|
|
||||||
|
|
@ -158,16 +217,14 @@ class FeatureModal(disnake.Modal):
|
||||||
title=title,
|
title=title,
|
||||||
description=description,
|
description=description,
|
||||||
labels=["feature", "discord-bot"],
|
labels=["feature", "discord-bot"],
|
||||||
|
requester_id=inter.author.id # ⚡ NOUVEAU: Passe le requester_id pour tracking auto
|
||||||
)
|
)
|
||||||
# Store tracking info
|
if issue:
|
||||||
issue_iid = str(issue["iid"])
|
await inter.edit_original_message(
|
||||||
self.cog.issue_data[issue_iid] = {
|
content=f"✅ Fonctionnalité créée : [{title}](https://gitlab.com/{GITLAB_PROJECT_ID}/-/issues/{issue['iid']})"
|
||||||
"requester_id": inter.author.id,
|
)
|
||||||
"last_note_id": 0,
|
else:
|
||||||
"type": "feature",
|
await inter.edit_original_message(content="❌ Erreur lors de la création de la fonctionnalité.")
|
||||||
}
|
|
||||||
save_issue_tracking(self.cog.issue_data)
|
|
||||||
await inter.edit_original_message(content=f"✅ Fonctionnalité créée : [{title}](https://gitlab.com/{GITLAB_PROJECT_ID}/-/issues/{issue_iid})")
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
await inter.edit_original_message(content=f"❌ Erreur lors de la création de la fonctionnalité : {e}")
|
await inter.edit_original_message(content=f"❌ Erreur lors de la création de la fonctionnalité : {e}")
|
||||||
|
|
||||||
|
|
@ -205,15 +262,14 @@ class BugModal(disnake.Modal):
|
||||||
title=title,
|
title=title,
|
||||||
description=description,
|
description=description,
|
||||||
labels=["bug", "discord-bot"],
|
labels=["bug", "discord-bot"],
|
||||||
|
requester_id=inter.author.id # ⚡ NOUVEAU: Passe le requester_id pour tracking auto
|
||||||
)
|
)
|
||||||
issue_iid = str(issue["iid"])
|
if issue:
|
||||||
self.cog.issue_data[issue_iid] = {
|
await inter.edit_original_message(
|
||||||
"requester_id": inter.author.id,
|
content=f"✅ Bug signalé : [{title}](https://gitlab.com/{GITLAB_PROJECT_ID}/-/issues/{issue['iid']})"
|
||||||
"last_note_id": 0,
|
)
|
||||||
"type": "bug",
|
else:
|
||||||
}
|
await inter.edit_original_message(content="❌ Erreur lors du signalement du bug.")
|
||||||
save_issue_tracking(self.cog.issue_data)
|
|
||||||
await inter.edit_original_message(content=f"✅ Bug créé : [{title}](https://gitlab.com/{GITLAB_PROJECT_ID}/-/issues/{issue_iid})")
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
await inter.edit_original_message(content=f"❌ Erreur lors du signalement du bug : {e}")
|
await inter.edit_original_message(content=f"❌ Erreur lors du signalement du bug : {e}")
|
||||||
|
|
||||||
|
|
|
||||||
42
commandes/gitlab_issues.json
Normal file
42
commandes/gitlab_issues.json
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
{
|
||||||
|
"4": {
|
||||||
|
"requester_id": 971446412690722826,
|
||||||
|
"last_note_id": 0,
|
||||||
|
"type": "legacy_report"
|
||||||
|
},
|
||||||
|
"5": {
|
||||||
|
"requester_id": 971446412690722826,
|
||||||
|
"last_note_id": 0,
|
||||||
|
"type": "legacy_report"
|
||||||
|
},
|
||||||
|
"15": {
|
||||||
|
"requester_id": 971446412690722826,
|
||||||
|
"last_note_id": 0,
|
||||||
|
"type": "legacy_report"
|
||||||
|
},
|
||||||
|
"153": {
|
||||||
|
"requester_id": 534003230435442690,
|
||||||
|
"last_note_id": 0,
|
||||||
|
"type": "legacy_report"
|
||||||
|
},
|
||||||
|
"160": {
|
||||||
|
"requester_id": 971446412690722826,
|
||||||
|
"last_note_id": 0,
|
||||||
|
"type": "legacy_report"
|
||||||
|
},
|
||||||
|
"161": {
|
||||||
|
"requester_id": 971446412690722826,
|
||||||
|
"last_note_id": 0,
|
||||||
|
"type": "legacy_report"
|
||||||
|
},
|
||||||
|
"162": {
|
||||||
|
"requester_id": 971446412690722826,
|
||||||
|
"last_note_id": 0,
|
||||||
|
"type": "legacy_report"
|
||||||
|
},
|
||||||
|
"164": {
|
||||||
|
"requester_id": 971446412690722826,
|
||||||
|
"last_note_id": 0,
|
||||||
|
"type": "legacy_report"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -74,6 +74,7 @@ class Invites(commands.Cog):
|
||||||
async def on_member_join(self, member):
|
async def on_member_join(self, member):
|
||||||
if member.bot: return
|
if member.bot: return
|
||||||
|
|
||||||
|
self._init_db()
|
||||||
guild = member.guild
|
guild = member.guild
|
||||||
try:
|
try:
|
||||||
old_invites = self.invite_cache.get(guild.id, {})
|
old_invites = self.invite_cache.get(guild.id, {})
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,15 @@ def get_log_channel(guild, settings):
|
||||||
return guild.get_channel(int(channel_id))
|
return guild.get_channel(int(channel_id))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def load_settings():
|
||||||
|
if not os.path.exists(SETTINGS_FILE):
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
with open(SETTINGS_FILE, "r", encoding="utf-8") as f:
|
||||||
|
return json.load(f)
|
||||||
|
except Exception:
|
||||||
|
return {}
|
||||||
|
|
||||||
class LogsManager(commands.Cog):
|
class LogsManager(commands.Cog):
|
||||||
def __init__(self, bot):
|
def __init__(self, bot):
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
|
|
@ -25,7 +34,7 @@ class LogsManager(commands.Cog):
|
||||||
@commands.Cog.listener()
|
@commands.Cog.listener()
|
||||||
async def on_message_delete(self, message):
|
async def on_message_delete(self, message):
|
||||||
if message.author.bot: return
|
if message.author.bot: return
|
||||||
settings = dict(json.load(open(SETTINGS_FILE, "r"))).get(str(message.guild.id), {})
|
settings = dict(load_settings()).get(str(message.guild.id), {})
|
||||||
|
|
||||||
log_chan = get_log_channel(message.guild, settings)
|
log_chan = get_log_channel(message.guild, settings)
|
||||||
if log_chan:
|
if log_chan:
|
||||||
|
|
@ -40,7 +49,7 @@ class LogsManager(commands.Cog):
|
||||||
|
|
||||||
@commands.Cog.listener()
|
@commands.Cog.listener()
|
||||||
async def on_member_remove(self, member):
|
async def on_member_remove(self, member):
|
||||||
settings = dict(json.load(open(SETTINGS_FILE, "r"))).get(str(member.guild.id), {})
|
settings = dict(load_settings()).get(str(member.guild.id), {})
|
||||||
log_chan = get_log_channel(member.guild, settings)
|
log_chan = get_log_channel(member.guild, settings)
|
||||||
if log_chan:
|
if log_chan:
|
||||||
embed = disnake.Embed(
|
embed = disnake.Embed(
|
||||||
|
|
@ -53,7 +62,7 @@ class LogsManager(commands.Cog):
|
||||||
|
|
||||||
@commands.Cog.listener()
|
@commands.Cog.listener()
|
||||||
async def on_member_ban(self, guild, user):
|
async def on_member_ban(self, guild, user):
|
||||||
settings = dict(json.load(open(SETTINGS_FILE, "r"))).get(str(guild.id), {})
|
settings = dict(load_settings()).get(str(guild.id), {})
|
||||||
log_chan = get_log_channel(guild, settings)
|
log_chan = get_log_channel(guild, settings)
|
||||||
if log_chan:
|
if log_chan:
|
||||||
embed = disnake.Embed(
|
embed = disnake.Embed(
|
||||||
|
|
|
||||||
77
scripts/migrate_gitlab_tracking.py
Normal file
77
scripts/migrate_gitlab_tracking.py
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Script de migration automatique pour fusionner gitlab_reports.json dans gitlab_issues.json.
|
||||||
|
|
||||||
|
Ce script est conçu pour être appelé AUTOMATIQUEMENT lors du déploiement.
|
||||||
|
Il garantit que toutes les issues sont suivies dans un seul fichier.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python scripts/migrate_gitlab_tracking.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Chemins des fichiers
|
||||||
|
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
TRACKING_FILE = os.path.join(BASE_DIR, "commandes", "gitlab_issues.json")
|
||||||
|
LEGACY_REPORTS_FILE = os.path.join(BASE_DIR, "data", "gitlab_reports.json")
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_tracking():
|
||||||
|
"""Fusionne gitlab_reports.json dans gitlab_issues.json"""
|
||||||
|
print("🔄 Début de la migration GitLab tracking...")
|
||||||
|
|
||||||
|
# Charge le fichier principal
|
||||||
|
tracking = {}
|
||||||
|
if os.path.exists(TRACKING_FILE):
|
||||||
|
try:
|
||||||
|
with open(TRACKING_FILE, "r", encoding="utf-8") as f:
|
||||||
|
tracking = json.load(f)
|
||||||
|
print(f"✅ Chargé {len(tracking)} issues depuis gitlab_issues.json")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ Erreur chargement gitlab_issues.json: {e}")
|
||||||
|
|
||||||
|
# Fusionne avec l'ancien fichier
|
||||||
|
migrated_count = 0
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
migrated_count += 1
|
||||||
|
|
||||||
|
print(f"✅ Migration: {migrated_count} issues fusionnées depuis gitlab_reports.json")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ Erreur migration gitlab_reports.json: {e}")
|
||||||
|
|
||||||
|
# Sauvegarde le résultat
|
||||||
|
if tracking:
|
||||||
|
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(tracking, f, ensure_ascii=False, indent=2)
|
||||||
|
os.replace(temp_file, TRACKING_FILE)
|
||||||
|
print(f"✅ Sauvegardé {len(tracking)} issues dans gitlab_issues.json")
|
||||||
|
|
||||||
|
print("✅ Migration terminée avec succès !")
|
||||||
|
return migrated_count
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
migrated = migrate_tracking()
|
||||||
|
|
||||||
|
# Si des données ont été migrées, on peut optionnellement nettoyer l'ancien fichier
|
||||||
|
if migrated > 0:
|
||||||
|
print(f"\n💡 {migrated} issues ont été migrées depuis gitlab_reports.json")
|
||||||
|
print("💡 Tu peux maintenant supprimer data/gitlab_reports.json si tu veux.")
|
||||||
|
|
||||||
|
sys.exit(0)
|
||||||
247
scripts/recover_gitlab_comments.py
Normal file
247
scripts/recover_gitlab_comments.py
Normal file
|
|
@ -0,0 +1,247 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Script de récupération des commentaires GitLab pour le bot Kuby.
|
||||||
|
|
||||||
|
Ce script permet de :
|
||||||
|
1. Récupérer toutes les issues suivies depuis gitlab_issues.json et data/gitlab_reports.json
|
||||||
|
2. Pour chaque issue, récupérer TOUS les commentaires via l'API GitLab
|
||||||
|
3. Reconstruire gitlab_issues.json avec les bons last_note_id
|
||||||
|
4. Afficher un rapport des commentaires manquants
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python scripts/recover_gitlab_comments.py
|
||||||
|
|
||||||
|
Requires:
|
||||||
|
- GITLAB_TOKEN (env var)
|
||||||
|
- GITLAB_PROJECT_ID (env var)
|
||||||
|
- aiohttp
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import asyncio
|
||||||
|
import sys
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Dict, List, Any, Optional
|
||||||
|
|
||||||
|
# Chemin des fichiers de tracking
|
||||||
|
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
GITLAB_ISSUES_FILE = os.path.join(BASE_DIR, "commandes", "gitlab_issues.json")
|
||||||
|
GITLAB_REPORTS_FILE = os.path.join(BASE_DIR, "data", "gitlab_reports.json")
|
||||||
|
|
||||||
|
# Configuration GitLab
|
||||||
|
GITLAB_TOKEN = os.getenv("GITLAB_TOKEN")
|
||||||
|
GITLAB_PROJECT_ID = os.getenv("GITLAB_PROJECT_ID")
|
||||||
|
|
||||||
|
if not GITLAB_TOKEN or not GITLAB_PROJECT_ID:
|
||||||
|
print("❌ ERREUR : Les variables d'environnement GITLAB_TOKEN et GITLAB_PROJECT_ID doivent être définies.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
GITLAB_API_URL = f"https://gitlab.com/api/v4/projects/{GITLAB_PROJECT_ID}"
|
||||||
|
|
||||||
|
|
||||||
|
async def fetch_all_notes(issue_iid: int) -> List[Dict[str, Any]]:
|
||||||
|
"""Récupère tous les commentaires d'une issue via l'API GitLab."""
|
||||||
|
url = f"{GITLAB_API_URL}/issues/{issue_iid}/notes?per_page=100"
|
||||||
|
headers = {"PRIVATE-TOKEN": GITLAB_TOKEN}
|
||||||
|
|
||||||
|
all_notes = []
|
||||||
|
page = 1
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
import aiohttp
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.get(f"{url}&page={page}", headers=headers) as resp:
|
||||||
|
if resp.status != 200:
|
||||||
|
print(f" ⚠️ Erreur API pour l'issue #{issue_iid} (page {page}): {resp.status}")
|
||||||
|
break
|
||||||
|
|
||||||
|
notes = await resp.json()
|
||||||
|
if not notes:
|
||||||
|
break
|
||||||
|
|
||||||
|
all_notes.extend(notes)
|
||||||
|
|
||||||
|
# Vérifie s'il y a plus de pages
|
||||||
|
if len(notes) < 100:
|
||||||
|
break
|
||||||
|
|
||||||
|
page += 1
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ❌ Erreur lors de la récupération des notes pour l'issue #{issue_iid}: {e}")
|
||||||
|
break
|
||||||
|
|
||||||
|
return all_notes
|
||||||
|
|
||||||
|
|
||||||
|
def load_existing_tracking() -> Dict[str, Any]:
|
||||||
|
"""Charge les données de suivi existantes."""
|
||||||
|
tracking_data = {}
|
||||||
|
|
||||||
|
# Charge gitlab_issues.json
|
||||||
|
if os.path.exists(GITLAB_ISSUES_FILE):
|
||||||
|
try:
|
||||||
|
with open(GITLAB_ISSUES_FILE, "r", encoding="utf-8") as f:
|
||||||
|
tracking_data = json.load(f)
|
||||||
|
print(f"✅ Chargé {len(tracking_data)} issues depuis gitlab_issues.json")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ Erreur lors du chargement de gitlab_issues.json: {e}")
|
||||||
|
|
||||||
|
# Charge gitlab_reports.json et fusionne
|
||||||
|
if os.path.exists(GITLAB_REPORTS_FILE):
|
||||||
|
try:
|
||||||
|
with open(GITLAB_REPORTS_FILE, "r", encoding="utf-8") as f:
|
||||||
|
reports_data = json.load(f)
|
||||||
|
|
||||||
|
for issue_iid, user_id in reports_data.items():
|
||||||
|
if issue_iid not in tracking_data:
|
||||||
|
tracking_data[issue_iid] = {
|
||||||
|
"requester_id": user_id,
|
||||||
|
"last_note_id": 0,
|
||||||
|
"type": "manual-report"
|
||||||
|
}
|
||||||
|
print(f" + Ajout de l'issue #{issue_iid} depuis gitlab_reports.json")
|
||||||
|
|
||||||
|
print(f"✅ Fusionné {len(reports_data)} issues depuis gitlab_reports.json")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ Erreur lors du chargement de gitlab_reports.json: {e}")
|
||||||
|
|
||||||
|
return tracking_data
|
||||||
|
|
||||||
|
|
||||||
|
def save_tracking_data(data: Dict[str, Any]) -> None:
|
||||||
|
"""Sauvegarde les données de suivi dans gitlab_issues.json."""
|
||||||
|
os.makedirs(os.path.dirname(GITLAB_ISSUES_FILE), exist_ok=True)
|
||||||
|
|
||||||
|
# Sauvegarde dans un fichier temporaire puis renomme
|
||||||
|
temp_file = f"{GITLAB_ISSUES_FILE}.tmp"
|
||||||
|
with open(temp_file, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||||
|
os.replace(temp_file, GITLAB_ISSUES_FILE)
|
||||||
|
print(f"✅ Sauvegardé {len(data)} issues dans gitlab_issues.json")
|
||||||
|
|
||||||
|
|
||||||
|
async def recover_comments():
|
||||||
|
"""Fonction principale de récupération."""
|
||||||
|
print("=" * 60)
|
||||||
|
print("🔍 RÉCUPÉRATION DES COMMENTAIRES GITLAB")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
# Charge les données existantes
|
||||||
|
print("\n📂 Chargement des données de suivi existantes...")
|
||||||
|
tracking_data = load_existing_tracking()
|
||||||
|
|
||||||
|
if not tracking_data:
|
||||||
|
print("❌ Aucune issue à suivre trouvée. Vérifiez que les fichiers existent.")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"\n🔄 Traitement de {len(tracking_data)} issues...")
|
||||||
|
print("-" * 60)
|
||||||
|
|
||||||
|
issues_with_new_comments = []
|
||||||
|
total_notes = 0
|
||||||
|
total_missing = 0
|
||||||
|
|
||||||
|
for issue_iid_str, info in tracking_data.items():
|
||||||
|
issue_iid = int(issue_iid_str)
|
||||||
|
last_known_id = info.get("last_note_id", 0)
|
||||||
|
requester_id = info.get("requester_id", "Inconnu")
|
||||||
|
|
||||||
|
print(f"\n📝 Issue #{issue_iid} (demandeur: {requester_id})")
|
||||||
|
print(f" Dernier note_id connu: {last_known_id}")
|
||||||
|
|
||||||
|
# Récupère tous les commentaires
|
||||||
|
notes = await fetch_all_notes(issue_iid)
|
||||||
|
|
||||||
|
if not notes:
|
||||||
|
print(f" ⚠️ Aucune note trouvée pour cette issue.")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Trie les notes par ID
|
||||||
|
notes.sort(key=lambda n: n["id"])
|
||||||
|
latest_note_id = max(n["id"] for n in notes)
|
||||||
|
total_notes += len(notes)
|
||||||
|
|
||||||
|
print(f" ✅ {len(notes)} commentaires trouvés (dernier ID: {latest_note_id})")
|
||||||
|
|
||||||
|
# Vérifie s'il y a des commentaires manquants
|
||||||
|
if last_known_id == 0:
|
||||||
|
# Premier lancement ou fichier réinitialisé
|
||||||
|
missing_count = len(notes)
|
||||||
|
total_missing += missing_count
|
||||||
|
print(f" ⚠️ {missing_count} commentaires NON SUIVIS (fichier réinitialisé)")
|
||||||
|
issues_with_new_comments.append({
|
||||||
|
"issue_iid": issue_iid,
|
||||||
|
"missing_notes": notes,
|
||||||
|
"requester_id": requester_id,
|
||||||
|
"new_last_note_id": latest_note_id
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
# Vérifie quels commentaires sont nouveaux
|
||||||
|
missing_notes = [n for n in notes if n["id"] > last_known_id]
|
||||||
|
if missing_notes:
|
||||||
|
missing_count = len(missing_notes)
|
||||||
|
total_missing += missing_count
|
||||||
|
print(f" ⚠️ {missing_count} NOUVEAUX commentaires détectés")
|
||||||
|
issues_with_new_comments.append({
|
||||||
|
"issue_iid": issue_iid,
|
||||||
|
"missing_notes": missing_notes,
|
||||||
|
"requester_id": requester_id,
|
||||||
|
"new_last_note_id": latest_note_id
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
print(f" ✅ Aucun nouveau commentaire")
|
||||||
|
|
||||||
|
# Met à jour le last_note_id
|
||||||
|
tracking_data[issue_iid_str]["last_note_id"] = latest_note_id
|
||||||
|
|
||||||
|
# Sauvegarde les données mises à jour
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("💾 SAUVEGARDE DES DONNÉES")
|
||||||
|
print("=" * 60)
|
||||||
|
save_tracking_data(tracking_data)
|
||||||
|
|
||||||
|
# Génère le rapport
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("📊 RAPPORT DE RÉCUPÉRATION")
|
||||||
|
print("=" * 60)
|
||||||
|
print(f"Total issues traitées: {len(tracking_data)}")
|
||||||
|
print(f"Total commentaires récupérés: {total_notes}")
|
||||||
|
print(f"Total commentaires manquants: {total_missing}")
|
||||||
|
|
||||||
|
if issues_with_new_comments:
|
||||||
|
print(f"\n📋 Issues avec des commentaires non suivis ({len(issues_with_new_comments)}):")
|
||||||
|
for item in issues_with_new_comments:
|
||||||
|
print(f"\n Issue #{item['issue_iid']}:")
|
||||||
|
print(f" Demandeur: {item['requester_id']}")
|
||||||
|
print(f" Nombre de commentaires manquants: {len(item['missing_notes'])}")
|
||||||
|
print(f" Nouveau last_note_id: {item['new_last_note_id']}")
|
||||||
|
|
||||||
|
# Affiche les commentaires manquants
|
||||||
|
for note in item["missing_notes"]:
|
||||||
|
author = note.get("author", {}).get("name", "Inconnu")
|
||||||
|
created_at = note.get("created_at", "Inconnu")
|
||||||
|
body = note.get("body", "")[:100] + "..." if len(note.get("body", "")) > 100 else note.get("body", "")
|
||||||
|
print(f" - [{created_at}] {author}: {body}")
|
||||||
|
else:
|
||||||
|
print("\n✅ Aucune issue avec des commentaires manquants !")
|
||||||
|
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("✅ RÉCUPÉRATION TERMINÉE")
|
||||||
|
print("=" * 60)
|
||||||
|
print("\nProchaines étapes:")
|
||||||
|
print("1. Vérifiez le fichier commandes/gitlab_issues.json")
|
||||||
|
print("2. Redémarrez le bot pour qu'il utilise les nouvelles données")
|
||||||
|
print("3. Les nouveaux commentaires seront désormais suivis correctement")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("SCRIPT DE RÉCUPÉRATION DES COMMENTAIRES GITLAB")
|
||||||
|
print("=" * 60)
|
||||||
|
print(f"Projet GitLab: {GITLAB_PROJECT_ID}")
|
||||||
|
print(f"Fichier de sortie: {GITLAB_ISSUES_FILE}")
|
||||||
|
print("=" * 60 + "\n")
|
||||||
|
|
||||||
|
asyncio.run(recover_comments())
|
||||||
|
|
@ -63,6 +63,20 @@ def run_mep_logic(author, commit_msg, branch="main"):
|
||||||
if use_rsync:
|
if use_rsync:
|
||||||
subprocess.run(["rsync", "-a", "--exclude=.git", "--exclude=venv", "--exclude=.venv", "--exclude=__pycache__", f"{target_path}/", f"{PROJECT_PATH}/"], check=True)
|
subprocess.run(["rsync", "-a", "--exclude=.git", "--exclude=venv", "--exclude=.venv", "--exclude=__pycache__", f"{target_path}/", f"{PROJECT_PATH}/"], check=True)
|
||||||
|
|
||||||
|
# ⚡ NOUVEAU: Migration automatique GitLab tracking
|
||||||
|
send_discord_log("MIGRATION", "Migration GitLab tracking en cours...", 16776960, author, commit_msg)
|
||||||
|
try:
|
||||||
|
subprocess.run(
|
||||||
|
["python3", os.path.join(target_path, "scripts", "migrate_gitlab_tracking.py")],
|
||||||
|
cwd=target_path,
|
||||||
|
check=True,
|
||||||
|
capture_output=True,
|
||||||
|
text=True
|
||||||
|
)
|
||||||
|
send_discord_log("MIGRATION", "✅ Migration GitLab tracking terminée.", 3066993, author, commit_msg)
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
send_discord_log("ERREUR MIGRATION", f"❌ Migration GitLab échouée: {e.stderr}", 15158332, author, commit_msg)
|
||||||
|
|
||||||
duration = (datetime.now() - start_time).total_seconds()
|
duration = (datetime.now() - start_time).total_seconds()
|
||||||
msg = f"✅ **Déploiement réussi en {duration:.2f}s**\nLe bot est à jour et redémarré."
|
msg = f"✅ **Déploiement réussi en {duration:.2f}s**\nLe bot est à jour et redémarré."
|
||||||
send_discord_log("VALIDÉ", msg, 3066993, author, commit_msg)
|
send_discord_log("VALIDÉ", msg, 3066993, author, commit_msg)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue