Voice et bot modif

This commit is contained in:
pi 2026-06-16 17:09:34 +00:00
parent 189d56026b
commit 7333a22bcd
10774 changed files with 634644 additions and 933308 deletions

6
core/__init__.py Normal file
View file

@ -0,0 +1,6 @@
"""Superviseur Bot - Discord bot with LLM integration."""
from .bot import Superviseur
__all__ = ['Superviseur']

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -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
View 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
View 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
View 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
View 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
View 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
View 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)

View file

@ -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
View 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)