Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
14c2cf0144 | ||
|
|
d24be973b8 | ||
|
|
5e4af406ff | ||
|
|
4d8038cc77 | ||
|
|
651773e63d | ||
|
|
60b90edcb3 | ||
|
|
179876d2ce | ||
|
|
830ce61796 | ||
|
|
5709b1c0a3 | ||
|
|
11348bda39 | ||
|
|
f7e85bac69 | ||
|
|
d1d4e3b5a5 | ||
|
|
5aa52c8137 | ||
|
|
9bbfa1fade | ||
|
|
920ddfa172 |
+42
-3
@@ -7,7 +7,7 @@ from webapp import webapp
|
||||
from database import db
|
||||
from database.helpers import ConfigurationHelper
|
||||
from database.models import Configuration, Humeur, Commande
|
||||
from discord import Message, TextChannel, Member, VoiceChannel
|
||||
from discord import Message, TextChannel, Member, VoiceChannel, app_commands
|
||||
from discordbot.humblebundle import checkHumbleBundleAndNotify
|
||||
from discordbot.freeloot import checkFreeLootAndNotify
|
||||
from discordbot.moderation import (
|
||||
@@ -21,18 +21,49 @@ from discordbot.moderation import (
|
||||
handle_ban_list_command,
|
||||
handle_staff_help_command,
|
||||
handle_timeout_command,
|
||||
handle_say_command
|
||||
handle_say_command,
|
||||
handle_transfer_command,
|
||||
transfer_message_context_menu
|
||||
)
|
||||
from discordbot.welcome import sendWelcomeMessage, sendLeaveMessage, updateInviteCache
|
||||
from discordbot.youtube import checkYouTubeVideos
|
||||
from discordbot.auto_rooms import on_voice_state_update_auto_rooms, on_raw_reaction_add_auto_rooms
|
||||
from discordbot.auto_rooms import on_voice_state_update_auto_rooms, on_raw_reaction_add_auto_rooms, on_message_auto_rooms
|
||||
from protondb import searhProtonDb
|
||||
|
||||
class DiscordBot(discord.Client):
|
||||
def __init__(self, *, intents: discord.Intents):
|
||||
super().__init__(intents=intents)
|
||||
self.tree = app_commands.CommandTree(self)
|
||||
self.synced = False
|
||||
|
||||
async def setup_hook(self):
|
||||
self.tree.add_command(transfer_message_context_menu)
|
||||
logging.info("Commande contextuelle 'Déplacer le message' ajoutée au CommandTree")
|
||||
|
||||
async def on_ready(self):
|
||||
logging.info(f'Connecté en tant que {self.user} (ID: {self.user.id})')
|
||||
webapp.config["BOT_STATUS"]["discord_connected"] = True
|
||||
webapp.config["BOT_STATUS"]["discord_guild_count"] = len(self.guilds)
|
||||
|
||||
if not self.synced:
|
||||
try:
|
||||
logging.info("Synchronisation des commandes d'application en cours...")
|
||||
|
||||
for guild in self.guilds:
|
||||
try:
|
||||
synced = await self.tree.sync(guild=guild)
|
||||
logging.info(f"✅ {len(synced)} commande(s) synchronisée(s) pour le serveur '{guild.name}' (ID: {guild.id})")
|
||||
except Exception as e:
|
||||
logging.error(f"❌ Erreur lors de la synchronisation pour {guild.name}: {e}")
|
||||
|
||||
synced_global = await self.tree.sync()
|
||||
logging.info(f"✅ {len(synced_global)} commande(s) synchronisée(s) globalement")
|
||||
|
||||
self.synced = True
|
||||
logging.info("🎉 Synchronisation complète terminée - Les commandes sont maintenant disponibles !")
|
||||
except Exception as e:
|
||||
logging.error(f"❌ Erreur lors de la synchronisation des commandes: {e}")
|
||||
|
||||
for c in self.get_all_channels() :
|
||||
logging.info(f'{c.id} {c.name}')
|
||||
|
||||
@@ -120,6 +151,10 @@ bot = DiscordBot(intents=intents)
|
||||
async def on_message(message: Message):
|
||||
if message.author == bot.user:
|
||||
return
|
||||
|
||||
# Gestion des messages dans les auto rooms (avant le check des commandes !)
|
||||
await on_message_auto_rooms(bot, message)
|
||||
|
||||
if not message.content.startswith('!'):
|
||||
return
|
||||
command_name = message.content.split()[0]
|
||||
@@ -167,6 +202,10 @@ async def on_message(message: Message):
|
||||
await handle_say_command(message, bot)
|
||||
return
|
||||
|
||||
if command_name in ['!transfert', '!transfer', '!move']:
|
||||
await handle_transfer_command(message, bot)
|
||||
return
|
||||
|
||||
if command_name in ['!aide', '!help']:
|
||||
await handle_staff_help_command(message, bot)
|
||||
return
|
||||
|
||||
+293
-68
@@ -16,13 +16,14 @@ _control_message_ids: dict[int, tuple[int, int]] = {}
|
||||
REACTIONS = [
|
||||
("🔓", "open", "Ouvert"),
|
||||
("🔒", "closed", "Fermé"),
|
||||
("🛡️", "private", "Privé"),
|
||||
("🔐", "private", "Privé"),
|
||||
("✅", "whitelist", "Liste blanche"),
|
||||
("🚫", "blacklist", "Liste noire"),
|
||||
("🧹", "purge", "Purge"),
|
||||
("👑", "transfer", "Propriété"),
|
||||
("🎤", "speak", "Micro"),
|
||||
("📹", "stream", "Vidéo"),
|
||||
("📊", "soundboards", "Soundboards"),
|
||||
("📝", "status", "Statut"),
|
||||
]
|
||||
|
||||
@@ -34,40 +35,85 @@ def _status_display(access_mode: str) -> str:
|
||||
if access_mode == "closed":
|
||||
return "🔒 Fermé"
|
||||
if access_mode == "private":
|
||||
return "🔒 Privé"
|
||||
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) -> discord.Embed:
|
||||
"""Construit l’embed de config avec infos du salon."""
|
||||
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",
|
||||
title="⚙️ Configuration du salon",
|
||||
description=(
|
||||
"Voici l’espace de configuration de votre salon vocal. "
|
||||
"Utilisez les réactions ci-dessous — seul le propriétaire peut réagir."
|
||||
"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.blurple()
|
||||
color=discord.Color.orange()
|
||||
)
|
||||
members_count = len(voice_channel.members)
|
||||
user_limit = voice_channel.user_limit or 0
|
||||
limit_text = f"{user_limit} max" if user_limit else "Illimitée"
|
||||
members_text = f"{members_count} / {user_limit}" if user_limit else str(members_count)
|
||||
bitrate_kbps = (voice_channel.bitrate or 0) // 1000
|
||||
|
||||
embed.add_field(name="Propriétaire", value=owner.mention, inline=True)
|
||||
embed.add_field(name="Statut du salon", value=_status_display(access_mode), inline=True)
|
||||
embed.add_field(name="Nom du salon", value=voice_channel.name, inline=True)
|
||||
embed.add_field(name="Membres", value=members_text, inline=True)
|
||||
embed.add_field(name="Limite", value=limit_text, inline=True)
|
||||
embed.add_field(name="Bitrate", value=f"{bitrate_kbps} kbps", inline=True)
|
||||
embed.add_field(name="Accès", value="🔓 Ouvert · 🔒 Fermé · 🛡️ Privé", inline=False)
|
||||
embed.add_field(name="Listes", value="✅ Liste blanche · 🚫 Liste noire", inline=False)
|
||||
embed.add_field(name="Actions", value="🧹 Purge · 👑 Propriété · 🎤 Micro · 📹 Vidéo · 📝 Statut", inline=False)
|
||||
# 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
|
||||
|
||||
|
||||
@@ -114,11 +160,26 @@ def _find_room_by_message(message_id: int) -> Optional[tuple[int, int, dict]]:
|
||||
async def _apply_access_mode(channel: discord.VoiceChannel, mode: str, whitelist: set, blacklist: set):
|
||||
guild = channel.guild
|
||||
everyone = guild.default_role
|
||||
overwrites = {}
|
||||
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
|
||||
# Retirer les overwrites des membres qui ne sont plus dans la blacklist
|
||||
for target in list(overwrites.keys()):
|
||||
if target != everyone and isinstance(target, discord.Member):
|
||||
if 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:
|
||||
@@ -126,6 +187,12 @@ async def _apply_access_mode(channel: discord.VoiceChannel, mode: str, whitelist
|
||||
elif mode == "closed":
|
||||
everyone_ow.connect = False
|
||||
everyone_ow.view_channel = True
|
||||
# Retirer les overwrites des membres qui ne sont plus dans la whitelist
|
||||
for target in list(overwrites.keys()):
|
||||
if target != everyone and isinstance(target, discord.Member):
|
||||
if 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:
|
||||
@@ -133,10 +200,17 @@ async def _apply_access_mode(channel: discord.VoiceChannel, mode: str, whitelist
|
||||
elif mode == "private":
|
||||
everyone_ow.connect = False
|
||||
everyone_ow.view_channel = False
|
||||
# Retirer les overwrites des membres qui ne sont plus dans la whitelist
|
||||
for target in list(overwrites.keys()):
|
||||
if target != everyone and isinstance(target, discord.Member):
|
||||
if 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)
|
||||
|
||||
@@ -145,7 +219,7 @@ async def _handle_reaction_action(bot: discord.Client, guild_id: int, owner_id:
|
||||
"""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.")
|
||||
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):
|
||||
@@ -155,36 +229,27 @@ async def _handle_reaction_action(bot: discord.Client, guild_id: int, owner_id:
|
||||
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()))
|
||||
# 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 **{action}**.")
|
||||
# Mettre à jour uniquement le statut (cadenas) dans le message de config
|
||||
control_message_id = room.get("control_message_id")
|
||||
if control_message_id:
|
||||
try:
|
||||
msg = await channel.fetch_message(control_message_id)
|
||||
if msg.embeds:
|
||||
embed = msg.embeds[0].copy()
|
||||
for i, f in enumerate(embed.fields):
|
||||
if f.name == "Statut du salon":
|
||||
embed.set_field_at(i, name="Statut du salon", value=_status_display(action), inline=f.inline)
|
||||
break
|
||||
else:
|
||||
embed.add_field(name="Statut du salon", value=_status_display(action), inline=False)
|
||||
await msg.edit(embed=embed)
|
||||
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":
|
||||
await channel.send("Liste blanche : mentionnez un membre pour l’ajouter/retirer.")
|
||||
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":
|
||||
await channel.send("Liste noire : mentionnez un membre pour l’ajouter/retirer.")
|
||||
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())
|
||||
@@ -197,30 +262,80 @@ async def _handle_reaction_action(bot: discord.Client, guild_id: int, owner_id:
|
||||
kicked += 1
|
||||
except discord.HTTPException:
|
||||
pass
|
||||
await channel.send(f"Purge effectuée : {kicked} membre(s) déconnecté(s).")
|
||||
await channel.send(f"🧹 Purge effectuée : {kicked} membre(s) déconnecté(s).")
|
||||
|
||||
elif action == "transfer":
|
||||
await channel.send("Transférer le salon : mentionnez le membre à qui donner la propriété.")
|
||||
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"):
|
||||
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)
|
||||
setattr(ow, action, not current if current is not None else False)
|
||||
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)
|
||||
label = "Micro" if action == "speak" else "Vidéo"
|
||||
await channel.send(f"{label} : {'autorisé' if getattr(ow, action) else 'désactivé'} pour tous.")
|
||||
|
||||
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":
|
||||
status_text = _status_display(room.get("access_mode", "open"))
|
||||
await channel.send(f"Statut du salon : {status_text}\nRépondez avec le nouveau nom du salon pour le modifier.")
|
||||
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 send_control_panel(bot: discord.Client, guild_id: int, owner: Member, voice_channel: discord.VoiceChannel) -> 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")
|
||||
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)
|
||||
@@ -229,7 +344,7 @@ async def send_control_panel(bot: discord.Client, guild_id: int, owner: Member,
|
||||
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}")
|
||||
logging.error(f"Impossible d'envoyer le panneau Auto Room dans le vocal : {e}")
|
||||
return None
|
||||
|
||||
|
||||
@@ -245,8 +360,8 @@ async def on_voice_state_update_auto_rooms(bot: discord.Client, member: Member,
|
||||
|
||||
if after.channel and after.channel.id == trigger_channel_id:
|
||||
category = after.channel.category
|
||||
# Nom du salon avec statut (cadenas) à la création
|
||||
channel_name = f"Salon de {member.display_name} {_status_emoji('open')}"
|
||||
# Nom du salon (simple, sans emoji pour éviter le rate limiting)
|
||||
channel_name = f"Salon de {member.display_name}"
|
||||
try:
|
||||
new_channel = await guild.create_voice_channel(
|
||||
name=channel_name,
|
||||
@@ -254,16 +369,22 @@ async def on_voice_state_update_auto_rooms(bot: discord.Client, member: Member,
|
||||
reason="Auto room"
|
||||
)
|
||||
await member.move_to(new_channel)
|
||||
control_message_id = await send_control_panel(bot, guild.id, member, new_channel)
|
||||
_set_room(guild.id, member.id, {
|
||||
|
||||
# Créer la room data d'abord
|
||||
room_data = {
|
||||
"guild_id": guild.id,
|
||||
"voice_channel_id": new_channel.id,
|
||||
"control_message_id": control_message_id,
|
||||
"control_message_id": None, # Sera mis à jour après
|
||||
"owner_id": member.id,
|
||||
"whitelist": set(),
|
||||
"blacklist": set(),
|
||||
"access_mode": "open",
|
||||
})
|
||||
}
|
||||
|
||||
control_message_id = await send_control_panel(bot, guild.id, member, new_channel, room_data)
|
||||
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}")
|
||||
@@ -287,6 +408,110 @@ async def on_voice_state_update_auto_rooms(bot: discord.Client, member: Member,
|
||||
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
|
||||
if not ConfigurationHelper().getValue("auto_rooms_enable"):
|
||||
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
|
||||
await _apply_access_mode(voice_channel, room.get("access_mode", "open"), whitelist, room.get("blacklist", set()))
|
||||
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
|
||||
await _apply_access_mode(voice_channel, room.get("access_mode", "open"), room.get("whitelist", set()), blacklist)
|
||||
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 (optionnel, peut être commenté pour éviter rate limit)
|
||||
try:
|
||||
new_name = f"Salon de {new_owner.display_name}"
|
||||
await voice_channel.edit(name=new_name)
|
||||
except discord.HTTPException as e:
|
||||
logging.warning(f"Impossible de renommer le salon lors du transfert : {e}")
|
||||
|
||||
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:
|
||||
|
||||
+542
-2
@@ -4,12 +4,14 @@ import time
|
||||
import os
|
||||
import re
|
||||
import discord
|
||||
import io
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from zoneinfo import ZoneInfo
|
||||
from database import db
|
||||
from database.helpers import ConfigurationHelper
|
||||
from database.models import ModerationEvent
|
||||
from discord import Message
|
||||
from discord import Message, TextChannel, ForumChannel, Thread, app_commands
|
||||
from discord.ui import Modal, TextInput, View, Select, ChannelSelect
|
||||
|
||||
def _get_local_tz():
|
||||
tz_name = os.environ.get('APP_TZ') or os.environ.get('TZ') or 'Europe/Paris'
|
||||
@@ -1055,7 +1057,15 @@ async def handle_staff_help_command(message: Message, bot):
|
||||
value=(
|
||||
"• `!say #channel message`\n"
|
||||
" Envoie un message en tant que bot\n"
|
||||
" Ex: `!say #annonces Nouvelle fonctionnalité !`"
|
||||
" Ex: `!say #annonces Nouvelle fonctionnalité !`\n\n"
|
||||
"• `!transfert #canal message_id [raison]`\n"
|
||||
" Transfère un message vers un autre canal\n"
|
||||
" *Alias: !transfer, !move*\n"
|
||||
" Ex: `!transfert #entraide 123456789012345678`\n"
|
||||
" Ex: `!transfert #général https://discord.com/channels/.../...`\n"
|
||||
" Le message sera envoyé comme si c'était l'auteur original\n"
|
||||
" Supporte les canaux textuels, threads et forums (crée un post)\n"
|
||||
" Pour les forums, la raison devient le titre du post"
|
||||
),
|
||||
inline=False
|
||||
)
|
||||
@@ -1405,3 +1415,533 @@ async def handle_say_command(message: Message, bot):
|
||||
except Exception as e:
|
||||
logging.error(f"Erreur lors de l'envoi du message: {e}")
|
||||
|
||||
async def handle_transfer_command(message: Message, bot):
|
||||
if not has_staff_role(message.author.roles):
|
||||
await send_access_denied(message.channel)
|
||||
return
|
||||
|
||||
parts = message.content.split(maxsplit=3)
|
||||
|
||||
if len(parts) < 3:
|
||||
embed = discord.Embed(
|
||||
title="📋 Utilisation de la commande",
|
||||
description="**Syntaxe :** `!transfert #canal message_id [raison]` ou `!transfert #canal lien_message [raison]`",
|
||||
color=discord.Color.blue()
|
||||
)
|
||||
embed.add_field(
|
||||
name="Exemples",
|
||||
value=(
|
||||
"• `!transfert #entraide 123456789012345678`\n"
|
||||
"• `!transfert #general https://discord.com/channels/.../...`\n"
|
||||
"• `!transfert #entraide 123456789012345678 Message posté dans le mauvais canal`"
|
||||
),
|
||||
inline=False
|
||||
)
|
||||
embed.add_field(
|
||||
name="Aliases",
|
||||
value="`!transfert`, `!transfer`, `!move`",
|
||||
inline=False
|
||||
)
|
||||
embed.add_field(
|
||||
name="💡 Astuce",
|
||||
value="Faites un clic droit sur un message → Copier l'identifiant du message, ou copiez le lien du message",
|
||||
inline=False
|
||||
)
|
||||
msg = await message.channel.send(embed=embed)
|
||||
asyncio.create_task(delete_after_delay(msg))
|
||||
return
|
||||
|
||||
target_channel = None
|
||||
if message.channel_mentions:
|
||||
target_channel = message.channel_mentions[0]
|
||||
else:
|
||||
try:
|
||||
channel_id = int(parts[1].strip('<#>'))
|
||||
target_channel = bot.get_channel(channel_id)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if not target_channel:
|
||||
embed = discord.Embed(
|
||||
title="❌ Erreur",
|
||||
description="Canal de destination invalide. Mentionnez un canal avec #canal.",
|
||||
color=discord.Color.red()
|
||||
)
|
||||
msg = await message.channel.send(embed=embed)
|
||||
asyncio.create_task(delete_after_delay(msg))
|
||||
return
|
||||
|
||||
if not isinstance(target_channel, (TextChannel, ForumChannel, Thread)):
|
||||
embed = discord.Embed(
|
||||
title="❌ Erreur",
|
||||
description=f"Le transfert n'est pas supporté vers ce type de canal. Utilisez un canal textuel, un forum ou un thread.",
|
||||
color=discord.Color.red()
|
||||
)
|
||||
msg = await message.channel.send(embed=embed)
|
||||
asyncio.create_task(delete_after_delay(msg))
|
||||
return
|
||||
|
||||
message_id = None
|
||||
source_channel = message.channel
|
||||
|
||||
if 'discord.com/channels/' in parts[2]:
|
||||
try:
|
||||
link_parts = parts[2].split('/')
|
||||
source_channel_id = int(link_parts[-2])
|
||||
message_id = int(link_parts[-1])
|
||||
source_channel = bot.get_channel(source_channel_id)
|
||||
|
||||
if not source_channel:
|
||||
embed = discord.Embed(
|
||||
title="❌ Erreur",
|
||||
description="Canal source introuvable.",
|
||||
color=discord.Color.red()
|
||||
)
|
||||
msg = await message.channel.send(embed=embed)
|
||||
asyncio.create_task(delete_after_delay(msg))
|
||||
return
|
||||
except (ValueError, IndexError):
|
||||
embed = discord.Embed(
|
||||
title="❌ Erreur",
|
||||
description="Lien de message invalide.",
|
||||
color=discord.Color.red()
|
||||
)
|
||||
msg = await message.channel.send(embed=embed)
|
||||
asyncio.create_task(delete_after_delay(msg))
|
||||
return
|
||||
else:
|
||||
try:
|
||||
message_id = int(parts[2])
|
||||
except ValueError:
|
||||
embed = discord.Embed(
|
||||
title="❌ Erreur",
|
||||
description="ID de message invalide. Utilisez un ID numérique ou un lien Discord.",
|
||||
color=discord.Color.red()
|
||||
)
|
||||
msg = await message.channel.send(embed=embed)
|
||||
asyncio.create_task(delete_after_delay(msg))
|
||||
return
|
||||
|
||||
try:
|
||||
original_message = await source_channel.fetch_message(message_id)
|
||||
except discord.NotFound:
|
||||
embed = discord.Embed(
|
||||
title="❌ Erreur",
|
||||
description="Message introuvable. Vérifiez l'ID ou le lien.",
|
||||
color=discord.Color.red()
|
||||
)
|
||||
msg = await message.channel.send(embed=embed)
|
||||
asyncio.create_task(delete_after_delay(msg))
|
||||
return
|
||||
except discord.Forbidden:
|
||||
embed = discord.Embed(
|
||||
title="❌ Erreur",
|
||||
description="Je n'ai pas la permission d'accéder à ce message.",
|
||||
color=discord.Color.red()
|
||||
)
|
||||
msg = await message.channel.send(embed=embed)
|
||||
asyncio.create_task(delete_after_delay(msg))
|
||||
return
|
||||
|
||||
reason = parts[3] if len(parts) > 3 else "Message posté dans le mauvais canal"
|
||||
|
||||
content = original_message.content
|
||||
embeds = original_message.embeds
|
||||
files_to_send = []
|
||||
|
||||
for attachment in original_message.attachments:
|
||||
try:
|
||||
file_data = await attachment.read()
|
||||
files_to_send.append(discord.File(
|
||||
fp=io.BytesIO(file_data),
|
||||
filename=attachment.filename
|
||||
))
|
||||
except Exception as e:
|
||||
logging.error(f"Erreur lors du téléchargement de la pièce jointe: {e}")
|
||||
|
||||
transferred_message = None
|
||||
|
||||
if isinstance(target_channel, ForumChannel):
|
||||
try:
|
||||
post_title = f"{original_message.author.display_name} - "
|
||||
if reason and reason != "Message posté dans le mauvais canal":
|
||||
remaining_length = 100 - len(post_title)
|
||||
post_title += reason[:remaining_length]
|
||||
elif content and len(content) > 0:
|
||||
remaining_length = 100 - len(post_title)
|
||||
post_title += content[:remaining_length]
|
||||
else:
|
||||
post_title += "Message transféré"
|
||||
|
||||
if len(post_title) > 100:
|
||||
post_title = post_title[:97] + "..."
|
||||
|
||||
transfer_notice = f"**Message original de {original_message.author.mention}**\n"
|
||||
transfer_notice += f"*Ce message a été transféré par un membre du staff depuis {source_channel.mention}*\n"
|
||||
transfer_notice += "─" * 50 + "\n\n"
|
||||
|
||||
full_content = transfer_notice + (content or "")
|
||||
|
||||
thread = await target_channel.create_thread(
|
||||
name=post_title,
|
||||
content=full_content,
|
||||
embeds=embeds[:10] if embeds else [],
|
||||
files=files_to_send,
|
||||
reason=f"Transfert depuis {source_channel.name} par {message.author.name}"
|
||||
)
|
||||
transferred_message = thread.message
|
||||
|
||||
except discord.HTTPException as e:
|
||||
logging.error(f"Erreur lors de la création du post dans le forum: {e}")
|
||||
embed = discord.Embed(
|
||||
title="❌ Erreur",
|
||||
description=f"Une erreur est survenue lors de la création du post dans le forum: {str(e)}",
|
||||
color=discord.Color.red()
|
||||
)
|
||||
msg = await message.channel.send(embed=embed)
|
||||
asyncio.create_task(delete_after_delay(msg))
|
||||
return
|
||||
|
||||
else:
|
||||
webhooks = await target_channel.webhooks()
|
||||
webhook = None
|
||||
|
||||
for wh in webhooks:
|
||||
if wh.user == bot.user:
|
||||
webhook = wh
|
||||
break
|
||||
|
||||
if not webhook:
|
||||
try:
|
||||
webhook = await target_channel.create_webhook(name="Mamie Henriette - Transfert")
|
||||
except discord.Forbidden:
|
||||
embed = discord.Embed(
|
||||
title="❌ Erreur",
|
||||
description="Je n'ai pas la permission de créer un webhook dans le canal de destination.",
|
||||
color=discord.Color.red()
|
||||
)
|
||||
msg = await message.channel.send(embed=embed)
|
||||
asyncio.create_task(delete_after_delay(msg))
|
||||
return
|
||||
|
||||
try:
|
||||
transferred_message = await webhook.send(
|
||||
content=content,
|
||||
username=original_message.author.display_name,
|
||||
avatar_url=original_message.author.display_avatar.url,
|
||||
embeds=embeds[:10] if embeds else [],
|
||||
files=files_to_send,
|
||||
allowed_mentions=discord.AllowedMentions.none(),
|
||||
wait=True
|
||||
)
|
||||
except discord.HTTPException as e:
|
||||
logging.error(f"Erreur lors du transfert du message: {e}")
|
||||
embed = discord.Embed(
|
||||
title="❌ Erreur",
|
||||
description=f"Une erreur est survenue lors du transfert du message: {str(e)}",
|
||||
color=discord.Color.red()
|
||||
)
|
||||
msg = await message.channel.send(embed=embed)
|
||||
asyncio.create_task(delete_after_delay(msg))
|
||||
return
|
||||
|
||||
try:
|
||||
await original_message.delete()
|
||||
except discord.Forbidden:
|
||||
logging.warning(f"Impossible de supprimer le message original (ID: {message_id})")
|
||||
|
||||
transfer_details = f"De {source_channel.name} vers {target_channel.name}"
|
||||
if isinstance(target_channel, ForumChannel):
|
||||
transfer_details += " (forum)"
|
||||
|
||||
transfer_event = ModerationEvent(
|
||||
type='transfer',
|
||||
username=original_message.author.name,
|
||||
discord_id=str(original_message.author.id),
|
||||
created_at=datetime.now(timezone.utc),
|
||||
reason=f"{reason} | {transfer_details}",
|
||||
staff_id=str(message.author.id),
|
||||
staff_name=message.author.name
|
||||
)
|
||||
db.session.add(transfer_event)
|
||||
_commit_with_retry()
|
||||
|
||||
local_now = _to_local(datetime.now(timezone.utc))
|
||||
destination_info = target_channel.mention if isinstance(target_channel, (TextChannel, Thread)) else f"le forum {target_channel.name}"
|
||||
|
||||
embed = discord.Embed(
|
||||
title="✅ Message transféré",
|
||||
description=f"Le message de **{original_message.author.name}** a été transféré vers {destination_info}",
|
||||
color=discord.Color.green(),
|
||||
timestamp=datetime.now(timezone.utc)
|
||||
)
|
||||
embed.add_field(name="👤 Auteur original", value=f"{original_message.author.mention}", inline=True)
|
||||
embed.add_field(name="📤 Canal source", value=source_channel.mention, inline=True)
|
||||
embed.add_field(name="📥 Canal destination", value=destination_info, inline=True)
|
||||
embed.add_field(name="🛡️ Modérateur", value=message.author.mention, inline=True)
|
||||
embed.add_field(name="📝 Raison", value=reason, inline=False)
|
||||
|
||||
if isinstance(target_channel, ForumChannel):
|
||||
embed.add_field(name="ℹ️ Type", value="Nouveau post créé dans le forum", inline=False)
|
||||
|
||||
confirmation_msg = await source_channel.send(embed=embed)
|
||||
asyncio.create_task(delete_after_delay(confirmation_msg))
|
||||
|
||||
log_embed = discord.Embed(
|
||||
title="📨 Transfert de message",
|
||||
description=f"Un message de **{original_message.author.name}** a été transféré",
|
||||
color=discord.Color.blue(),
|
||||
timestamp=datetime.now(timezone.utc)
|
||||
)
|
||||
log_embed.add_field(name="👤 Auteur original", value=f"{original_message.author.name}\n`{original_message.author.id}`", inline=True)
|
||||
log_embed.add_field(name="🛡️ Modérateur", value=f"**{message.author.name}**", inline=True)
|
||||
log_embed.add_field(name="📅 Date et heure", value=local_now.strftime('%d/%m/%Y à %H:%M'), inline=True)
|
||||
log_embed.add_field(name="📤 De", value=source_channel.mention, inline=True)
|
||||
log_embed.add_field(name="📥 Vers", value=f"{target_channel.name} ({type(target_channel).__name__})", inline=True)
|
||||
log_embed.add_field(name="📝 Raison", value=reason, inline=False)
|
||||
|
||||
preview = content[:100] + "..." if content and len(content) > 100 else content
|
||||
if preview:
|
||||
log_embed.add_field(name="💬 Aperçu du message", value=preview, inline=False)
|
||||
|
||||
log_embed.set_footer(text=f"ID Auteur: {original_message.author.id} • Serveur: {message.guild.name}")
|
||||
|
||||
await send_to_moderation_log_channel(bot, log_embed)
|
||||
|
||||
await safe_delete_message(message)
|
||||
|
||||
class TransferReasonModal(Modal, title="Raison du transfert"):
|
||||
reason = TextInput(
|
||||
label="Raison / Titre du post (forum)",
|
||||
placeholder="Pour les forums, ceci devient le titre du post",
|
||||
required=False,
|
||||
max_length=200,
|
||||
style=discord.TextStyle.paragraph
|
||||
)
|
||||
|
||||
def __init__(self, message: discord.Message, bot, target_channel, selected_tags=None):
|
||||
super().__init__()
|
||||
self.message = message
|
||||
self.bot = bot
|
||||
self.target_channel = target_channel
|
||||
self.selected_tags = selected_tags or []
|
||||
|
||||
async def on_submit(self, interaction: discord.Interaction):
|
||||
await interaction.response.defer(ephemeral=True)
|
||||
|
||||
reason = self.reason.value.strip() if self.reason.value else "Message posté dans le mauvais canal"
|
||||
target_channel = self.target_channel
|
||||
source_channel = self.message.channel
|
||||
content = self.message.content
|
||||
embeds = self.message.embeds
|
||||
files_to_send = []
|
||||
|
||||
for attachment in self.message.attachments:
|
||||
try:
|
||||
file_data = await attachment.read()
|
||||
files_to_send.append(discord.File(
|
||||
fp=io.BytesIO(file_data),
|
||||
filename=attachment.filename
|
||||
))
|
||||
except Exception as e:
|
||||
logging.error(f"Erreur lors du téléchargement de la pièce jointe: {e}")
|
||||
|
||||
transferred_message = None
|
||||
|
||||
if isinstance(target_channel, ForumChannel):
|
||||
try:
|
||||
post_title = f"{self.message.author.display_name} - "
|
||||
if reason and reason != "Message posté dans le mauvais canal":
|
||||
remaining_length = 100 - len(post_title)
|
||||
post_title += reason[:remaining_length]
|
||||
elif content and len(content) > 0:
|
||||
remaining_length = 100 - len(post_title)
|
||||
post_title += content[:remaining_length]
|
||||
else:
|
||||
post_title += "Message transféré"
|
||||
|
||||
if len(post_title) > 100:
|
||||
post_title = post_title[:97] + "..."
|
||||
|
||||
transfer_notice = f"**Message original de {self.message.author.mention}**\n"
|
||||
transfer_notice += f"*Ce message a été transféré par un membre du staff depuis {source_channel.mention}*\n"
|
||||
transfer_notice += "─" * 50 + "\n\n"
|
||||
|
||||
full_content = transfer_notice + (content or "")
|
||||
|
||||
thread = await target_channel.create_thread(
|
||||
name=post_title,
|
||||
content=full_content,
|
||||
embeds=embeds[:10] if embeds else [],
|
||||
files=files_to_send,
|
||||
applied_tags=self.selected_tags,
|
||||
reason=f"Transfert depuis {source_channel.name} par {interaction.user.name}"
|
||||
)
|
||||
transferred_message = thread.message
|
||||
|
||||
if self.selected_tags:
|
||||
tag_names = ", ".join([tag.name for tag in self.selected_tags])
|
||||
logging.info(f"Post créé avec les tags: {tag_names}")
|
||||
|
||||
except discord.HTTPException as e:
|
||||
logging.error(f"Erreur lors de la création du post dans le forum: {e}")
|
||||
await interaction.followup.send(f"❌ Erreur lors de la création du post: {str(e)}", ephemeral=True)
|
||||
return
|
||||
else:
|
||||
webhooks = await target_channel.webhooks()
|
||||
webhook = None
|
||||
|
||||
for wh in webhooks:
|
||||
if wh.user == self.bot.user:
|
||||
webhook = wh
|
||||
break
|
||||
|
||||
if not webhook:
|
||||
try:
|
||||
webhook = await target_channel.create_webhook(name="Mamie Henriette - Transfert")
|
||||
except discord.Forbidden:
|
||||
await interaction.followup.send("❌ Je n'ai pas la permission de créer un webhook dans le canal de destination.", ephemeral=True)
|
||||
return
|
||||
|
||||
try:
|
||||
transferred_message = await webhook.send(
|
||||
content=content,
|
||||
username=self.message.author.display_name,
|
||||
avatar_url=self.message.author.display_avatar.url,
|
||||
embeds=embeds[:10] if embeds else [],
|
||||
files=files_to_send,
|
||||
allowed_mentions=discord.AllowedMentions.none(),
|
||||
wait=True
|
||||
)
|
||||
except discord.HTTPException as e:
|
||||
logging.error(f"Erreur lors du transfert du message: {e}")
|
||||
await interaction.followup.send(f"❌ Erreur lors du transfert: {str(e)}", ephemeral=True)
|
||||
return
|
||||
|
||||
try:
|
||||
await self.message.delete()
|
||||
except discord.Forbidden:
|
||||
logging.warning(f"Impossible de supprimer le message original (ID: {self.message.id})")
|
||||
|
||||
transfer_details = f"De {source_channel.name} vers {target_channel.name}"
|
||||
if isinstance(target_channel, ForumChannel):
|
||||
transfer_details += " (forum)"
|
||||
|
||||
transfer_event = ModerationEvent(
|
||||
type='transfer',
|
||||
username=self.message.author.name,
|
||||
discord_id=str(self.message.author.id),
|
||||
created_at=datetime.now(timezone.utc),
|
||||
reason=f"{reason} | {transfer_details}",
|
||||
staff_id=str(interaction.user.id),
|
||||
staff_name=interaction.user.name
|
||||
)
|
||||
db.session.add(transfer_event)
|
||||
_commit_with_retry()
|
||||
|
||||
destination_info = target_channel.mention if isinstance(target_channel, (TextChannel, Thread)) else f"le forum {target_channel.name}"
|
||||
|
||||
await interaction.followup.send(
|
||||
f"✅ Message de **{self.message.author.name}** transféré vers {destination_info}",
|
||||
ephemeral=True
|
||||
)
|
||||
|
||||
local_now = _to_local(datetime.now(timezone.utc))
|
||||
log_embed = discord.Embed(
|
||||
title="📨 Transfert de message",
|
||||
description=f"Un message de **{self.message.author.name}** a été transféré",
|
||||
color=discord.Color.blue(),
|
||||
timestamp=datetime.now(timezone.utc)
|
||||
)
|
||||
log_embed.add_field(name="👤 Auteur original", value=f"{self.message.author.name}\n`{self.message.author.id}`", inline=True)
|
||||
log_embed.add_field(name="🛡️ Modérateur", value=f"**{interaction.user.name}**", inline=True)
|
||||
log_embed.add_field(name="📅 Date et heure", value=local_now.strftime('%d/%m/%Y à %H:%M'), inline=True)
|
||||
log_embed.add_field(name="📤 De", value=source_channel.mention, inline=True)
|
||||
log_embed.add_field(name="📥 Vers", value=f"{target_channel.name} ({type(target_channel).__name__})", inline=True)
|
||||
log_embed.add_field(name="📝 Raison", value=reason, inline=False)
|
||||
|
||||
preview = content[:100] + "..." if content and len(content) > 100 else content
|
||||
if preview:
|
||||
log_embed.add_field(name="💬 Aperçu du message", value=preview, inline=False)
|
||||
|
||||
log_embed.set_footer(text=f"ID Auteur: {self.message.author.id} • Serveur: {interaction.guild.name}")
|
||||
|
||||
await send_to_moderation_log_channel(self.bot, log_embed)
|
||||
|
||||
class ForumTagSelect(Select):
|
||||
def __init__(self, message: discord.Message, bot, target_channel: ForumChannel):
|
||||
options = []
|
||||
for tag in target_channel.available_tags[:25]:
|
||||
options.append(discord.SelectOption(
|
||||
label=tag.name,
|
||||
value=str(tag.id),
|
||||
emoji=tag.emoji if tag.emoji else None
|
||||
))
|
||||
|
||||
super().__init__(
|
||||
placeholder="Sélectionnez un ou plusieurs tags...",
|
||||
options=options,
|
||||
min_values=1,
|
||||
max_values=min(5, len(options))
|
||||
)
|
||||
self.message = message
|
||||
self.bot = bot
|
||||
self.target_channel = target_channel
|
||||
|
||||
async def callback(self, interaction: discord.Interaction):
|
||||
selected_tags = []
|
||||
for tag_id_str in self.values:
|
||||
tag_id = int(tag_id_str)
|
||||
tag = discord.utils.get(self.target_channel.available_tags, id=tag_id)
|
||||
if tag:
|
||||
selected_tags.append(tag)
|
||||
|
||||
modal = TransferReasonModal(self.message, self.bot, self.target_channel, selected_tags)
|
||||
await interaction.response.send_modal(modal)
|
||||
|
||||
class ForumTagView(View):
|
||||
def __init__(self, message: discord.Message, bot, target_channel: ForumChannel):
|
||||
super().__init__(timeout=180)
|
||||
self.add_item(ForumTagSelect(message, bot, target_channel))
|
||||
|
||||
class TransferChannelSelect(ChannelSelect):
|
||||
def __init__(self, message: discord.Message, bot):
|
||||
super().__init__(
|
||||
placeholder="Sélectionnez le canal de destination...",
|
||||
channel_types=[discord.ChannelType.text, discord.ChannelType.forum, discord.ChannelType.public_thread, discord.ChannelType.private_thread],
|
||||
min_values=1,
|
||||
max_values=1
|
||||
)
|
||||
self.message = message
|
||||
self.bot = bot
|
||||
|
||||
async def callback(self, interaction: discord.Interaction):
|
||||
selected_channel = self.values[0]
|
||||
target_channel = self.bot.get_channel(selected_channel.id)
|
||||
|
||||
if not target_channel:
|
||||
await interaction.response.send_message("❌ Impossible de récupérer le canal sélectionné.", ephemeral=True)
|
||||
return
|
||||
|
||||
if isinstance(target_channel, ForumChannel) and target_channel.available_tags:
|
||||
view = ForumTagView(self.message, self.bot, target_channel)
|
||||
await interaction.response.send_message("🏷️ Sélectionnez un ou plusieurs tags pour ce post :", view=view, ephemeral=True)
|
||||
else:
|
||||
modal = TransferReasonModal(self.message, self.bot, target_channel)
|
||||
await interaction.response.send_modal(modal)
|
||||
|
||||
class TransferView(View):
|
||||
def __init__(self, message: discord.Message, bot):
|
||||
super().__init__(timeout=180)
|
||||
self.add_item(TransferChannelSelect(message, bot))
|
||||
|
||||
@app_commands.context_menu(name="Déplacer le message")
|
||||
@app_commands.default_permissions(manage_messages=True)
|
||||
async def transfer_message_context_menu(interaction: discord.Interaction, message: discord.Message):
|
||||
if not has_staff_role(interaction.user.roles):
|
||||
await interaction.response.send_message("❌ Vous n'avez pas les permissions nécessaires pour utiliser cette commande.", ephemeral=True)
|
||||
return
|
||||
|
||||
view = TransferView(message, interaction.client)
|
||||
await interaction.response.send_message("📨 Sélectionnez le canal de destination :", view=view, ephemeral=True)
|
||||
|
||||
|
||||
@@ -1,603 +0,0 @@
|
||||
/* MVP.css v1.17.2 - https://github.com/andybrewer/mvp */
|
||||
|
||||
:root {
|
||||
--active-brightness: 0.85;
|
||||
--border-radius: 5px;
|
||||
--box-shadow: 2px 2px 10px;
|
||||
--color-accent: #118bee15;
|
||||
--color-bg: #fff;
|
||||
--color-bg-secondary: #e9e9e9;
|
||||
--color-link: #118bee;
|
||||
--color-secondary: #920de9;
|
||||
--color-secondary-accent: #920de90b;
|
||||
--color-shadow: #f4f4f4;
|
||||
--color-table: #118bee;
|
||||
--color-text: #000;
|
||||
--color-text-secondary: #999;
|
||||
--color-scrollbar: #cacae8;
|
||||
--font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
|
||||
--hover-brightness: 1.2;
|
||||
--justify-important: center;
|
||||
--justify-normal: left;
|
||||
--line-height: 1.5;
|
||||
--width-card: 285px;
|
||||
--width-card-medium: 460px;
|
||||
--width-card-wide: 800px;
|
||||
--width-content: 1080px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root[color-mode="user"] {
|
||||
--color-accent: #0097fc4f;
|
||||
--color-bg: #333;
|
||||
--color-bg-secondary: #555;
|
||||
--color-link: #0097fc;
|
||||
--color-secondary: #e20de9;
|
||||
--color-secondary-accent: #e20de94f;
|
||||
--color-shadow: #bbbbbb20;
|
||||
--color-table: #0097fc;
|
||||
--color-text: #f7f7f7;
|
||||
--color-text-secondary: #aaa;
|
||||
}
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
html {
|
||||
scroll-behavior: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* Layout */
|
||||
article aside {
|
||||
background: var(--color-secondary-accent);
|
||||
border-left: 4px solid var(--color-secondary);
|
||||
padding: 0.01rem 0.8rem;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
font-family: var(--font-family);
|
||||
line-height: var(--line-height);
|
||||
margin: 0;
|
||||
overflow-x: hidden;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
footer,
|
||||
header,
|
||||
main {
|
||||
margin: 0 auto;
|
||||
max-width: var(--width-content);
|
||||
/* padding: 3rem 1rem; */
|
||||
padding: 1rem 1rem;
|
||||
}
|
||||
|
||||
hr {
|
||||
background-color: var(--color-bg-secondary);
|
||||
border: none;
|
||||
height: 1px;
|
||||
margin: 4rem 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
section {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: var(--justify-important);
|
||||
}
|
||||
|
||||
section img,
|
||||
article img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
section pre {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
section aside {
|
||||
border: 1px solid var(--color-bg-secondary);
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: var(--box-shadow) var(--color-shadow);
|
||||
margin: 1rem;
|
||||
padding: 1.25rem;
|
||||
width: var(--width-card);
|
||||
}
|
||||
|
||||
section aside:hover {
|
||||
box-shadow: var(--box-shadow) var(--color-bg-secondary);
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Headers */
|
||||
article header,
|
||||
div header,
|
||||
main header {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
header {
|
||||
text-align: var(--justify-important);
|
||||
}
|
||||
|
||||
header a b,
|
||||
header a em,
|
||||
header a i,
|
||||
header a strong {
|
||||
margin-left: 0.5rem;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
/* header nav img {
|
||||
margin: 1rem 0;
|
||||
} */
|
||||
|
||||
section header {
|
||||
padding-top: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Nav */
|
||||
nav {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
font-weight: bold;
|
||||
justify-content: space-between;
|
||||
/* margin-bottom: 7rem; */
|
||||
}
|
||||
|
||||
nav ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
nav ul li {
|
||||
display: inline-block;
|
||||
margin: 0 0.5rem;
|
||||
position: relative;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/* Nav Dropdown */
|
||||
nav ul li:hover ul {
|
||||
display: block;
|
||||
}
|
||||
|
||||
nav ul li ul {
|
||||
background: var(--color-bg);
|
||||
border: 1px solid var(--color-bg-secondary);
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: var(--box-shadow) var(--color-shadow);
|
||||
display: none;
|
||||
height: auto;
|
||||
left: -2px;
|
||||
padding: 0.5rem 1rem;
|
||||
position: absolute;
|
||||
top: 1.7rem;
|
||||
white-space: nowrap;
|
||||
width: auto;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
nav ul li ul::before {
|
||||
/* fill gap above to make mousing over them easier */
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: -0.5rem;
|
||||
height: 0.5rem;
|
||||
}
|
||||
|
||||
nav ul li ul li,
|
||||
nav ul li ul li a {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Nav for Mobile */
|
||||
@media (max-width: 768px) {
|
||||
nav {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
nav ul li {
|
||||
width: calc(100% - 1em);
|
||||
}
|
||||
|
||||
nav ul li ul {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
display: block;
|
||||
position: static;
|
||||
}
|
||||
}
|
||||
|
||||
/* Typography */
|
||||
code,
|
||||
samp {
|
||||
background-color: var(--color-accent);
|
||||
border-radius: var(--border-radius);
|
||||
color: var(--color-text);
|
||||
display: inline-block;
|
||||
margin: 0 0.1rem;
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
|
||||
details {
|
||||
margin: 1.3rem 0;
|
||||
}
|
||||
|
||||
details summary {
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
line-height: var(--line-height);
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
mark {
|
||||
padding: 0.1rem;
|
||||
}
|
||||
|
||||
ol li,
|
||||
ul li {
|
||||
padding: 0.2rem 0;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0.75rem 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
pre {
|
||||
margin: 1rem 0;
|
||||
max-width: var(--width-card-wide);
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
pre code,
|
||||
pre samp {
|
||||
display: block;
|
||||
max-width: var(--width-card-wide);
|
||||
padding: 0.5rem 2rem;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
small {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
sup {
|
||||
background-color: var(--color-secondary);
|
||||
border-radius: var(--border-radius);
|
||||
color: var(--color-bg);
|
||||
font-size: xx-small;
|
||||
font-weight: bold;
|
||||
margin: 0.2rem;
|
||||
padding: 0.2rem 0.3rem;
|
||||
position: relative;
|
||||
top: -2px;
|
||||
}
|
||||
|
||||
/* Links */
|
||||
a {
|
||||
color: var(--color-link);
|
||||
display: inline-block;
|
||||
font-weight: bold;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
filter: brightness(var(--hover-brightness));
|
||||
}
|
||||
|
||||
a:active {
|
||||
filter: brightness(var(--active-brightness));
|
||||
}
|
||||
|
||||
a b,
|
||||
a em,
|
||||
a i,
|
||||
a strong,
|
||||
button,
|
||||
input[type="submit"] {
|
||||
border-radius: var(--border-radius);
|
||||
display: inline-block;
|
||||
font-size: medium;
|
||||
font-weight: bold;
|
||||
line-height: var(--line-height);
|
||||
margin: 0.5rem 0;
|
||||
padding: 1rem 2rem;
|
||||
}
|
||||
|
||||
button,
|
||||
input[type="submit"] {
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
button:hover,
|
||||
input[type="submit"]:hover {
|
||||
cursor: pointer;
|
||||
filter: brightness(var(--hover-brightness));
|
||||
}
|
||||
|
||||
button:active,
|
||||
input[type="submit"]:active {
|
||||
filter: brightness(var(--active-brightness));
|
||||
}
|
||||
|
||||
a b,
|
||||
a strong,
|
||||
button,
|
||||
input[type="submit"] {
|
||||
background-color: var(--color-link);
|
||||
border: 2px solid var(--color-link);
|
||||
color: var(--color-bg);
|
||||
}
|
||||
|
||||
a em,
|
||||
a i {
|
||||
border: 2px solid var(--color-link);
|
||||
border-radius: var(--border-radius);
|
||||
color: var(--color-link);
|
||||
display: inline-block;
|
||||
padding: 1rem 2rem;
|
||||
}
|
||||
|
||||
article aside a {
|
||||
color: var(--color-secondary);
|
||||
}
|
||||
|
||||
/* Images */
|
||||
figure {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
figure img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
figure figcaption {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
/* Forms */
|
||||
button:disabled,
|
||||
input:disabled {
|
||||
background: var(--color-bg-secondary);
|
||||
border-color: var(--color-bg-secondary);
|
||||
color: var(--color-text-secondary);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
button[disabled]:hover,
|
||||
input[type="submit"][disabled]:hover {
|
||||
filter: none;
|
||||
}
|
||||
|
||||
form {
|
||||
border: 1px solid var(--color-bg-secondary);
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: var(--box-shadow) var(--color-shadow);
|
||||
display: block;
|
||||
max-width: var(--width-card-wide);
|
||||
min-width: var(--width-card);
|
||||
padding: 1.5rem;
|
||||
text-align: var(--justify-normal);
|
||||
}
|
||||
|
||||
form header {
|
||||
margin: 1.5rem 0;
|
||||
padding: 1.5rem 0;
|
||||
}
|
||||
|
||||
input,
|
||||
label,
|
||||
select,
|
||||
textarea {
|
||||
display: block;
|
||||
font-size: inherit;
|
||||
max-width: var(--width-card-wide);
|
||||
}
|
||||
|
||||
input[type="checkbox"],
|
||||
input[type="radio"] {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
input[type="checkbox"]+label,
|
||||
input[type="radio"]+label {
|
||||
display: inline-block;
|
||||
font-weight: normal;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
|
||||
input[type="range"] {
|
||||
padding: 0.4rem 0;
|
||||
}
|
||||
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
border: 1px solid var(--color-bg-secondary);
|
||||
border-radius: var(--border-radius);
|
||||
margin-bottom: 1rem;
|
||||
padding: 0.4rem 0.8rem;
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="password"],
|
||||
input[type="email"],
|
||||
textarea {
|
||||
width: calc(100% - 1.6rem);
|
||||
}
|
||||
|
||||
input[readonly],
|
||||
textarea[readonly] {
|
||||
background-color: var(--color-bg-secondary);
|
||||
}
|
||||
|
||||
label {
|
||||
font-weight: bold;
|
||||
margin-bottom: 0.2rem;
|
||||
}
|
||||
|
||||
/* Popups */
|
||||
dialog {
|
||||
max-width: 90%;
|
||||
max-height: 85dvh;
|
||||
margin: auto;
|
||||
padding-block: 0;
|
||||
padding-inline: 20px;
|
||||
border: 1px solid var(--color-bg-secondary);
|
||||
border-radius: 0.5rem;
|
||||
overscroll-behavior: contain;
|
||||
scroll-behavior: smooth;
|
||||
scrollbar-width: none;
|
||||
/* Hide scrollbar for Firefox */
|
||||
-ms-overflow-style: none;
|
||||
/* Hide scrollbar for IE and Edge */
|
||||
scrollbar-color: transparent transparent;
|
||||
animation: bottom-to-top 0.25s ease-in-out forwards;
|
||||
}
|
||||
|
||||
dialog::-webkit-scrollbar {
|
||||
width: 0;
|
||||
display: none;
|
||||
}
|
||||
|
||||
dialog::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
dialog::-webkit-scrollbar-thumb {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
@media (min-width: 650px) {
|
||||
dialog {
|
||||
max-width: 39rem;
|
||||
}
|
||||
}
|
||||
|
||||
dialog::backdrop {
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
@keyframes bottom-to-top {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(10%);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
dialog hr {
|
||||
margin-block: 1rem;
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
table {
|
||||
border: 1px solid var(--color-bg-secondary);
|
||||
border-radius: var(--border-radius);
|
||||
border-spacing: 0;
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
padding: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
table td,
|
||||
table th,
|
||||
table tr {
|
||||
padding: 0.4rem 0.8rem;
|
||||
text-align: var(--justify-important);
|
||||
}
|
||||
|
||||
table thead {
|
||||
background-color: var(--color-table);
|
||||
border-collapse: collapse;
|
||||
border-radius: var(--border-radius);
|
||||
color: var(--color-bg);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
table thead tr:first-child th:first-child {
|
||||
border-top-left-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
table thead tr:first-child th:last-child {
|
||||
border-top-right-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
table thead th:first-child,
|
||||
table tr td:first-child {
|
||||
text-align: var(--justify-normal);
|
||||
}
|
||||
|
||||
table tr:nth-child(even) {
|
||||
background-color: var(--color-accent);
|
||||
}
|
||||
|
||||
/* Quotes */
|
||||
blockquote {
|
||||
display: block;
|
||||
font-size: x-large;
|
||||
line-height: var(--line-height);
|
||||
margin: 1rem auto;
|
||||
max-width: var(--width-card-medium);
|
||||
padding: 1.5rem 1rem;
|
||||
text-align: var(--justify-important);
|
||||
}
|
||||
|
||||
blockquote footer {
|
||||
color: var(--color-text-secondary);
|
||||
display: block;
|
||||
font-size: small;
|
||||
line-height: var(--line-height);
|
||||
padding: 1.5rem 0;
|
||||
}
|
||||
|
||||
/* Scrollbars */
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--color-scrollbar) transparent;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb {
|
||||
background-color: var(--color-scrollbar);
|
||||
border-radius: 10px;
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
header nav img {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
table th,
|
||||
table td {
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
overflow: hidden;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
table.live-alert tr td:last-child {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
a.icon {
|
||||
text-decoration: none;
|
||||
}
|
||||
@@ -95,6 +95,10 @@
|
||||
<code class="text-slate-700 dark:text-slate-300 font-mono">!banlist</code>
|
||||
<span class="text-slate-500 dark:text-slate-400">Liste des utilisateurs bannis</span>
|
||||
</div>
|
||||
<div class="px-5 py-3 flex flex-col sm:flex-row sm:items-start gap-1 sm:gap-4">
|
||||
<code class="text-slate-700 dark:text-slate-300 font-mono">!transfert #canal message_id</code>
|
||||
<span class="text-slate-500 dark:text-slate-400">Transfère un message vers un autre canal (textuel, thread ou forum)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
@@ -129,6 +133,10 @@
|
||||
<span class="text-xs font-medium text-yellow-600 dark:text-yellow-400">Warn</span>
|
||||
{% elif mod_event.type == 'unban' %}
|
||||
<span class="text-xs font-medium text-green-600 dark:text-green-400">Unban</span>
|
||||
{% elif mod_event.type == 'transfer' %}
|
||||
<span class="text-xs font-medium text-blue-600 dark:text-blue-400">Transfert</span>
|
||||
{% elif mod_event.type == 'timeout' %}
|
||||
<span class="text-xs font-medium text-purple-600 dark:text-purple-400">Timeout</span>
|
||||
{% else %}
|
||||
<span class="text-xs font-medium text-slate-600 dark:text-slate-400">{{ mod_event.type }}</span>
|
||||
{% endif %}
|
||||
|
||||
@@ -1,35 +1,92 @@
|
||||
{% extends "template.html" %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Procédure de configuration de Twitch</h1>
|
||||
<p>
|
||||
<strong>Avant toute chose, activez l'authentification à deux facteurs (2FA) :</strong>
|
||||
<a href="https://help.twitch.tv/s/article/two-factor-authentication?language=en_US" target="_blank">Guide officiel
|
||||
Twitch pour la 2FA</a>
|
||||
</p>
|
||||
<p>
|
||||
Rendez-vous sur <a href="https://dev.twitch.tv/console" target="_blank">la console d'applications Twitch</a> et
|
||||
ajoutez une application. Renseignez :
|
||||
<ul>
|
||||
<li>URL de redirection : {{token_redirect_url}}</li>
|
||||
<li>Catégorie : Chat Bot</li>
|
||||
</ul>
|
||||
</p>
|
||||
<div class="mb-6">
|
||||
<h1 class="text-2xl font-semibold text-slate-800 dark:text-white mb-1">Configuration Twitch</h1>
|
||||
<p class="text-sm text-slate-600 dark:text-slate-400">
|
||||
Guide étape par étape pour configurer l'API Twitch.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<img src="/static/img/twitch-api-01.jpg">
|
||||
<div class="space-y-4">
|
||||
<div class="bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700 overflow-hidden">
|
||||
<div class="px-5 py-4 border-b border-slate-200 dark:border-slate-700">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="w-6 h-6 rounded-full bg-slate-200 dark:bg-slate-700 flex items-center justify-center text-slate-600 dark:text-slate-400 text-xs font-medium">1</span>
|
||||
<h2 class="font-medium text-slate-800 dark:text-white">Activer l'authentification à deux facteurs (2FA)</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-5">
|
||||
<p class="text-sm text-slate-600 dark:text-slate-400 mb-4">
|
||||
Avant de créer une application Twitch, vous devez activer la 2FA sur votre compte.
|
||||
</p>
|
||||
<a href="https://help.twitch.tv/s/article/two-factor-authentication?language=en_US" target="_blank" class="inline-flex items-center gap-2 text-sm text-slate-600 dark:text-slate-400 hover:text-slate-800 dark:hover:text-white">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"></path></svg>
|
||||
Guide officiel Twitch
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Créez le bot. Puis, de retour à la liste, éditez-le en cliquant sur Gérer. Puis cliquez sur <strong>Nouveau
|
||||
Secret</strong>. Vous trouverez ici le <strong>Client ID</strong> et le <strong>Client Secret</strong>.
|
||||
</p>
|
||||
<div class="bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700 overflow-hidden">
|
||||
<div class="px-5 py-4 border-b border-slate-200 dark:border-slate-700">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="w-6 h-6 rounded-full bg-slate-200 dark:bg-slate-700 flex items-center justify-center text-slate-600 dark:text-slate-400 text-xs font-medium">2</span>
|
||||
<h2 class="font-medium text-slate-800 dark:text-white">Créer une application Twitch</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-5 space-y-4">
|
||||
<p class="text-sm text-slate-600 dark:text-slate-400">
|
||||
Rendez-vous sur la console Twitch et créez une nouvelle application :
|
||||
</p>
|
||||
<a href="https://dev.twitch.tv/console" target="_blank" class="inline-flex items-center gap-2 px-3 py-1.5 bg-slate-800 dark:bg-slate-700 text-white text-sm rounded-lg hover:bg-slate-700 dark:hover:bg-slate-600 transition-colors">
|
||||
Console Twitch
|
||||
</a>
|
||||
|
||||
<img src="/static/img/twitch-api-02.jpg">
|
||||
<div class="bg-slate-50 dark:bg-slate-700/50 rounded-lg p-4 space-y-2 text-sm">
|
||||
<div><span class="text-slate-500 dark:text-slate-400">URL de redirection :</span> <code class="ml-1 px-1.5 py-0.5 bg-slate-200 dark:bg-slate-600 rounded text-xs">{{ token_redirect_url }}</code></div>
|
||||
<div><span class="text-slate-500 dark:text-slate-400">Catégorie :</span> <span class="ml-1 text-slate-800 dark:text-white">Chat Bot</span></div>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Ensuite, retournez sur la page de <a href="{{url_for('openConfigurations')}}">Configuration</a>, après avoir
|
||||
enregistré le <strong>Client ID</strong> et le <strong>Client Secret</strong>, cliquez sur le lien <strong>Obtenir
|
||||
token et refresh token</strong>. Si tout se passe bien les champs <strong>Access Token</strong> et
|
||||
<strong>Refresh Token</strong> sont remplis.
|
||||
</p>
|
||||
<div class="rounded-lg overflow-hidden border border-slate-200 dark:border-slate-700">
|
||||
<img src="/static/img/twitch-api-01.jpg" alt="Création d'application Twitch" class="w-full">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700 overflow-hidden">
|
||||
<div class="px-5 py-4 border-b border-slate-200 dark:border-slate-700">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="w-6 h-6 rounded-full bg-slate-200 dark:bg-slate-700 flex items-center justify-center text-slate-600 dark:text-slate-400 text-xs font-medium">3</span>
|
||||
<h2 class="font-medium text-slate-800 dark:text-white">Récupérer les identifiants</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-5 space-y-4">
|
||||
<p class="text-sm text-slate-600 dark:text-slate-400">
|
||||
Cliquez sur <strong class="text-slate-800 dark:text-white">Gérer</strong> puis <strong class="text-slate-800 dark:text-white">Nouveau Secret</strong> pour obtenir le Client ID et Client Secret.
|
||||
</p>
|
||||
<div class="rounded-lg overflow-hidden border border-slate-200 dark:border-slate-700">
|
||||
<img src="/static/img/twitch-api-02.jpg" alt="Récupération des identifiants" class="w-full">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700 overflow-hidden">
|
||||
<div class="px-5 py-4 border-b border-slate-200 dark:border-slate-700">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="w-6 h-6 rounded-full bg-slate-200 dark:bg-slate-700 flex items-center justify-center text-slate-600 dark:text-slate-400 text-xs font-medium">4</span>
|
||||
<h2 class="font-medium text-slate-800 dark:text-white">Configurer Mamie Henriette</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-5 space-y-4">
|
||||
<ol class="list-decimal list-inside space-y-1 text-sm text-slate-600 dark:text-slate-400">
|
||||
<li>Entrez le Client ID et Client Secret</li>
|
||||
<li>Cliquez sur Enregistrer</li>
|
||||
<li>Cliquez sur "Obtenir token et refresh token"</li>
|
||||
</ol>
|
||||
<a href="{{ url_for('openConfigurations') }}" class="inline-flex items-center gap-2 px-3 py-1.5 bg-slate-800 dark:bg-slate-700 text-white text-sm rounded-lg hover:bg-slate-700 dark:hover:bg-slate-600 transition-colors">
|
||||
Aller à la Configuration
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -218,7 +218,7 @@
|
||||
{{ 'Enregistrer' if notification else 'Ajouter la notification' }}
|
||||
</button>
|
||||
{% if notification %}
|
||||
<a href="{{ url_for('youtube') }}"
|
||||
<a href="{{ url_for('openYouTube') }}"
|
||||
class="px-6 py-2.5 bg-gray-200 dark:bg-gray-600 hover:bg-gray-300 dark:hover:bg-gray-500 text-gray-700 dark:text-gray-200 font-medium rounded-lg transition-colors">
|
||||
Annuler
|
||||
</a>
|
||||
|
||||
@@ -27,7 +27,7 @@ def twitchConfigurationHelp():
|
||||
def twitchRequestToken():
|
||||
global auth
|
||||
helper = ConfigurationHelper()
|
||||
twitch = asyncio.run(Twitch(helper.getValue('twitch_client_id'), helper.getValue('twitch_client_secret')))
|
||||
twitch = Twitch(helper.getValue('twitch_client_id'), helper.getValue('twitch_client_secret'))
|
||||
auth = UserAuthenticator(twitch, USER_SCOPE, url=_buildUrl())
|
||||
return redirect(auth.return_auth_url())
|
||||
|
||||
|
||||
Reference in New Issue
Block a user