Volver al índice

src/main/java/ar/com/companeros/suggestions/SuggestionStore.java

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<String> 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<String> 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;
    }
}