Ajout de la gestion des jeux gratuits et des invitations Discord. Création des modèles FreeGame et DiscordInvite, mise à jour de la base de données et des fichiers de configuration. Intégration de nouvelles fonctionnalités pour synchroniser et révoquer les invitations, ainsi que l'ajout d'un flux RSS pour les jeux gratuits. Amélioration de l'interface d'administration avec des statistiques et des options de gestion des serveurs.

This commit is contained in:
Mow
2025-12-06 11:25:45 +01:00
parent 54b014c4c8
commit 42db84f010
23 changed files with 3671 additions and 697 deletions
+241
View File
@@ -0,0 +1,241 @@
import threading
import asyncio
import logging
from dataclasses import dataclass, field
from typing import Dict, List, Optional, Callable
from datetime import datetime
@dataclass
class BotStats:
"""Statistiques des bots"""
# Discord
discord_connected: bool = False
discord_guilds: int = 0
discord_members: int = 0
discord_channels: int = 0
discord_bot_name: str = ""
discord_bot_id: int = 0
# Twitch
twitch_connected: bool = False
twitch_channel: str = ""
# Cogs/Fonctionnalités activées
cogs_enabled: Dict[str, bool] = field(default_factory=dict)
# Dernière mise à jour
last_update: datetime = None
class StatsManager:
"""Gestionnaire thread-safe des statistiques"""
_instance = None
_lock = threading.Lock()
def __new__(cls):
if cls._instance is None:
with cls._lock:
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._stats = BotStats()
cls._instance._stats_lock = threading.Lock()
return cls._instance
def update_discord_stats(self, connected: bool = None, guilds: int = None,
members: int = None, channels: int = None,
bot_name: str = None, bot_id: int = None):
"""Met à jour les stats Discord"""
with self._stats_lock:
if connected is not None:
self._stats.discord_connected = connected
if guilds is not None:
self._stats.discord_guilds = guilds
if members is not None:
self._stats.discord_members = members
if channels is not None:
self._stats.discord_channels = channels
if bot_name is not None:
self._stats.discord_bot_name = bot_name
if bot_id is not None:
self._stats.discord_bot_id = bot_id
self._stats.last_update = datetime.now()
def update_twitch_stats(self, connected: bool = None, channel: str = None):
"""Met à jour les stats Twitch"""
with self._stats_lock:
if connected is not None:
self._stats.twitch_connected = connected
if channel is not None:
self._stats.twitch_channel = channel
self._stats.last_update = datetime.now()
def update_cogs(self, cogs: Dict[str, bool]):
"""Met à jour les cogs activés"""
with self._stats_lock:
self._stats.cogs_enabled = cogs.copy()
self._stats.last_update = datetime.now()
def get_stats(self) -> BotStats:
"""Retourne une copie des stats"""
with self._stats_lock:
return BotStats(
discord_connected=self._stats.discord_connected,
discord_guilds=self._stats.discord_guilds,
discord_members=self._stats.discord_members,
discord_channels=self._stats.discord_channels,
discord_bot_name=self._stats.discord_bot_name,
discord_bot_id=self._stats.discord_bot_id,
twitch_connected=self._stats.twitch_connected,
twitch_channel=self._stats.twitch_channel,
cogs_enabled=self._stats.cogs_enabled.copy(),
last_update=self._stats.last_update
)
class DiscordBridge:
"""
Pont de communication entre Flask et le bot Discord.
Permet d'exécuter des actions Discord depuis Flask de manière thread-safe.
"""
_instance = None
_lock = threading.Lock()
def __new__(cls):
if cls._instance is None:
with cls._lock:
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._bot = None
cls._instance._loop = None
return cls._instance
def register_bot(self, bot, loop):
"""Enregistre le bot Discord et sa boucle événementielle"""
with self._lock:
self._bot = bot
self._loop = loop
logging.info("Bot Discord enregistré dans le bridge")
def is_ready(self) -> bool:
"""Vérifie si le bot est prêt"""
return self._bot is not None and self._loop is not None and not self._loop.is_closed()
def get_text_channels(self) -> List[Dict]:
"""Retourne la liste des canaux texte disponibles"""
if not self.is_ready():
return []
channels = []
try:
for guild in self._bot.guilds:
for channel in guild.text_channels:
channels.append({
'id': channel.id,
'name': channel.name,
'guild_name': guild.name,
'guild_id': guild.id
})
except Exception as e:
logging.error(f"Erreur lors de la récupération des canaux: {e}")
return channels
def send_message(self, channel_id: int, message: str) -> tuple[bool, str]:
"""
Envoie un message dans un canal Discord.
Retourne (succès, message d'erreur ou de confirmation)
"""
if not self.is_ready():
return False, "Le bot Discord n'est pas connecté"
if not message or not message.strip():
return False, "Le message ne peut pas être vide"
try:
future = asyncio.run_coroutine_threadsafe(
self._send_message_async(channel_id, message),
self._loop
)
# Attendre le résultat avec timeout de 10 secondes
return future.result(timeout=10)
except asyncio.TimeoutError:
return False, "Timeout lors de l'envoi du message"
except Exception as e:
logging.error(f"Erreur lors de l'envoi du message: {e}")
return False, f"Erreur: {str(e)}"
async def _send_message_async(self, channel_id: int, message: str) -> tuple[bool, str]:
"""Coroutine interne pour envoyer le message"""
try:
channel = self._bot.get_channel(channel_id)
if not channel:
return False, "Canal introuvable"
await channel.send(message)
return True, f"Message envoyé dans #{channel.name}"
except Exception as e:
return False, f"Erreur: {str(e)}"
def sync_invites(self, guild_id: int = None) -> dict:
"""Synchronise les invitations Discord avec la base de données"""
if not self.is_ready():
return {'success': False, 'message': "Le bot Discord n'est pas connecté", 'synced': 0}
try:
future = asyncio.run_coroutine_threadsafe(
self._bot.syncInvites(guild_id),
self._loop
)
result = future.result(timeout=30)
result['success'] = True
return result
except asyncio.TimeoutError:
return {'success': False, 'message': "Timeout lors de la synchronisation", 'synced': 0}
except Exception as e:
logging.error(f"Erreur lors de la synchronisation des invitations: {e}")
return {'success': False, 'message': f"Erreur: {str(e)}", 'synced': 0}
def revoke_invite(self, invite_code: str) -> dict:
"""Révoque une invitation Discord"""
if not self.is_ready():
return {'success': False, 'message': "Le bot Discord n'est pas connecté"}
try:
future = asyncio.run_coroutine_threadsafe(
self._bot.revokeInvite(invite_code),
self._loop
)
return future.result(timeout=10)
except asyncio.TimeoutError:
return {'success': False, 'message': "Timeout lors de la révocation"}
except Exception as e:
logging.error(f"Erreur lors de la révocation de l'invitation: {e}")
return {'success': False, 'message': f"Erreur: {str(e)}"}
def get_invites(self, guild_id: int = None, include_revoked: bool = False) -> list:
"""Récupère les invitations depuis la base de données"""
if not self.is_ready():
return []
try:
return self._bot.getInvites(guild_id, include_revoked)
except Exception as e:
logging.error(f"Erreur lors de la récupération des invitations: {e}")
return []
def get_guilds(self) -> list:
"""Retourne la liste des guilds"""
if not self.is_ready():
return []
try:
return self._bot.getAllGuilds()
except Exception as e:
logging.error(f"Erreur lors de la récupération des guilds: {e}")
return []
# Instances globales
stats_manager = StatsManager()
discord_bridge = DiscordBridge()