Migration des intégration gitlab --> Forgejo
This commit is contained in:
parent
879355f792
commit
9b4fbabbc5
1 changed files with 91 additions and 156 deletions
|
|
@ -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"],
|
if issue:
|
||||||
requester_id=inter.author.id # ⚡ NOUVEAU: Passe le requester_id pour tracking auto
|
url = f"https://omegakubeserv.tail951d2f.ts.net/{FORGEJO_OWNER}/{FORGEJO_REPO}/issues/{issue['number']}"
|
||||||
)
|
await inter.edit_original_message(content=f"✅ Fonctionnalité créée : [{title}]({url})")
|
||||||
if issue:
|
else:
|
||||||
await inter.edit_original_message(
|
await inter.edit_original_message(content="❌ Erreur lors de la création sur Forgejo.")
|
||||||
content=f"✅ Fonctionnalité créée : [{title}](https://gitlab.com/{GITLAB_PROJECT_ID}/-/issues/{issue['iid']})"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
await inter.edit_original_message(content="❌ Erreur lors de la création de la fonctionnalité.")
|
|
||||||
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"],
|
if issue:
|
||||||
requester_id=inter.author.id # ⚡ NOUVEAU: Passe le requester_id pour tracking auto
|
url = f"https://omegakubeserv.tail951d2f.ts.net/{FORGEJO_OWNER}/{FORGEJO_REPO}/issues/{issue['number']}"
|
||||||
)
|
await inter.edit_original_message(content=f"✅ Bug signalé : [{title}]({url})")
|
||||||
if issue:
|
else:
|
||||||
await inter.edit_original_message(
|
await inter.edit_original_message(content="❌ Erreur lors du signalement sur Forgejo.")
|
||||||
content=f"✅ Bug signalé : [{title}](https://gitlab.com/{GITLAB_PROJECT_ID}/-/issues/{issue['iid']})"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
await inter.edit_original_message(content="❌ Erreur lors du signalement du bug.")
|
|
||||||
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))
|
||||||
Loading…
Add table
Add a link
Reference in a new issue