Преглед изворни кода

Mémoire : un fichier plus long que sa limite s'ouvre de nouveau dans « Modifier »

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Johnounet пре 2 дана
родитељ
комит
3a1eeb44d9
3 измењених фајлова са 71 додато и 2 уклоњено
  1. 18 2
      bot/memory_views.py
  2. 6 0
      changelog.txt
  3. 47 0
      tests/test_memory.py

+ 18 - 2
bot/memory_views.py

@@ -130,6 +130,15 @@ class MemoryCard:
     # -------------------------------------------------------------- actions
     # -------------------------------------------------------------- actions
 
 
     async def edit(self, interaction: discord.Interaction) -> None:
     async def edit(self, interaction: discord.Interaction) -> None:
+        content = self.store.read(self.target.file)
+        if len(content) > INPUT_MAX * MAX_INPUTS:  # au-delà de ce qu'une fenêtre Discord peut contenir
+            file = discord.File(io.BytesIO(content.encode("utf-8")), filename="contenu_complet.txt")
+            limit = self.store.limit(self.target.file)
+            await interaction.response.send_message(
+                f"Ce fichier fait {len(content)} caractères, trop pour la fenêtre de Discord (sa limite est de "
+                f"{limit}). Son contenu est joint : efface-le, puis recopie ce qui compte avec « Modifier », ou "
+                f"demande à l'administrateur du bot de le raccourcir sur le disque.", file=file, ephemeral=True)
+            return
         await interaction.response.send_modal(EditModal(self))
         await interaction.response.send_modal(EditModal(self))
 
 
     async def save(self, interaction: discord.Interaction, text: str, original: str | None = None) -> None:
     async def save(self, interaction: discord.Interaction, text: str, original: str | None = None) -> None:
@@ -224,13 +233,20 @@ class EditModal(discord.ui.Modal):
             "Modifier le fichier général" if card.target.member_id is None else f"Fiche de {card.target.name}")
             "Modifier le fichier général" if card.target.member_id is None else f"Fiche de {card.target.name}")
         super().__init__(title=title[:45], timeout=VIEW_TIMEOUT)
         super().__init__(title=title[:45], timeout=VIEW_TIMEOUT)
         self.card = card
         self.card = card
-        sizes = input_sizes(card.store.limit(card.target.file))
-        count = len(sizes)
+        limit = card.store.limit(card.target.file)
         self.original = card.store.read(card.target.file)  # comparé à l'enregistrement (voir overwrite)
         self.original = card.store.read(card.target.file)  # comparé à l'enregistrement (voir overwrite)
+        # Fichier plus long que sa limite (limite baissée, fichier modifié sur le disque) : les zones
+        # s'agrandissent pour le contenir en entier, à raccourcir avant d'enregistrer. Tronqué, il serait
+        # perdu en partie ; plus long que sa zone, Discord refuserait d'ouvrir la fenêtre.
+        too_long = len(self.original) > limit
+        sizes = input_sizes(max(limit, len(self.original)))
+        count = len(sizes)
         parts = split_for_inputs(self.original, sizes)
         parts = split_for_inputs(self.original, sizes)
         self.inputs = []
         self.inputs = []
         for i in range(count):
         for i in range(count):
             label = "Contenu" if count == 1 else f"Contenu ({i + 1}/{count})"
             label = "Contenu" if count == 1 else f"Contenu ({i + 1}/{count})"
+            if too_long and i == 0:
+                label += f" — {limit} caractères au plus"
             text_input = discord.ui.TextInput(
             text_input = discord.ui.TextInput(
                 label=label, style=discord.TextStyle.paragraph, required=False,
                 label=label, style=discord.TextStyle.paragraph, required=False,
                 max_length=sizes[i], default=parts[i] or None,
                 max_length=sizes[i], default=parts[i] or None,

+ 6 - 0
changelog.txt

@@ -126,6 +126,12 @@ Corrections
 - Mémoire : la fenêtre « Modifier » du fichier général n'accepte plus
 - Mémoire : la fenêtre « Modifier » du fichier général n'accepte plus
   que 5000 caractères (MEMORY_SERVER_LIMIT) : sa deuxième zone de texte
   que 5000 caractères (MEMORY_SERVER_LIMIT) : sa deuxième zone de texte
   laissait en écrire 8000, refusés ensuite à l'enregistrement.
   laissait en écrire 8000, refusés ensuite à l'enregistrement.
+- Mémoire : un fichier plus long que sa limite (MEMORY_*_LIMIT baissée,
+  fichier modifié sur le disque) s'ouvre de nouveau dans « Modifier »,
+  en entier, avec la limite rappelée pour le raccourcir. Avant, la
+  fenêtre ne s'ouvrait pas (« Échec de l'interaction »). Au-delà de
+  20000 caractères, trop pour une fenêtre Discord, son contenu est
+  envoyé en fichier joint.
 - Mémoire : un membre ne peut plus prendre le nom Discord d'un autre
 - Mémoire : un membre ne peut plus prendre le nom Discord d'un autre
   (pseudo, nom d'utilisateur ou ancien pseudo) comme surnom dans sa
   (pseudo, nom d'utilisateur ou ancien pseudo) comme surnom dans sa
   fiche. Ce surnom est ignoré dans la liste des membres : Rika ne note
   fiche. Ce surnom est ignoré dans la liste des membres : Rika ne note

+ 47 - 0
tests/test_memory.py

@@ -579,6 +579,53 @@ class InputSizesTest(MemoryTestCase):
         self.assertEqual(split_for_inputs("", [3000]), [""])
         self.assertEqual(split_for_inputs("", [3000]), [""])
 
 
 
 
+class TooLongFileTest(MemoryTestCase):
+    """Fichier plus long que sa limite (limite baissée, modification sur le disque) : « Modifier » s'ouvre
+    quand même, sans rien tronquer."""
+
+    def card(self) -> MemoryCard:
+        return MemoryCard(self.store, Target(f"membres/{MEMBER_ID}.md", MEMBER_ID, "Bob", is_self=True),
+                          owner_id=MEMBER_ID, bot_name="Rika")
+
+    @staticmethod
+    def interaction() -> mock.Mock:
+        interaction = mock.Mock()
+        interaction.response.send_modal = mock.AsyncMock()
+        interaction.response.send_message = mock.AsyncMock()
+        return interaction
+
+    async def test_window_holds_whole_file(self):
+        # Scénario de l'audit : fiche de 3500 caractères, limite de 3000.
+        text = "\n".join(["a" * 1999, "b" * 1500])
+        self.write(f"membres/{MEMBER_ID}.md", text)
+        interaction = self.interaction()
+        await self.card().edit(interaction)
+        window = interaction.response.send_modal.call_args.args[0]
+        self.assertEqual([text_input.max_length for text_input in window.inputs], [3500])
+        self.assertEqual(window.inputs[0].default, text)
+        self.assertIn("3000 caractères au plus", window.inputs[0].label)
+        self.assertLessEqual(len(window.inputs[0].label), 45)
+
+    async def test_saving_unshortened_refused(self):
+        self.write(f"membres/{MEMBER_ID}.md", "a" * 3500)
+        window = EditModal(self.card())
+        window.inputs[0]._value = "a" * 3500
+        interaction = self.interaction()
+        await window.on_submit(interaction)
+        self.assertIn("Trop long", interaction.response.send_message.call_args.args[0])
+        self.assertEqual(len(self.store.read(f"membres/{MEMBER_ID}.md")), 3500)
+
+    async def test_beyond_discord_window(self):
+        # Plus que 5 zones de 4000 : pas de fenêtre, le contenu complet est envoyé en fichier joint.
+        self.write(f"membres/{MEMBER_ID}.md", "a" * 25139)
+        interaction = self.interaction()
+        await self.card().edit(interaction)
+        interaction.response.send_modal.assert_not_awaited()
+        message = interaction.response.send_message.call_args
+        self.assertIn("25139 caractères", message.args[0])
+        self.assertEqual(len(message.kwargs["file"].fp.getvalue()), 25139)
+
+
 class AdminAccessTest(MemoryTestCase):
 class AdminAccessTest(MemoryTestCase):
     """Les membres savent que les administrateurs voient leur fiche, et chaque consultation est tracée."""
     """Les membres savent que les administrateurs voient leur fiche, et chaque consultation est tracée."""