diff --git a/common/build.gradle.kts b/common/build.gradle.kts
index aa1ea84..45be7fa 100644
--- a/common/build.gradle.kts
+++ b/common/build.gradle.kts
@@ -11,6 +11,7 @@ dependencies {
implementation("net.kyori:adventure-api:4.25.0")
implementation("net.kyori:adventure-text-minimessage:4.25.0")
+ implementation("org.slf4j:slf4j-api:2.0.17")
}
java {
diff --git a/common/src/main/java/online/mineroo/common/BindRequest.java b/common/src/main/java/online/mineroo/common/BindRequest.java
new file mode 100644
index 0000000..3edce4e
--- /dev/null
+++ b/common/src/main/java/online/mineroo/common/BindRequest.java
@@ -0,0 +1,153 @@
+package online.mineroo.common;
+
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import online.mineroo.common.NetworkServiceInterface;
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+
+import java.util.concurrent.CompletableFuture;
+
+/**
+ * BindRequest handles server binding and verification with the Mineroo API.
+ *
+ * This class operates asynchronously and relies on the injected
+ * {@link NetworkService}
+ * to handle the actual HTTP transport and authentication.
+ */
+public class BindRequest {
+
+ private final Logger logger;
+ private final NetworkServiceInterface networkService;
+
+ // State cache
+ private String motdToken = "";
+ private String sessionToken = "";
+
+ /**
+ * Constructs a BindRequest instance.
+ *
+ * @param logger Logger for logging errors and information
+ * @param networkService The network service instance (Local or Proxy) that
+ * handles transport and auth
+ */
+ public BindRequest(Logger logger, NetworkServiceInterface networkService) {
+ this.logger = logger;
+ this.networkService = networkService;
+ }
+
+ /**
+ * Checks the bind status asynchronously.
+ *
+ * @return A future that completes with true if status is 'bound', false
+ * otherwise
+ */
+ public CompletableFuture checkBindStatus() {
+ // NetworkService is already configured with BaseURL, only need the path here
+ String path = "/server/server-bind-status";
+
+ return networkService.getData(path, null)
+ .thenApply(responseBody -> {
+ try {
+ JsonObject responseData = JsonParser.parseString(responseBody).getAsJsonObject();
+ boolean bound = responseData.has("bound") && responseData.get("bound").getAsBoolean();
+
+ if (!bound) {
+ logger.warn("Mineroo Bind: Server not bound. 'bound' field is false.");
+ }
+ return bound;
+ } catch (Exception e) {
+ logger.error("Mineroo Bind: Failed to parse bind status response", e);
+ return false;
+ }
+ })
+ .exceptionally(e -> {
+ logger.error("Mineroo Bind: Network error while checking status", e);
+ return false;
+ });
+ }
+
+ /**
+ * Initiates a MOTD verification request asynchronously.
+ *
+ * @param hostname The server hostname
+ * @param port The server port
+ * @return A future that completes with true if tokens are received
+ */
+ public CompletableFuture initialMotdVerifyRequest(String hostname, Integer port) {
+ String path = "/server/motd-verify";
+
+ JsonObject json = new JsonObject();
+ json.addProperty("server_address", hostname);
+ json.addProperty("server_port", port);
+
+ return networkService.postData(path, json)
+ .thenApply(responseBody -> {
+ try {
+ JsonObject responseData = JsonParser.parseString(responseBody).getAsJsonObject();
+
+ if (responseData.has("motd_token")) {
+ this.motdToken = responseData.get("motd_token").getAsString();
+ } else {
+ logger.error("Mineroo Bind: MOTD Token not received.");
+ return false;
+ }
+
+ if (responseData.has("session_token")) {
+ this.sessionToken = responseData.get("session_token").getAsString();
+ } else {
+ logger.error("Mineroo Bind: Session Token not received.");
+ return false;
+ }
+
+ return true;
+
+ } catch (Exception e) {
+ logger.error("Mineroo Bind: Failed to parse MOTD verify response: " + responseBody, e);
+ return false;
+ }
+ })
+ .exceptionally(e -> {
+ logger.error("Mineroo Bind: Network error during MOTD verify", e);
+ return false;
+ });
+ }
+
+ /**
+ * Finalizes the server binding asynchronously.
+ *
+ * @param name The server name (nullable)
+ * @param description The server description (nullable)
+ * @return A future that completes with true if the binding is successful
+ */
+ public CompletableFuture finalizeServerBinding(@Nullable String name, @Nullable String description) {
+ String path = "/server/server-bind";
+
+ JsonObject json = new JsonObject();
+ json.addProperty("session_token", this.sessionToken);
+ json.addProperty("name", name);
+ json.addProperty("description", description);
+
+ return networkService.postData(path, json)
+ .thenApply(responseBody -> {
+ // Assume as long as the request returns successfully and no exception is
+ // thrown, it is considered successful (HTTP 200)
+ // If the API returns a specific {"success": false}, you need to parse the JSON
+ // and check here
+ return true;
+ })
+ .exceptionally(e -> {
+ logger.error("Mineroo Bind: Failed to finalize binding", e);
+ return false;
+ });
+ }
+
+ /**
+ * Gets the MOTD token received from the API.
+ *
+ * @return the motdToken string
+ */
+ public String getMotdToken() {
+ return motdToken;
+ }
+}
diff --git a/common/src/main/java/online/mineroo/common/HttpNetworkService.java b/common/src/main/java/online/mineroo/common/HttpNetworkService.java
new file mode 100644
index 0000000..930ba18
--- /dev/null
+++ b/common/src/main/java/online/mineroo/common/HttpNetworkService.java
@@ -0,0 +1,93 @@
+package online.mineroo.common;
+
+import com.google.gson.JsonObject;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.util.Base64;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+
+public class HttpNetworkService implements NetworkServiceInterface {
+
+ private final HttpClient client;
+ private final String baseUrl;
+ private final String authHeaderValue; // Precomputed Authorization header value
+
+ /**
+ * @param baseUrl Base URL of the API (e.g., "https://oapi.mineroo.online")
+ * @param username Username for Basic Auth (usually "mbind" in your case)
+ * @param password Password for Basic Auth (the token)
+ */
+ public HttpNetworkService(String baseUrl, String username, String password) {
+ this.baseUrl = baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl;
+
+ // It is recommended to configure connectTimeout
+ this.client = HttpClient.newBuilder()
+ .connectTimeout(Duration.ofSeconds(10))
+ .build();
+
+ // Pre-generate Basic Auth string
+ String auth = username + ":" + password;
+ this.authHeaderValue = "Basic " + Base64.getEncoder().encodeToString(auth.getBytes(StandardCharsets.UTF_8));
+ }
+
+ @Override
+ public CompletableFuture getData(String endpoint, Map params) {
+ return CompletableFuture.supplyAsync(() -> {
+ try {
+ // 1. Build URL and query parameters
+ StringBuilder urlBuilder = new StringBuilder(baseUrl + endpoint);
+ if (params != null && !params.isEmpty()) {
+ urlBuilder.append("?");
+ params.forEach((k, v) -> urlBuilder.append(k).append("=").append(v).append("&"));
+ // Simple trailing '&' removal (usually harmless, but cleaner)
+ urlBuilder.setLength(urlBuilder.length() - 1);
+ }
+
+ // 2. Build the request
+ HttpRequest request = HttpRequest.newBuilder()
+ .uri(URI.create(urlBuilder.toString()))
+ .header("Authorization", authHeaderValue) // Inject Auth
+ .header("User-Agent", "Mineroo-Plugin/1.0") // Recommended to add User-Agent
+ .GET()
+ .build();
+
+ // 3. Send the request
+ HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
+
+ // You can do a simple status code check here, or leave it to the upper layer
+ // To keep the interface pure, we just return the body
+ return response.body();
+
+ } catch (Exception e) {
+ // Wrap checked exceptions as runtime exceptions, Future will catch them
+ throw new RuntimeException("HTTP GET failed: " + endpoint, e);
+ }
+ });
+ }
+
+ @Override
+ public CompletableFuture postData(String endpoint, JsonObject body) {
+ return CompletableFuture.supplyAsync(() -> {
+ try {
+ HttpRequest request = HttpRequest.newBuilder()
+ .uri(URI.create(baseUrl + endpoint))
+ .header("Authorization", authHeaderValue) // Inject Auth
+ .header("Content-Type", "application/json")
+ .header("User-Agent", "Mineroo-Plugin/1.0")
+ .POST(HttpRequest.BodyPublishers.ofString(body.toString(), StandardCharsets.UTF_8))
+ .build();
+
+ HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
+ return response.body();
+
+ } catch (Exception e) {
+ throw new RuntimeException("HTTP POST failed: " + endpoint, e);
+ }
+ });
+ }
+}
diff --git a/common/src/main/java/online/mineroo/common/NetworkServiceInterface.java b/common/src/main/java/online/mineroo/common/NetworkServiceInterface.java
new file mode 100644
index 0000000..9823824
--- /dev/null
+++ b/common/src/main/java/online/mineroo/common/NetworkServiceInterface.java
@@ -0,0 +1,12 @@
+package online.mineroo.common;
+
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+
+import com.google.gson.JsonObject;
+
+public interface NetworkServiceInterface {
+ CompletableFuture getData(String endpoint, Map params);
+
+ CompletableFuture postData(String endpoint, JsonObject body);
+}
diff --git a/kls_database.db b/kls_database.db
index ffd8076..f26bd3f 100644
Binary files a/kls_database.db and b/kls_database.db differ
diff --git a/velocity/src/main/java/online/mineroo/velocity/commands/MainCommand.java b/velocity/src/main/java/online/mineroo/velocity/commands/MainCommand.java
index 7379db4..bbf1e1c 100644
--- a/velocity/src/main/java/online/mineroo/velocity/commands/MainCommand.java
+++ b/velocity/src/main/java/online/mineroo/velocity/commands/MainCommand.java
@@ -1,6 +1,7 @@
package online.mineroo.velocity.commands;
import java.util.List;
+import java.util.concurrent.CompletionException;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -12,9 +13,11 @@ import com.velocitypowered.api.command.SimpleCommand;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import online.mineroo.common.MessageManager;
+import online.mineroo.common.NetworkServiceInterface;
import online.mineroo.velocity.Config;
import online.mineroo.velocity.MinerooCore;
-import online.mineroo.velocity.utils.BindRequest;
+import online.mineroo.common.BindRequest;
+import online.mineroo.common.HttpNetworkService;
public class MainCommand implements SimpleCommand {
@@ -94,43 +97,64 @@ public class MainCommand implements SimpleCommand {
Logger logger = plugin.getLogger();
+ // Run in a separate worker thread to allow blocking (Thread.sleep)
plugin.getServer().getScheduler().buildTask(plugin, () -> {
- BindRequest request = new BindRequest(logger, bind_token);
+ // 1. Initialize the Network Service locally (Velocity connects directly)
+ NetworkServiceInterface networkService = new HttpNetworkService(
+ "https://oapi.mineroo.online",
+ "mbind",
+ bind_token);
+
+ BindRequest request = new BindRequest(logger, networkService);
try {
- // Status check before initial bind
- if (request.checkBindStatus()) {
+ // 2. Check Status
+ // We use .join() to block the thread until the Future completes.
+ if (request.checkBindStatus().join()) {
source.sendMessage(msg.get("command.bind.server.already-bound"));
return;
}
- boolean inital_bind = request.initalMotdVerifyRequest(bind_address, bind_port);
+ // 3. Initial Verification
+ boolean inital_bind = request.initialMotdVerifyRequest(bind_address, bind_port).join();
+
if (inital_bind) {
String motdToken = request.getMotdToken();
plugin.getBindListener().setVerificationToken(motdToken);
source.sendMessage(msg.get("command.bind.server.wait"));
- Thread.sleep(2 * 60 * 1000 + 5000);
+ // 4. Wait for external refresh (Blocking this worker thread is intended)
+ try {
+ Thread.sleep(2 * 60 * 1000 + 5000); // 2m 5s
+ } catch (InterruptedException e) {
+ logger.error("Bind thread interrupted", e);
+ return;
+ }
- // Final Bind
+ // 5. Final Bind & Retry Loop
boolean success = false;
int maxRetries = 3;
for (int i = 1; i <= maxRetries; i++) {
- if (request.finalizeServerBinding(bind_name, bind_description)) {
+ // Check final status synchronously using join()
+ if (request.finalizeServerBinding(bind_name, bind_description).join()) {
success = true;
break;
}
if (i < maxRetries) {
-
source.sendMessage(msg.get("command.bind.server.retry",
"current", String.valueOf(i),
"max", String.valueOf(maxRetries)));
- Thread.sleep(10000);
+
+ try {
+ Thread.sleep(10000); // Wait 10s before retry
+ } catch (InterruptedException e) {
+ break;
+ }
}
}
@@ -145,14 +169,16 @@ public class MainCommand implements SimpleCommand {
Component.text("Initial handshake failed. Check your token or network.", NamedTextColor.RED));
}
+ } catch (CompletionException e) {
+ // Unpack the wrapper exception from join() to see the real network error
+ logger.error("Mineroo Bind Network Error: " + e.getCause().getMessage());
} catch (Exception e) {
- logger.error("Mineroo Bind: " + e.toString());
+ logger.error("Mineroo Bind Error: " + e.toString());
} finally {
plugin.getBindListener().clearVerificationToken();
}
}).schedule();
-
}
public void sendHelp(CommandSource source) {
diff --git a/velocity/src/main/java/online/mineroo/velocity/commands/NetworkService.java b/velocity/src/main/java/online/mineroo/velocity/commands/NetworkService.java
new file mode 100644
index 0000000..e69de29
diff --git a/velocity/src/main/java/online/mineroo/velocity/utils/BindRequest.java b/velocity/src/main/java/online/mineroo/velocity/utils/BindRequest.java
deleted file mode 100644
index 7d699b8..0000000
--- a/velocity/src/main/java/online/mineroo/velocity/utils/BindRequest.java
+++ /dev/null
@@ -1,205 +0,0 @@
-package online.mineroo.velocity.utils;
-
-import java.io.IOException;
-import java.net.URI;
-import java.net.http.HttpClient;
-import java.net.http.HttpRequest;
-import java.net.http.HttpResponse;
-import java.nio.charset.StandardCharsets;
-import java.util.Base64;
-
-import org.jetbrains.annotations.Nullable;
-import org.slf4j.Logger;
-
-import com.google.gson.JsonObject;
-import com.google.gson.JsonParseException;
-import com.google.gson.JsonParser;
-
-/**
- * BindRequest handles server binding and verification with the Mineroo API.
- *
- * This class provides methods to:
- *
- * - Check if the server is bound using the bind token
- * - Initiate a MOTD verification request
- * - Finalize the server binding with additional server details
- *
- * It manages authentication, HTTP requests, and response parsing for the binding process.
- */
-public class BindRequest {
-
- /**
- * The base endpoint for Mineroo API requests.
- */
- private static final String endpoint = "https://oapi.mineroo.online";
-
- /**
- * Logger instance for logging errors and information.
- */
- private final Logger logger;
- /**
- * HTTP client used for sending requests to the Mineroo API.
- */
- private final HttpClient httpClient;
-
- /**
- * The bind token used for authenticating API requests.
- */
- private final String bindToken;
-
- /**
- * The MOTD token received from the API after verification.
- */
- private String motdToken = "";
- /**
- * The session token received from the API after verification.
- */
- private String sessionToken = "";
-
- /**
- * Constructs a BindRequest instance.
- *
- * @param logger Logger for logging errors and information
- * @param bindToken The bind token for API authentication
- */
- public BindRequest(Logger logger, String bindToken) {
-
- this.logger = logger;
- this.bindToken = bindToken;
-
- this.httpClient = HttpClient.newHttpClient();
- }
-
- /**
- * Checks the bind status from the oapi.mineroo.online API.
- *
- * @return true if status is 'bound', false otherwise
- */
- /**
- * Checks the bind status from the oapi.mineroo.online API.
- *
- * @return true if status is 'bound', false otherwise
- */
- public boolean checkBindStatus() {
- String uri = endpoint + "/server/server-bind-status";
- String basicAuth = "mbind:" + bindToken;
- String encodedAuth = Base64.getEncoder().encodeToString(basicAuth.getBytes(StandardCharsets.UTF_8));
- try {
- HttpRequest request = HttpRequest.newBuilder().uri(URI.create(uri))
- .header("Authorization", "Basic " + encodedAuth)
- .GET().build();
- HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
- if (response.statusCode() == 200) {
- JsonObject responseData = JsonParser.parseString(response.body()).getAsJsonObject();
- boolean bound = responseData.has("bound") && responseData.get("bound").getAsBoolean();
- if (bound) {
- return true;
- } else {
- logger.error("Mineroo Bind: Server not bound. 'bound' field is false.");
- }
- } else {
- logger.error("Mineroo Bind: Failed to check bind status. Response: " + response.body());
- }
- } catch (Exception e) {
- logger.error("Mineroo Bind: Exception while checking bind status", e);
- }
- return false;
- }
-
- /**
- * Initiates a MOTD verification request to the Mineroo API.
- *
- * @param hostname The server hostname
- * @param port The server port
- * @return true if both motd_token and session_token are received, false otherwise
- * @throws IOException, InterruptedException, JsonParseException on request or parsing failure
- */
- public boolean initalMotdVerifyRequest(String hostname, Integer port)
- throws IOException, InterruptedException, JsonParseException {
-
- // generate uri
- String uri = endpoint + "/server/motd-verify";
-
- // build jsonBody
- JsonObject json = new JsonObject();
- json.addProperty("server_address", hostname);
- json.addProperty("server_port", port);
-
- String basicAuth = "mbind:" + bindToken;
- String encodedAuth = Base64.getEncoder().encodeToString(basicAuth.getBytes(StandardCharsets.UTF_8));
-
- HttpRequest request = HttpRequest.newBuilder().uri(URI.create(uri))
- .header("Content-Type", "application/json")
- .header("Authorization", "Basic " + encodedAuth)
- .POST(HttpRequest.BodyPublishers.ofString(json.toString())).build();
-
- HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
-
- if (response.statusCode() == 200) {
- String body = response.body();
- JsonObject responseData = JsonParser.parseString(body).getAsJsonObject();
-
- if (responseData.has("motd_token")) {
- motdToken = responseData.get("motd_token").getAsString();
- } else {
- logger.error("Mineroo Bind: MOTD Token not received.");
- return false;
- }
-
- if (responseData.has("session_token")) {
- sessionToken = responseData.get("session_token").getAsString();
- } else {
- logger.error("Mineroo Bind: Session Token not received.");
- return false;
- }
-
- return true;
-
- } else {
- logger.error("Mineroo Bind: " + response.body().toString());
- }
-
- return false;
- }
-
- /**
- * Finalizes the server binding by sending server details to the Mineroo API.
- *
- * @param name The server name (nullable)
- * @param description The server description (nullable)
- * @return true if the binding is successful (HTTP 200), false otherwise
- * @throws IOException, InterruptedException, JsonParseException on request or parsing failure
- */
- public boolean finalizeServerBinding(@Nullable String name, @Nullable String description)
- throws IOException, InterruptedException, JsonParseException {
-
- String uri = endpoint + "/server/server-bind";
-
- JsonObject json = new JsonObject();
- json.addProperty("session_token", sessionToken);
- json.addProperty("name", name);
- json.addProperty("description", description);
-
- String basicAuth = "mbind:" + bindToken;
- String encodedAuth = Base64.getEncoder().encodeToString(basicAuth.getBytes(StandardCharsets.UTF_8));
-
- HttpRequest request = HttpRequest.newBuilder().uri(URI.create(uri))
- .header("Content-Type", "application/json")
- .header("Authorization", "Basic " + encodedAuth)
- .POST(HttpRequest.BodyPublishers.ofString(json.toString())).build();
-
- HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
-
- return response.statusCode() == 200;
-
- }
-
- /**
- * Gets the MOTD token received from the API.
- *
- * @return the motdToken string
- */
- public String getMotdToken() {
- return motdToken;
- }
-}