From 2220f11e6495a0715665663f1377491e122a8cdd Mon Sep 17 00:00:00 2001 From: Marcos Paulo Date: Tue, 18 Aug 2026 19:15:02 -0300 Subject: [PATCH] fix(ia): prevent private notes leak in places tool, guard recordTurn against disk failure, and add tests --- .../java/dev/marcospaulo/canalhandia/Ai.java | 8 +++-- .../dev/marcospaulo/canalhandia/Tools.java | 17 ++++++++-- .../marcospaulo/canalhandia/ToolsTest.java | 33 +++++++++++++++++++ 3 files changed, 54 insertions(+), 4 deletions(-) diff --git a/src/main/java/dev/marcospaulo/canalhandia/Ai.java b/src/main/java/dev/marcospaulo/canalhandia/Ai.java index 7d7b25c..4249733 100644 --- a/src/main/java/dev/marcospaulo/canalhandia/Ai.java +++ b/src/main/java/dev/marcospaulo/canalhandia/Ai.java @@ -437,6 +437,7 @@ final class Ai { return; } String clean = String.join(" ", segments); + lastAnswer = new Answered(askerId, question, clean); // Only remember if the asker is still online: a PlayerQuitEvent forgets // the player's history (carry-forward #6), and re-adding here after the // quit would resurrect it. lastAnswer stays regardless, so /ia corrigir @@ -444,10 +445,13 @@ final class Ai { if (asker != null) { conversations.remember(askerId, question, clean); if (plugin.playerMemory() != null) { - plugin.playerMemory().recordTurn(askerId, asker.getName(), question, clean); + try { + plugin.playerMemory().recordTurn(askerId, asker.getName(), question, clean); + } catch (RuntimeException e) { + plugin.getLogger().warning("Falha ao gravar memória da IA para " + asker.getName() + ": " + e); + } } } - lastAnswer = new Answered(askerId, question, clean); if (isPrivate || !settings.aiPublic()) { if (asker != null) { diff --git a/src/main/java/dev/marcospaulo/canalhandia/Tools.java b/src/main/java/dev/marcospaulo/canalhandia/Tools.java index 15804c9..c65fd4e 100644 --- a/src/main/java/dev/marcospaulo/canalhandia/Tools.java +++ b/src/main/java/dev/marcospaulo/canalhandia/Tools.java @@ -146,7 +146,20 @@ final class Tools { } String uuidStr = who.uuid(); List deaths = plugin.deathLog().forPlayer(uuidStr); - List notes = plugin.notes().visibleTo(uuidStr, null, ""); + List notes = plugin.notes().visibleTo(uuidStr, Note.Scope.PUBLICA, ""); + return formatPlayerPlaces(who, deaths, notes); + } + + String formatPlayerPlaces(OfflineStats.Known who, List deaths, List visibleNotes) { + String uuidStr = who.uuid(); + List notes = new java.util.ArrayList<>(); + if (visibleNotes != null) { + for (Note n : visibleNotes) { + if (n.scope() == Note.Scope.PUBLICA && n.authorId() != null && n.authorId().equals(uuidStr)) { + notes.add(n); + } + } + } StringBuilder sb = new StringBuilder(); sb.append("Lugares conhecidos de ").append(who.name()).append(":\n"); @@ -164,7 +177,7 @@ final class Tools { sb.append("\n"); } - if (deaths.isEmpty()) { + if (deaths == null || deaths.isEmpty()) { sb.append("- Mortes recentes: nenhum registro de morte recente."); } else { sb.append("- Mortes recentes: "); diff --git a/src/test/java/dev/marcospaulo/canalhandia/ToolsTest.java b/src/test/java/dev/marcospaulo/canalhandia/ToolsTest.java index 06535a1..8b32f65 100644 --- a/src/test/java/dev/marcospaulo/canalhandia/ToolsTest.java +++ b/src/test/java/dev/marcospaulo/canalhandia/ToolsTest.java @@ -3,6 +3,8 @@ package dev.marcospaulo.canalhandia; import com.google.gson.JsonArray; import org.junit.jupiter.api.Test; +import java.util.List; + import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -42,4 +44,35 @@ class ToolsTest { String res = tools.run("wiki", "malformed json"); assertTrue(res.contains("argumentos inválidos")); } + + @Test + void formatPlayerPlacesFiltersOutPrivateNotesAndOtherAuthors() { + Tools tools = new Tools(null, null, null, s -> {}); + OfflineStats.Known who = new OfflineStats.Known("uuid-ana", "ana"); + + Note privateNote = new Note(1, Note.Scope.PRIVADA, "ana", "uuid-ana", "segredo", "world", 10, 20, 30, 1000L); + Note publicNote = new Note(2, Note.Scope.PUBLICA, "ana", "uuid-ana", "vila do spawn", "world", 100, 64, 200, 1000L); + Note otherAuthorPublicNote = new Note(3, Note.Scope.PUBLICA, "bob", "uuid-bob", "base do bob", "world", 500, 64, 500, 1000L); + + String formatted = tools.formatPlayerPlaces(who, List.of(), List.of(privateNote, publicNote, otherAuthorPublicNote)); + + assertTrue(formatted.contains("vila do spawn")); + assertFalse(formatted.contains("segredo"), "Private notes must never be included in places output"); + assertFalse(formatted.contains("base do bob"), "Notes of other players must not be included"); + assertTrue(formatted.contains("nenhum registro de morte recente")); + } + + @Test + void formatPlayerPlacesIncludesDeathsAndFormatsPlaces() { + Tools tools = new Tools(null, null, null, s -> {}); + OfflineStats.Known who = new OfflineStats.Known("uuid-ana", "ana"); + + DeathLog.Entry d = new DeathLog.Entry("uuid-ana", "abraçou um Creeper", "Mundo normal", 12, 64, -80, 1000L); + String formatted = tools.formatPlayerPlaces(who, List.of(d), List.of()); + + assertTrue(formatted.contains("Lugares conhecidos de ana:")); + assertTrue(formatted.contains("nenhuma base salva")); + assertTrue(formatted.contains("12, 64, -80 (Mundo normal)")); + assertTrue(formatted.contains("abraçou um Creeper")); + } }