Merge branch 'dev' into 'main'

Dev

See merge request Omega_Kube/kuby!36
This commit is contained in:
Gameur 2026-06-25 17:49:09 +02:00
commit d5167448df
6 changed files with 1412 additions and 202 deletions

3
.gitignore vendored
View file

@ -37,3 +37,6 @@ logs/
.idea/
.DS_Store
.roo/
.continue/mcpServers/new-mcp-server.yaml
.continue/mcpServers/new-mcp-server-1.yaml
.gitignore

View file

176
commandes/team_json.py Normal file
View file

@ -0,0 +1,176 @@
# -*- coding: utf-8 -*-
"""
Team JSON Generator
===================
Ce module parcourt les membres d'un serveur Discord, filtre ceux qui possèdent
un ou plusieurs rôles spécifiés, télécharge leurs avatars et génère un fichier
JSON compatible avec le site de production.
Le JSON attendu ressemble à:
[
{
"id": "123456789012345678",
"name": "Nom affiché",
"role": "Nom du rôle filtré",
"avatar": "123456789012345678.png",
"bio": "",
"stats": [],
"skills": [],
"social": {"discord":"","twitter":"",...}
},
...
]
Le champ ``avatar`` correspond au nom de fichier stocké dans ``website/avatars/``.
"""
import os
import json
import aiohttp
from pathlib import Path
from typing import List
import disnake
from disnake.ext import commands, tasks
# ----------------------------------------------------------------------
# Configuration variables d'environnement (dans le .env du projet)
# ----------------------------------------------------------------------
TARGET_GUILD_ID = os.getenv("TARGET_GUILD_ID") # ID du serveur contenant l'équipe
# Liste d'IDs de rôles séparés par des virgules (ex. "123,456,789")
TEAM_ROLE_IDS = os.getenv("TEAM_ROLE_IDS", "").split(",") if os.getenv("TEAM_ROLE_IDS") else []
# Dossiers où seront stockés les avatars et le JSON final
AVATAR_ROOT = Path(__file__).parents[2] / "website" / "avatars"
AVATAR_ROOT.mkdir(parents=True, exist_ok=True)
JSON_OUTPUT = Path(__file__).parents[2] / "website" / "team.json"
# ----------------------------------------------------------------------
class TeamJSON(commands.Cog):
"""Cog responsable de la génération du JSON d'équipe et du téléchargement des avatars."""
def __init__(self, bot: commands.Bot):
self.bot = bot
# Démarre la tâche quotidienne (synchronisation 24h/24)
self.sync_daily.start()
# ------------------------------------------------------------------
@tasks.loop(hours=24)
async def sync_daily(self):
"""Tâche planifiée exécutée chaque jour (cron)."""
await self._generate_team(trigger="cron")
@sync_daily.before_loop
async def before_sync(self):
await self.bot.wait_until_ready()
# ------------------------------------------------------------------
async def _download_avatar(self, session: aiohttp.ClientSession, member: disnake.Member) -> str:
"""Télécharge l'avatar du *member* et le sauvegarde dans ``AVATAR_ROOT``.
Retourne le nom de fichier (ex. ``"123456789.png"``)."""
avatar_url = str(member.display_avatar.url)
filename = f"{member.id}.png"
dest_path = AVATAR_ROOT / filename
try:
async with session.get(avatar_url) as resp:
if resp.status == 200:
dest_path.write_bytes(await resp.read())
else:
self.bot.logger.warning(
f"[TeamJSON] Avatar download failed for {member.id} HTTP {resp.status}"
)
except Exception as exc:
self.bot.logger.error(f"[TeamJSON] Exception while downloading avatar {member.id}: {exc}")
return filename
# ------------------------------------------------------------------
async def _generate_team(self, *, trigger: str = "manual") -> None:
"""Construit le JSON de l'équipe et télécharge les avatars.
Le JSON produit suit exactement le format attendu par le site de production.
Les champs ``stats``, ``skills`` et ``social`` sont laissés vides car ils ne
sont pas disponibles via l'API bot.
"""
if not TARGET_GUILD_ID:
self.bot.logger.error("[TeamJSON] TARGET_GUILD_ID non défini arrêt du générateur.")
return
if not TEAM_ROLE_IDS:
self.bot.logger.error("[TeamJSON] TEAM_ROLE_IDS non défini arrêt du générateur.")
return
# Récupération de la guilde
guild = self.bot.get_guild(int(TARGET_GUILD_ID))
if not guild:
try:
guild = await self.bot.fetch_guild(int(TARGET_GUILD_ID))
except Exception as exc:
self.bot.logger.error(f"[TeamJSON] Impossible de récupérer la guilde {TARGET_GUILD_ID}: {exc}")
return
# Convertir les IDs de rôle en entiers pour la comparaison
role_ids = {int(r) for r in TEAM_ROLE_IDS if r.strip()}
members_data: List[dict] = []
async with aiohttp.ClientSession() as session:
async for member in guild.fetch_members(limit=None):
if member.bot:
continue
# Intersection entre les rôles du membre et les rôles ciblés
member_role_ids = {r.id for r in member.roles}
intersect = member_role_ids.intersection(role_ids)
if not intersect:
continue # le membre ne possède aucun des rôles recherchés
# On prend le premier rôle correspondant comme "role" dans le JSON
role_name = next((r.name for r in member.roles if r.id in intersect), "")
avatar_file = await self._download_avatar(session, member)
entry = {
"id": str(member.id),
"name": member.display_name,
"role": role_name,
"avatar": avatar_file,
"bio": "", # non récupérable via l'API bot
"stats": [],
"skills": [],
"social": {
"discord": "",
"twitter": "",
"instagram": "",
"youtube": "",
"tiktok": "",
"github": "",
},
}
members_data.append(entry)
# Écriture du JSON final
try:
JSON_OUTPUT.parent.mkdir(parents=True, exist_ok=True)
JSON_OUTPUT.write_text(
json.dumps(members_data, indent=2, ensure_ascii=False),
encoding="utf-8",
)
self.bot.logger.info(
f"[TeamJSON] JSON généré ({len(members_data)} membres) → {JSON_OUTPUT}"
)
except Exception as exc:
self.bot.logger.error(f"[TeamJSON] Échec de l'écriture du JSON : {exc}")
# ------------------------------------------------------------------
@commands.slash_command(
name="generate_team_json",
description="Génère le JSON de l'équipe (admin only)"
)
@commands.has_permissions(administrator=True)
async def generate_team_json(self, inter: disnake.ApplicationCommandInteraction):
"""Commande manuelle pour (re)générer le JSON et les avatars."""
await inter.response.defer(ephemeral=True)
await self._generate_team(trigger="manual")
await inter.edit_original_response(
content="✅ JSON de l'équipe généré et avatars synchronisés."
)
# ----------------------------------------------------------------------
def setup(bot: commands.Bot):
bot.add_cog(TeamJSON(bot))

View file

@ -127,18 +127,21 @@ class RatingSelect(disnake.ui.Select):
await interaction.response.defer()
class FeedbackModal(disnake.ui.Modal):
def __init__(self, view):
super().__init__(components=[], title="Votre avis")
"""Modal that collects a freeform comment for a ticket feedback."""
def __init__(self, view: FeedbackView):
self.view = view
self.comment = disnake.ui.TextInput(
label="Commentaire",
style=disnake.TextInputStyle.paragraph,
placeholder="Dites-nous ce que vous avez pensé du support...",
required=True,
max_length=1000
max_length=1000,
)
self.add_item(self.comment)
super().__init__(title="Votre avis", components=[self.comment])
async def callback(self, interaction: disnake.Interaction):
self.view.comment = self.comment.value
await interaction.response.send_message("Commentaire enregistré ! N'oubliez pas de valider avec 'Envoyer'.", ephemeral=True)
await interaction.response.send_message(
"Commentaire enregistré! N'oubliez pas de valider avec «Envoyer».",
ephemeral=True,
)

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,9 @@
{
"questions": [
{"label": "Pseudo Epic (Fortnite)", "custom_id": "pseudo", "placeholder": "JohnDoe", "required": true, "style": "short", "max_length": 16},
{"label": "Âge", "custom_id": "age", "placeholder": "18", "required": true, "style": "short", "max_length": 2},
{"label": "Expérience RP ?", "custom_id": "experience", "placeholder": "Oui/Non", "required": true, "style": "paragraph", "max_length": 50},
{"label": "Comment astu découvert le serveur ?", "custom_id": "decouverte", "placeholder": "", "required": true, "style": "short", "max_length": 20},
{"label": "Astu un micro de qualité ?", "custom_id": "micro", "placeholder": "", "required": true, "style": "short", "max_length": 3}
]
}