Forráskód Böngészése

Mémoire : la fenêtre « Modifier » n'accepte plus que la limite du fichier

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Johnounet 2 napja
szülő
commit
ee0ce0e1b5
3 módosított fájl, 62 hozzáadás és 11 törlés
  1. 26 10
      bot/memory_views.py
  2. 3 0
      changelog.txt
  3. 33 1
      tests/test_memory.py

+ 26 - 10
bot/memory_views.py

@@ -224,16 +224,16 @@ 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
-        limit = card.store.limit(card.target.file)
-        count = min(MAX_INPUTS, -(-limit // INPUT_MAX))
+        sizes = input_sizes(card.store.limit(card.target.file))
+        count = len(sizes)
         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)
-        parts = split_for_inputs(self.original, count)
+        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})"
             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=min(INPUT_MAX, limit), default=parts[i] or None,
+                max_length=sizes[i], default=parts[i] or None,
                 placeholder="Une information par ligne. Tout vider efface le fichier." if i == 0 else None,
                 placeholder="Une information par ligne. Tout vider efface le fichier." if i == 0 else None,
             )
             )
             self.inputs.append(text_input)
             self.inputs.append(text_input)
@@ -249,12 +249,28 @@ class EditModal(discord.ui.Modal):
             await interaction.response.send_message("La modification a échoué, regarde les logs.", ephemeral=True)
             await interaction.response.send_message("La modification a échoué, regarde les logs.", ephemeral=True)
 
 
 
 
-def split_for_inputs(text: str, count: int) -> list[str]:
-    """Répartit un texte dans count zones de texte, de préférence entre les lignes."""
-    parts = split_message(text, INPUT_MAX)
-    if len(parts) > count:  # lignes trop mal réparties : découpage exact
-        parts = [text[i:i + INPUT_MAX] for i in range(0, len(text), INPUT_MAX)]
-    return (parts + [""] * count)[:count]
+def input_sizes(limit: int) -> list[int]:
+    """Taille maximale de chaque zone de texte : INPUT_MAX, sauf la dernière, réduite pour que le total ne
+    dépasse pas la limite du fichier (5000 : 4000 + 1000). Sinon le compteur de Discord laisserait écrire
+    un texte refusé à l'enregistrement."""
+    count = min(MAX_INPUTS, -(-limit // INPUT_MAX))
+    return [min(INPUT_MAX, limit - INPUT_MAX * i) for i in range(count)]
+
+
+def split_for_inputs(text: str, sizes: list[int]) -> list[str]:
+    """Répartit un texte dans des zones de texte de ces tailles, de préférence entre les lignes. Une zone
+    ne reçoit jamais plus que sa taille : Discord refuserait d'ouvrir la fenêtre."""
+    parts, rest = [], text
+    for size in sizes:
+        cut = len(rest) if len(rest) <= size else rest.rfind("\n", 0, size + 1)
+        if cut <= 0 < len(rest):  # ligne plus longue que la zone : coupée au milieu
+            cut = min(size, len(rest))
+        parts.append(rest[:cut])
+        rest = rest[cut:].lstrip("\n")
+    if rest:  # lignes trop mal réparties : découpage exact
+        starts = [sum(sizes[:i]) for i in range(len(sizes))]
+        parts = [text[start:start + size] for start, size in zip(starts, sizes)]
+    return parts
 
 
 
 
 def member_target(store: MemoryStore, member: discord.User | discord.Member, owner_id: int) -> Target:
 def member_target(store: MemoryStore, member: discord.User | discord.Member, owner_id: int) -> Target:

+ 3 - 0
changelog.txt

@@ -123,6 +123,9 @@ Corrections
   perdue, ou la ligne retirée revenait. Quand un texte n'est pas
   perdue, ou la ligne retirée revenait. Quand un texte n'est pas
   enregistré (fichier changé, trop long…), il est renvoyé en fichier
   enregistré (fichier changé, trop long…), il est renvoyé en fichier
   joint pour ne pas être perdu.
   joint pour ne pas être perdu.
+- 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
+  laissait en écrire 8000, refusés ensuite à l'enregistrement.
 - 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

+ 33 - 1
tests/test_memory.py

@@ -9,7 +9,7 @@ from unittest import mock
 
 
 from bot.members import MemberRegistry, with_username
 from bot.members import MemberRegistry, with_username
 from bot.discord_bot import memory_command
 from bot.discord_bot import memory_command
-from bot.memory_views import ADMINS_NOTE, EditModal, MemoryCard, Target
+from bot.memory_views import ADMINS_NOTE, EditModal, MemoryCard, Target, input_sizes, split_for_inputs
 from bot.memory import (MAX_ATTACHED_NOTES, MAX_FORGOTTEN_LINES, MAX_NICKNAME_LENGTH, MAX_NICKNAMES,
 from bot.memory import (MAX_ATTACHED_NOTES, MAX_FORGOTTEN_LINES, MAX_NICKNAME_LENGTH, MAX_NICKNAMES,
                         SERVER_FILE, MemoryStore)
                         SERVER_FILE, MemoryStore)
 
 
@@ -547,6 +547,38 @@ class NotEnabledWriteTest(MemoryTestCase):
         self.assertFalse({item.label: item.disabled for item in self.card.view().children}["Modifier"])
         self.assertFalse({item.label: item.disabled for item in self.card.view().children}["Modifier"])
 
 
 
 
+class InputSizesTest(MemoryTestCase):
+    """La fenêtre « Modifier » n'accepte pas plus que la limite du fichier."""
+
+    def test_sizes(self):
+        self.assertEqual(input_sizes(3000), [3000])
+        self.assertEqual(input_sizes(5000), [4000, 1000])
+        self.assertEqual(input_sizes(8000), [4000, 4000])
+        self.assertEqual(input_sizes(30000), [4000] * 5)
+
+    def test_server_window_limited(self):
+        # Scénario de l'audit : limite de 5000, deux zones de 4000 laissaient écrire 8000 caractères.
+        self.store.server_limit = 5000
+        window = EditModal(MemoryCard(self.store, Target(SERVER_FILE, None), owner_id=999, bot_name="Rika"))
+        self.assertEqual([text_input.max_length for text_input in window.inputs], [4000, 1000])
+
+    def test_split_between_lines(self):
+        text = "\n".join(["a" * 2000, "b" * 1500, "c" * 900])
+        self.assertEqual(split_for_inputs(text, [4000, 1000]), ["a" * 2000 + "\n" + "b" * 1500, "c" * 900])
+
+    def test_split_never_exceeds_sizes(self):
+        # Coupé entre les lignes, le reste (1400) ne tiendrait pas dans la zone de 1000 : découpage exact.
+        for text in ["\n".join(["a" * 3500, "b" * 1399]), "x" * 5000, "\n" + "y" * 4500]:
+            with self.subTest(text=text[:5]):
+                parts = split_for_inputs(text, [4000, 1000])
+                self.assertTrue(all(len(part) <= size for part, size in zip(parts, [4000, 1000])))
+                self.assertEqual(len("".join(parts).replace("\n", "")), len(text.replace("\n", "")))
+
+    def test_short_text(self):
+        self.assertEqual(split_for_inputs("habite à Lyon", [4000, 1000]), ["habite à Lyon", ""])
+        self.assertEqual(split_for_inputs("", [3000]), [""])
+
+
 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."""