diff --git a/discordbot/__init__.py b/discordbot/__init__.py index 45f8da3..5b05c8f 100644 --- a/discordbot/__init__.py +++ b/discordbot/__init__.py @@ -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 ( @@ -22,7 +22,8 @@ from discordbot.moderation import ( handle_staff_help_command, handle_timeout_command, handle_say_command, - handle_transfer_command + handle_transfer_command, + transfer_message_context_menu ) from discordbot.welcome import sendWelcomeMessage, sendLeaveMessage, updateInviteCache from discordbot.youtube import checkYouTubeVideos @@ -30,10 +31,39 @@ from discordbot.auto_rooms import on_voice_state_update_auto_rooms, on_raw_react 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}') diff --git a/discordbot/moderation.py b/discordbot/moderation.py index 4935080..e0bf198 100644 --- a/discordbot/moderation.py +++ b/discordbot/moderation.py @@ -10,7 +10,8 @@ from zoneinfo import ZoneInfo from database import db from database.helpers import ConfigurationHelper from database.models import ModerationEvent -from discord import Message, TextChannel, ForumChannel, Thread +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' @@ -1709,3 +1710,192 @@ async def handle_transfer_command(message: Message, bot): 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): + super().__init__() + self.message = message + self.bot = bot + self.target_channel = target_channel + + 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, + reason=f"Transfert depuis {source_channel.name} par {interaction.user.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}") + 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 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 + + 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) +