diff --git a/src/main/java/org/itqop/whitelist/WhitelistApiClient.java b/src/main/java/org/itqop/whitelist/WhitelistApiClient.java index 6f2aefe..c4c0cff 100644 --- a/src/main/java/org/itqop/whitelist/WhitelistApiClient.java +++ b/src/main/java/org/itqop/whitelist/WhitelistApiClient.java @@ -15,12 +15,16 @@ import java.time.Duration; import java.time.Instant; import java.util.Objects; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.TimeUnit; 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 Gson(); - private final HttpClient http; + + private volatile HttpClient http; private volatile String baseUrl; public WhitelistApiClient() { @@ -38,12 +42,16 @@ public class WhitelistApiClient { private static String normalizeBaseUrl(String url) { if (url == null || url.isBlank()) return ""; String u = url.trim(); - while (u.endsWith("/")) u = u.substring(0, u.length() - 1); + if (u.endsWith("/")) u = u.substring(0, u.length() - 1); return u; } - public CompletableFuture addPlayer(String playerName) { - return addPlayer(playerName, null, null, null); + 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 addPlayer(String playerName, String reason) { @@ -52,120 +60,59 @@ public class WhitelistApiClient { public CompletableFuture addPlayer(String playerName, String playerUuid, String addedBy, String addedAt) { Objects.requireNonNull(playerName, "playerName"); - Instant t0 = Instant.now(); - if (Config.enableLogging) LOGGER.info("[ADD] start -> player={}, reason=null", playerName); + if (Config.enableLogging) LOGGER.info("[ADD] player={}", playerName); JsonObject body = new JsonObject(); body.addProperty("player_name", playerName); if (playerUuid != null && !playerUuid.isBlank()) body.addProperty("player_uuid", playerUuid); if (addedBy != null && !addedBy.isBlank()) body.addProperty("added_by", addedBy); if (addedAt != null && !addedAt.isBlank()) body.addProperty("added_at", addedAt); - return makeRequest("POST", "/add", body) - .orTimeout(Config.requestTimeout, TimeUnit.SECONDS) - .thenApply(resp -> handleBooleanResponse(resp, "ADD", playerName, t0)) - .exceptionally(ex -> { - long ms = Duration.between(t0, Instant.now()).toMillis(); - LOGGER.error("[ADD] fail player={} in {} ms: {}", playerName, ms, ex.toString()); - return false; - }); + return makeRequest("POST", "/add", HttpRequest.BodyPublishers.ofString(GSON.toJson(body))) + .thenApply(resp -> resp != null && resp.statusCode() / 100 == 2); } public CompletableFuture removePlayer(String playerName) { Objects.requireNonNull(playerName, "playerName"); - Instant t0 = Instant.now(); - if (Config.enableLogging) LOGGER.info("[REMOVE] start -> player={}", playerName); + if (Config.enableLogging) LOGGER.info("[REMOVE] player={}", playerName); JsonObject body = new JsonObject(); body.addProperty("player_name", playerName); - return makeRequest("POST", "/remove", body) - .orTimeout(Config.requestTimeout, TimeUnit.SECONDS) - .thenApply(resp -> handleBooleanResponse(resp, "REMOVE", playerName, t0)) - .exceptionally(ex -> { - long ms = Duration.between(t0, Instant.now()).toMillis(); - LOGGER.error("[REMOVE] fail player={} in {} ms: {}", playerName, ms, ex.toString()); - return false; - }); + return makeRequest("POST", "/remove", HttpRequest.BodyPublishers.ofString(GSON.toJson(body))) + .thenApply(resp -> resp != null && resp.statusCode() / 100 == 2); } public CompletableFuture checkPlayer(String playerName) { Objects.requireNonNull(playerName, "playerName"); - Instant t0 = Instant.now(); - if (Config.enableLogging) LOGGER.info("[CHECK] start -> player={}", playerName); + if (Config.enableLogging) LOGGER.info("[CHECK] player={}", playerName); JsonObject body = new JsonObject(); body.addProperty("player_name", playerName); - return makeRequest("POST", "/check", body) - .orTimeout(Config.requestTimeout, TimeUnit.SECONDS) - .thenApply(resp -> { - long ms = Duration.between(t0, Instant.now()).toMillis(); - int code = resp.statusCode(); - String bodyStr = resp.body(); - if (code / 100 != 2) { - LOGGER.warn("[CHECK] non-2xx code={} for player={}, {} ms; body={}", - code, playerName, ms, HttpUtils.abbreviate(bodyStr, 512)); - return new CheckResponse(false, false, null); - } - if (bodyStr == null || bodyStr.isBlank()) return new CheckResponse(true, false, null); + return makeRequest("POST", "/check", HttpRequest.BodyPublishers.ofString(GSON.toJson(body))) + .thenApply(response -> { + if (response == null) return new CheckResponse(false, false, null); try { - JsonObject json = JsonParser.parseString(bodyStr).getAsJsonObject(); + String bodyStr = response.body(); + JsonElement el = JsonParser.parseString(bodyStr); + JsonObject json = el.getAsJsonObject(); + boolean ok = json.has("ok") && json.get("ok").getAsBoolean(); boolean isWhitelisted = json.has("is_whitelisted") && json.get("is_whitelisted").getAsBoolean(); String uuid = json.has("player_uuid") && !json.get("player_uuid").isJsonNull() ? json.get("player_uuid").getAsString() : null; - if (Config.enableLogging) LOGGER.info("[CHECK] ok player={} -> isWhitelisted={}, uuid={}, {} ms", - playerName, isWhitelisted, uuid, ms); - return new CheckResponse(true, isWhitelisted, uuid); + return new CheckResponse(ok, isWhitelisted, uuid); } catch (Exception e) { - LOGGER.error("[CHECK] parse error player={} in {} ms: {}\nbody={}", - playerName, ms, e.toString(), HttpUtils.abbreviate(bodyStr, 512)); return new CheckResponse(false, false, null); } }) - .exceptionally(ex -> { - long ms = Duration.between(t0, Instant.now()).toMillis(); - LOGGER.error("[CHECK] fail player={} in {} ms: {}", playerName, ms, ex.toString()); - return new CheckResponse(false, false, null); - }); + .exceptionally(ex -> new CheckResponse(false, false, null)); } - private CompletableFuture> makeRequest(String method, String path, JsonObject body) { - String url = baseUrl + path; - String payload = body == null ? "{}" : GSON.toJson(body); - if (Config.enableLogging) LOGGER.info("[HTTP] {} {} payload_len={}", method, url, payload.length()); - HttpRequest.Builder b = HttpRequest.newBuilder() - .uri(URI.create(url)) - .timeout(Duration.ofSeconds(Math.max(1, Config.requestTimeout))) - .header("Accept", "application/json"); - if (Config.apiKey != null && !Config.apiKey.isBlank()) b.header("X-API-Key", Config.apiKey); - if ("GET".equals(method)) { - b.GET(); - } else if ("POST".equals(method)) { - b.header("Content-Type", "application/json"); - b.POST(HttpRequest.BodyPublishers.ofString(payload)); - } else { - throw new IllegalArgumentException("Unsupported method: " + method); - } + private CompletableFuture> 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 boolean handleBooleanResponse(HttpResponse resp, String op, String playerName, Instant t0) { - long ms = Duration.between(t0, Instant.now()).toMillis(); - int code = resp.statusCode(); - String body = resp.body(); - if (code / 100 != 2) { - LOGGER.warn("[{}] non-2xx code={} player={} {} ms; body={}", op, code, playerName, ms, HttpUtils.abbreviate(body, 512)); - return false; - } - if (body == null || body.isBlank()) return true; - try { - JsonElement el = JsonParser.parseString(body); - if (el.isJsonObject()) { - JsonObject json = el.getAsJsonObject(); - if (json.has("success")) return json.get("success").getAsBoolean(); - } - return true; - } catch (Exception e) { - return true; - } - } - public static final class CheckResponse { private final boolean success; private final boolean isWhitelisted; @@ -179,12 +126,4 @@ public class WhitelistApiClient { public boolean isWhitelisted() { return isWhitelisted; } public String getPlayerUuid() { return playerUuid; } } - - private static final class HttpUtils { - static String abbreviate(String s, int max) { - if (s == null) return null; - if (s.length() <= max) return s; - return s.substring(0, Math.max(0, max - 3)) + "..."; - } - } } diff --git a/src/main/java/org/itqop/whitelist/WhitelistCommands.java b/src/main/java/org/itqop/whitelist/WhitelistCommands.java index f783c8c..537271c 100644 --- a/src/main/java/org/itqop/whitelist/WhitelistCommands.java +++ b/src/main/java/org/itqop/whitelist/WhitelistCommands.java @@ -3,29 +3,32 @@ package org.itqop.whitelist; import com.mojang.brigadier.CommandDispatcher; import com.mojang.brigadier.arguments.StringArgumentType; import com.mojang.brigadier.context.CommandContext; -import com.mojang.logging.LogUtils; +import com.mojang.brigadier.suggestion.SuggestionProvider; import net.minecraft.commands.CommandSourceStack; import net.minecraft.commands.Commands; +import net.minecraft.commands.SharedSuggestionProvider; import net.minecraft.network.chat.Component; import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerPlayer; -import org.slf4j.Logger; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; import java.time.Instant; -import java.util.Locale; +import java.util.List; import java.util.UUID; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import net.neoforged.fml.loading.FMLPaths; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; public class WhitelistCommands { - private static final Logger LOGGER = LogUtils.getLogger(); private static final WhitelistApiClient API = new WhitelistApiClient(); + private static final SuggestionProvider ONLINE_PLAYER_SUGGESTIONS = (ctx, builder) -> { + List names = ctx.getSource().getServer().getPlayerList().getPlayers() + .stream() + .map(p -> p.getGameProfile().getName()) + .collect(Collectors.toList()); + return SharedSuggestionProvider.suggest(names, builder); + }; + public static void register(CommandDispatcher dispatcher) { dispatcher.register(Commands.literal("wl") .requires(src -> src.hasPermission(3)) @@ -34,152 +37,134 @@ public class WhitelistCommands { .then(Commands.literal("status").executes(WhitelistCommands::whitelistStatus)) .then(Commands.literal("add") .then(Commands.argument("player", StringArgumentType.string()) + .suggests(ONLINE_PLAYER_SUGGESTIONS) .executes(WhitelistCommands::addPlayer) .then(Commands.argument("reason", StringArgumentType.greedyString()) .executes(WhitelistCommands::addPlayer)))) .then(Commands.literal("remove") .then(Commands.argument("player", StringArgumentType.string()) + .suggests(ONLINE_PLAYER_SUGGESTIONS) .executes(WhitelistCommands::removePlayer))) .then(Commands.literal("check") .then(Commands.argument("player", StringArgumentType.string()) + .suggests(ONLINE_PLAYER_SUGGESTIONS) .executes(WhitelistCommands::checkPlayer))) ); } private static int enableWhitelist(CommandContext ctx) { boolean changed = setEnableWhitelistPersisted(ctx, true); - if (!changed) return 0; - sendOnServer(ctx, Component.literal("Whitelist enabled"), true); + if (!changed) { + ctx.getSource().sendFailure(Component.literal("Whitelist already enabled")); + return 0; + } + ctx.getSource().sendSuccess(() -> Component.literal("Whitelist enabled"), true); return 1; } private static int disableWhitelist(CommandContext ctx) { boolean changed = setEnableWhitelistPersisted(ctx, false); - if (!changed) return 0; - sendOnServer(ctx, Component.literal("Whitelist disabled"), true); + if (!changed) { + ctx.getSource().sendFailure(Component.literal("Whitelist already disabled")); + return 0; + } + ctx.getSource().sendSuccess(() -> Component.literal("Whitelist disabled"), true); return 1; } private static int whitelistStatus(CommandContext ctx) { - boolean enabled = Config.enableWhitelist; - sendOnServer(ctx, Component.literal("Whitelist status: " + (enabled ? "ON" : "OFF")), false); + ctx.getSource().sendSuccess(() -> Component.literal("Whitelist is " + (Config.enableWhitelist ? "ON" : "OFF")), false); return 1; } private static int addPlayer(CommandContext ctx) { - final String player = StringArgumentType.getString(ctx, "player"); - final String reason = ctx.getInput().contains(" add ") && ctx.getInput().split(" add ").length > 1 && ctx.getInput().split(" add ")[1].contains(" ") - ? ctx.getInput().substring(ctx.getInput().indexOf(" add ") + 5 + player.length()).trim() - : null; - final String addedBy = getSourceName(ctx); - final String addedAt = Instant.now().toString(); - final String uuid = resolveUuid(ctx.getSource().getServer(), player); + String player = StringArgumentType.getString(ctx, "player"); + String input = ctx.getInput(); + String reason = null; + int idx = input.indexOf(" add "); + if (idx >= 0) { + String tail = input.substring(idx + 5); + int sp = tail.indexOf(' '); + if (sp >= 0) reason = tail.substring(sp + 1).trim(); + } + String addedBy = getSourceName(ctx); + String addedAt = Instant.now().toString(); + String uuid = resolveUuid(ctx.getSource().getServer(), player); sendOnServer(ctx, Component.literal("Adding player to whitelist: " + player), false); - API.addPlayer(player, uuid, addedBy, addedAt) - .thenAccept(success -> dispatchBack(ctx, () -> { + API.addPlayer(player, uuid, addedBy, addedAt).thenAccept(success -> + dispatchBack(ctx, () -> { if (success) { - ctx.getSource().sendSuccess(() -> Component.literal("Player " + player + " added to whitelist"), true); + ctx.getSource().sendSuccess(() -> Component.literal("Player " + player + " added"), false); } else { - ctx.getSource().sendFailure(Component.literal("Failed to add player " + player + " to whitelist")); + ctx.getSource().sendFailure(Component.literal("Failed to add " + player)); } - })) - .exceptionally(ex -> { - dispatchBack(ctx, () -> ctx.getSource().sendFailure(Component.literal("Error adding player: " + ex.getMessage()))); - return null; - }); + }) + ); return 1; } private static int removePlayer(CommandContext ctx) { - final String player = StringArgumentType.getString(ctx, "player"); + String player = StringArgumentType.getString(ctx, "player"); sendOnServer(ctx, Component.literal("Removing player from whitelist: " + player), false); - API.removePlayer(player) - .thenAccept(success -> dispatchBack(ctx, () -> { + API.removePlayer(player).thenAccept(success -> + dispatchBack(ctx, () -> { if (success) { - ctx.getSource().sendSuccess(() -> Component.literal("Player " + player + " removed from whitelist"), true); + ctx.getSource().sendSuccess(() -> Component.literal("Player " + player + " removed"), false); + ServerPlayer sp = ctx.getSource().getServer().getPlayerList().getPlayerByName(player); + if (sp != null) { + sp.connection.disconnect(Component.literal("Removed from whitelist")); + } } else { - ctx.getSource().sendFailure(Component.literal("Failed to remove player " + player + " from whitelist")); + ctx.getSource().sendFailure(Component.literal("Failed to remove " + player)); } - })) - .exceptionally(ex -> { - dispatchBack(ctx, () -> ctx.getSource().sendFailure(Component.literal("Error removing player: " + ex.getMessage()))); - return null; - }); + }) + ); return 1; } private static int checkPlayer(CommandContext ctx) { - final String player = StringArgumentType.getString(ctx, "player"); - sendOnServer(ctx, Component.literal("Checking whitelist status: " + player), false); - API.checkPlayer(player) - .thenAccept(res -> dispatchBack(ctx, () -> { - if (!res.isSuccess()) { - ctx.getSource().sendFailure(Component.literal("Check failed for player " + player)); - return; + String player = StringArgumentType.getString(ctx, "player"); + API.checkPlayer(player).thenAccept(resp -> + dispatchBack(ctx, () -> { + if (resp != null && resp.isSuccess()) { + String msg = resp.isWhitelisted() ? "is in whitelist" : "is NOT in whitelist"; + ctx.getSource().sendSuccess(() -> Component.literal(player + " " + msg), false); + } else { + ctx.getSource().sendFailure(Component.literal("Check failed for " + player)); } - String msg = String.format(Locale.ROOT, - "Player %s -> whitelisted=%s%s", - player, - res.isWhitelisted(), - res.getPlayerUuid() != null ? ", uuid=" + res.getPlayerUuid() : ""); - ctx.getSource().sendSuccess(() -> Component.literal(msg), false); - })) - .exceptionally(ex -> { - dispatchBack(ctx, () -> ctx.getSource().sendFailure(Component.literal("Error checking player: " + ex.getMessage()))); - return null; - }); + }) + ); return 1; } - private static void sendOnServer(CommandContext ctx, Component msg, boolean broadcastToAdmins) { - dispatchBack(ctx, () -> ctx.getSource().sendSuccess(() -> msg, broadcastToAdmins)); - } - - private static void dispatchBack(CommandContext ctx, Runnable action) { - MinecraftServer srv = ctx.getSource().getServer(); - srv.execute(action); - } - - private static boolean setEnableWhitelistPersisted(CommandContext ctx, boolean value) { - try { - Config.enableWhitelist = value; - Path cfgPath = FMLPaths.CONFIGDIR.get().resolve(Whitelist.MODID + "-common.toml"); - if (Files.exists(cfgPath)) { - String content = Files.readString(cfgPath); - String updated = replaceEnableWhitelist(content, value); - if (!content.equals(updated)) { - Files.writeString(cfgPath, updated); - } - } - API.refreshFromConfig(); - return true; - } catch (Exception e) { - dispatchBack(ctx, () -> ctx.getSource().sendFailure(Component.literal("Failed to persist config: " + e.getMessage()))); - LOGGER.error("Persist enableWhitelist error", e); - return false; + private static void sendOnServer(CommandContext ctx, Component msg, boolean broadcast) { + if (broadcast) { + ctx.getSource().getServer().getPlayerList().broadcastSystemMessage(msg, false); + } else { + ctx.getSource().sendSuccess(() -> msg, false); } } - private static String replaceEnableWhitelist(String toml, boolean value) { - Pattern p = Pattern.compile("(?m)^\\s*enableWhitelist\\s*=\\s*(true|false)\\s*$"); - Matcher m = p.matcher(toml); - String replacement = "enableWhitelist = " + (value ? "true" : "false"); - if (m.find()) { - return m.replaceFirst(replacement); - } - String sep = toml.endsWith("\n") ? "" : "\n"; - return toml + sep + replacement + "\n"; + private static void dispatchBack(CommandContext ctx, Runnable task) { + ctx.getSource().getServer().execute(task); } private static String getSourceName(CommandContext ctx) { try { - if (ctx.getSource().getEntity() instanceof ServerPlayer sp) { - return sp.getGameProfile().getName(); - } + String s = ctx.getSource().getDisplayName().getString(); + if (!s.isEmpty()) return s; } catch (Exception ignored) {} return "server"; } + private static boolean setEnableWhitelistPersisted(CommandContext ctx, boolean enabled) { + boolean changed = Config.enableWhitelist != enabled; + Config.enableWhitelist = enabled; + WhitelistApiClient.refreshConfigFromSpec(); + ctx.getSource().getServer().submit(() -> {}).join(); + return changed; + } + private static String resolveUuid(MinecraftServer srv, String player) { try { ServerPlayer sp = srv.getPlayerList().getPlayerByName(player); diff --git a/src/main/java/org/itqop/whitelist/WhitelistEventHandler.java b/src/main/java/org/itqop/whitelist/WhitelistEventHandler.java index 3465b5a..c0e74a7 100644 --- a/src/main/java/org/itqop/whitelist/WhitelistEventHandler.java +++ b/src/main/java/org/itqop/whitelist/WhitelistEventHandler.java @@ -1,18 +1,43 @@ package org.itqop.whitelist; -import com.mojang.logging.LogUtils; import net.neoforged.bus.api.SubscribeEvent; import net.neoforged.fml.common.EventBusSubscriber; import net.neoforged.neoforge.event.RegisterCommandsEvent; -import org.slf4j.Logger; +import net.neoforged.neoforge.event.entity.player.PlayerEvent; +import net.minecraft.network.chat.ClickEvent; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerPlayer; @EventBusSubscriber(modid = Whitelist.MODID) public class WhitelistEventHandler { - private static final Logger LOGGER = LogUtils.getLogger(); @SubscribeEvent public static void registerCommands(RegisterCommandsEvent event) { WhitelistCommands.register(event.getDispatcher()); - LOGGER.info("Whitelist commands registered"); + } + + @SubscribeEvent + public static void onPlayerLogin(PlayerEvent.PlayerLoggedInEvent event) { + if (!Config.enableWhitelist) return; + if (!(event.getEntity() instanceof ServerPlayer sp)) return; + String name = sp.getGameProfile().getName(); + WhitelistApiClient.get().checkPlayer(name).thenAccept(resp -> { + if (resp != null && resp.isSuccess() && !resp.isWhitelisted()) { + sp.server.execute(() -> { + Component msg = Component.literal("Вас нету в белом списке сервера.\n") + .append( + Component.literal("Для входа необходимо приобрести проходку на сайте ") + .append( + Component.literal("hubmc.org/shop") + .withStyle(style -> style + .withUnderlined(true) + .withClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, "https://hubmc.org/shop")) + ) + ) + ); + sp.connection.disconnect(msg); + }); + } + }); } }