242 lines
8.8 KiB
Python
242 lines
8.8 KiB
Python
|
|
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()
|
|
|