Merge branch 'dev' into 'main'
Dev See merge request Omega_Kube/kuby!36
This commit is contained in:
commit
d5167448df
6 changed files with 1412 additions and 202 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -36,4 +36,7 @@ logs/
|
|||
.vscode/
|
||||
.idea/
|
||||
.DS_Store
|
||||
.roo/
|
||||
.roo/
|
||||
.continue/mcpServers/new-mcp-server.yaml
|
||||
.continue/mcpServers/new-mcp-server-1.yaml
|
||||
.gitignore
|
||||
|
|
|
|||
0
TODO.md
0
TODO.md
176
commandes/team_json.py
Normal file
176
commandes/team_json.py
Normal 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))
|
||||
|
|
@ -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 free‑form 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
9
config/whitelist_questions.json
Normal file
9
config/whitelist_questions.json
Normal 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 as‑tu découvert le serveur ?", "custom_id": "decouverte", "placeholder": "", "required": true, "style": "short", "max_length": 20},
|
||||
{"label": "As‑tu un micro de qualité ?", "custom_id": "micro", "placeholder": "", "required": true, "style": "short", "max_length": 3}
|
||||
]
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue