hubmc_essentionals/apiClient_examle.md

15 KiB

package org.itqop.whitelist;

import com.google.gson.*;
import com.mojang.logging.LogUtils;
import org.slf4j.Logger;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.time.Instant;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;

public class WhitelistApiClient {
private static final WhitelistApiClient INSTANCE = new WhitelistApiClient();
public static WhitelistApiClient get() { return INSTANCE; }
public static void refreshConfigFromSpec() { INSTANCE.refreshFromConfig(); }

    private static final Logger LOGGER = LogUtils.getLogger();
    private static final Gson GSON = new GsonBuilder().create();

    // Circuit breaker to prevent log spam
    private volatile long lastErrorLogTime = 0;
    private volatile int consecutiveErrors = 0;
    private static final long ERROR_LOG_COOLDOWN_MS = 60_000; // 1 minute
    private static final int ERROR_THRESHOLD = 3;

    private volatile HttpClient http;
    private volatile String baseUrl;

    public WhitelistApiClient() {
        this.http = HttpClient.newBuilder()
                .version(HttpClient.Version.HTTP_1_1)
                .connectTimeout(Duration.ofSeconds(Math.max(1, Config.requestTimeout)))
                .build();
        this.baseUrl = normalizeBaseUrl(Config.apiBaseUrl);
    }

    public void refreshFromConfig() {
        this.baseUrl = normalizeBaseUrl(Config.apiBaseUrl);
    }

    private static String normalizeBaseUrl(String url) {
        if (url == null || url.isBlank()) return "";
        String u = url.trim();
        if (u.endsWith("/")) u = u.substring(0, u.length() - 1);
        return u;
    }

    private HttpRequest.Builder baseBuilder(String path) {
        return HttpRequest.newBuilder(URI.create(baseUrl + path))
                .timeout(Duration.ofSeconds(Math.max(1, Config.requestTimeout)))
                .header("Content-Type", "application/json")
                .header("Accept", "application/json")
                .header("X-API-Key", Config.apiKey);
    }

    public CompletableFuture<Boolean> addPlayer(String playerName, String reason) {
        return addPlayer(playerName, null, null);
    }

    public CompletableFuture<Boolean> addPlayer(String playerName, String addedBy, String addedAtIso) {
        Objects.requireNonNull(playerName, "playerName");
        JsonObject body = new JsonObject();
        body.addProperty("player_name", playerName);
        if (addedBy != null && !addedBy.isBlank()) body.addProperty("added_by", addedBy);
        if (addedAtIso != null && !addedAtIso.isBlank()) body.addProperty("added_at", addedAtIso);
        return makeRequest("POST", "/add", HttpRequest.BodyPublishers.ofString(GSON.toJson(body)))
                .thenApply(resp -> {
                    if (resp == null) return false;
                    int code = resp.statusCode();
                    if (code == 201) {
                        resetErrorCounter();
                        return true;
                    }
                    logHttpError("POST /add", code, resp.body());
                    return false;
                })
                .exceptionally(ex -> {
                    LOGGER.error("Failed to add player '{}': {}", playerName, ex.getMessage(), ex);
                    return false;
                });
    }

    public CompletableFuture<WhitelistEntry> addPlayer(String playerName,
                                                       String addedBy,
                                                       Instant addedAt,
                                                       Instant expiresAt,
                                                       Boolean isActive,
                                                       String reason) {
        Objects.requireNonNull(playerName, "playerName");
        JsonObject body = new JsonObject();
        body.addProperty("player_name", playerName);
        if (addedBy != null && !addedBy.isBlank()) body.addProperty("added_by", addedBy);
        if (addedAt != null) body.addProperty("added_at", addedAt.toString());
        if (expiresAt != null) body.addProperty("expires_at", expiresAt.toString());
        if (isActive != null) body.addProperty("is_active", isActive);
        if (reason != null && !reason.isBlank()) body.addProperty("reason", reason);
        return makeRequest("POST", "/add", HttpRequest.BodyPublishers.ofString(GSON.toJson(body)))
                .thenApply(resp -> {
                    if (resp == null) return null;
                    int code = resp.statusCode();
                    if (code != 201) {
                        logHttpError("POST /add", code, resp.body());
                        return null;
                    }
                    try {
                        resetErrorCounter();
                        JsonObject json = JsonParser.parseString(resp.body()).getAsJsonObject();
                        return WhitelistEntry.fromJson(json);
                    } catch (Exception e) {
                        LOGGER.error("Failed to parse addPlayer response for '{}': {}", playerName, e.getMessage(), e);
                        return null;
                    }
                })
                .exceptionally(ex -> {
                    LOGGER.error("Failed to add player '{}': {}", playerName, ex.getMessage(), ex);
                    return null;
                });
    }

    public CompletableFuture<Boolean> removePlayer(String playerName) {
        Objects.requireNonNull(playerName, "playerName");
        JsonObject body = new JsonObject();
        body.addProperty("player_name", playerName);
        return makeRequest("POST", "/remove", HttpRequest.BodyPublishers.ofString(GSON.toJson(body)))
                .thenApply(resp -> {
                    if (resp == null) return false;
                    int code = resp.statusCode();
                    if (code == 204) {
                        resetErrorCounter();
                        return true;
                    }
                    logHttpError("POST /remove", code, resp.body());
                    return false;
                })
                .exceptionally(ex -> {
                    LOGGER.error("Failed to remove player '{}': {}", playerName, ex.getMessage(), ex);
                    return false;
                });
    }

    public CompletableFuture<CheckResponse> checkPlayer(String playerName) {
        Objects.requireNonNull(playerName, "playerName");
        JsonObject body = new JsonObject();
        body.addProperty("player_name", playerName);
        return makeRequest("POST", "/check", HttpRequest.BodyPublishers.ofString(GSON.toJson(body)))
                .thenApply(response -> {
                    if (response == null) return new CheckResponse(false, false);
                    try {
                        int code = response.statusCode();
                        if (code < 200 || code >= 300) {
                            logHttpError("POST /check", code, response.body());
                            return new CheckResponse(false, false);
                        }
                        resetErrorCounter();
                        JsonObject json = JsonParser.parseString(response.body()).getAsJsonObject();
                        boolean isWhitelisted = json.has("is_whitelisted") && json.get("is_whitelisted").getAsBoolean();
                        return new CheckResponse(true, isWhitelisted);
                    } catch (Exception e) {
                        LOGGER.warn("Failed to parse checkPlayer response for '{}': {}", playerName, e.getMessage());
                        return new CheckResponse(false, false);
                    }
                })
                .exceptionally(ex -> {
                    logApiError("Failed to check player '" + playerName + "'", ex);
                    return new CheckResponse(false, false);
                });
    }

    public CompletableFuture<WhitelistListResponse> listAll() {
        return makeRequest("GET", "/", HttpRequest.BodyPublishers.noBody())
                .thenApply(resp -> {
                    if (resp == null) return null;
                    int code = resp.statusCode();
                    if (code < 200 || code >= 300) {
                        logHttpError("GET /", code, resp.body());
                        return null;
                    }
                    try {
                        JsonObject json = JsonParser.parseString(resp.body()).getAsJsonObject();
                        JsonArray arr = json.getAsJsonArray("entries");
                        int total = json.has("total") ? json.get("total").getAsInt() : 0;
                        WhitelistEntry[] entries = new WhitelistEntry[arr.size()];
                        for (int i = 0; i < arr.size(); i++) {
                            entries[i] = WhitelistEntry.fromJson(arr.get(i).getAsJsonObject());
                        }
                        return new WhitelistListResponse(List.of(entries), total);
                    } catch (Exception e) {
                        LOGGER.error("Failed to parse listAll response: {}", e.getMessage(), e);
                        return null;
                    }
                })
                .exceptionally(ex -> {
                    LOGGER.error("Failed to list whitelist entries: {}", ex.getMessage(), ex);
                    return null;
                });
    }

    public CompletableFuture<Integer> count() {
        return makeRequest("GET", "/count", HttpRequest.BodyPublishers.noBody())
                .thenApply(resp -> {
                    if (resp == null) return null;
                    int code = resp.statusCode();
                    if (code < 200 || code >= 300) {
                        logHttpError("GET /count", code, resp.body());
                        return null;
                    }
                    try {
                        JsonObject json = JsonParser.parseString(resp.body()).getAsJsonObject();
                        return json.has("total") ? json.get("total").getAsInt() : 0;
                    } catch (Exception e) {
                        LOGGER.error("Failed to parse count response: {}", e.getMessage(), e);
                        return null;
                    }
                })
                .exceptionally(ex -> {
                    LOGGER.error("Failed to get whitelist count: {}", ex.getMessage(), ex);
                    return null;
                });
    }

    private CompletableFuture<HttpResponse<String>> makeRequest(String method, String path, HttpRequest.BodyPublisher body) {
        HttpRequest.Builder b = baseBuilder(path);
        if ("POST".equalsIgnoreCase(method)) b.POST(body);
        else if ("PUT".equalsIgnoreCase(method)) b.PUT(body);
        else if ("DELETE".equalsIgnoreCase(method)) b.method("DELETE", body);
        else b.method(method, body);
        return http.sendAsync(b.build(), HttpResponse.BodyHandlers.ofString());
    }

    private void logHttpError(String endpoint, int statusCode, String responseBody) {
        consecutiveErrors++;

        // Log only first error and then once per minute to prevent spam
        long now = System.currentTimeMillis();
        if (consecutiveErrors == 1 || (now - lastErrorLogTime) > ERROR_LOG_COOLDOWN_MS) {
            if (Config.enableLogging) {
                LOGGER.warn("API request failed: {} returned {} - Response: {}", endpoint, statusCode, responseBody);
            } else {
                LOGGER.warn("API request failed: {} returned {}", endpoint, statusCode);
            }
            lastErrorLogTime = now;

            if (consecutiveErrors > ERROR_THRESHOLD) {
                LOGGER.warn("API has failed {} times consecutively. Further errors will be throttled.", consecutiveErrors);
            }
        }
    }

    private void logApiError(String message, Throwable ex) {
        consecutiveErrors++;

        // Log only first error and then once per minute to prevent spam
        long now = System.currentTimeMillis();
        if (consecutiveErrors == 1 || (now - lastErrorLogTime) > ERROR_LOG_COOLDOWN_MS) {
            LOGGER.warn("{}: {} (errors: {})", message, ex.getMessage(), consecutiveErrors);
            lastErrorLogTime = now;

            if (consecutiveErrors > ERROR_THRESHOLD) {
                LOGGER.warn("API has failed {} times consecutively. Further errors will be throttled.", consecutiveErrors);
            }
        }
    }

    private void resetErrorCounter() {
        if (consecutiveErrors > 0) {
            LOGGER.info("API connection restored after {} consecutive errors", consecutiveErrors);
            consecutiveErrors = 0;
        }
    }

    public static final class CheckResponse {
        private final boolean success;
        private final boolean isWhitelisted;
        public CheckResponse(boolean success, boolean isWhitelisted) {
            this.success = success;
            this.isWhitelisted = isWhitelisted;
        }
        public boolean isSuccess() { return success; }
        public boolean isWhitelisted() { return isWhitelisted; }
    }

    public static final class WhitelistEntry {
        public final String id;
        public final String playerName;
        public final String addedBy;
        public final String addedAt;
        public final String expiresAt;
        public final boolean isActive;
        public final String reason;

        public WhitelistEntry(String id, String playerName, String addedBy, String addedAt, String expiresAt, boolean isActive, String reason) {
            this.id = id;
            this.playerName = playerName;
            this.addedBy = addedBy;
            this.addedAt = addedAt;
            this.expiresAt = expiresAt;
            this.isActive = isActive;
            this.reason = reason;
        }

        public static WhitelistEntry fromJson(JsonObject json) {
            String id = json.has("id") && !json.get("id").isJsonNull() ? json.get("id").getAsString() : null;
            String pn = json.has("player_name") && !json.get("player_name").isJsonNull() ? json.get("player_name").getAsString() : null;
            String by = json.has("added_by") && !json.get("added_by").isJsonNull() ? json.get("added_by").getAsString() : null;
            String at = json.has("added_at") && !json.get("added_at").isJsonNull() ? json.get("added_at").getAsString() : null;
            String exp = json.has("expires_at") && !json.get("expires_at").isJsonNull() ? json.get("expires_at").getAsString() : null;
            boolean active = json.has("is_active") && !json.get("is_active").isJsonNull() && json.get("is_active").getAsBoolean();
            String rsn = json.has("reason") && !json.get("reason").isJsonNull() ? json.get("reason").getAsString() : null;
            return new WhitelistEntry(id, pn, by, at, exp, active, rsn);
        }
    }

    public static final class WhitelistListResponse {
        public final List<WhitelistEntry> entries;
        public final int total;
        public WhitelistListResponse(List<WhitelistEntry> entries, int total) {
            this.entries = entries;
            this.total = total;
        }
    }
}
```