Kuby/commandes/gitlab_integration.py
2026-05-19 23:01:34 +02:00

213 lines
8.7 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import os
import json
import asyncio
from datetime import datetime, timezone
from typing import Dict, Any, List
import aiohttp
import disnake
from disnake.ext import commands, tasks
# Environment variables for GitLab access
GITLAB_TOKEN = os.getenv("GITLAB_TOKEN")
GITLAB_PROJECT_ID = os.getenv("GITLAB_PROJECT_ID") # numeric ID or URL-encoded path
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")
# File to persist issue tracking information
ISSUE_TRACK_FILE = os.path.join(os.path.dirname(__file__), "gitlab_issues.json")
def load_issue_tracking() -> Dict[str, Any]:
if os.path.exists(ISSUE_TRACK_FILE):
with open(ISSUE_TRACK_FILE, "r", encoding="utf-8") as f:
return json.load(f)
return {}
def save_issue_tracking(data: Dict[str, Any]) -> None:
with open(ISSUE_TRACK_FILE, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
class GitLabIntegration(commands.Cog):
"""Cog providing slash commands to create GitLab issues (feature request / bug) and notify requesters when comments are added."""
def __init__(self, bot: commands.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.check_new_notes.start()
def cog_unload(self):
self.check_new_notes.cancel()
async def _create_gitlab_issue(self, title: str, description: str, labels: List[str]) -> Dict[str, Any]:
"""Create an issue on GitLab and return the JSON response."""
url = f"https://gitlab.com/api/v4/projects/{GITLAB_PROJECT_ID}/issues"
headers = {
"PRIVATE-TOKEN": GITLAB_TOKEN,
"Content-Type": "application/json",
}
payload = {
"title": title,
"description": description,
"labels": ",".join(labels),
}
async with aiohttp.ClientSession() as session:
async with session.post(url, headers=headers, json=payload) as resp:
resp.raise_for_status()
return await resp.json()
async def _get_issue_notes(self, issue_iid: int) -> List[Dict[str, Any]]:
"""Fetch all notes (comments) for a given issue."""
url = f"https://gitlab.com/api/v4/projects/{GITLAB_PROJECT_ID}/issues/{issue_iid}/notes"
headers = {"PRIVATE-TOKEN": GITLAB_TOKEN}
async with aiohttp.ClientSession() as session:
async with session.get(url, headers=headers) as resp:
resp.raise_for_status()
return await resp.json()
async def _notify_requester(self, requester_id: int, issue_iid: int, note: Dict[str, Any]):
"""Send a private DM to the requester informing them of a new comment."""
user = await self.bot.fetch_user(requester_id)
author = note.get("author", {}).get("name", "Someone")
body = note.get("body", "")
embed = disnake.Embed(
title=f"Nouveau commentaire sur votre demande GitLab (#{issue_iid})",
description=body,
color=disnake.Color.blurple(),
timestamp=datetime.fromisoformat(note["created_at"]).replace(tzinfo=timezone.utc),
)
embed.set_footer(text=f"Commentaire de {author}")
try:
await user.send(embed=embed)
except disnake.HTTPException:
# The user may have DMs disabled; ignore silently.
pass
@tasks.loop(minutes=1)
async def check_new_notes(self):
"""Periodic task that checks for new notes on tracked issues and notifies requesters."""
for issue_iid_str, info in list(self.issue_data.items()):
issue_iid = int(issue_iid_str)
try:
notes = await self._get_issue_notes(issue_iid)
except Exception as exc:
# Skip this issue if we cannot fetch notes.
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 ----------
@commands.slash_command(name="suggerer_fonctionnalite", description="Suggérer une fonctionnalité pour le projet")
async def suggere_fonctionnalite(self, inter: disnake.ApplicationCommandInteraction):
await inter.response.send_modal(modal=FeatureModal(self))
# ---------- Slash command: /signaler_bug ----------
@commands.slash_command(name="signaler_bug", description="Signaler un bug du projet")
async def signaler_bug(self, inter: disnake.ApplicationCommandInteraction):
await inter.response.send_modal(modal=BugModal(self))
# ---------- Modals ----------
class FeatureModal(disnake.Modal):
def __init__(self, cog: GitLabIntegration):
components = [
disnake.ui.TextInput(
label="Titre de la fonctionnalité",
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__(
title="Suggestion de fonctionnalité",
custom_id="feature_modal",
components=components,
)
self.cog = cog
async def callback(self, inter: disnake.ModalInteraction):
title = inter.text_values["title"]
description = inter.text_values["description"]
await inter.response.defer(ephemeral=True)
try:
issue = await self.cog._create_gitlab_issue(
title=title,
description=description,
labels=["feature", "discord-bot"],
)
# Store tracking info
issue_iid = str(issue["iid"])
self.cog.issue_data[issue_iid] = {
"requester_id": inter.author.id,
"last_note_id": 0,
"type": "feature",
}
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:
await inter.edit_original_message(content=f"❌ Erreur lors de la création de la fonctionnalité: {e}")
class BugModal(disnake.Modal):
def __init__(self, cog: GitLabIntegration):
components = [
disnake.ui.TextInput(
label="Titre du bug",
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__(
title="Signalement de bug",
custom_id="bug_modal",
components=components,
)
self.cog = cog
async def callback(self, inter: disnake.ModalInteraction):
title = inter.text_values["title"]
description = inter.text_values["description"]
await inter.response.defer(ephemeral=True)
try:
issue = await self.cog._create_gitlab_issue(
title=title,
description=description,
labels=["bug", "discord-bot"],
)
issue_iid = str(issue["iid"])
self.cog.issue_data[issue_iid] = {
"requester_id": inter.author.id,
"last_note_id": 0,
"type": "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:
await inter.edit_original_message(content=f"❌ Erreur lors du signalement du bug: {e}")
def setup(bot: commands.Bot):
bot.add_cog(GitLabIntegration(bot))