Migration des intégration gitlab --> Forgejo

This commit is contained in:
Mathis 2026-07-11 18:54:27 +02:00
parent 879355f792
commit 9b4fbabbc5

View file

@ -8,83 +8,64 @@ import aiohttp
import disnake import disnake
from disnake.ext import commands, tasks from disnake.ext import commands, tasks
# Environment variables for GitLab access # Configuration des variables d'environnement pour Forgejo
GITLAB_TOKEN = os.getenv("GITLAB_TOKEN") FORGEJO_TOKEN = os.getenv("FORGEJO_TOKEN")
GITLAB_PROJECT_ID = os.getenv("GITLAB_PROJECT_ID") # numeric ID or URL-encoded path FORGEJO_OWNER = os.getenv("FORGEJO_OWNER") # Exemple: "Gameur" ou "Omega_Kube"
FORGEJO_REPO = os.getenv("FORGEJO_REPO") # Exemple: "kuby"
BASE_URL = "https://omegakubeserv.tail951d2f.ts.net/api/v1"
if not GITLAB_TOKEN or not GITLAB_PROJECT_ID: if not FORGEJO_TOKEN or not FORGEJO_OWNER or not FORGEJO_REPO:
raise RuntimeError("GitLab token and project ID must be set in environment variables GITLAB_TOKEN and GITLAB_PROJECT_ID") raise RuntimeError("Forgejo variables (FORGEJO_TOKEN, FORGEJO_OWNER, FORGEJO_REPO) must be set.")
# Files to persist issue tracking information # Fichiers de persistance mis à jour pour Forgejo
ISSUE_TRACK_FILE = os.path.join(os.path.dirname(__file__), "gitlab_issues.json") ISSUE_TRACK_FILE = os.path.join(os.path.dirname(__file__), "forgejo_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 = {} tracking = {}
# Charge le fichier principal
if os.path.exists(ISSUE_TRACK_FILE): if os.path.exists(ISSUE_TRACK_FILE):
try: try:
with open(ISSUE_TRACK_FILE, "r", encoding="utf-8") as f: with open(ISSUE_TRACK_FILE, "r", encoding="utf-8") as f:
tracking = json.load(f) tracking = json.load(f)
except Exception as e: except Exception as e:
print(f"⚠️ Erreur chargement gitlab_issues.json: {e}") print(f"⚠️ Erreur chargement forgejo_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 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:
json.dump(data, f, ensure_ascii=False, indent=2) json.dump(data, f, ensure_ascii=False, indent=2)
class GitLabIntegration(commands.Cog): class ForgejoIntegration(commands.Cog):
"""Cog providing slash commands to create GitLab issues (feature request / bug) and notify requesters when comments are added.""" """Cog fournissant des commandes slash pour créer des tickets sur Forgejo et notifier les utilisateurs."""
def __init__(self, bot: commands.Bot): def __init__(self, bot: commands.Bot):
self.bot = bot self.bot = bot
self.issue_data: Dict[str, Any] = load_issue_tracking() # key: issue_iid (str) -> {"requester_id": int, "last_note_id": int} self.issue_data: Dict[str, Any] = load_issue_tracking()
self.check_new_notes.start() self.check_new_comments.start()
def cog_unload(self): def cog_unload(self):
self.check_new_notes.cancel() self.check_new_comments.cancel()
async def _create_gitlab_issue(self, title: str, description: str, labels: List[str], requester_id: int = None) -> Optional[Dict[str, Any]]: async def _create_forgejo_issue(self, title: str, description: str, labels: List[str], requester_id: int = None) -> Optional[Dict[str, Any]]:
"""Crée une issue sur GitLab et la track automatiquement.""" """Crée un ticket sur Forgejo et le track automatiquement."""
url = f"https://gitlab.com/api/v4/projects/{GITLAB_PROJECT_ID}/issues" url = f"{BASE_URL}/repos/{FORGEJO_OWNER}/{FORGEJO_REPO}/issues"
headers = { headers = {
"PRIVATE-TOKEN": GITLAB_TOKEN, "Authorization": f"token {FORGEJO_TOKEN}",
"Content-Type": "application/json", "Content-Type": "application/json",
} }
payload = { payload = {
"title": title, "title": title,
"description": description, "body": description, # GitLab utilisait 'description', Forgejo utilise 'body'
"labels": ",".join(labels), "labels": labels, # Forgejo accepte directement une liste ['bug', 'bot']
} }
try: 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()
result = await resp.json() result = await resp.json()
# Track automatiquement la nouvelle issue
if requester_id: if requester_id:
issue_iid = str(result["iid"]) issue_number = str(result["number"]) # 'iid' devient 'number'
self.issue_data[issue_iid] = { self.issue_data[issue_number] = {
"requester_id": requester_id, "requester_id": requester_id,
"last_note_id": 0, "last_note_id": 0,
"type": "discord-command" "type": "discord-command"
@ -92,186 +73,140 @@ class GitLabIntegration(commands.Cog):
save_issue_tracking(self.issue_data) save_issue_tracking(self.issue_data)
return result return result
except Exception as e: except Exception as e:
print(f"❌ Erreur création issue GitLab: {e}") print(f"❌ Erreur création issue Forgejo: {e}")
return None return None
async def _get_issue_notes(self, issue_iid: int) -> List[Dict[str, Any]]: async def _get_issue_comments(self, issue_number: int) -> List[Dict[str, Any]]:
"""Fetch all notes (comments) for a given issue.""" """Récupère tous les commentaires d'un ticket spécifique."""
url = f"https://gitlab.com/api/v4/projects/{GITLAB_PROJECT_ID}/issues/{issue_iid}/notes" url = f"{BASE_URL}/repos/{FORGEJO_OWNER}/{FORGEJO_REPO}/issues/{issue_number}/comments"
headers = {"PRIVATE-TOKEN": GITLAB_TOKEN} headers = {"Authorization": f"token {FORGEJO_TOKEN}"}
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.get(url, headers=headers) as resp: async with session.get(url, headers=headers) as resp:
resp.raise_for_status() resp.raise_for_status()
return await resp.json() return await resp.json()
async def _notify_requester(self, requester_id: int, issue_iid: int, note: Dict[str, Any]): async def _notify_requester(self, requester_id: int, issue_number: int, comment: Dict[str, Any]):
"""Send a private DM to the requester informing them of a new comment.""" """Envoie un DM privé à l'utilisateur lors d'un nouveau commentaire."""
user = await self.bot.fetch_user(requester_id) user = await self.bot.fetch_user(requester_id)
author = note.get("author", {}).get("name", "Someone") author = comment.get("user", {}).get("username", "Quelqu'un") # 'author.name' devient 'user.username'
body = note.get("body", "") body = comment.get("body", "")
embed = disnake.Embed( embed = disnake.Embed(
title=f"Nouveau commentaire sur votre demande GitLab (#{issue_iid})", title=f"Nouveau commentaire sur votre demande Forgejo (#{issue_number})",
description=body, description=body,
color=disnake.Color.blurple(), color=disnake.Color.brand_green(),
timestamp=datetime.fromisoformat(note["created_at"]).replace(tzinfo=timezone.utc), timestamp=datetime.fromisoformat(comment["created_at"].replace("Z", "+00:00")),
) )
embed.set_footer(text=f"Commentaire de {author}") embed.set_footer(text=f"Commentaire de {author}")
try: try:
await user.send(embed=embed) await user.send(embed=embed)
except disnake.HTTPException: except disnake.HTTPException:
# The user may have DMs disabled; ignore silently.
pass pass
@tasks.loop(minutes=1) @tasks.loop(minutes=1)
async def check_new_notes(self): async def check_new_comments(self):
"""Vérifie TOUS les nouveaux commentaires (pas seulement le dernier) et notifie.""" """Boucle de vérification des commentaires Forgejo."""
for issue_iid_str, info in list(self.issue_data.items()): for issue_num_str, info in list(self.issue_data.items()):
issue_iid = int(issue_iid_str) issue_number = int(issue_num_str)
last_known_id = info.get("last_note_id", 0) last_known_id = info.get("last_note_id", 0)
requester_id = info.get("requester_id") requester_id = info.get("requester_id")
try: try:
notes = await self._get_issue_notes(issue_iid) comments = await self._get_issue_comments(issue_number)
if not notes: if not comments:
continue continue
# ⚡ NOUVEAU: Notifie TOUS les commentaires manquants, pas seulement le dernier missing_comments = [c for c in comments if c["id"] > last_known_id]
missing_notes = [n for n in notes if n["id"] > last_known_id] if missing_comments:
if missing_notes: for comment in missing_comments:
for note in missing_notes: await self._notify_requester(requester_id, issue_number, comment)
await self._notify_requester(requester_id, issue_iid, note)
# Met à jour avec le dernier ID vu new_last_id = max(c["id"] for c in comments)
new_last_id = max(n["id"] for n in notes) self.issue_data[issue_num_str]["last_note_id"] = new_last_id
self.issue_data[issue_iid_str]["last_note_id"] = new_last_id
except Exception as e: except Exception as e:
print(f"⚠️ Erreur vérification issue #{issue_iid}: {e}") print(f"⚠️ Erreur vérification issue #{issue_number}: {e}")
continue continue
save_issue_tracking(self.issue_data) 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") @commands.slash_command(name="recuperer_suivi_issues", description="Récupère le suivi des commentaires Forgejo après un incident")
async def recuperer_suivi(self, inter: disnake.ApplicationCommandInteraction): async def recuperer_suivi(self, inter: disnake.ApplicationCommandInteraction):
"""Reconstruit gitlab_issues.json avec les derniers commentaires."""
await inter.response.defer(ephemeral=True) await inter.response.defer(ephemeral=True)
updated_count = 0 updated_count = 0
for issue_iid_str, info in list(self.issue_data.items()): for issue_num_str, info in list(self.issue_data.items()):
issue_iid = int(issue_iid_str) issue_number = int(issue_num_str)
try: try:
notes = await self._get_issue_notes(issue_iid) comments = await self._get_issue_comments(issue_number)
if notes: if comments:
latest_id = max(n["id"] for n in notes) latest_id = max(c["id"] for c in comments)
if latest_id > info.get("last_note_id", 0): if latest_id > info.get("last_note_id", 0):
self.issue_data[issue_iid_str]["last_note_id"] = latest_id self.issue_data[issue_num_str]["last_note_id"] = latest_id
updated_count += 1 updated_count += 1
except Exception as e: except Exception as e:
print(f"⚠️ Erreur récupération issue #{issue_iid}: {e}") print(f"⚠️ Erreur récupération issue #{issue_number}: {e}")
continue continue
save_issue_tracking(self.issue_data) save_issue_tracking(self.issue_data)
await inter.edit_original_message( await inter.edit_original_message(
content=f"✅ Suivi récupéré pour **{updated_count}/{len(self.issue_data)}** issues." 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))
@commands.slash_command(name="signaler_bug", description="Signaler un bug") @commands.slash_command(name="signaler_bug", description="Signaler un bug sur Kuby")
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))
# ---------- Modals ----------
# ---------- Modals Acteurs ----------
class FeatureModal(disnake.Modal): class FeatureModal(disnake.Modal):
def __init__(self, cog: GitLabIntegration): def __init__(self, cog: ForgejoIntegration):
components = [ components = [
disnake.ui.TextInput( disnake.ui.TextInput(label="Titre de la fonctionnalité", custom_id="title", max_length=100, required=True),
label="Titre de la fonctionnalité", disnake.ui.TextInput(label="Description détaillée", custom_id="description", style=disnake.TextInputStyle.paragraph, max_length=2000, required=True),
custom_id="title",
style=disnake.TextInputStyle.short,
max_length=100,
required=True,
),
disnake.ui.TextInput(
label="Description détaillée",
custom_id="description",
style=disnake.TextInputStyle.paragraph,
max_length=2000,
required=True,
),
] ]
super().__init__( super().__init__(title="Suggestion de fonctionnalité", custom_id="feature_modal", components=components)
title="Suggestion de fonctionnalité",
custom_id="feature_modal",
components=components,
)
self.cog = cog self.cog = cog
async def callback(self, inter: disnake.ModalInteraction): async def callback(self, inter: disnake.ModalInteraction):
title = inter.text_values["title"] title = inter.text_values["title"]
description = inter.text_values["description"] description = inter.text_values["description"]
await inter.response.defer(ephemeral=True) await inter.response.defer(ephemeral=True)
try:
issue = await self.cog._create_gitlab_issue( issue = await self.cog._create_forgejo_issue(
title=title, title=title, description=description, labels=["feature", "discord-bot"], requester_id=inter.author.id
description=description,
labels=["feature", "discord-bot"],
requester_id=inter.author.id # ⚡ NOUVEAU: Passe le requester_id pour tracking auto
) )
if issue: if issue:
await inter.edit_original_message( url = f"https://omegakubeserv.tail951d2f.ts.net/{FORGEJO_OWNER}/{FORGEJO_REPO}/issues/{issue['number']}"
content=f"✅ Fonctionnalité créée: [{title}](https://gitlab.com/{GITLAB_PROJECT_ID}/-/issues/{issue['iid']})" await inter.edit_original_message(content=f"✅ Fonctionnalité créée : [{title}]({url})")
)
else: else:
await inter.edit_original_message(content="❌ Erreur lors de la création de la fonctionnalité.") await inter.edit_original_message(content="❌ Erreur lors de la création sur Forgejo.")
except Exception as e:
await inter.edit_original_message(content=f"❌ Erreur lors de la création de la fonctionnalité: {e}")
class BugModal(disnake.Modal): class BugModal(disnake.Modal):
def __init__(self, cog: GitLabIntegration): def __init__(self, cog: ForgejoIntegration):
components = [ components = [
disnake.ui.TextInput( disnake.ui.TextInput(label="Titre du bug", custom_id="title", max_length=100, required=True),
label="Titre du bug", disnake.ui.TextInput(label="Description et étapes", style=disnake.TextInputStyle.paragraph, custom_id="description", max_length=2000, required=True),
custom_id="title",
style=disnake.TextInputStyle.short,
max_length=100,
required=True,
),
disnake.ui.TextInput(
label="Description et étapes pour reproduire",
custom_id="description",
style=disnake.TextInputStyle.paragraph,
max_length=2000,
required=True,
),
] ]
super().__init__( super().__init__(title="Signalement de bug", custom_id="bug_modal", components=components)
title="Signalement de bug",
custom_id="bug_modal",
components=components,
)
self.cog = cog self.cog = cog
async def callback(self, inter: disnake.ModalInteraction): async def callback(self, inter: disnake.ModalInteraction):
title = inter.text_values["title"] title = inter.text_values["title"]
description = inter.text_values["description"] description = inter.text_values["description"]
await inter.response.defer(ephemeral=True) await inter.response.defer(ephemeral=True)
try:
issue = await self.cog._create_gitlab_issue( issue = await self.cog._create_forgejo_issue(
title=title, title=title, description=description, labels=["bug", "discord-bot"], requester_id=inter.author.id
description=description,
labels=["bug", "discord-bot"],
requester_id=inter.author.id # ⚡ NOUVEAU: Passe le requester_id pour tracking auto
) )
if issue: if issue:
await inter.edit_original_message( url = f"https://omegakubeserv.tail951d2f.ts.net/{FORGEJO_OWNER}/{FORGEJO_REPO}/issues/{issue['number']}"
content=f"✅ Bug signalé: [{title}](https://gitlab.com/{GITLAB_PROJECT_ID}/-/issues/{issue['iid']})" await inter.edit_original_message(content=f"✅ Bug signalé : [{title}]({url})")
)
else: else:
await inter.edit_original_message(content="❌ Erreur lors du signalement du bug.") await inter.edit_original_message(content="❌ Erreur lors du signalement sur Forgejo.")
except Exception as e:
await inter.edit_original_message(content=f"❌ Erreur lors du signalement du bug: {e}")
def setup(bot: commands.Bot): def setup(bot: commands.Bot):
bot.add_cog(GitLabIntegration(bot)) bot.add_cog(ForgejoIntegration(bot))