diff --git a/commandes/rapports.py b/commandes/rapports.py index 8f1bf12..dd549d5 100644 --- a/commandes/rapports.py +++ b/commandes/rapports.py @@ -368,7 +368,7 @@ class ReportsFilterView(disnake.ui.View): val = inter.values[0] self.selected_status = None if val == "all" else val self.update_components() - await inter.response.edit_message(components=self.get_components_v2()) + await inter.response.edit_message(embed=self.get_filter_embed(), view=self) status_select.callback = status_callback self.add_item(status_select) @@ -392,7 +392,7 @@ class ReportsFilterView(disnake.ui.View): val = inter.values[0] self.selected_subject = None if val == "all" else val self.update_components() - await inter.response.edit_message(components=self.get_components_v2()) + await inter.response.edit_message(embed=self.get_filter_embed(), view=self) subject_select.callback = subject_callback self.add_item(subject_select) @@ -417,7 +417,7 @@ class ReportsFilterView(disnake.ui.View): else: self.selected_user = None self.update_components() - await inter.response.edit_message(components=self.get_components_v2()) + await inter.response.edit_message(embed=self.get_filter_embed(), view=self) user_select.callback = user_callback self.add_item(user_select) current_row += 1 @@ -439,7 +439,7 @@ class ReportsFilterView(disnake.ui.View): else: self.selected_role = None self.update_components() - await inter.response.edit_message(components=self.get_components_v2()) + await inter.response.edit_message(embed=self.get_filter_embed(), view=self) role_select.callback = role_callback self.add_item(role_select) current_row += 1 @@ -453,8 +453,6 @@ class ReportsFilterView(disnake.ui.View): row=current_row ) async def search_callback(inter: disnake.MessageInteraction): - await inter.response.defer(ephemeral=True) - author_filter = None if not self.has_global_access: author_filter = self.viewer_id @@ -474,11 +472,11 @@ class ReportsFilterView(disnake.ui.View): ) if not reports: - await inter.followup.send("❌ Aucun rapport ne correspond à votre recherche.", ephemeral=True) + await inter.response.send_message("❌ Aucun rapport ne correspond à votre recherche.", ephemeral=True) return browser = ReportsBrowserView(self.cog, self.interaction, reports, filter_view=self) - await inter.edit_original_message(content=None, embed=browser.get_embed(), view=browser) + await inter.response.edit_message(embed=browser.get_embed(), view=browser) search_btn.callback = search_callback self.add_item(search_btn) @@ -496,49 +494,40 @@ class ReportsFilterView(disnake.ui.View): self.selected_status = None self.selected_subject = None self.update_components() - await inter.response.edit_message(components=self.get_components_v2()) + await inter.response.edit_message(embed=self.get_filter_embed(), view=self) reset_btn.callback = reset_callback self.add_item(reset_btn) - def get_components_v2(self): - sections = [ - disnake.ui.Section( - "# 🔍 Recherche & Filtres de Rapports", - accessory=disnake.ui.Thumbnail(self.interaction.author.display_avatar.url) - ), - disnake.ui.Separator(divider=True) - ] - - filters_desc = [] - if self.selected_status: - filters_desc.append(f"🚦 **Statut** : {self.selected_status}") - else: - filters_desc.append("🚦 **Statut** : Tous") - + def get_filter_embed(self) -> disnake.Embed: + """Génère l'embed résumant les filtres actifs.""" + author = self.interaction.author + lines = [] + lines.append(f"🚦 **Statut** : {self.selected_status or 'Tous'}") if self.has_global_access: if self.selected_user: - mention = self.selected_user.mention if hasattr(self.selected_user, "mention") else f"<@{self.selected_user}>" - filters_desc.append(f"👤 **Auteur** : {mention}") + mention = self.selected_user.mention if hasattr(self.selected_user, 'mention') else f'<@{self.selected_user}>' + lines.append(f"👤 **Auteur** : {mention}") else: - filters_desc.append("👤 **Auteur** : Tous") - + lines.append("👤 **Auteur** : Tous") if self.selected_role: - mention = self.selected_role.mention if hasattr(self.selected_role, "mention") else f"<@&{self.selected_role}>" - filters_desc.append(f"🛡️ **Rôle Auteur** : {mention}") + mention = self.selected_role.mention if hasattr(self.selected_role, 'mention') else f'<@&{self.selected_role}>' + lines.append(f"🛡️ **Rôle Auteur** : {mention}") else: - filters_desc.append("🛡️ **Rôle Auteur** : Tous") + lines.append("🛡️ **Rôle Auteur** : Tous") else: - filters_desc.append(f"👤 **Auteur** : {self.interaction.author.mention} *(Accès restreint à vos propres rapports)*") - - if self.selected_subject: - filters_desc.append(f"📁 **Objet / Catégorie** : `{self.selected_subject}`") - else: - filters_desc.append("📁 **Objet / Catégorie** : Tous") - - sections.append(disnake.ui.TextDisplay(content="\n".join(filters_desc))) - sections.append(disnake.ui.Separator(divider=True)) - - return [disnake.ui.Container(*sections)] + self.children + lines.append(f"👤 **Auteur** : {author.mention} *(accès limité à vos rapports)*") + lines.append(f"📁 **Objet / Catégorie** : {self.selected_subject or 'Tous'}") + embed = disnake.Embed( + title="🔍 Recherche & Filtres de Rapports", + description="\n".join(lines), + color=disnake.Color.blurple() + ) + embed.set_thumbnail(url=author.display_avatar.url) + return embed + + def get_components_v2(self): + """Kept for backward compatibility — returns view children only.""" + return self.children async def interaction_check(self, interaction: disnake.MessageInteraction) -> bool: if interaction.author.id != self.viewer_id: @@ -701,9 +690,14 @@ class ReportsBrowserView(disnake.ui.View): if not self.reports: if self.filter_view: self.filter_view.update_components() - await inter.response.edit_message(content=None, embed=None, components=self.filter_view.get_components_v2()) + await inter.response.edit_message( + embed=self.filter_view.get_filter_embed(), view=self.filter_view + ) else: - await inter.response.edit_message(content="✅ Le rapport a été supprimé. Plus aucun rapport dans cette recherche.", embed=None, view=None) + await inter.response.edit_message( + content="✅ Le rapport a été supprimé. Plus aucun rapport dans cette recherche.", + embed=None, view=None + ) return if self.current_index >= len(self.reports): @@ -717,10 +711,14 @@ class ReportsBrowserView(disnake.ui.View): async def back_callback(inter: disnake.MessageInteraction): if self.filter_view: self.filter_view.update_components() - await inter.response.edit_message(embed=None, components=self.filter_view.get_components_v2()) + await inter.response.edit_message( + embed=self.filter_view.get_filter_embed(), view=self.filter_view + ) else: new_filter_view = ReportsFilterView(self.cog, self.interaction) - await inter.response.edit_message(embed=None, components=new_filter_view.get_components_v2()) + await inter.response.edit_message( + embed=new_filter_view.get_filter_embed(), view=new_filter_view + ) edit_btn.callback = edit_callback delete_btn.callback = delete_callback @@ -732,9 +730,9 @@ class ReportsBrowserView(disnake.ui.View): # Row 3: Menu Déroulant "Changer l'état" (Admin uniquement) if self.is_admin: status_options = [ - disnake.SelectOption(label="En attente", emoji="🟡", value="En attente", default=(report['status'] == "En attente")), - disnake.SelectOption(label="Traité", emoji="🟢", value="Traité", default=(report['status'] == "Traité")), - disnake.SelectOption(label="Classé", emoji="🔴", value="Classé", default=(report['status'] == "Classé")) + disnake.SelectOption(label="En attente", emoji="🟡", value="En attente"), + disnake.SelectOption(label="Traité", emoji="🟢", value="Traité"), + disnake.SelectOption(label="Classé", emoji="🔴", value="Classé") ] status_menu = disnake.ui.StringSelect( placeholder="Changer le statut...", @@ -746,16 +744,81 @@ class ReportsBrowserView(disnake.ui.View): async def status_callback(inter: disnake.MessageInteraction): if not inter.author.guild_permissions.administrator: return await inter.response.send_message("❌ Seuls les administrateurs peuvent modifier le statut.", ephemeral=True) - + + old_status = report['status'] new_status = inter.values[0] + + if old_status == new_status: + return await inter.response.send_message(f"ℹ️ Le rapport est déjà en statut **{new_status}**.", ephemeral=True) + self.cog.db.update_report_status(report['id'], new_status) - updated_rep = self.cog.db.get_report(report['id']) self.reports[self.current_index] = updated_rep - + self.update_components() await inter.response.edit_message(embed=self.get_embed(), view=self) - await inter.followup.send(f"✅ Statut du rapport #{report['id']} mis à jour vers : **{new_status}**.", ephemeral=True) + await inter.followup.send( + f"✅ Statut du rapport **#{report['id']}** mis à jour : **{old_status}** → **{new_status}**.", + ephemeral=True + ) + + # === Notification DM à l'auteur === + status_colors = { + "En attente": disnake.Color.yellow(), + "Traité": disnake.Color.green(), + "Classé": disnake.Color.greyple(), + } + status_emojis = {"En attente": "🟡", "Traité": "🟢", "Classé": "🔴"} + dm_sent = False + try: + author_member = inter.guild.get_member(report['author_id']) + if not author_member: + author_member = await inter.guild.fetch_member(report['author_id']) + + if author_member: + dm_embed = disnake.Embed( + title="📋 Mise à jour de votre rapport", + description=( + f"Le statut de votre rapport **#{report['id']} — {report['title']}** " + f"vient d'être modifié." + ), + color=status_colors.get(new_status, disnake.Color.blurple()), + timestamp=datetime.now(PARIS_TZ) + ) + dm_embed.add_field( + name="Ancien statut", + value=f"{status_emojis.get(old_status, '⚪')} {old_status}", + inline=True + ) + dm_embed.add_field( + name="Nouveau statut", + value=f"{status_emojis.get(new_status, '⚪')} {new_status}", + inline=True + ) + dm_embed.add_field(name="Modifié par", value=inter.author.mention, inline=False) + dm_embed.add_field(name="Serveur", value=inter.guild.name, inline=False) + dm_embed.set_footer(text=f"Rapport #{report['id']} • {inter.guild.name}") + if inter.guild.icon: + dm_embed.set_thumbnail(url=inter.guild.icon.url) + try: + await author_member.send(embed=dm_embed) + dm_sent = True + except disnake.Forbidden: + kuby_logger.info( + f"[Rapports] DM impossible pour {author_member} (DMs fermés) — rapport #{report['id']}" + ) + except Exception as e: + kuby_logger.error(f"[Rapports] Erreur DM notification: {e}", exc_info=True) + except disnake.NotFound: + pass + except Exception as e: + kuby_logger.error(f"[Rapports] Erreur récupération membre pour DM: {e}", exc_info=True) + + dm_note = " • 📬 Auteur notifié par DM." if dm_sent else " • ⚠️ DM non envoyé (DMs fermés ou membre introuvable)." + await inter.followup.send( + f"ℹ️{dm_note}", + ephemeral=True + ) status_menu.callback = status_callback self.add_item(status_menu) @@ -898,7 +961,20 @@ class Rapports(commands.Cog): filter_view.selected_role = role filter_view.update_components() - await interaction.response.send_message(components=filter_view.get_components_v2(), ephemeral=True) + await interaction.response.send_message( + embed=disnake.Embed( + title="🔍 Recherche & Filtres de Rapports", + description=( + f"🚦 **Statut** : {filter_view.selected_status or 'Tous'}\n" + f"📁 **Objet / Catégorie** : {filter_view.selected_subject or 'Tous'}\n" + + (f"👤 **Auteur** : {filter_view.selected_user.mention if hasattr(filter_view.selected_user, 'mention') else 'Tous'}\n" if filter_view.has_global_access else f"👤 **Auteur** : {interaction.author.mention} *(accès limité à vos rapports)*\n") + + (f"🛡️ **Rôle Auteur** : {filter_view.selected_role.mention if hasattr(filter_view.selected_role, 'mention') else 'Tous'}" if filter_view.has_global_access else "") + ), + color=disnake.Color.blurple() + ), + view=filter_view, + ephemeral=True + ) def setup(bot): bot.add_cog(Rapports(bot)) diff --git a/commandes/sendbotmessages.py b/commandes/sendbotmessages.py index e77819e..b2c9f56 100755 --- a/commandes/sendbotmessages.py +++ b/commandes/sendbotmessages.py @@ -1,24 +1,166 @@ import disnake from disnake.ext import commands + +OWNER_ID = 971446412690722826 + + +def is_authorized(interaction: disnake.ApplicationCommandInteraction) -> bool: + return interaction.user.id == OWNER_ID + + +class AnnouncementModal(disnake.ui.Modal): + def __init__(self, cog: "SentBotMessages"): + self.cog = cog + components = [ + disnake.ui.TextInput( + label="Titre principal de l'annonce", + placeholder="Ex: Mise à jour important / Maintenance / Nouveau système", + custom_id="announcement_title", + required=True, + max_length=100, + ), + disnake.ui.TextInput( + label="Message de l'annonce (contenu réel)", + style=disnake.TextInputStyle.paragraph, + placeholder="Écris le vrai contenu de l'annonce ici.", + custom_id="announcement_message", + required=True, + max_length=4000, + ), + disnake.ui.TextInput( + label="Message de prévention / contexte MP", + style=disnake.TextInputStyle.paragraph, + placeholder="Ex: Ce MP vous informe d'une annonce importante.", + custom_id="announcement_prevention", + required=True, + max_length=1000, + ), + ] + super().__init__( + title="Annonce aux propriétaires", + custom_id="announcement_modal", + components=components, + ) + + async def callback(self, interaction: disnake.ModalInteraction): + title = (interaction.text_values.get("announcement_title") or "").strip() + message = (interaction.text_values.get("announcement_message") or "").strip() + prevention = (interaction.text_values.get("announcement_prevention") or "").strip() + + await interaction.response.defer(ephemeral=True) + + preview_container = disnake.ui.Container( + disnake.ui.TextDisplay(content=f"# {title}"), + disnake.ui.Separator(divider=True), + disnake.ui.TextDisplay(content=prevention), + disnake.ui.Separator(divider=True), + disnake.ui.TextDisplay(content=message), + disnake.ui.Separator(divider=True), + disnake.ui.TextDisplay( + content=( + "📍 **Prévisualisation**\n" + "Le même format sera envoyé aux propriétaires." + ) + ) + ) + + successful = 0 + failed_servers = [] + + for guild in self.cog.bot.guilds: + try: + owner = guild.owner + if owner is None: + owner = await guild.fetch_member(guild.owner_id) + + container = disnake.ui.Container( + disnake.ui.TextDisplay(content=f"# {title}"), + disnake.ui.Separator(divider=True), + disnake.ui.TextDisplay(content=prevention), + disnake.ui.Separator(divider=True), + disnake.ui.TextDisplay(content=message), + disnake.ui.Separator(divider=True), + disnake.ui.TextDisplay( + content=( + f"📍 **Serveur** : {guild.name}\n" + f"🆔 **ID** : `{guild.id}`" + ) + ) + ) + + await owner.send(components=[container]) + successful += 1 + except Exception as e: + failed_servers.append(f"{guild.name} ({guild.id}) : {e}") + + result_text = ( + f"✅ Annonce envoyée à {successful} propriétaire(s)." + if not failed_servers + else ( + f"✅ Annonce envoyée à {successful} propriétaire(s).\n" + f"❌ Échecs ({len(failed_servers)}):\n" + f"- {'\n- '.join(failed_servers[:10])}" + f"{'\n... et ' + str(len(failed_servers) - 10) + ' autres' if len(failed_servers) > 10 else ''}" + ) + ) + + result_container = disnake.ui.Container( + disnake.ui.TextDisplay(content=result_text), + disnake.ui.Separator(divider=True), + disnake.ui.TextDisplay(content="🧩 Aperçu du format envoyé aux propriétaires :"), + disnake.ui.Separator(divider=True), + *preview_container.children + ) + + await interaction.edit_original_response(components=[result_container]) + + class SentBotMessages(commands.Cog): def __init__(self, bot): self.bot = bot - @commands.slash_command(name="sentbotmessages", description="Le bot envoie un message dans un salon choisi.") - async def sentbotmessages(self, interaction: disnake.ApplicationCommandInteraction, - message: str = commands.Param(description="Le message à envoyer"), - salon: disnake.TextChannel = commands.Param(description="Le salon où envoyer le message")): + @commands.slash_command( + name="sentbotmessages", + description="Le bot envoie un message dans un salon choisi." + ) + async def sentbotmessages( + self, + interaction: disnake.ApplicationCommandInteraction, + message: str = commands.Param(description="Le message à envoyer"), + salon: disnake.TextChannel = commands.Param(description="Le salon où envoyer le message") + ): # Vérifie si l'utilisateur est admin if not interaction.user.guild_permissions.administrator: - await interaction.response.send_message("⛔ Tu n'as pas la permission d'utiliser cette commande.", ephemeral=True) + await interaction.response.send_message( + "⛔ Tu n'as pas la permission d'utiliser cette commande.", + ephemeral=True + ) return try: await salon.send(message) - await interaction.response.send_message(f"✅ Message envoyé dans {salon.mention}.", ephemeral=True) + await interaction.response.send_message( + f"✅ Message envoyé dans {salon.mention}.", + ephemeral=True + ) except Exception as e: - await interaction.response.send_message(f"❌ Une erreur est survenue : {e}", ephemeral=True) + await interaction.response.send_message( + f"❌ Une erreur est survenue : {e}", + ephemeral=True + ) + + @commands.slash_command( + name="annonce_proprietaires", + description="Ouvrir le formulaire d'annonce pour les propriétaires." + ) + @commands.check(is_authorized) + async def annonce_proprietaires( + self, + interaction: disnake.ApplicationCommandInteraction, + ): + await interaction.response.send_modal(AnnouncementModal(self)) + def setup(bot): bot.add_cog(SentBotMessages(bot)) diff --git a/commandes/ticket/__init__.py b/commandes/ticket/__init__.py index 0f632c3..4516f05 100644 --- a/commandes/ticket/__init__.py +++ b/commandes/ticket/__init__.py @@ -3158,13 +3158,11 @@ class TicketCommands(commands.Cog): if not custom_id: return - # Accept both ticket_* / catv2_* / close_* custom_ids - - + # Accept only ticket_* / catv2_* / cat_* / close_* custom_ids + # reports_filter_* are intentionally excluded — handled by ReportsFilterView callbacks if not (str(custom_id).startswith("ticket_") or str(custom_id).startswith("catv2_") or str(custom_id).startswith("cat_") - or str(custom_id).startswith("reports_filter_") or str(custom_id) in {"close_confirm", "close_cancel", "delay_cancel"}): return @@ -3222,6 +3220,7 @@ class TicketCommands(commands.Cog): await inter.response.send_message(f"Erreur interne: {e}", ephemeral=True) # backward compatibility (legacy cat__) + elif custom_id.startswith("cat_"): try: parts = str(custom_id).split("_") @@ -3402,13 +3401,11 @@ class TicketCommands(commands.Cog): await self._send_recruitment_decision(inter, ticket, category, accepted=False) # === Fermeture via composants V2 (sans view=, donc custom_id) === - elif custom_id == "close_cancel": - await inter.response.defer() - # Retire la réponse éphémère du user (si c'est bien une réponse d'interaction) - try: - await inter.delete_original_response() - except Exception: - pass + + elif custom_id.startswith("reports_filter_"): + # No direct response; let the view's callback handle the interaction + return + elif custom_id == "close_confirm": try: