Files

732 lines
26 KiB
Python

# discordbot/auto_rooms.py — Auto rooms : message et réactions dans la partie texte du salon vocal (onglet Discussion)
import logging
import re
import json
from typing import Optional
import discord
from discord import Member, VoiceState
from database import db
from database.helpers import ConfigurationHelper
from database.models import AutoRoom
from webapp import webapp
# (guild_id, owner_id) -> room_data (voice_channel_id, control_message_id, whitelist, blacklist, access_mode)
_rooms: dict[tuple[int, int], dict] = {}
# message_id -> (guild_id, owner_id) pour retrouver la room depuis une réaction
_control_message_ids: dict[int, tuple[int, int]] = {}
# Emoji -> action
REACTIONS = [
("🔓", "open", "Ouvert"),
("🔒", "closed", "Fermé"),
("🔐", "private", "Privé"),
("✅", "whitelist", "Liste blanche"),
("🚫", "blacklist", "Liste noire"),
("🧹", "purge", "Purge"),
("👑", "transfer", "Propriété"),
("🎤", "speak", "Micro"),
("📹", "stream", "Vidéo"),
("📊", "soundboards", "Soundboards"),
("📝", "status", "Statut"),
]
def _status_display(access_mode: str) -> str:
"""Cadenas ouvert ou fermé selon si le salon est ouvert ou pas."""
if access_mode == "open":
return "🔓 Ouvert"
if access_mode == "closed":
return "🔒 Fermé"
if access_mode == "private":
return "🔐 Privé"
return "🔓 Ouvert"
def _status_emoji(access_mode: str) -> str:
"""Emoji cadenas seul pour le nom du channel."""
if access_mode == "private":
return "🔐"
return "🔓" if access_mode == "open" else "🔒"
def _build_control_embed(owner: Member, voice_channel: discord.VoiceChannel, access_mode: str, room: dict = None) -> discord.Embed:
"""Construit l'embed de config avec infos du salon."""
embed = discord.Embed(
title="⚙️ Configuration du salon",
description=(
"Voici l'espace de configuration de votre salon vocal temporaire. "
"Les différentes options disponibles vous permettent de personnaliser les permissions de votre salon selon vos préférences."
),
color=discord.Color.orange()
)
# Récupération des infos
whitelist = room.get("whitelist", set()) if room else set()
blacklist = room.get("blacklist", set()) if room else set()
whitelist_text = f"{len(whitelist)} membre(s)" if whitelist else "Aucun"
blacklist_text = f"{len(blacklist)} membre(s)" if blacklist else "Aucun"
# Section Propriétaire
embed.add_field(
name=f"👤 Propriétaire du salon : {owner.display_name}",
value="",
inline=False
)
# Section Modes d'accès
mode_open = "🔓 **Ouvert**\nLe salon sera ouvert à tous les membres, sauf ceux figurant sur la liste noire."
mode_closed = "🔒 **Fermé**\nLe salon sera visible de tous, mais seulement accessible à la liste blanche."
mode_private = "🔐 **Privé**\nLe salon ne sera visible et accessible qu'aux membres de la liste blanche."
embed.add_field(name=mode_open, value="", inline=True)
embed.add_field(name=mode_closed, value="", inline=True)
embed.add_field(name=mode_private, value="", inline=True)
# Section Listes
embed.add_field(
name="📝 **Liste blanche**",
value=f"Les membres présents dans cette liste pourront toujours rejoindre le salon.\n\n{whitelist_text}",
inline=True
)
embed.add_field(
name="🚫 **Liste noire**",
value=f"Les membres présents dans cette liste ne pourront jamais rejoindre le salon.\n\n{blacklist_text}",
inline=True
)
embed.add_field(name="\u200b", value="", inline=True) # Spacer
# Section Purge
embed.add_field(
name="🧹 **Purge**",
value="Déconnecter tous les membres du salon vocal à l'exception de ceux présents dans la liste blanche.",
inline=False
)
# Section Transfert
embed.add_field(
name="👑 **Transférer**",
value="Transférer la gestion du salon au membre de votre choix.",
inline=False
)
# Note importante
embed.add_field(
name="💡",
value="Les membres de la liste blanche ne sont pas impactés par les permissions refusées aux membres.",
inline=False
)
embed.set_footer(text="Réagissez avec les émojis ci-dessous pour configurer votre salon")
return embed
def _room_key(guild_id: int, owner_id: int) -> tuple[int, int]:
return (guild_id, owner_id)
def _get_room(guild_id: int, owner_id: int) -> Optional[dict]:
room = _rooms.get(_room_key(guild_id, owner_id))
if room:
return room
# Le cache peut être perdu après un redémarrage ou une reconnexion : la base
# reste la source de vérité pour éviter de créer une deuxième room au même membre.
with webapp.app_context():
record = AutoRoom.query.filter_by(guild_id=str(guild_id), owner_id=str(owner_id)).first()
if not record:
return None
try:
room = {
"guild_id": guild_id,
"voice_channel_id": int(record.voice_channel_id),
"control_message_id": int(record.control_message_id) if record.control_message_id else None,
"owner_id": owner_id,
"whitelist": set(json.loads(record.whitelist or "[]")),
"blacklist": set(json.loads(record.blacklist or "[]")),
"managed_member_ids": set(json.loads(record.managed_member_ids or "[]")),
"access_mode": record.access_mode or "open",
}
except (TypeError, ValueError):
return None
_rooms[_room_key(guild_id, owner_id)] = room
if room["control_message_id"]:
_control_message_ids[room["control_message_id"]] = (guild_id, owner_id)
return room
def _set_room(guild_id: int, owner_id: int, data: dict):
_rooms[_room_key(guild_id, owner_id)] = data
mid = data.get("control_message_id")
if mid:
_control_message_ids[mid] = (guild_id, owner_id)
_persist_room(guild_id, owner_id, data)
def _del_room(guild_id: int, owner_id: int):
data = _rooms.pop(_room_key(guild_id, owner_id), None)
if data and data.get("control_message_id"):
_control_message_ids.pop(data["control_message_id"], None)
if data:
with webapp.app_context():
AutoRoom.query.filter_by(guild_id=str(guild_id), voice_channel_id=str(data["voice_channel_id"])).delete()
db.session.commit()
def _persist_room(guild_id: int, owner_id: int, data: dict):
"""Sauvegarde l'état nécessaire à la reprise après redémarrage."""
with webapp.app_context():
record = AutoRoom.query.filter_by(
guild_id=str(guild_id), voice_channel_id=str(data["voice_channel_id"])
).first()
if not record:
record = AutoRoom(guild_id=str(guild_id), voice_channel_id=str(data["voice_channel_id"]))
db.session.add(record)
record.owner_id = str(owner_id)
record.control_message_id = str(data["control_message_id"]) if data.get("control_message_id") else None
record.access_mode = data.get("access_mode", "open")
record.whitelist = json.dumps(sorted(data.get("whitelist", set())))
record.blacklist = json.dumps(sorted(data.get("blacklist", set())))
record.managed_member_ids = json.dumps(sorted(data.get("managed_member_ids", set())))
db.session.commit()
def _auto_rooms_config() -> tuple[bool, int]:
with webapp.app_context():
config = ConfigurationHelper()
return bool(config.getValue("auto_rooms_enable")), config.getIntValue("auto_rooms_channel_id")
def _find_room_by_channel(guild_id: int, channel_id: int) -> Optional[tuple[int, dict]]:
for (gid, oid), data in _rooms.items():
if gid == guild_id and data.get("voice_channel_id") == channel_id:
return (oid, data)
with webapp.app_context():
record = AutoRoom.query.filter_by(guild_id=str(guild_id), voice_channel_id=str(channel_id)).first()
if record:
room = _get_room(guild_id, int(record.owner_id))
if room:
return (int(record.owner_id), room)
return None
def _find_room_by_message(message_id: int) -> Optional[tuple[int, int, dict]]:
key = _control_message_ids.get(message_id)
if not key:
return None
guild_id, owner_id = key
data = _get_room(guild_id, owner_id)
if not data:
_control_message_ids.pop(message_id, None)
return None
return (guild_id, owner_id, data)
async def _apply_access_mode(channel: discord.VoiceChannel, mode: str, whitelist: set, blacklist: set, room: dict):
guild = channel.guild
everyone = guild.default_role
overwrites = dict(channel.overwrites) # Récupérer les overwrites existants
# Préserver les permissions existantes pour everyone (stream, speak, soundboards, etc.)
existing_everyone_ow = overwrites.get(everyone, discord.PermissionOverwrite())
everyone_ow = discord.PermissionOverwrite()
# Copier les permissions importantes qui ne doivent pas être écrasées
everyone_ow.stream = existing_everyone_ow.stream
everyone_ow.speak = existing_everyone_ow.speak
everyone_ow.use_soundboard = existing_everyone_ow.use_soundboard
if mode == "open":
everyone_ow.connect = True
everyone_ow.view_channel = True
# Ne toucher qu'aux overwrites créés par l'auto room, jamais aux droits
# ajoutés manuellement par la modération.
for target in list(overwrites.keys()):
if target != everyone and isinstance(target, discord.Member):
if target.id in room.get("managed_member_ids", set()) and target.id not in blacklist:
overwrites.pop(target, None)
# Ajouter les overwrites pour la blacklist
for uid in blacklist:
m = guild.get_member(uid)
if m:
overwrites[m] = discord.PermissionOverwrite(connect=False, view_channel=True)
elif mode == "closed":
everyone_ow.connect = False
everyone_ow.view_channel = True
for target in list(overwrites.keys()):
if target != everyone and isinstance(target, discord.Member):
if target.id in room.get("managed_member_ids", set()) and target.id not in whitelist:
overwrites.pop(target, None)
# Ajouter les overwrites pour la whitelist
for uid in whitelist:
m = guild.get_member(uid)
if m:
overwrites[m] = discord.PermissionOverwrite(connect=True, view_channel=True)
elif mode == "private":
everyone_ow.connect = False
everyone_ow.view_channel = False
for target in list(overwrites.keys()):
if target != everyone and isinstance(target, discord.Member):
if target.id in room.get("managed_member_ids", set()) and target.id not in whitelist:
overwrites.pop(target, None)
# Ajouter les overwrites pour la whitelist
for uid in whitelist:
m = guild.get_member(uid)
if m:
overwrites[m] = discord.PermissionOverwrite(connect=True, view_channel=True)
overwrites[everyone] = everyone_ow
await channel.edit(overwrites=overwrites)
room["managed_member_ids"] = set(blacklist if mode == "open" else whitelist)
async def _handle_reaction_action(bot: discord.Client, guild_id: int, owner_id: int, action: str, channel):
"""channel = salon vocal (partie texte / onglet Discussion)."""
room = _get_room(guild_id, owner_id)
if not room:
await channel.send("Ce salon n'existe plus.")
return
voice_channel = bot.get_channel(room["voice_channel_id"])
if not voice_channel or not isinstance(voice_channel, discord.VoiceChannel):
await channel.send("Salon vocal introuvable.")
return
if action in ("open", "closed", "private"):
room["access_mode"] = action
await _apply_access_mode(voice_channel, action, room.get("whitelist", set()), room.get("blacklist", set()), room)
_persist_room(guild_id, owner_id, room)
# Mettre à jour le cadenas dans le nom du channel
try:
base_name = voice_channel.name.rstrip(" 🔓🔒🔐")
new_name = f"{base_name} {_status_emoji(action)}"
await voice_channel.edit(name=new_name)
except discord.HTTPException:
pass
await channel.send(f"Accès du salon défini sur **{_status_display(action)}**.")
# Mettre à jour l'embed
await _update_control_panel(bot, guild_id, owner_id, channel)
elif action == "whitelist":
whitelist = room.get("whitelist", set())
whitelist_text = ", ".join([f"<@{uid}>" for uid in whitelist]) if whitelist else "Aucun membre"
await channel.send(
f"**📝 Liste blanche actuelle :** {whitelist_text}\n\n"
f"Mentionnez un membre pour l'ajouter ou le retirer de la liste blanche."
)
room["awaiting_whitelist"] = True
elif action == "blacklist":
blacklist = room.get("blacklist", set())
blacklist_text = ", ".join([f"<@{uid}>" for uid in blacklist]) if blacklist else "Aucun membre"
await channel.send(
f"**🚫 Liste noire actuelle :** {blacklist_text}\n\n"
f"Mentionnez un membre pour l'ajouter ou le retirer de la liste noire."
)
room["awaiting_blacklist"] = True
elif action == "purge":
whitelist = room.get("whitelist", set())
kicked = 0
for member in list(voice_channel.members):
if member.id == owner_id or member.id in whitelist:
continue
try:
await member.move_to(None)
kicked += 1
except discord.HTTPException:
pass
await channel.send(f"🧹 Purge effectuée : {kicked} membre(s) déconnecté(s).")
elif action == "transfer":
await channel.send(
f"**👑 Transfert de propriété**\n\n"
f"Mentionnez le membre à qui vous souhaitez transférer la gestion du salon."
)
room["awaiting_transfer"] = True
elif action in ("speak", "stream", "soundboards"):
everyone = voice_channel.guild.default_role
overwrites = dict(voice_channel.overwrites)
ow = overwrites.get(everyone) or discord.PermissionOverwrite()
# BUG FIX : Par défaut, Discord autorise stream, speak et soundboards
# Si la permission n'est pas explicitement définie (None), on considère qu'elle est True
current = getattr(ow, action)
if current is None:
# Permission non définie = autorisée par défaut dans Discord
# On veut la désactiver lors du premier clic
setattr(ow, action, False)
new_value = False
else:
# Permission définie, on l'inverse
setattr(ow, action, not current)
new_value = not current
overwrites[everyone] = ow
await voice_channel.edit(overwrites=overwrites)
labels = {"speak": "Micro", "stream": "Vidéo/Partage d'écran", "soundboards": "Soundboards"}
label = labels.get(action, action.capitalize())
await channel.send(f"{label} : {'autorisé' if new_value else 'désactivé'} pour tous.")
elif action == "status":
current_status = voice_channel.status or "Aucun statut défini"
await channel.send(
f"**Statut actuel du salon :** {current_status}\n\n"
f"Pour modifier le statut du salon (le texte affiché en haut du salon vocal), "
f"répondez avec le nouveau statut (max 500 caractères).\n"
f"💡 Pour supprimer le statut, répondez avec `clear` ou `effacer`."
)
room["awaiting_status"] = True
async def _update_control_panel(bot: discord.Client, guild_id: int, owner_id: int, channel):
"""Met à jour le panneau de contrôle avec les nouvelles informations."""
room = _get_room(guild_id, owner_id)
if not room:
return
control_message_id = room.get("control_message_id")
if not control_message_id:
return
voice_channel = bot.get_channel(room["voice_channel_id"])
if not voice_channel or not isinstance(voice_channel, discord.VoiceChannel):
return
owner = voice_channel.guild.get_member(owner_id)
if not owner:
return
try:
msg = await channel.fetch_message(control_message_id)
embed = _build_control_embed(owner, voice_channel, room.get("access_mode", "open"), room)
await msg.edit(embed=embed)
except discord.HTTPException:
pass
async def send_control_panel(bot: discord.Client, guild_id: int, owner: Member, voice_channel: discord.VoiceChannel, room: dict) -> Optional[int]:
"""Envoie le message de config avec réactions dans la partie texte du salon vocal (onglet Discussion). Seul le proprio peut réagir. Retourne l'id du message."""
embed = _build_control_embed(owner, voice_channel, "open", room)
try:
# Message dans la partie texte du vocal (onglet Discussion à droite)
msg = await voice_channel.send(embed=embed)
for emoji, _action, _label in REACTIONS:
await msg.add_reaction(emoji)
return msg.id
except discord.HTTPException as e:
logging.error(f"Impossible d'envoyer le panneau Auto Room dans le vocal : {e}")
return None
_AUTO_ROOM_NAME_PATTERN = re.compile(r"^Salon de .+ [🔓🔒🔐]$")
async def cleanup_orphaned_auto_rooms(bot: discord.Client):
"""Supprime les auto rooms orphelines (vides) au démarrage du bot."""
enabled, trigger_channel_id = _auto_rooms_config()
if not enabled:
return
if not trigger_channel_id:
return
deleted = 0
# Les rooms persistées sont connues même si leur nom a été modifié manuellement.
for (guild_id, owner_id), room in list(_rooms.items()):
channel = bot.get_channel(room["voice_channel_id"])
if isinstance(channel, discord.VoiceChannel) and not channel.members:
try:
await channel.delete(reason="Nettoyage auto room vide au démarrage")
_del_room(guild_id, owner_id)
deleted += 1
except discord.HTTPException:
pass
for guild in bot.guilds:
trigger_channel = guild.get_channel(trigger_channel_id)
if not trigger_channel or not trigger_channel.category:
continue
category = trigger_channel.category
for channel in list(category.voice_channels):
if channel.id == trigger_channel_id:
continue
if not _AUTO_ROOM_NAME_PATTERN.match(channel.name):
continue
if len(channel.members) == 0:
try:
await channel.delete(reason="Nettoyage auto room orpheline au démarrage")
result = _find_room_by_channel(guild.id, channel.id)
if result:
_del_room(guild.id, result[0])
deleted += 1
except discord.HTTPException:
pass
if deleted > 0:
logging.info(f"Nettoyage auto rooms : {deleted} salon(s) orphelin(s) supprimé(s)")
async def restore_auto_rooms(bot: discord.Client):
"""Recharge les salons encore existants après un redémarrage du bot."""
with webapp.app_context():
records = AutoRoom.query.all()
for record in records:
guild = bot.get_guild(int(record.guild_id))
channel = guild.get_channel(int(record.voice_channel_id)) if guild else None
if not isinstance(channel, discord.VoiceChannel):
with webapp.app_context():
db.session.delete(db.session.merge(record))
db.session.commit()
continue
try:
whitelist = set(json.loads(record.whitelist or "[]"))
blacklist = set(json.loads(record.blacklist or "[]"))
managed_member_ids = set(json.loads(record.managed_member_ids or "[]"))
except (TypeError, ValueError):
whitelist, blacklist, managed_member_ids = set(), set(), set()
_set_room(int(record.guild_id), int(record.owner_id), {
"guild_id": int(record.guild_id),
"voice_channel_id": int(record.voice_channel_id),
"control_message_id": int(record.control_message_id) if record.control_message_id else None,
"owner_id": int(record.owner_id),
"whitelist": whitelist,
"blacklist": blacklist,
"managed_member_ids": managed_member_ids,
"access_mode": record.access_mode or "open",
})
async def on_voice_state_update_auto_rooms(bot: discord.Client, member: Member, before: VoiceState, after: VoiceState):
enabled, trigger_channel_id = _auto_rooms_config()
if not enabled:
return
if not trigger_channel_id:
return
guild = member.guild
if after.channel and after.channel.id == trigger_channel_id:
existing_room = _get_room(guild.id, member.id)
if existing_room:
old_channel = bot.get_channel(existing_room["voice_channel_id"])
if old_channel and isinstance(old_channel, discord.VoiceChannel):
# Ne jamais abandonner une room encore occupée : le propriétaire y retourne.
await member.move_to(old_channel)
return
_del_room(guild.id, member.id)
category = after.channel.category
channel_name = f"Salon de {member.display_name} {_status_emoji('open')}"
try:
new_channel = await guild.create_voice_channel(
name=channel_name,
category=category,
reason="Auto room"
)
await member.move_to(new_channel)
# Créer la room data d'abord
room_data = {
"guild_id": guild.id,
"voice_channel_id": new_channel.id,
"control_message_id": None, # Sera mis à jour après
"owner_id": member.id,
"whitelist": set(),
"blacklist": set(),
"managed_member_ids": set(),
"access_mode": "open",
}
control_message_id = await send_control_panel(bot, guild.id, member, new_channel, room_data)
if not control_message_id:
await new_channel.delete(reason="Panneau Auto Room impossible à créer")
await member.move_to(after.channel)
return
room_data["control_message_id"] = control_message_id
_set_room(guild.id, member.id, room_data)
logging.info(f"Auto room créé : {new_channel.name} pour {member.display_name}")
except discord.HTTPException as e:
logging.error(f"Erreur création auto room : {e}")
if before.channel and before.channel != after.channel and before.channel.id != trigger_channel_id:
result = _find_room_by_channel(guild.id, before.channel.id)
if result:
owner_id, room = result
remaining = [m for m in before.channel.members if m.id != member.id]
if member.id == owner_id and remaining:
new_owner = remaining[0]
_del_room(guild.id, owner_id)
room["owner_id"] = new_owner.id
_set_room(guild.id, new_owner.id, room)
await before.channel.send(f"👑 {new_owner.mention} est maintenant propriétaire du salon.")
await _update_control_panel(bot, guild.id, new_owner.id, before.channel)
elif len(remaining) == 0:
_del_room(guild.id, owner_id)
try:
await before.channel.delete(reason="Auto room vide")
except discord.HTTPException:
pass
async def on_message_auto_rooms(bot: discord.Client, message: discord.Message):
"""Gère les messages dans les salons vocaux pour les actions (statut, liste blanche/noire, etc.)."""
if message.author.bot:
return
enabled, _ = _auto_rooms_config()
if not enabled:
return
# Vérifier si c'est dans un salon vocal (partie texte)
if not isinstance(message.channel, discord.VoiceChannel):
return
# Trouver si c'est une auto room
result = _find_room_by_channel(message.guild.id, message.channel.id)
if not result:
return
owner_id, room = result
# Seul le propriétaire peut interagir
if message.author.id != owner_id:
return
voice_channel = message.channel
# Gestion du statut de salon
if room.get("awaiting_status"):
room["awaiting_status"] = False
new_status = message.content.strip()
try:
if new_status.lower() in ("clear", "effacer", "supprimer", "delete"):
await voice_channel.edit(status=None)
await message.channel.send("✅ Le statut du salon a été supprimé.")
elif len(new_status) > 500:
await message.channel.send("❌ Le statut ne peut pas dépasser 500 caractères.")
room["awaiting_status"] = True # Réessayer
else:
await voice_channel.edit(status=new_status)
await message.channel.send(f"✅ Le statut du salon a été mis à jour : **{new_status}**")
except discord.HTTPException as e:
await message.channel.send(f"❌ Erreur lors de la modification du statut : {e}")
return
# Gestion de la liste blanche (si en attente)
if room.get("awaiting_whitelist"):
room["awaiting_whitelist"] = False
if message.mentions:
target = message.mentions[0]
whitelist = room.get("whitelist", set())
if target.id in whitelist:
whitelist.remove(target.id)
await message.channel.send(f"✅ {target.mention} a été retiré de la liste blanche.")
else:
whitelist.add(target.id)
await message.channel.send(f"✅ {target.mention} a été ajouté à la liste blanche.")
room["whitelist"] = whitelist
blacklist = room.get("blacklist", set())
blacklist.discard(target.id)
room["blacklist"] = blacklist
await _apply_access_mode(voice_channel, room.get("access_mode", "open"), whitelist, blacklist, room)
_persist_room(message.guild.id, owner_id, room)
await _update_control_panel(bot, message.guild.id, owner_id, message.channel)
return
# Gestion de la liste noire (si en attente)
if room.get("awaiting_blacklist"):
room["awaiting_blacklist"] = False
if message.mentions:
target = message.mentions[0]
blacklist = room.get("blacklist", set())
if target.id in blacklist:
blacklist.remove(target.id)
await message.channel.send(f"✅ {target.mention} a été retiré de la liste noire.")
else:
blacklist.add(target.id)
await message.channel.send(f"✅ {target.mention} a été ajouté à la liste noire.")
room["blacklist"] = blacklist
whitelist = room.get("whitelist", set())
whitelist.discard(target.id)
room["whitelist"] = whitelist
await _apply_access_mode(voice_channel, room.get("access_mode", "open"), whitelist, blacklist, room)
_persist_room(message.guild.id, owner_id, room)
await _update_control_panel(bot, message.guild.id, owner_id, message.channel)
return
# Gestion du transfert de propriété (si en attente)
if room.get("awaiting_transfer"):
room["awaiting_transfer"] = False
if message.mentions:
new_owner = message.mentions[0]
if new_owner.id == owner_id:
await message.channel.send("❌ Vous êtes déjà le propriétaire du salon.")
return
# Transférer la propriété
old_owner_id = owner_id
_del_room(message.guild.id, old_owner_id)
room["owner_id"] = new_owner.id
_set_room(message.guild.id, new_owner.id, room)
# Renommer le salon
try:
base_name = f"Salon de {new_owner.display_name}"
new_name = f"{base_name} {_status_emoji(room.get('access_mode', 'open'))}"
await voice_channel.edit(name=new_name)
except discord.HTTPException:
pass
await message.channel.send(f"✅ La propriété du salon a été transférée à {new_owner.mention}.")
await _update_control_panel(bot, message.guild.id, new_owner.id, message.channel)
return
async def on_raw_reaction_add_auto_rooms(bot: discord.Client, payload: discord.RawReactionActionEvent):
"""Seul le propriétaire peut réagir ; on retire la réaction des autres."""
if payload.user_id == bot.user.id:
return
enabled, _ = _auto_rooms_config()
if not enabled:
return
room_info = _find_room_by_message(payload.message_id)
if not room_info:
return
guild_id, owner_id, room = room_info
if payload.user_id != owner_id:
try:
channel = bot.get_channel(payload.channel_id)
if channel:
msg = await channel.fetch_message(payload.message_id)
user = payload.member or await bot.fetch_user(payload.user_id)
await msg.remove_reaction(payload.emoji, user)
except discord.HTTPException:
pass
return
emoji_str = str(payload.emoji)
action = None
for e, a, _ in REACTIONS:
if e == emoji_str:
action = a
break
if not action:
return
# Canal = salon vocal (le message est dans la partie texte du vocal)
channel = bot.get_channel(payload.channel_id)
if not channel or not hasattr(channel, "send"):
return
await _handle_reaction_action(bot, guild_id, owner_id, action, channel)
try:
msg = await channel.fetch_message(payload.message_id)
user = payload.member or await bot.fetch_user(payload.user_id)
await msg.remove_reaction(payload.emoji, user)
except discord.HTTPException:
pass