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:
Gameur 2026-06-04 08:40:51 +02:00
commit b30bb51e59
8 changed files with 682 additions and 308 deletions

View file

@ -8,37 +8,55 @@ 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",
placeholder="Décrivez brièvement le problème...", placeholder="Décrivez brièvement le problème...",
@ -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,21 +124,10 @@ 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",
placeholder="Que voulez-vous ajouter ?", placeholder="Que voulez-vous ajouter ?",
@ -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)

View file

@ -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}")

View 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"
}
}

View file

@ -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, {})

View file

@ -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(

View 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)

View 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())

View file

@ -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)