Voice et bot modif
This commit is contained in:
parent
189d56026b
commit
7333a22bcd
10774 changed files with 634644 additions and 933308 deletions
6
core/__init__.py
Normal file
6
core/__init__.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
"""Superviseur Bot - Discord bot with LLM integration."""
|
||||
|
||||
from .bot import Superviseur
|
||||
|
||||
__all__ = ['Superviseur']
|
||||
|
||||
BIN
core/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
core/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
core/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
core/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
core/__pycache__/action_router.cpython-312.pyc
Normal file
BIN
core/__pycache__/action_router.cpython-312.pyc
Normal file
Binary file not shown.
BIN
core/__pycache__/action_router.cpython-313.pyc
Normal file
BIN
core/__pycache__/action_router.cpython-313.pyc
Normal file
Binary file not shown.
BIN
core/__pycache__/bot.cpython-312.pyc
Normal file
BIN
core/__pycache__/bot.cpython-312.pyc
Normal file
Binary file not shown.
BIN
core/__pycache__/bot.cpython-313.pyc
Normal file
BIN
core/__pycache__/bot.cpython-313.pyc
Normal file
Binary file not shown.
BIN
core/__pycache__/commands.cpython-312.pyc
Normal file
BIN
core/__pycache__/commands.cpython-312.pyc
Normal file
Binary file not shown.
BIN
core/__pycache__/commands.cpython-313.pyc
Normal file
BIN
core/__pycache__/commands.cpython-313.pyc
Normal file
Binary file not shown.
BIN
core/__pycache__/context.cpython-312.pyc
Normal file
BIN
core/__pycache__/context.cpython-312.pyc
Normal file
Binary file not shown.
BIN
core/__pycache__/context.cpython-313.pyc
Normal file
BIN
core/__pycache__/context.cpython-313.pyc
Normal file
Binary file not shown.
BIN
core/__pycache__/dispatcher.cpython-312.pyc
Normal file
BIN
core/__pycache__/dispatcher.cpython-312.pyc
Normal file
Binary file not shown.
BIN
core/__pycache__/dispatcher.cpython-313.pyc
Normal file
BIN
core/__pycache__/dispatcher.cpython-313.pyc
Normal file
Binary file not shown.
BIN
core/__pycache__/llm.cpython-312.pyc
Normal file
BIN
core/__pycache__/llm.cpython-312.pyc
Normal file
Binary file not shown.
BIN
core/__pycache__/llm.cpython-313.pyc
Normal file
BIN
core/__pycache__/llm.cpython-313.pyc
Normal file
Binary file not shown.
BIN
core/__pycache__/messaging.cpython-312.pyc
Normal file
BIN
core/__pycache__/messaging.cpython-312.pyc
Normal file
Binary file not shown.
BIN
core/__pycache__/messaging.cpython-313.pyc
Normal file
BIN
core/__pycache__/messaging.cpython-313.pyc
Normal file
Binary file not shown.
BIN
core/__pycache__/permissions.cpython-312.pyc
Normal file
BIN
core/__pycache__/permissions.cpython-312.pyc
Normal file
Binary file not shown.
BIN
core/__pycache__/permissions.cpython-313.pyc
Normal file
BIN
core/__pycache__/permissions.cpython-313.pyc
Normal file
Binary file not shown.
BIN
core/__pycache__/redis_worker.cpython-312.pyc
Normal file
BIN
core/__pycache__/redis_worker.cpython-312.pyc
Normal file
Binary file not shown.
BIN
core/__pycache__/redis_worker.cpython-313.pyc
Normal file
BIN
core/__pycache__/redis_worker.cpython-313.pyc
Normal file
Binary file not shown.
BIN
core/__pycache__/tasks.cpython-312.pyc
Normal file
BIN
core/__pycache__/tasks.cpython-312.pyc
Normal file
Binary file not shown.
BIN
core/__pycache__/tasks.cpython-313.pyc
Normal file
BIN
core/__pycache__/tasks.cpython-313.pyc
Normal file
Binary file not shown.
BIN
core/__pycache__/utils.cpython-312.pyc
Normal file
BIN
core/__pycache__/utils.cpython-312.pyc
Normal file
Binary file not shown.
BIN
core/__pycache__/utils.cpython-313.pyc
Normal file
BIN
core/__pycache__/utils.cpython-313.pyc
Normal file
Binary file not shown.
BIN
core/__pycache__/voice.cpython-312.pyc
Normal file
BIN
core/__pycache__/voice.cpython-312.pyc
Normal file
Binary file not shown.
BIN
core/__pycache__/voice.cpython-313.pyc
Normal file
BIN
core/__pycache__/voice.cpython-313.pyc
Normal file
Binary file not shown.
BIN
core/__pycache__/whisper_worker.cpython-312.pyc
Normal file
BIN
core/__pycache__/whisper_worker.cpython-312.pyc
Normal file
Binary file not shown.
BIN
core/__pycache__/whisper_worker.cpython-313.pyc
Normal file
BIN
core/__pycache__/whisper_worker.cpython-313.pyc
Normal file
Binary file not shown.
28
core/bot.py
28
core/bot.py
|
|
@ -171,7 +171,14 @@ class Superviseur(commands.Bot):
|
|||
"""Internal telemetry log - bypassing AI to preserve resources."""
|
||||
if not self.introspection_channel_id: return
|
||||
|
||||
channel = self.get_channel(self.introspection_channel_id) or await self.fetch_channel(self.introspection_channel_id)
|
||||
channel = self.get_channel(self.introspection_channel_id)
|
||||
if not channel:
|
||||
try:
|
||||
channel = await self.fetch_channel(self.introspection_channel_id)
|
||||
except (discord.NotFound, discord.Forbidden, discord.HTTPException) as e:
|
||||
logger.warning(f"Introspection channel {self.introspection_channel_id} not accessible: {e}")
|
||||
return
|
||||
|
||||
if channel:
|
||||
await self.messaging.send_embed(
|
||||
channel,
|
||||
|
|
@ -254,7 +261,9 @@ class Superviseur(commands.Bot):
|
|||
if message.author == self.user:
|
||||
return
|
||||
|
||||
logger.debug(f"Message received from {message.author} in {message.channel}")
|
||||
channel_name = message.channel.name if hasattr(message.channel, 'name') else "DM"
|
||||
guild_name = message.guild.name if message.guild else "DM"
|
||||
logger.debug(f"Message received from {message.author} in #{channel_name} ({guild_name})")
|
||||
|
||||
# 1. Silent Moderation Scan (Background)
|
||||
# On ne bloque pas le bot pour l'analyse, mais on la lance pour chaque message
|
||||
|
|
@ -271,7 +280,10 @@ class Superviseur(commands.Bot):
|
|||
if not self._should_respond(message):
|
||||
return
|
||||
|
||||
logger.info(f"◈ SYSTEM: Interaction directe détectée pour '{message.author.display_name}'")
|
||||
# Show server and channel context
|
||||
guild_name = message.guild.name if message.guild else "DM"
|
||||
channel_name = message.channel.name if hasattr(message.channel, 'name') else "DM"
|
||||
logger.info(f"◈ SYSTEM: Interaction directe détectée pour '{message.author.display_name}' dans #{channel_name} ({guild_name})")
|
||||
# Direct interaction (AI Response)
|
||||
await self._handle_ai_interaction(message)
|
||||
|
||||
|
|
@ -342,18 +354,22 @@ class Superviseur(commands.Bot):
|
|||
|
||||
def _should_respond(self, message) -> bool:
|
||||
"""Check if bot should respond to message."""
|
||||
guild_name = message.guild.name if message.guild else "DM"
|
||||
channel_name = message.channel.name if hasattr(message.channel, 'name') else "DM"
|
||||
location = f"#{channel_name} ({guild_name})"
|
||||
|
||||
if message.content.startswith(self.command_prefix):
|
||||
return True
|
||||
if self.user in message.mentions:
|
||||
logger.info("◈ DEBUG: Triggered by standard mentions list")
|
||||
logger.info(f"◈ DEBUG: Triggered by mentions list in {location}")
|
||||
return True
|
||||
|
||||
# Robust Mention Check (Regex Fallback)
|
||||
bot_id = str(self.user.id)
|
||||
logger.info(f"◈ DEBUG: Checking mention for ID {bot_id} in '{message.content[:30]}...'")
|
||||
logger.info(f"◈ DEBUG: Checking mention for ID {bot_id} in '{message.content[:30]}...' ({location})")
|
||||
|
||||
if f"<@{bot_id}>" in message.content or f"<@!{bot_id}>" in message.content:
|
||||
logger.info("◈ DEBUG: Triggered by string match (Regex Fallback)")
|
||||
logger.info(f"◈ DEBUG: Triggered by string match in {location}")
|
||||
return True
|
||||
|
||||
|
||||
|
|
|
|||
379
core/commands.py
Normal file
379
core/commands.py
Normal file
|
|
@ -0,0 +1,379 @@
|
|||
"""Command definitions for Superviseur bot."""
|
||||
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
|
||||
|
||||
class ChannelCommands(commands.Cog):
|
||||
"""Commands for channel management."""
|
||||
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
|
||||
@commands.command(name="create_channel")
|
||||
async def cmd_create_channel(self, ctx, *args):
|
||||
if not ctx.author.guild_permissions.manage_channels:
|
||||
await ctx.send("Vous n'avez pas les permissions nécessaires.")
|
||||
return
|
||||
if len(args) == 0:
|
||||
await ctx.send("Usage: !create_channel name1 [name2 ...] [category_name type]")
|
||||
return
|
||||
|
||||
if len(args) >= 3 and args[-2].lower() in ['textuel', 'vocal', 'text', 'voice']:
|
||||
channels = args[:-2]
|
||||
category = args[-2]
|
||||
type_ = args[-1]
|
||||
elif len(args) >= 2 and args[-1].lower() in ['textuel', 'vocal', 'text', 'voice']:
|
||||
channels = args[:-1]
|
||||
category = None
|
||||
type_ = args[-1].lower()
|
||||
if type_ == 'text': type_ = 'textuel'
|
||||
elif type_ == 'voice': type_ = 'vocal'
|
||||
else:
|
||||
channels = args
|
||||
category = None
|
||||
type_ = 'textuel'
|
||||
|
||||
params = {'channels': [{'channel_name': name, 'category_name': category, 'channel_type': type_} for name in channels]}
|
||||
from commandes.salons.creer import execute
|
||||
await execute(self.bot, params, ctx.message)
|
||||
|
||||
@commands.command(name="delete_channel")
|
||||
async def cmd_delete_channel(self, ctx, *channels):
|
||||
if not ctx.author.guild_permissions.manage_channels:
|
||||
await ctx.send("Vous n'avez pas les permissions nécessaires.")
|
||||
return
|
||||
if len(channels) == 0:
|
||||
await ctx.send("Usage: !delete_channel channel1 [channel2 ...]")
|
||||
return
|
||||
params = {'channels': [{'channel_name': name} for name in channels]}
|
||||
from commandes.salons.supprimer import execute
|
||||
await execute(self.bot, params, ctx.message)
|
||||
|
||||
@commands.command(name="move_channel")
|
||||
async def cmd_move_channel(self, ctx, *args):
|
||||
if not ctx.author.guild_permissions.manage_channels:
|
||||
await ctx.send("Vous n'avez pas les permissions nécessaires.")
|
||||
return
|
||||
if len(args) % 2 != 0:
|
||||
await ctx.send("Usage: !move_channel channel1 new_category [channel2 new_category ...]")
|
||||
return
|
||||
params = {'channels': [{'channel_name': args[i], 'new_category_name': args[i+1]} for i in range(0, len(args), 2)]}
|
||||
from commandes.salons.deplacer import execute
|
||||
await execute(self.bot, params, ctx.message)
|
||||
|
||||
@commands.command(name="modify_channel")
|
||||
async def cmd_modify_channel(self, ctx, channel_name, new_name=None, new_category=None, topic=None, user_limit=None):
|
||||
if not ctx.author.guild_permissions.manage_channels:
|
||||
await ctx.send("Vous n'avez pas les permissions nécessaires.")
|
||||
return
|
||||
params = {'channels': [{'channel_name': channel_name, 'new_name': new_name, 'new_category_name': new_category, 'topic': topic, 'user_limit': int(user_limit) if user_limit else None}]}
|
||||
from commandes.salons.modifier import execute
|
||||
await execute(self.bot, params, ctx.message)
|
||||
|
||||
@commands.command(name="rename_channel")
|
||||
async def cmd_rename_channel(self, ctx, *args):
|
||||
if not ctx.author.guild_permissions.manage_channels:
|
||||
await ctx.send("Vous n'avez pas les permissions nécessaires.")
|
||||
return
|
||||
if len(args) % 2 != 0:
|
||||
await ctx.send("Usage: !rename_channel channel1 new_name [channel2 new_name ...]")
|
||||
return
|
||||
params = {'channels': [{'channel_name': args[i], 'new_name': args[i+1]} for i in range(0, len(args), 2)]}
|
||||
from commandes.salons.renommer import execute
|
||||
await execute(self.bot, params, ctx.message)
|
||||
|
||||
|
||||
class CategoryCommands(commands.Cog):
|
||||
"""Commands for category management."""
|
||||
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
|
||||
@commands.command(name="create_category")
|
||||
async def cmd_create_category(self, ctx, *categories):
|
||||
if not ctx.author.guild_permissions.manage_channels:
|
||||
await ctx.send("Vous n'avez pas les permissions nécessaires.")
|
||||
return
|
||||
if len(categories) == 0:
|
||||
await ctx.send("Usage: !create_category name1 [name2 ...]")
|
||||
return
|
||||
params = {'categories': [{'category_name': name} for name in categories]}
|
||||
from commandes.categories.creer import execute
|
||||
await execute(self.bot, params, ctx.message)
|
||||
|
||||
@commands.command(name="delete_category")
|
||||
async def cmd_delete_category(self, ctx, *categories):
|
||||
if not ctx.author.guild_permissions.manage_channels:
|
||||
await ctx.send("Vous n'avez pas les permissions nécessaires.")
|
||||
return
|
||||
if len(categories) == 0:
|
||||
await ctx.send("Usage: !delete_category name1 [name2 ...]")
|
||||
return
|
||||
params = {'categories': [{'category_name': name} for name in categories]}
|
||||
from commandes.categories.supprimer import execute
|
||||
await execute(self.bot, params, ctx.message)
|
||||
|
||||
@commands.command(name="modify_category")
|
||||
async def cmd_modify_category(self, ctx, category_name, new_name=None):
|
||||
if not ctx.author.guild_permissions.manage_channels:
|
||||
await ctx.send("Vous n'avez pas les permissions nécessaires.")
|
||||
return
|
||||
if new_name is None:
|
||||
await ctx.send("Usage: !modify_category category_name new_name")
|
||||
return
|
||||
params = {'categories': [{'category_name': category_name, 'new_name': new_name}]}
|
||||
from commandes.categories.modifier import execute
|
||||
await execute(self.bot, params, ctx.message)
|
||||
|
||||
@commands.command(name="rename_category")
|
||||
async def cmd_rename_category(self, ctx, *args):
|
||||
if not ctx.author.guild_permissions.manage_channels:
|
||||
await ctx.send("Vous n'avez pas les permissions nécessaires.")
|
||||
return
|
||||
if len(args) % 2 != 0:
|
||||
await ctx.send("Usage: !rename_category category1 new_name [category2 new_name ...]")
|
||||
return
|
||||
params = {'categories': [{'category_name': args[i], 'new_name': args[i+1]} for i in range(0, len(args), 2)]}
|
||||
from commandes.categories.renommer import execute
|
||||
await execute(self.bot, params, ctx.message)
|
||||
|
||||
@commands.command(name="move_category")
|
||||
async def cmd_move_category(self, ctx, *args):
|
||||
if not ctx.author.guild_permissions.manage_channels:
|
||||
await ctx.send("Vous n'avez pas les permissions nécessaires.")
|
||||
return
|
||||
if len(args) % 2 != 0:
|
||||
await ctx.send("Usage: !move_category category1 position [category2 position ...]")
|
||||
return
|
||||
try:
|
||||
params = {'categories': [{'category_name': args[i], 'position': int(args[i+1])} for i in range(0, len(args), 2)]}
|
||||
except ValueError:
|
||||
await ctx.send("La position doit être un nombre entier.")
|
||||
return
|
||||
from commandes.categories.deplacer import execute
|
||||
await execute(self.bot, params, ctx.message)
|
||||
|
||||
|
||||
class SecurityCommands(commands.Cog):
|
||||
"""Commands for security/moderation."""
|
||||
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
|
||||
@commands.command(name="kick")
|
||||
async def cmd_kick(self, ctx, *users_mentions_or_ids):
|
||||
if not ctx.author.guild_permissions.kick_members:
|
||||
await ctx.send("Vous n'avez pas les permissions nécessaires pour expulser des membres.")
|
||||
return
|
||||
if len(users_mentions_or_ids) == 0:
|
||||
await ctx.send("Usage: !kick @user1 @user2 ...")
|
||||
return
|
||||
params = {'users': [{'target_user_mention': user} for user in users_mentions_or_ids]}
|
||||
from commandes.security.kick import execute
|
||||
await execute(self.bot, params, ctx.message)
|
||||
|
||||
@commands.command(name="ban")
|
||||
async def cmd_ban(self, ctx, *users_mentions_or_ids):
|
||||
if not ctx.author.guild_permissions.ban_members:
|
||||
await ctx.send("Vous n'avez pas les permissions nécessaires pour bannir des membres.")
|
||||
return
|
||||
if len(users_mentions_or_ids) == 0:
|
||||
await ctx.send("Usage: !ban @user1 @user2 ...")
|
||||
return
|
||||
params = {'users': [{'target_user_mention': user} for user in users_mentions_or_ids]}
|
||||
from commandes.security.ban import execute
|
||||
await execute(self.bot, params, ctx.message)
|
||||
|
||||
@commands.command(name="unban")
|
||||
async def cmd_unban(self, ctx, *users_mentions_or_ids):
|
||||
if not ctx.author.guild_permissions.ban_members:
|
||||
await ctx.send("Vous n'avez pas les permissions nécessaires pour débannir des membres.")
|
||||
return
|
||||
if len(users_mentions_or_ids) == 0:
|
||||
await ctx.send("Usage: !unban user_id1 user_id2 ...")
|
||||
return
|
||||
params = {'users': [{'target_user_mention': user} for user in users_mentions_or_ids]}
|
||||
from commandes.security.unban import execute
|
||||
await execute(self.bot, params, ctx.message)
|
||||
|
||||
@commands.command(name="mute")
|
||||
async def cmd_mute(self, ctx, user_mention_or_id, duration_minutes: int, reason=None):
|
||||
if not ctx.author.guild_permissions.moderate_members:
|
||||
await ctx.send("Vous n'avez pas les permissions nécessaires pour muter des membres.")
|
||||
return
|
||||
params = {'users': [{'target_user_mention': user_mention_or_id, 'duration': duration_minutes, 'reason': reason or "Aucune raison spécifiée"}]}
|
||||
from commandes.security.mute import execute
|
||||
await execute(self.bot, params, ctx.message)
|
||||
|
||||
@commands.command(name="timeout")
|
||||
async def cmd_timeout(self, ctx, user_mention_or_id, duration_minutes: int, reason=None):
|
||||
if not ctx.author.guild_permissions.moderate_members:
|
||||
await ctx.send("Vous n'avez pas les permissions nécessaires pour appliquer un timeout.")
|
||||
return
|
||||
params = {'users': [{'target_user_mention': user_mention_or_id, 'duration': duration_minutes, 'reason': reason or "Aucune raison spécifiée"}]}
|
||||
from commandes.security.timeout import execute
|
||||
await execute(self.bot, params, ctx.message)
|
||||
|
||||
@commands.command(name="purge")
|
||||
async def cmd_purge(self, ctx, amount: int, channel_name=None):
|
||||
if not ctx.author.guild_permissions.manage_messages:
|
||||
await ctx.send("Vous n'avez pas les permissions nécessaires pour purger des messages.")
|
||||
return
|
||||
params = {'purges': [{'amount': amount, 'channel_name': channel_name or ctx.channel.name}]}
|
||||
from commandes.security.purge import execute
|
||||
await execute(self.bot, params, ctx.message)
|
||||
|
||||
@commands.command(name="warn")
|
||||
async def cmd_warn(self, ctx, user_mention_or_id, *, reason):
|
||||
if not ctx.author.guild_permissions.moderate_members:
|
||||
await ctx.send("Vous n'avez pas les permissions nécessaires pour avertir des membres.")
|
||||
return
|
||||
params = {'warnings': [{'target_user_mention': user_mention_or_id, 'reason': reason}]}
|
||||
from commandes.security.warn import execute
|
||||
await execute(self.bot, params, ctx.message)
|
||||
|
||||
@commands.command(name="list_warnings")
|
||||
async def cmd_list_warnings(self, ctx, user_mention_or_id=None):
|
||||
if not ctx.author.guild_permissions.moderate_members:
|
||||
await ctx.send("Vous n'avez pas les permissions nécessaires pour lister les avertissements.")
|
||||
return
|
||||
params = {'target_user_mention': user_mention_or_id}
|
||||
from commandes.mod.list_warnings import execute
|
||||
await execute(self.bot, params, ctx.message)
|
||||
|
||||
|
||||
class OtherCommands(commands.Cog):
|
||||
"""Miscellaneous commands."""
|
||||
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
|
||||
@commands.command(name="set_welcome_channel")
|
||||
async def cmd_set_welcome_channel(self, ctx, channel_name):
|
||||
if not ctx.author.guild_permissions.manage_guild:
|
||||
await ctx.send("Vous n'avez pas les permissions nécessaires pour définir le salon de bienvenue.")
|
||||
return
|
||||
params = {'channel_name': channel_name}
|
||||
from commandes.autres.setwelcomechannel import execute
|
||||
await execute(self.bot, params, ctx.message)
|
||||
|
||||
@commands.command(name="set_goodbye_channel")
|
||||
async def cmd_set_goodbye_channel(self, ctx, channel_name):
|
||||
if not ctx.author.guild_permissions.manage_guild:
|
||||
await ctx.send("Vous n'avez pas les permissions nécessaires pour définir le salon d'au revoir.")
|
||||
return
|
||||
params = {'channel_name': channel_name}
|
||||
from commandes.autres.setgoodbyechannel import execute
|
||||
await execute(self.bot, params, ctx.message)
|
||||
|
||||
@commands.command(name="set_mute_role")
|
||||
async def cmd_set_mute_role(self, ctx, role_name):
|
||||
if not ctx.author.guild_permissions.manage_roles:
|
||||
await ctx.send("Vous n'avez pas les permissions nécessaires pour définir le rôle mute.")
|
||||
return
|
||||
params = {'role_name': role_name}
|
||||
from commandes.autres.setmuterole import execute
|
||||
await execute(self.bot, params, ctx.message)
|
||||
|
||||
@commands.command(name="send_message")
|
||||
async def cmd_send_message(self, ctx, channel_name=None, dm_user=None, *, message_content):
|
||||
if not ctx.author.guild_permissions.manage_messages:
|
||||
await ctx.send("Vous n'avez pas les permissions nécessaires pour envoyer des messages via cette commande.")
|
||||
return
|
||||
if not channel_name and not dm_user:
|
||||
await ctx.send("Usage: !send_message [channel_name] [dm_user] message_content")
|
||||
return
|
||||
params = {'channel_name': channel_name, 'dm_user': dm_user, 'message': message_content}
|
||||
from commandes.autres.send_message import execute
|
||||
await execute(self.bot, params, ctx.message)
|
||||
|
||||
|
||||
class BêtaExpansionCommands(commands.Cog):
|
||||
"""Expanded Bêta commands for Social Index and Voice."""
|
||||
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
|
||||
@commands.command(name="voice_diag")
|
||||
async def cmd_voice_diag(self, ctx):
|
||||
"""Diagnose tactical voice connectivity issues."""
|
||||
if not (ctx.author.id in self.bot.admin_ids or self.bot.has_whitelist_permission(ctx.author.id, 'wl')):
|
||||
return
|
||||
|
||||
guild = ctx.guild
|
||||
me = guild.me
|
||||
report = ["**◈ VOICE DIAGNOSTICS ◈**"]
|
||||
|
||||
# Check current state
|
||||
active_vc = guild.voice_client
|
||||
report.append(f"- Active Voice Client: `{'CONNECTED' if active_vc else 'IDLE'}`")
|
||||
report.append(f"- Ear Protocol (Library): `{'READY' if self.bot.voice_manager.HAS_VOICE_RECV else 'MISSING'}`")
|
||||
report.append(f"- Monitoring Status: `{'ACTIVE' if self.bot.voice_manager.is_monitoring else 'INACTIVE'}`")
|
||||
if active_vc:
|
||||
report.append(f"- Current Channel: `{active_vc.channel.name}`")
|
||||
|
||||
# Check permissions for all channels
|
||||
report.append("\n**◈ CANAL PERMISSIONS ◈**")
|
||||
for channel in guild.voice_channels:
|
||||
perms = channel.permissions_for(me)
|
||||
status = "✅" if (perms.connect and perms.view_channel) else "❌"
|
||||
reason = []
|
||||
if not perms.view_channel: reason.append("CANNOT VIEW")
|
||||
if not perms.connect: reason.append("CANNOT CONNECT")
|
||||
|
||||
report.append(f"- `{channel.name}`: {status} {'(' + ', '.join(reason) + ')' if reason else ''}")
|
||||
|
||||
# Check Admin identity match
|
||||
user_match = "MATCHED" if ctx.author.id in self.bot.admin_ids else "NOMINAL"
|
||||
report.append(f"\n- Admin IDs (Config): `{self.bot.admin_ids}`")
|
||||
report.append(f"- Your ID: `{ctx.author.id}` ({user_match})")
|
||||
|
||||
await self.bot.messaging.send_embed(
|
||||
ctx.author,
|
||||
"REPORT: TACTICAL CONNECTIVITY",
|
||||
"\n".join(report),
|
||||
color=discord.Color.blue(),
|
||||
is_asset=True
|
||||
)
|
||||
|
||||
@commands.command(name="voice_report")
|
||||
async def cmd_voice_report(self, ctx, mode: str = None):
|
||||
"""Get an instant clinical summary of the current voice session. Use 'adv' for AI analysis."""
|
||||
if not (ctx.author.id in self.bot.admin_ids or self.bot.has_whitelist_permission(ctx.author.id, 'wl')):
|
||||
return
|
||||
|
||||
if not self.bot.voice_manager.is_monitoring:
|
||||
await ctx.send("◈ SYSTEM: Aucune session de surveillance vocale active actuellement.")
|
||||
return
|
||||
|
||||
is_advanced = mode and (mode.lower() in ["adv", "advanced", "full", "ai", "llm"])
|
||||
|
||||
if is_advanced:
|
||||
await ctx.send("◈ SYSTEM: Lancement de l'analyse heuristique profonde (Gpt-Oss)...")
|
||||
async with ctx.typing():
|
||||
report = await self.bot.voice_manager.generate_session_report(advanced=True)
|
||||
else:
|
||||
report = await self.bot.voice_manager.generate_session_report(advanced=False)
|
||||
|
||||
await self.bot.messaging.send_embed(
|
||||
ctx.author,
|
||||
"INSTANT VOICE SESSION REPORT",
|
||||
report,
|
||||
color=discord.Color.blue(),
|
||||
is_asset=True
|
||||
)
|
||||
|
||||
|
||||
async def setup_commands(bot):
|
||||
"""Register all commands with the bot."""
|
||||
# Channel commands
|
||||
await bot.add_cog(ChannelCommands(bot))
|
||||
# Category commands
|
||||
await bot.add_cog(CategoryCommands(bot))
|
||||
# Security commands
|
||||
await bot.add_cog(SecurityCommands(bot))
|
||||
# Other commands
|
||||
await bot.add_cog(OtherCommands(bot))
|
||||
# Bêta Expansion commands
|
||||
await bot.add_cog(BêtaExpansionCommands(bot))
|
||||
134
core/messaging.py
Normal file
134
core/messaging.py
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
"""Messaging utilities for Superviseur bot."""
|
||||
|
||||
import discord
|
||||
import logging
|
||||
from typing import Optional, List, Dict
|
||||
|
||||
logger = logging.getLogger('Superviseur')
|
||||
|
||||
|
||||
class MessagingManager:
|
||||
"""Manages message sending with rate limiting and chunking."""
|
||||
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
|
||||
async def send_message_with_limit(
|
||||
self,
|
||||
channel: discord.abc.Messageable,
|
||||
content: str
|
||||
) -> None:
|
||||
"""Send a message, splitting into multiple if it exceeds 2000 chars."""
|
||||
while len(content) > 2000:
|
||||
split_index = content.rfind(' ', 0, 2000)
|
||||
if split_index == -1:
|
||||
split_index = 2000
|
||||
chunk = content[:split_index]
|
||||
await channel.send(chunk)
|
||||
content = content[split_index:].lstrip()
|
||||
if content:
|
||||
await channel.send(content)
|
||||
|
||||
async def reply_with_limit(
|
||||
self,
|
||||
message: discord.Message,
|
||||
content: str
|
||||
) -> None:
|
||||
"""Reply to a message, splitting if it exceeds 2000 chars."""
|
||||
if not message.channel:
|
||||
logger.warning(f"Cannot send message: no channel")
|
||||
return
|
||||
|
||||
if hasattr(message.channel, 'permissions_for'):
|
||||
perms = message.channel.permissions_for(
|
||||
message.guild.me if message.guild else message.author
|
||||
)
|
||||
if not perms.send_messages:
|
||||
logger.warning(f"Cannot send message in channel {message.channel}")
|
||||
return
|
||||
|
||||
first_chunk = True
|
||||
|
||||
while len(content) > 2000:
|
||||
split_index = content.rfind(' ', 0, 2000)
|
||||
if split_index == -1:
|
||||
split_index = 2000
|
||||
chunk = content[:split_index]
|
||||
|
||||
if first_chunk:
|
||||
try:
|
||||
await message.reply(chunk)
|
||||
except (discord.errors.HTTPException, discord.errors.NotFound):
|
||||
try:
|
||||
await message.channel.send(chunk)
|
||||
except discord.errors.Forbidden:
|
||||
logger.error(f"Forbidden to send in channel {message.channel}")
|
||||
first_chunk = False
|
||||
else:
|
||||
try:
|
||||
await message.channel.send(chunk)
|
||||
except discord.errors.Forbidden:
|
||||
logger.error(f"Forbidden to send in channel {message.channel}")
|
||||
return
|
||||
|
||||
content = content[split_index:].lstrip()
|
||||
|
||||
if content:
|
||||
if first_chunk:
|
||||
try:
|
||||
await message.reply(content)
|
||||
except (discord.errors.HTTPException, discord.errors.NotFound):
|
||||
try:
|
||||
await message.channel.send(content)
|
||||
except discord.errors.Forbidden:
|
||||
logger.error(f"Forbidden to send in channel {message.channel}")
|
||||
else:
|
||||
try:
|
||||
await message.channel.send(content)
|
||||
except discord.errors.Forbidden:
|
||||
logger.error(f"Forbidden to send in channel {message.channel}")
|
||||
|
||||
async def send_embed(
|
||||
self,
|
||||
target: discord.abc.Messageable,
|
||||
title: str,
|
||||
description: str,
|
||||
color: discord.Color = discord.Color.light_grey(),
|
||||
thumbnail_url: Optional[str] = None,
|
||||
is_asset: bool = False,
|
||||
fields: Optional[List[Dict[str, str]]] = None
|
||||
) -> None:
|
||||
"""Send a clinical Samaritan-style embed with support for fields and clickable links."""
|
||||
# Clinical formatting - we avoid the global ``` to allow markdown (links, mentions)
|
||||
display_title = f"◈ {title} ◈"
|
||||
|
||||
embed = discord.Embed(
|
||||
title=display_title,
|
||||
description=description,
|
||||
color=color
|
||||
)
|
||||
|
||||
if fields:
|
||||
for field in fields:
|
||||
embed.add_field(
|
||||
name=field.get("name", "---"),
|
||||
value=field.get("value", "---"),
|
||||
inline=field.get("inline", True)
|
||||
)
|
||||
|
||||
if is_asset:
|
||||
embed.set_author(
|
||||
name="SYSTEM: BÊTA | PRIMARY ASSET LINK",
|
||||
icon_url="https://i.imgur.com/xH3X5rR.png"
|
||||
)
|
||||
|
||||
if thumbnail_url:
|
||||
embed.set_thumbnail(url=thumbnail_url)
|
||||
|
||||
embed.set_footer(text=f"HEURISTIC ANALYSIS COMPLETE | {self.bot.system_name} v2.0")
|
||||
|
||||
try:
|
||||
await target.send(embed=embed)
|
||||
except discord.errors.Forbidden:
|
||||
logger.error(f"Cannot send DM/Message to {target}")
|
||||
|
||||
103
core/permissions.py
Normal file
103
core/permissions.py
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
"""Permissions and whitelist handling for Superviseur bot."""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from typing import Dict, Optional
|
||||
|
||||
logger = logging.getLogger('Superviseur')
|
||||
|
||||
|
||||
class WhitelistManager:
|
||||
"""Manages user whitelist and permissions."""
|
||||
|
||||
def __init__(self, whitelist_file: str = 'whitelist.json'):
|
||||
self.whitelist_file = whitelist_file
|
||||
self.whitelist = None
|
||||
self.load_whitelist()
|
||||
|
||||
def load_whitelist(self) -> None:
|
||||
"""Load whitelist from JSON file."""
|
||||
try:
|
||||
with open(self.whitelist_file, 'r', encoding='utf-8') as f:
|
||||
self.whitelist = json.load(f)
|
||||
logger.info("Whitelist loaded successfully")
|
||||
except FileNotFoundError:
|
||||
# Supprimer le message de warning pour whitelist.json
|
||||
self.whitelist = None
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"Error parsing whitelist.json: {e}")
|
||||
self.whitelist = None
|
||||
|
||||
def get_user_level(self, user_id: int, user_name: Optional[str] = None) -> Optional[str]:
|
||||
"""Get user's whitelist level by ID or name."""
|
||||
if not self.whitelist:
|
||||
return None
|
||||
|
||||
user_levels = self.whitelist.get('user_levels', {})
|
||||
|
||||
# Try by ID first
|
||||
level = user_levels.get(str(user_id))
|
||||
if level:
|
||||
return level
|
||||
|
||||
# Try by name if provided
|
||||
if user_name:
|
||||
level = user_levels.get(user_name)
|
||||
if level:
|
||||
return level
|
||||
|
||||
return None
|
||||
|
||||
def has_permission(self, user_id: int, required_level: str, user_name: Optional[str] = None) -> bool:
|
||||
"""Check if user has required whitelist level."""
|
||||
level = self.get_user_level(user_id, user_name)
|
||||
if not level:
|
||||
return False
|
||||
levels = ['wl', 'wl+']
|
||||
try:
|
||||
user_index = levels.index(level)
|
||||
req_index = levels.index(required_level)
|
||||
return user_index >= req_index
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
def get_required_level(self, action: str) -> Optional[str]:
|
||||
"""Get required whitelist level for an action."""
|
||||
if not self.whitelist:
|
||||
return None
|
||||
return self.whitelist.get('commands_permissions', {}).get(action)
|
||||
|
||||
|
||||
def check_is_admin(permissions) -> bool:
|
||||
"""Check if user has admin permissions."""
|
||||
if not permissions:
|
||||
return False
|
||||
|
||||
return (
|
||||
permissions.administrator or
|
||||
permissions.manage_guild or
|
||||
permissions.manage_roles or
|
||||
permissions.manage_channels or
|
||||
permissions.kick_members or
|
||||
permissions.ban_members or
|
||||
permissions.moderate_members
|
||||
)
|
||||
|
||||
|
||||
def get_permissions_info(permissions, user_level: str = 'none', role_name: str = 'Sujet') -> str:
|
||||
"""Get formatted permissions info string including role awareness."""
|
||||
if not permissions:
|
||||
return f"RÔLE: {role_name}, Permissions (DM): non, Niveau whitelist: {user_level}"
|
||||
|
||||
has_permissions = (
|
||||
permissions.administrator or
|
||||
permissions.manage_guild or
|
||||
permissions.manage_roles or
|
||||
permissions.manage_channels or
|
||||
permissions.kick_members or
|
||||
permissions.ban_members or
|
||||
permissions.moderate_members or
|
||||
permissions.manage_messages
|
||||
)
|
||||
return f"RÔLE: {role_name}, Permissions: {'oui' if has_permissions else 'non'}, Niveau whitelist: {user_level}"
|
||||
|
||||
98
core/redis_worker.py
Normal file
98
core/redis_worker.py
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
"""Redis worker support for Superviseur bot."""
|
||||
|
||||
import json
|
||||
import uuid
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Dict, Optional, Any
|
||||
|
||||
logger = logging.getLogger('Superviseur')
|
||||
|
||||
|
||||
class RedisWorker:
|
||||
"""Manages Redis-based LLM job queue."""
|
||||
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
self.redis_client = None
|
||||
self._pending_jobs: Dict[str, asyncio.Future] = {}
|
||||
self._listener_task: Optional[asyncio.Task] = None
|
||||
self._response_queue = 'llm_queue:responses'
|
||||
self._job_queue = 'llm_queue:jobs'
|
||||
|
||||
async def initialize(self, redis_url: str) -> bool:
|
||||
"""Initialize Redis connection."""
|
||||
if not self.bot.redis_available:
|
||||
return False
|
||||
|
||||
try:
|
||||
self.redis_client = self.bot.aioredis.from_url(redis_url)
|
||||
self._listener_task = asyncio.create_task(self._listen())
|
||||
logger.info("Redis LLM worker enabled; listening for responses")
|
||||
return True
|
||||
except Exception:
|
||||
logger.exception("Failed to initialize redis client")
|
||||
return False
|
||||
|
||||
async def close(self) -> None:
|
||||
"""Close Redis connection."""
|
||||
if self._listener_task:
|
||||
try:
|
||||
self._listener_task.cancel()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if self.redis_client:
|
||||
try:
|
||||
await self.redis_client.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
async def enqueue_job(self, payload: Dict[str, Any], timeout: int = 60) -> Optional[Dict[str, Any]]:
|
||||
"""Enqueue an LLM job and wait for result."""
|
||||
if not self.redis_client:
|
||||
return None
|
||||
|
||||
job_id = str(uuid.uuid4())
|
||||
future = self.bot.loop.create_future()
|
||||
self._pending_jobs[job_id] = future
|
||||
payload['job_id'] = job_id
|
||||
|
||||
await self.redis_client.lpush(self._job_queue, json.dumps(payload))
|
||||
logger.debug(f"LLM job {job_id} enqueued.")
|
||||
|
||||
try:
|
||||
return await asyncio.wait_for(future, timeout=timeout)
|
||||
finally:
|
||||
self._pending_jobs.pop(job_id, None)
|
||||
|
||||
async def _listen(self) -> None:
|
||||
"""Listen for Redis responses."""
|
||||
logger.info(f"Starting Redis response listener on {self._response_queue}")
|
||||
|
||||
while True:
|
||||
try:
|
||||
result = await self.redis_client.brpop(self._response_queue, timeout=1)
|
||||
if result is None:
|
||||
await asyncio.sleep(0.1)
|
||||
continue
|
||||
|
||||
data_bytes = result[1]
|
||||
response = json.loads(data_bytes.decode('utf-8'))
|
||||
job_id = response.get('job_id')
|
||||
|
||||
if job_id in self._pending_jobs:
|
||||
future = self._pending_jobs[job_id]
|
||||
if not future.done():
|
||||
future.set_result(response)
|
||||
logger.debug(f"LLM job {job_id} completed via Redis.")
|
||||
else:
|
||||
logger.warning(f"Response for unknown job {job_id}")
|
||||
|
||||
except asyncio.CancelledError:
|
||||
logger.info("Redis listener cancelled")
|
||||
break
|
||||
except Exception as e:
|
||||
logger.exception(f"Redis listener error: {e}")
|
||||
await asyncio.sleep(5)
|
||||
|
||||
159
core/tasks.py
Normal file
159
core/tasks.py
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
import asyncio
|
||||
import datetime
|
||||
import logging
|
||||
import time
|
||||
import os
|
||||
import discord
|
||||
|
||||
logger = logging.getLogger('Superviseur')
|
||||
|
||||
class TaskManager:
|
||||
"""Handles global background tasks, scheduled loops and reporting."""
|
||||
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
|
||||
def start_all(self):
|
||||
"""Starts all background loops attached to the bot's event loop."""
|
||||
self.bot.loop.create_task(self._introspection_report_loop())
|
||||
self.bot.loop.create_task(self._voice_report_loop())
|
||||
self.bot.loop.create_task(self._daily_report_check_loop())
|
||||
self.bot.loop.create_task(self._gdpr_purge_loop())
|
||||
self.bot.loop.create_task(self._voice_evaluation_loop())
|
||||
|
||||
# Start interviews for missing assets
|
||||
for asset_id in self.bot.asset_ids:
|
||||
if str(asset_id) not in self.bot.dispatcher.assets:
|
||||
self.bot.loop.create_task(self.bot.dispatcher.start_interview(asset_id))
|
||||
|
||||
async def _voice_evaluation_loop(self):
|
||||
"""Periodic check for tactical voice connections."""
|
||||
await self.bot.wait_until_ready()
|
||||
while not self.bot.is_closed():
|
||||
for guild in self.bot.guilds:
|
||||
await self.bot.voice_manager.evaluate_and_connect(guild)
|
||||
await asyncio.sleep(30) # Reduit à 30s pour plus de réactivité en tâche de fond
|
||||
|
||||
async def _introspection_report_loop(self):
|
||||
"""Periodic broadcast of system health metrics."""
|
||||
await self.bot.wait_until_ready()
|
||||
while not self.bot.is_closed():
|
||||
await asyncio.sleep(600) # Every 10 minutes
|
||||
await self._send_health_report()
|
||||
|
||||
async def _send_health_report(self):
|
||||
"""Build and send a clinical health report embed."""
|
||||
total = self.bot.metrics["total_llm_requests"]
|
||||
success = self.bot.metrics["total_llm_success"]
|
||||
rate = (success / total * 100) if total > 0 else 100
|
||||
avg_time = (self.bot.metrics["total_llm_time"] / success) if success > 0 else 0
|
||||
|
||||
status_report = (
|
||||
f"**◈ INTELLIGENCE CORE**\n"
|
||||
f"- Requests Processed : `{total}`\n"
|
||||
f"- Reliability Rate : `{rate:.1f}%`\n"
|
||||
f"- Latency Heuristic : `{avg_time:.2f}s` (Target: < 5s)\n\n"
|
||||
f"**◈ TACTICAL OVERVIEW**\n"
|
||||
f"- Voice Protocol : `READY`\n"
|
||||
f"- Active Deployment : `{self.bot.voice_manager.current_channel.name if self.bot.voice_manager.current_channel else 'NONE'}`\n"
|
||||
)
|
||||
|
||||
await self.bot.introspection_log(
|
||||
"SYSTEM STATUS REPORT",
|
||||
status_report,
|
||||
discord.Color.dark_blue()
|
||||
)
|
||||
|
||||
async def _daily_report_check_loop(self):
|
||||
"""Check for 21:30 and trigger the daily report using local timezone."""
|
||||
await self.bot.wait_until_ready()
|
||||
report_sent_today = False
|
||||
while not self.bot.is_closed():
|
||||
now = datetime.datetime.now(self.bot.timezone)
|
||||
if now.hour == 21 and now.minute == 30:
|
||||
if not report_sent_today:
|
||||
await self._send_daily_summary()
|
||||
report_sent_today = True
|
||||
else:
|
||||
report_sent_today = False
|
||||
await asyncio.sleep(60)
|
||||
|
||||
async def _gdpr_purge_loop(self):
|
||||
"""Daily purge of old memory files (30 days) for GDPR compliance."""
|
||||
from brain.memoire import purge_old_memories
|
||||
while not self.bot.is_closed():
|
||||
now = datetime.datetime.now(self.bot.timezone)
|
||||
if now.hour == 3 and now.minute == 0:
|
||||
logger.info("◈ RGPD: Démarrage de la purge quotidienne...")
|
||||
deleted = await purge_old_memories(days=30)
|
||||
logger.info(f"◈ RGPD: Purge terminée. {deleted} fichiers supprimés.")
|
||||
await asyncio.sleep(65)
|
||||
await asyncio.sleep(30)
|
||||
|
||||
async def _send_daily_summary(self):
|
||||
"""Generate and send a dense, highly detailed clinical report at 21:30."""
|
||||
tasks = self.bot.dispatcher.tasks
|
||||
tense_sessions = self.bot.metrics.get("daily_tense_sessions", 0)
|
||||
|
||||
asset_stats = {}
|
||||
for t_id, t_data in tasks.items():
|
||||
a_id = t_data.get("asset_id")
|
||||
if a_id not in asset_stats:
|
||||
asset_name = "Inconnu"
|
||||
if str(a_id) in self.bot.dispatcher.assets:
|
||||
asset_name = self.bot.dispatcher.assets[str(a_id)].get("name", "Anonyme")
|
||||
asset_stats[a_id] = {"name": asset_name, "accepted": 0, "missed": 0, "total": 0}
|
||||
|
||||
asset_stats[a_id]["total"] += 1
|
||||
if t_data["status"] == "ACCEPTED":
|
||||
asset_stats[a_id]["accepted"] += 1
|
||||
elif t_data["status"] == "PENDING" and t_data.get("deadline", 0) < time.time():
|
||||
asset_stats[a_id]["missed"] += 1
|
||||
|
||||
stats_lines = []
|
||||
for a_id, s in asset_stats.items():
|
||||
stats_lines.append(f"- **{s['name']}** : `{s['accepted']}/{s['total']}` acceptées, `{s['missed']}` manquées.")
|
||||
|
||||
task_details = []
|
||||
for t_id, t_data in list(tasks.items())[-15:]:
|
||||
status_emoji = "✅" if t_data["status"] == "ACCEPTED" else "❌" if t_data.get("deadline", 0) < time.time() else "⏳"
|
||||
asset_name = self.bot.dispatcher.assets.get(str(t_data['asset_id']), {}).get('name', 'N/A')
|
||||
task_details.append(f"{status_emoji} `{t_id}` | Staff: {asset_name} | Urgence: {t_data['importance']} | Context: {t_data['content'][:50]}...")
|
||||
|
||||
summary = (
|
||||
f"**◈ RAPPORT ANALYTIQUE DE FIN DE CYCLE ◈**\n"
|
||||
f"Période : Dernières 24h | Heure : 21h30\n\n"
|
||||
f"**◈ BILAN GLOBAL DES OPÉRATIONS**\n"
|
||||
f"- Volume total d'actions de modération : `{len(tasks)}` \n"
|
||||
f"- Sessions vocales sous tension : `{tense_sessions}`\n\n"
|
||||
f"**◈ PERFORMANCE DES ASSETS**\n"
|
||||
+ ("\n".join(stats_lines) if stats_lines else "Aucune activité de staff enregistrée.") + "\n\n"
|
||||
f"**◈ LOG DÉTAILLÉ DES DERNIERS SIGNAUX**\n"
|
||||
+ ("\n".join(task_details) if task_details else "Aucun signal capturé.") + "\n\n"
|
||||
f"**◈ CONCLUSION HEURISTIQUE**\n"
|
||||
f"Le système est nominal. " + ("PRÉCONISATION : Vigilance accrue requise sur les membres suspects." if tense_sessions > 0 else "État de l'écosystème : Stable.")
|
||||
)
|
||||
|
||||
if summary and self.bot.admin_ids:
|
||||
for admin_id in self.bot.admin_ids:
|
||||
admin = self.bot.get_user(admin_id) or await self.bot.fetch_user(admin_id)
|
||||
if admin:
|
||||
await self.bot.messaging.send_embed(admin, "SYNTHÈSE CLINIQUE QUOTIDIENNE", summary, color=discord.Color.dark_purple())
|
||||
|
||||
self.bot.metrics["daily_tense_sessions"] = 0
|
||||
self.bot.dispatcher.tasks = {k: v for k, v in tasks.items() if v['status'] == 'PENDING' and v.get('deadline', 0) > time.time()}
|
||||
|
||||
# Le chemin de Active tasks doit être vers /data si on l'a déplacé.
|
||||
# Pour l'instant, on sauvegarde dans data/
|
||||
data_dir = os.path.join(os.path.dirname(__file__), "..", "data")
|
||||
os.makedirs(data_dir, exist_ok=True)
|
||||
self.bot.dispatcher._save_data(self.bot.dispatcher.tasks, os.path.join(data_dir, "active_tasks.json"))
|
||||
|
||||
async def _voice_report_loop(self):
|
||||
"""Periodic broadcast of active voice session summaries."""
|
||||
await self.bot.wait_until_ready()
|
||||
while not self.bot.is_closed():
|
||||
await asyncio.sleep(900) # Every 15 minutes
|
||||
if self.bot.voice_manager.is_monitoring and self.bot.voice_manager.session_buffer:
|
||||
report = await self.bot.voice_manager.generate_session_report()
|
||||
await self.bot.introspection_log("VOICE SESSION UPDATE (15m)", report, discord.Color.blue())
|
||||
16
core/utils.py
Normal file
16
core/utils.py
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
"""Utility functions for Superviseur bot."""
|
||||
|
||||
import re
|
||||
|
||||
|
||||
def format_mentions_in_text(text: str) -> str:
|
||||
"""Convert user ID numbers in text to Discord mention format <@id>."""
|
||||
# Match sequences of 15-20 digits
|
||||
pattern = r'\b(\d{15,20})\b'
|
||||
return re.sub(pattern, r'<@\1>', text)
|
||||
|
||||
|
||||
def sanitize_guild_name(name: str) -> str:
|
||||
"""Sanitize guild name for use in file paths."""
|
||||
return re.sub(r'[\/\\:*?"<>|\s]', '_', name)
|
||||
|
||||
|
|
@ -3,7 +3,7 @@ import logging
|
|||
import asyncio
|
||||
import os
|
||||
import time
|
||||
from typing import Dict, Optional, List
|
||||
from typing import Dict, Optional, List, Any
|
||||
from .whisper_worker import WhisperWorker
|
||||
|
||||
logger = logging.getLogger('Superviseur')
|
||||
|
|
@ -80,8 +80,14 @@ class VoiceManager:
|
|||
|
||||
|
||||
async def evaluate_and_connect(self, guild):
|
||||
"""Analyze voice channels in guild and connect if strategically relevant."""
|
||||
if self.current_channel and self.current_channel.guild == guild:
|
||||
"""Analyze voice channels in guild and connect if strategically relevant.
|
||||
Only connects if the bot is not already connected to ANY voice channel."""
|
||||
# Prevent connecting to multiple guilds simultaneously
|
||||
if self.current_channel is not None:
|
||||
return
|
||||
|
||||
# Also check if any voice client is already active for this guild
|
||||
if guild.voice_client is not None:
|
||||
return
|
||||
|
||||
best_channel = None
|
||||
|
|
@ -100,6 +106,11 @@ class VoiceManager:
|
|||
|
||||
async def join_channel(self, channel: discord.VoiceChannel):
|
||||
"""Securely join a voice channel with permission checking."""
|
||||
# Prevent joining if already connected somewhere
|
||||
if self.current_channel is not None:
|
||||
logger.debug(f"◈ Already connected to '{self.current_channel.name}', ignoring join request for '{channel.name}'.")
|
||||
return
|
||||
|
||||
try:
|
||||
# Physical authorization check
|
||||
me = channel.guild.me
|
||||
|
|
@ -367,4 +378,4 @@ class VoiceManager:
|
|||
"VOICE TERMINATION",
|
||||
"Connexion vocale terminée. Libération des ressources audio.",
|
||||
discord.Color.dark_grey()
|
||||
)
|
||||
)
|
||||
58
core/whisper_worker.py
Normal file
58
core/whisper_worker.py
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import os
|
||||
import logging
|
||||
import asyncio
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from typing import Optional
|
||||
|
||||
logger = logging.getLogger('Superviseur')
|
||||
|
||||
class WhisperWorker:
|
||||
"""Handles local STT transcription using faster-whisper."""
|
||||
|
||||
def __init__(self, model_size="base", device="cpu"):
|
||||
self.model_size = model_size
|
||||
self.device = device
|
||||
self.model = None
|
||||
self.executor = ThreadPoolExecutor(max_workers=2)
|
||||
self.initialized = False
|
||||
|
||||
def initialize(self):
|
||||
"""Load the model into RAM/VRAM."""
|
||||
try:
|
||||
from faster_whisper import WhisperModel
|
||||
logger.info(f"◈ SYSTEM: Initializing Whisper ({self.model_size}) on {self.device}...")
|
||||
|
||||
# On tente le chargement. Si device="cuda" échoue (AMD), on bascule sur CPU.
|
||||
try:
|
||||
self.model = WhisperModel(self.model_size, device=self.device, compute_type="int8")
|
||||
except Exception as e:
|
||||
if self.device != "cpu":
|
||||
logger.warning(f"◈ GPU failure ({e}). Falling back to CPU.")
|
||||
self.device = "cpu"
|
||||
self.model = WhisperModel(self.model_size, device="cpu", compute_type="int8")
|
||||
else:
|
||||
raise e
|
||||
|
||||
self.initialized = True
|
||||
logger.info(f"◈ SYSTEM: Whisper Model Loaded on {self.device}. Ear Protocol Active.")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to initialize Whisper: {e}")
|
||||
self.initialized = False
|
||||
|
||||
async def transcribe(self, audio_path: str) -> Optional[str]:
|
||||
"""Transcribe an audio file asynchronously."""
|
||||
if not self.initialized or not self.model:
|
||||
return None
|
||||
|
||||
def _run():
|
||||
try:
|
||||
segments, info = self.model.transcribe(audio_path, beam_size=5)
|
||||
text = " ".join([segment.text for segment in segments]).strip()
|
||||
logger.debug(f"◈ Whisper: Transcription result: '{text}' (lang: {info.language})")
|
||||
return text
|
||||
except Exception as e:
|
||||
logger.error(f"Transcription error: {e}")
|
||||
return None
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
return await loop.run_in_executor(self.executor, _run)
|
||||
Loading…
Add table
Add a link
Reference in a new issue