package ar.com.companeros.suggestions; import com.google.gson.*; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.*; import java.time.Instant; import java.util.*; /** Pedidos de los agentes para desarrolladores. Los borradores se guardan como texto, sin ejecutarlos. */ public final class SuggestionStore { private final Path directory; public SuggestionStore(Path directory) throws IOException { this.directory = directory.toAbsolutePath().normalize(); if (Files.isSymbolicLink(this.directory)) throw new IOException("Suggestions no puede ser un enlace"); Files.createDirectories(this.directory); Path guide = this.directory.resolve("LEEME.md"); if (!Files.exists(guide)) Files.writeString(guide, "# Sugerencias de los agentes\n\nCada carpeta contiene un pedido con autor, fecha, contexto y estado pendiente. " + "Los archivos draft son borradores de texto para revisión; no se cargan como herramientas ni mods. " + "Eventos e integraciones se prueban en un mundo separado antes de incorporarse.\n", StandardOpenOption.CREATE_NEW); } // El actor lo aporta el servidor; el JSON nunca puede elegir autor, rutas o estado aprobado. public synchronized Path submit(UUID actor, String name, String context, String source, long now) throws IOException { if (source == null || source.getBytes(StandardCharsets.UTF_8).length > 4096) throw new IllegalArgumentException("Sugerencia demasiado extensa"); JsonObject request = JsonParser.parseString(source).getAsJsonObject(); Set allowed = Set.of("title", "kind", "description", "language", "source", "dependencies"); if (!allowed.containsAll(request.keySet())) throw new IllegalArgumentException("Campo de sugerencia desconocido"); String title = text(request, "title", 100, false), kind = text(request, "kind", 20, false); String description = text(request, "description", 1800, false); String language = request.has("language") ? text(request, "language", 12, false) : "none"; String code = request.has("source") ? text(request, "source", 1800, true) : ""; if (!Set.of("capability", "event", "mod").contains(kind) || !Set.of("none", "java", "json", "python").contains(language)) throw new IllegalArgumentException("Tipo o lenguaje desconocido"); List dependencies = new ArrayList<>(); if (request.has("dependencies")) { JsonArray values = request.getAsJsonArray("dependencies"); if (values.size() > 8) throw new IllegalArgumentException("Demasiadas dependencias"); for (JsonElement value : values) { if (!value.isJsonPrimitive() || !value.getAsJsonPrimitive().isString() || value.getAsString().length() > 80) throw new IllegalArgumentException("Dependencia inválida"); dependencies.add(value.getAsString()); } } // Límites persistentes: reiniciar el servidor no permite inundar la carpeta. int total = 0, own = 0; long last = 0; try (var files = Files.list(directory)) { for (Path folder : files.filter(p -> Files.isDirectory(p, LinkOption.NOFOLLOW_LINKS)).limit(129).toList()) { total++; if (folder.getFileName().toString().startsWith(actor + "-")) { own++; last = Math.max(last, Files.getLastModifiedTime(folder).toMillis()); } } } if (total >= 128 || own >= 16 || (last > 0 && now - last < 300_000)) throw new IllegalArgumentException("Límite de sugerencias: esperá cinco minutos o revisá los pedidos pendientes"); UUID id = UUID.randomUUID(); Path folder = directory.resolve(actor + "-" + id); Files.createDirectory(folder); JsonObject record = request.deepCopy(); record.addProperty("id", id.toString()); record.addProperty("authorUuid", actor.toString()); record.addProperty("authorName", name); record.addProperty("createdAt", Instant.ofEpochMilli(now).toString()); record.addProperty("context", context); record.addProperty("status", "pending_review"); Files.writeString(folder.resolve("proposal.json"), new GsonBuilder().setPrettyPrinting().create().toJson(record), StandardCharsets.UTF_8, StandardOpenOption.CREATE_NEW); Files.writeString(folder.resolve("proposal.md"), "# " + title.replace('\n', ' ') + "\n\nAutor: " + name + " (" + actor + ")\nFecha: " + Instant.ofEpochMilli(now) + "\nTipo: " + kind + "\nEstado: pendiente de revisión\nContexto: " + context + "\n\n" + description + "\n\nDependencias: " + String.join(", ", dependencies) + "\n", StandardCharsets.UTF_8, StandardOpenOption.CREATE_NEW); if (!code.isBlank()) Files.writeString(folder.resolve("draft." + language + ".txt"), code, StandardCharsets.UTF_8, StandardOpenOption.CREATE_NEW); Files.setLastModifiedTime(folder, java.nio.file.attribute.FileTime.fromMillis(now)); return folder; } private static String text(JsonObject value, String key, int limit, boolean empty) { JsonElement field = value.get(key); if (field == null || !field.isJsonPrimitive() || !field.getAsJsonPrimitive().isString()) throw new IllegalArgumentException("Falta texto: " + key); String result = field.getAsString(); if ((!empty && result.isBlank()) || result.length() > limit || result.indexOf('\0') >= 0) throw new IllegalArgumentException("Texto fuera de límite: " + key); return result; } }