""" Module de gestion des informations serveur pour le bot Superviseur. Ce module permet de stocker et charger des informations sur les serveurs Discord de manière similaire au système de mémoire utilisateur. Structure : - Infos_serveurs/[nom_sanitisé]/[type].json Exemple: Infos_serveurs/Omega_Kube/roles.json Infos_serveurs/Omega_Kube/membres.json Infos_serveurs/Omega_Kube/config.json Usage : from ia.infos_serveurs import save_server_info, load_server_info, list_server_files # Sauvegarder des rôles await save_server_info("Omega_Kube", "roles", roles_data) # Charger des membres membres = await load_server_info("Omega_Kube", "membres") # Lister les fichiers d'un serveur fichiers = list_server_files("Omega_Kube") """ from __future__ import annotations import os import json import asyncio import aiofiles import logging import re from typing import Any, Dict, List, Optional # Configuration INFOS_DIR = os.environ.get("SUPERVISEUR_INFOS_DIR", "Infos_serveurs") # Logger logger = logging.getLogger('infos_serveurs') # Métriques metrics = { "total_saves": 0, "total_loads": 0, "server_count": 0 } def _sanitize_guild_name(name: str) -> str: """Sanitize guild name for use in filesystem path.""" # Remplacer les caractères invalides par _ sanitized = re.sub(r'[\/\\:*?"<>|\s]', '_', name) return sanitized def _get_server_dir(guild_id: str) -> str: """Get the directory path for a server.""" return os.path.join(INFOS_DIR, guild_id) def _get_file_path(guild_id: str, info_type: str) -> str: """Get the file path for a specific info type.""" server_dir = _get_server_dir(guild_id) os.makedirs(server_dir, exist_ok=True) return os.path.join(server_dir, f"{info_type}.json") def _ensure_infos_dir() -> None: """Ensure the root infos directory exists.""" os.makedirs(INFOS_DIR, exist_ok=True) logger.debug(f"Répertoire infos serveur assuré: {INFOS_DIR}") async def _atomic_write_async(path: str, data: Dict[str, Any]) -> None: """Atomic write using temporary file + replace.""" import tempfile dirpath = os.path.dirname(path) fd, tmp_path = tempfile.mkstemp(dir=dirpath) try: async with aiofiles.open(tmp_path, "w", encoding="utf-8") as f: await f.write(json.dumps(data, ensure_ascii=False, indent=2)) await f.flush() # Ensure durability at OS level try: with open(tmp_path, "rb") as f: os.fsync(f.fileno()) except Exception: pass os.replace(tmp_path, path) finally: if os.path.exists(tmp_path): try: os.remove(tmp_path) except Exception: pass async def save_server_info( guild_id: str, info_type: str, data: Dict[str, Any], persist_immediately: bool = True ) -> bool: """Sauvegarder des informations pour un serveur. Args: guild_id: Identifiant sanitarisé du serveur (ex: "Omega_Kube") info_type: Type d'information (ex: "roles", "membres", "config") data: Données à sauvegarder persist_immediately: Si True, écrit immédiatement sur disque Returns: True si succès, False sinon """ try: _ensure_infos_dir() file_path = _get_file_path(guild_id, info_type) if persist_immediately: await _atomic_write_async(file_path, data) else: # Écriture différée async with aiofiles.open(file_path, "w", encoding="utf-8") as f: await f.write(json.dumps(data, ensure_ascii=False, indent=2)) metrics["total_saves"] += 1 logger.debug(f"Infos sauvegardées: {guild_id}/{info_type}.json") return True except Exception as e: logger.error(f"Erreur lors de la sauvegarde {guild_id}/{info_type}: {e}") return False async def load_server_info( guild_id: str, info_type: str ) -> Optional[Dict[str, Any]]: """Charger des informations pour un serveur. Args: guild_id: Identifiant sanitarisé du serveur info_type: Type d'information à charger Returns: Les données chargées ou None si erreur/inexistant """ try: file_path = _get_file_path(guild_id, info_type) if not os.path.exists(file_path): logger.debug(f"Fichier inexistant: {file_path}") return None async with aiofiles.open(file_path, "r", encoding="utf-8") as f: content = await f.read() data = json.loads(content) metrics["total_loads"] += 1 logger.debug(f"Infos chargées: {guild_id}/{info_type}.json") return data except json.JSONDecodeError as e: logger.error(f"Erreur de parsing JSON pour {guild_id}/{info_type}: {e}") return None except Exception as e: logger.error(f"Erreur lors du chargement {guild_id}/{info_type}: {e}") return None def list_server_files(guild_id: str) -> List[str]: """Lister les fichiers disponibles pour un serveur. Args: guild_id: Identifiant sanitarisé du serveur Returns: Liste des noms de fichiers (sans extension .json) """ try: server_dir = _get_server_dir(guild_id) if not os.path.exists(server_dir): return [] files = [] for f in os.listdir(server_dir): if f.endswith(".json"): # Enlever l'extension .json files.append(f[:-5]) return files except Exception as e: logger.error(f"Erreur lors du listage des fichiers pour {guild_id}: {e}") return [] def list_all_servers() -> List[str]: """Lister tous les serveurs ayant des informations stockées. Returns: Liste des identifiants de serveurs """ try: _ensure_infos_dir() servers = [] for item in os.listdir(INFOS_DIR): item_path = os.path.join(INFOS_DIR, item) if os.path.isdir(item_path): servers.append(item) metrics["server_count"] = len(servers) return servers except Exception as e: logger.error(f"Erreur lors du listage des serveurs: {e}") return [] async def delete_server_info(guild_id: str, info_type: str) -> bool: """Supprimer un fichier d'information pour un serveur. Args: guild_id: Identifiant sanitarisé du serveur info_type: Type d'information à supprimer Returns: True si succès, False sinon """ try: file_path = _get_file_path(guild_id, info_type) if os.path.exists(file_path): os.remove(file_path) logger.debug(f"Infos supprimées: {guild_id}/{info_type}.json") return True return False except Exception as e: logger.error(f"Erreur lors de la suppression {guild_id}/{info_type}: {e}") return False async def delete_all_server_info(guild_id: str) -> int: """Supprimer toutes les informations d'un serveur. Args: guild_id: Identifiant sanitarisé du serveur Returns: Nombre de fichiers supprimés """ try: server_dir = _get_server_dir(guild_id) if not os.path.exists(server_dir): return 0 count = 0 for f in os.listdir(server_dir): if f.endswith(".json"): file_path = os.path.join(server_dir, f) os.remove(file_path) count += 1 logger.debug(f"Toutes les infos supprimées pour {guild_id}: {count} fichiers") return count except Exception as e: logger.error(f"Erreur lors de la suppression totale pour {guild_id}: {e}") return 0 def get_metrics() -> Dict[str, Any]: """Obtenir les métriques du module.""" return metrics.copy() # Fonctions de commodité pour les cas d'usage courants async def save_roles(guild_id: str, roles: List[Dict[str, Any]]) -> bool: """Sauvegarder la liste des rôles d'un serveur.""" data = { "timestamp": str(__import__('datetime').datetime.now(__import__('datetime').timezone.utc).isoformat()), "roles": roles } return await save_server_info(guild_id, "roles", data) async def save_members(guild_id: str, members: List[Dict[str, Any]]) -> bool: """Sauvegarder la liste des membres d'un serveur.""" data = { "timestamp": str(__import__('datetime').datetime.now(__import__('datetime').timezone.utc).isoformat()), "members": members } return await save_server_info(guild_id, "membres", data) async def save_config(guild_id: str, config: Dict[str, Any]) -> bool: """Sauvegarder la configuration d'un serveur.""" data = { "timestamp": str(__import__('datetime').datetime.now(__import__('datetime').timezone.utc).isoformat()), "config": config } return await save_server_info(guild_id, "config", data) async def load_roles(guild_id: str) -> Optional[List[Dict[str, Any]]]: """Charger les rôles d'un serveur.""" data = await load_server_info(guild_id, "roles") return data.get("roles") if data else None async def load_members(guild_id: str) -> Optional[List[Dict[str, Any]]]: """Charger les membres d'un serveur.""" data = await load_server_info(guild_id, "membres") return data.get("members") if data else None async def load_config(guild_id: str) -> Optional[Dict[str, Any]]: """Charger la configuration d'un serveur.""" data = await load_server_info(guild_id, "config") return data.get("config") if data else None # Tests unitaires def run_tests(): """Exécuter des tests basiques.""" import tempfile import shutil import asyncio test_dir = tempfile.mkdtemp() global INFOS_DIR original_dir = INFOS_DIR INFOS_DIR = test_dir try: guild_id = "Test_Serveur" # Test save/load data = {"test": "data", "numbers": [1, 2, 3]} result = asyncio.run(save_server_info(guild_id, "test_type", data)) assert result, "Save failed" loaded = asyncio.run(load_server_info(guild_id, "test_type")) assert loaded == data, "Load failed" # Test list files files = list_server_files(guild_id) assert "test_type" in files, "List files failed" # Test save_roles roles = [{"name": "Admin", "id": 123}, {"name": "Member", "id": 456}] result = asyncio.run(save_roles(guild_id, roles)) assert result, "Save roles failed" loaded_roles = asyncio.run(load_roles(guild_id)) assert loaded_roles == roles, "Load roles failed" # Test delete result = asyncio.run(delete_server_info(guild_id, "test_type")) assert result, "Delete failed" loaded = asyncio.run(load_server_info(guild_id, "test_type")) assert loaded is None, "Delete verification failed" print("Tous les tests d'Infos_serveurs passent !") finally: shutil.rmtree(test_dir) INFOS_DIR = original_dir if __name__ == "__main__": import asyncio async def demo(): guild_id = "Omega_Kube" # Exemple de sauvegarde de rôles roles = [ {"name": "Admin", "id": 123456789, "permissions": "all"}, {"name": "Modérateur", "id": 987654321, "permissions": "kick,ban,mute"} ] await save_roles(guild_id, roles) print(f"Rôles sauvegardés pour {guild_id}") # Exemple de sauvegarde de membres members = [ {"name": "Utilisateur1", "id": 111111111, "roles": ["Member"]}, {"name": "Utilisateur2", "id": 222222222, "roles": ["Admin", "Member"]} ] await save_members(guild_id, members) print(f"Membres sauvegardés pour {guild_id}") # Lister les fichiers fichiers = list_server_files(guild_id) print(f"Fichiers disponibles: {fichiers}") # Charger les rôles roles_charges = await load_roles(guild_id) print(f"Rôles chargés: {roles_charges}") # Métriques print(f"Métriques: {get_metrics()}") asyncio.run(demo()) run_tests()