221 lines
9.3 KiB
Python
221 lines
9.3 KiB
Python
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="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):
|
||
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))
|