diff --git a/discordbot/__init__.py b/discordbot/__init__.py
index ce06bef..45f8da3 100644
--- a/discordbot/__init__.py
+++ b/discordbot/__init__.py
@@ -21,7 +21,8 @@ from discordbot.moderation import (
handle_ban_list_command,
handle_staff_help_command,
handle_timeout_command,
- handle_say_command
+ handle_say_command,
+ handle_transfer_command
)
from discordbot.welcome import sendWelcomeMessage, sendLeaveMessage, updateInviteCache
from discordbot.youtube import checkYouTubeVideos
@@ -167,6 +168,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
diff --git a/discordbot/moderation.py b/discordbot/moderation.py
index 366aeb9..4935080 100644
--- a/discordbot/moderation.py
+++ b/discordbot/moderation.py
@@ -4,12 +4,13 @@ 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
def _get_local_tz():
tz_name = os.environ.get('APP_TZ') or os.environ.get('TZ') or 'Europe/Paris'
@@ -1055,7 +1056,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 +1414,298 @@ 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)
+
diff --git a/webapp/templates/moderation.html b/webapp/templates/moderation.html
index b35f643..13bbad8 100644
--- a/webapp/templates/moderation.html
+++ b/webapp/templates/moderation.html
@@ -95,6 +95,10 @@
!banlist
Liste des utilisateurs bannis
+
!transfert #canal message_id
+ Transfère un message vers un autre canal (textuel, thread ou forum)
+