diff --git a/src/main/java/de/florianmichael/viafabricplus/definition/c0_30/classicube/ClassiCubeAccountHandler.java b/src/main/java/de/florianmichael/viafabricplus/definition/c0_30/classicube/ClassiCubeAccountHandler.java new file mode 100644 index 00000000..514dec1f --- /dev/null +++ b/src/main/java/de/florianmichael/viafabricplus/definition/c0_30/classicube/ClassiCubeAccountHandler.java @@ -0,0 +1,58 @@ +package de.florianmichael.viafabricplus.definition.c0_30.classicube; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import de.florianmichael.viafabricplus.definition.c0_30.classicube.auth.ClassiCubeAccount; +import de.florianmichael.viafabricplus.definition.c0_30.classicube.auth.process.ILoginProcessHandler; +import de.florianmichael.viafabricplus.util.FileSaver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.security.auth.login.LoginException; +import java.util.Map; + +public class ClassiCubeAccountHandler extends FileSaver implements ILoginProcessHandler { + public final static Logger LOGGER = LoggerFactory.getLogger("ViaFabricPlus/ClassiCube Account Handler"); + public static ClassiCubeAccountHandler INSTANCE; + private ClassiCubeAccount account; + + public ClassiCubeAccountHandler() { + super("classicube.account"); + } + + public static void create() { + ClassiCubeAccountHandler.INSTANCE = new ClassiCubeAccountHandler(); + ClassiCubeAccountHandler.INSTANCE.init(); + } + + @Override + public void write(JsonObject object) { + for (Map.Entry entry : account.toJson().entrySet()) { + object.add(entry.getKey(), entry.getValue()); + } + } + + @Override + public void read(JsonObject object) { + try { + account = ClassiCubeAccount.fromJson(object); + account.login(this); + } catch (LoginException e) { + LOGGER.error("Failed to log into ClassiCube account!", e); + } + } + + @Override + public void handleMfa(ClassiCubeAccount account) { + LOGGER.error("Failed to log into ClassiCube account due to MFA request."); + } + + @Override + public void handleSuccessfulLogin(ClassiCubeAccount account) { + LOGGER.info("Successfully logged into ClassiCube!"); + } + + public ClassiCubeAccount getAccountClone() { + return new ClassiCubeAccount(account.token, account.username, account.password); + } +} diff --git a/src/main/java/de/florianmichael/viafabricplus/definition/c0_30/classicube/auth/ClassiCubeAccount.java b/src/main/java/de/florianmichael/viafabricplus/definition/c0_30/classicube/auth/ClassiCubeAccount.java new file mode 100644 index 00000000..340bfc79 --- /dev/null +++ b/src/main/java/de/florianmichael/viafabricplus/definition/c0_30/classicube/auth/ClassiCubeAccount.java @@ -0,0 +1,75 @@ +package de.florianmichael.viafabricplus.definition.c0_30.classicube.auth; + +import com.google.gson.JsonObject; +import de.florianmichael.viafabricplus.definition.c0_30.classicube.auth.process.ILoginProcessHandler; +import de.florianmichael.viafabricplus.definition.c0_30.classicube.request.auth.ClassiCubeAuthenticationLoginRequest; +import de.florianmichael.viafabricplus.definition.c0_30.classicube.request.auth.ClassiCubeAuthenticationTokenRequest; +import de.florianmichael.viafabricplus.definition.c0_30.classicube.response.auth.ClassiCubeAuthenticationResponse; + +import javax.security.auth.login.LoginException; + +public class ClassiCubeAccount { + public String token; + public String username; + public String password; + + public ClassiCubeAccount(String username, String password) { + this.username = username; + this.password = password; + } + + public ClassiCubeAccount(String token, String username, String password) { + this(username, password); + this.token = token; + } + + public JsonObject toJson() { + final JsonObject object = new JsonObject(); + + object.addProperty("token", this.token); + object.addProperty("username", this.username); + object.addProperty("password", this.password); + + return object; + } + + public static ClassiCubeAccount fromJson(JsonObject jsonObject) { + final String token = jsonObject.getAsString(); + final String username = jsonObject.getAsString(); + final String password = jsonObject.getAsString(); + + return new ClassiCubeAccount(token, username, password); + } + + public void login(ILoginProcessHandler processHandler) throws LoginException { + final ClassiCubeAuthenticationTokenRequest initialTokenRequest = new ClassiCubeAuthenticationTokenRequest(); + final ClassiCubeAuthenticationResponse initialTokenResponse = initialTokenRequest.send() + .join(); + + // There should NEVER be any errors on the initial token response! + if (initialTokenResponse.shouldError()) { + final String errorDisplay = initialTokenResponse.getErrorDisplay(); + + throw new LoginException(errorDisplay); + } + + final ClassiCubeAuthenticationLoginRequest loginRequest = new ClassiCubeAuthenticationLoginRequest(initialTokenResponse, this.username, this.password); + final ClassiCubeAuthenticationResponse loginResponse = loginRequest.send() + .join(); + + if (loginResponse.shouldError()) { + final String errorDisplay = loginResponse.getErrorDisplay(); + + throw new LoginException(errorDisplay); + } + + this.token = loginResponse.token; + + if (initialTokenResponse.mfaRequired()) { + processHandler.handleMfa(this); + return; + } + + processHandler.handleSuccessfulLogin(this); + } +} diff --git a/src/main/java/de/florianmichael/viafabricplus/definition/c0_30/classicube/auth/ClassiCubeAuthenticationData.java b/src/main/java/de/florianmichael/viafabricplus/definition/c0_30/classicube/auth/ClassiCubeAuthenticationData.java new file mode 100644 index 00000000..3203b839 --- /dev/null +++ b/src/main/java/de/florianmichael/viafabricplus/definition/c0_30/classicube/auth/ClassiCubeAuthenticationData.java @@ -0,0 +1,46 @@ +package de.florianmichael.viafabricplus.definition.c0_30.classicube.auth; + +import javax.annotation.Nullable; + +/** + * Contains the authentication data that will be used in the URL parameters of the /api/login request. + * Taken from the official ClassiCube API documentation + */ +public class ClassiCubeAuthenticationData { + private final String username; + private final String password; + private final String previousToken; + @Nullable private final String loginCode; + + public ClassiCubeAuthenticationData(String username, String password, String previousToken) { + this(username, password, previousToken, null); + } + + public ClassiCubeAuthenticationData(String username, String password, String previousToken, @Nullable String loginCode) { + this.username = username; + this.password = password; + this.previousToken = previousToken; + this.loginCode = loginCode; + } + + public ClassiCubeAuthenticationData getWithLoginToken(final String loginCode) { + return new ClassiCubeAuthenticationData(this.username, this.password, this.previousToken, loginCode); + } + + public String getRequestBody() { + final StringBuilder builder = new StringBuilder("username=") + .append(username); + + builder.append("&password="); + builder.append(password); + builder.append("&token="); + builder.append(previousToken); + + if (loginCode != null) { + builder.append("&login_code="); + builder.append(loginCode); + } + + return builder.toString(); + } +} diff --git a/src/main/java/de/florianmichael/viafabricplus/definition/c0_30/classicube/auth/ClassiCubeError.java b/src/main/java/de/florianmichael/viafabricplus/definition/c0_30/classicube/auth/ClassiCubeError.java new file mode 100644 index 00000000..dd585f97 --- /dev/null +++ b/src/main/java/de/florianmichael/viafabricplus/definition/c0_30/classicube/auth/ClassiCubeError.java @@ -0,0 +1,37 @@ +package de.florianmichael.viafabricplus.definition.c0_30.classicube.auth; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; + +import java.lang.reflect.Type; +import java.util.Arrays; + +public enum ClassiCubeError implements JsonDeserializer { + TOKEN("Incorrect token. Is your ViaFabricPlus out of date?"), + USERNAME("Invalid username."), + PASSWORD("Invalid password."), + VERIFICATION("User hasn't verified their E-mail address yet.", false), + LOGIN_CODE("Multi-factor authentication requested. Please check your E-mail."); + + public final String description; + public final boolean fatal; + + ClassiCubeError(String description) { + this(description, true); + } + + ClassiCubeError(String description, boolean fatal) { + this.description = description; + this.fatal = fatal; + } + + @Override + public ClassiCubeError deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + return Arrays.stream(ClassiCubeError.values()) + .filter(e -> e.name().toLowerCase().equals(json.getAsString())) + .findFirst() + .orElse(null); + } +} diff --git a/src/main/java/de/florianmichael/viafabricplus/definition/c0_30/classicube/auth/process/ILoginProcessHandler.java b/src/main/java/de/florianmichael/viafabricplus/definition/c0_30/classicube/auth/process/ILoginProcessHandler.java new file mode 100644 index 00000000..18dcd5a2 --- /dev/null +++ b/src/main/java/de/florianmichael/viafabricplus/definition/c0_30/classicube/auth/process/ILoginProcessHandler.java @@ -0,0 +1,8 @@ +package de.florianmichael.viafabricplus.definition.c0_30.classicube.auth.process; + +import de.florianmichael.viafabricplus.definition.c0_30.classicube.auth.ClassiCubeAccount; + +public interface ILoginProcessHandler { + void handleMfa(final ClassiCubeAccount account); + void handleSuccessfulLogin(final ClassiCubeAccount account); +} diff --git a/src/main/java/de/florianmichael/viafabricplus/definition/c0_30/classicube/data/ClassiCubeServerInfo.java b/src/main/java/de/florianmichael/viafabricplus/definition/c0_30/classicube/data/ClassiCubeServerInfo.java new file mode 100644 index 00000000..0a5e4afd --- /dev/null +++ b/src/main/java/de/florianmichael/viafabricplus/definition/c0_30/classicube/data/ClassiCubeServerInfo.java @@ -0,0 +1,34 @@ +package de.florianmichael.viafabricplus.definition.c0_30.classicube.data; + +import com.google.gson.annotations.SerializedName; + +public class ClassiCubeServerInfo { + public final String hash; + public final int maxplayers; + public final String name; + public final int players; + public final String software; + public final long uptime; + @SerializedName("country_abbr") + public final String countryCode; + public final boolean web; + public final boolean featured; + public final String ip; + public final int port; + public final String mppass; + + public ClassiCubeServerInfo(String hash, int maxplayers, String name, int players, String software, long uptime, String countryCode, boolean web, boolean featured, String ip, int port, String mppass) { + this.hash = hash; + this.maxplayers = maxplayers; + this.name = name; + this.players = players; + this.software = software; + this.uptime = uptime; + this.countryCode = countryCode; + this.web = web; + this.featured = featured; + this.ip = ip; + this.port = port; + this.mppass = mppass; + } +} diff --git a/src/main/java/de/florianmichael/viafabricplus/definition/c0_30/classicube/request/ClassiCubeRequest.java b/src/main/java/de/florianmichael/viafabricplus/definition/c0_30/classicube/request/ClassiCubeRequest.java new file mode 100644 index 00000000..9b6c8eab --- /dev/null +++ b/src/main/java/de/florianmichael/viafabricplus/definition/c0_30/classicube/request/ClassiCubeRequest.java @@ -0,0 +1,26 @@ +package de.florianmichael.viafabricplus.definition.c0_30.classicube.request; + +import de.florianmichael.viafabricplus.definition.c0_30.classicube.ClassiCubeAccountHandler; +import de.florianmichael.viafabricplus.definition.c0_30.classicube.auth.ClassiCubeAccount; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; + +public abstract class ClassiCubeRequest { + + private final static URI CLASSICUBE_ROOT_URI = URI.create("https://www.classicube.net"); + protected final static URI AUTHENTICATION_URI = CLASSICUBE_ROOT_URI.resolve("/api/login/"); + protected final static URI SERVER_INFO_URI = CLASSICUBE_ROOT_URI.resolve("/api/server/"); + protected final static URI SERVER_LIST_INFO_URI = CLASSICUBE_ROOT_URI.resolve("/api/servers/"); + protected final static HttpClient HTTP_CLIENT = HttpClient.newHttpClient(); + + public HttpRequest buildWithTokenHeader(final HttpRequest.Builder builder) { + final ClassiCubeAccountHandler accountHandler = ClassiCubeAccountHandler.INSTANCE; + final ClassiCubeAccount account = accountHandler.getAccountClone(); + + builder.header("Cookie", "session=" + account.token); + + return builder.build(); + } +} diff --git a/src/main/java/de/florianmichael/viafabricplus/definition/c0_30/classicube/request/auth/ClassiCubeAuthenticationLoginRequest.java b/src/main/java/de/florianmichael/viafabricplus/definition/c0_30/classicube/request/auth/ClassiCubeAuthenticationLoginRequest.java new file mode 100644 index 00000000..28d84e3a --- /dev/null +++ b/src/main/java/de/florianmichael/viafabricplus/definition/c0_30/classicube/request/auth/ClassiCubeAuthenticationLoginRequest.java @@ -0,0 +1,39 @@ +package de.florianmichael.viafabricplus.definition.c0_30.classicube.request.auth; + +import de.florianmichael.viafabricplus.definition.c0_30.classicube.auth.ClassiCubeAuthenticationData; +import de.florianmichael.viafabricplus.definition.c0_30.classicube.request.ClassiCubeRequest; +import de.florianmichael.viafabricplus.definition.c0_30.classicube.response.auth.ClassiCubeAuthenticationResponse; + +import javax.annotation.Nullable; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.concurrent.CompletableFuture; + +public class ClassiCubeAuthenticationLoginRequest extends ClassiCubeAuthenticationRequest { + private final ClassiCubeAuthenticationData authenticationData; + + public ClassiCubeAuthenticationLoginRequest(ClassiCubeAuthenticationResponse previousResponse, String username, String password) { + this(previousResponse, username, password, null); + } + + public ClassiCubeAuthenticationLoginRequest(ClassiCubeAuthenticationResponse previousResponse, String username, String password, @Nullable String loginCode) { + this.authenticationData = new ClassiCubeAuthenticationData(username, password, previousResponse.token, loginCode); + } + + @Override + public CompletableFuture send() { + return CompletableFuture.supplyAsync(() -> { + final String requestBody = authenticationData.getRequestBody(); + final HttpRequest request = HttpRequest.newBuilder() + .POST(HttpRequest.BodyPublishers.ofString(requestBody)) + .uri(ClassiCubeRequest.AUTHENTICATION_URI) + .build(); + + final HttpResponse response = HTTP_CLIENT.sendAsync(request, HttpResponse.BodyHandlers.ofString()) + .join(); + + final String responseBody = response.body(); + return ClassiCubeAuthenticationResponse.fromJson(responseBody); + }); + } +} diff --git a/src/main/java/de/florianmichael/viafabricplus/definition/c0_30/classicube/request/auth/ClassiCubeAuthenticationRequest.java b/src/main/java/de/florianmichael/viafabricplus/definition/c0_30/classicube/request/auth/ClassiCubeAuthenticationRequest.java new file mode 100644 index 00000000..1816e746 --- /dev/null +++ b/src/main/java/de/florianmichael/viafabricplus/definition/c0_30/classicube/request/auth/ClassiCubeAuthenticationRequest.java @@ -0,0 +1,11 @@ +package de.florianmichael.viafabricplus.definition.c0_30.classicube.request.auth; + +import de.florianmichael.viafabricplus.definition.c0_30.classicube.request.ClassiCubeRequest; +import de.florianmichael.viafabricplus.definition.c0_30.classicube.response.auth.ClassiCubeAuthenticationResponse; + +import java.util.concurrent.CompletableFuture; + +public abstract class ClassiCubeAuthenticationRequest extends ClassiCubeRequest { + + public abstract CompletableFuture send(); +} diff --git a/src/main/java/de/florianmichael/viafabricplus/definition/c0_30/classicube/request/auth/ClassiCubeAuthenticationTokenRequest.java b/src/main/java/de/florianmichael/viafabricplus/definition/c0_30/classicube/request/auth/ClassiCubeAuthenticationTokenRequest.java new file mode 100644 index 00000000..5e109d10 --- /dev/null +++ b/src/main/java/de/florianmichael/viafabricplus/definition/c0_30/classicube/request/auth/ClassiCubeAuthenticationTokenRequest.java @@ -0,0 +1,29 @@ +package de.florianmichael.viafabricplus.definition.c0_30.classicube.request.auth; + +import de.florianmichael.viafabricplus.definition.c0_30.classicube.request.ClassiCubeRequest; +import de.florianmichael.viafabricplus.definition.c0_30.classicube.response.auth.ClassiCubeAuthenticationResponse; + +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.concurrent.CompletableFuture; + +public class ClassiCubeAuthenticationTokenRequest extends ClassiCubeAuthenticationRequest { + private static final HttpClient HTTP_CLIENT = HttpClient.newHttpClient(); + + @Override + public CompletableFuture send() { + return CompletableFuture.supplyAsync(() -> { + final HttpRequest request = HttpRequest.newBuilder() + .GET() + .uri(ClassiCubeRequest.AUTHENTICATION_URI) + .build(); + + final HttpResponse response = HTTP_CLIENT.sendAsync(request, HttpResponse.BodyHandlers.ofString()) + .join(); + + final String responseBody = response.body(); + return ClassiCubeAuthenticationResponse.fromJson(responseBody); + }); + } +} diff --git a/src/main/java/de/florianmichael/viafabricplus/definition/c0_30/classicube/request/server/ClassiCubeServerInfoRequest.java b/src/main/java/de/florianmichael/viafabricplus/definition/c0_30/classicube/request/server/ClassiCubeServerInfoRequest.java new file mode 100644 index 00000000..6e813838 --- /dev/null +++ b/src/main/java/de/florianmichael/viafabricplus/definition/c0_30/classicube/request/server/ClassiCubeServerInfoRequest.java @@ -0,0 +1,43 @@ +package de.florianmichael.viafabricplus.definition.c0_30.classicube.request.server; + +import de.florianmichael.viafabricplus.definition.c0_30.classicube.request.ClassiCubeRequest; +import de.florianmichael.viafabricplus.definition.c0_30.classicube.response.server.ClassiCubeServerInfoResponse; + +import java.net.URI; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.Set; +import java.util.concurrent.CompletableFuture; + +public class ClassiCubeServerInfoRequest extends ClassiCubeRequest { + private final Set serverHashes; + + public ClassiCubeServerInfoRequest(final String serverHash) { + this(Set.of(serverHash)); + } + + public ClassiCubeServerInfoRequest(final Set serverHashes) { + this.serverHashes = serverHashes; + } + + private URI generateUri() { + final String joined = String.join(",", serverHashes); + + return SERVER_INFO_URI.resolve(joined); + } + + public CompletableFuture send() { + return CompletableFuture.supplyAsync(() -> { + final URI uri = this.generateUri(); + final HttpRequest request = buildWithTokenHeader(HttpRequest.newBuilder() + .GET() + .uri(uri)); + final HttpResponse response = HTTP_CLIENT.sendAsync(request, HttpResponse.BodyHandlers.ofString()) + .join(); + + final String body = response.body(); + + return ClassiCubeServerInfoResponse.fromJson(body); + }); + } +} diff --git a/src/main/java/de/florianmichael/viafabricplus/definition/c0_30/classicube/request/server/ClassiCubeServerListRequest.java b/src/main/java/de/florianmichael/viafabricplus/definition/c0_30/classicube/request/server/ClassiCubeServerListRequest.java new file mode 100644 index 00000000..19a1d374 --- /dev/null +++ b/src/main/java/de/florianmichael/viafabricplus/definition/c0_30/classicube/request/server/ClassiCubeServerListRequest.java @@ -0,0 +1,24 @@ +package de.florianmichael.viafabricplus.definition.c0_30.classicube.request.server; + +import de.florianmichael.viafabricplus.definition.c0_30.classicube.request.ClassiCubeRequest; +import de.florianmichael.viafabricplus.definition.c0_30.classicube.response.server.ClassiCubeServerInfoResponse; + +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.concurrent.CompletableFuture; + +public class ClassiCubeServerListRequest extends ClassiCubeRequest { + public CompletableFuture send() { + return CompletableFuture.supplyAsync(() -> { + final HttpRequest request = buildWithTokenHeader(HttpRequest.newBuilder() + .GET() + .uri(SERVER_LIST_INFO_URI)); + final HttpResponse response = HTTP_CLIENT.sendAsync(request, HttpResponse.BodyHandlers.ofString()) + .join(); + + final String body = response.body(); + + return ClassiCubeServerInfoResponse.fromJson(body); + }); + } +} diff --git a/src/main/java/de/florianmichael/viafabricplus/definition/c0_30/classicube/response/ClassiCubeResponse.java b/src/main/java/de/florianmichael/viafabricplus/definition/c0_30/classicube/response/ClassiCubeResponse.java new file mode 100644 index 00000000..13270c12 --- /dev/null +++ b/src/main/java/de/florianmichael/viafabricplus/definition/c0_30/classicube/response/ClassiCubeResponse.java @@ -0,0 +1,10 @@ +package de.florianmichael.viafabricplus.definition.c0_30.classicube.response; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +public abstract class ClassiCubeResponse { + protected final static Gson GSON = new GsonBuilder() + .serializeNulls() + .create(); +} diff --git a/src/main/java/de/florianmichael/viafabricplus/definition/c0_30/classicube/response/auth/ClassiCubeAuthenticationResponse.java b/src/main/java/de/florianmichael/viafabricplus/definition/c0_30/classicube/response/auth/ClassiCubeAuthenticationResponse.java new file mode 100644 index 00000000..843bdae3 --- /dev/null +++ b/src/main/java/de/florianmichael/viafabricplus/definition/c0_30/classicube/response/auth/ClassiCubeAuthenticationResponse.java @@ -0,0 +1,57 @@ +package de.florianmichael.viafabricplus.definition.c0_30.classicube.response.auth; + +import de.florianmichael.viafabricplus.definition.c0_30.classicube.auth.ClassiCubeError; +import de.florianmichael.viafabricplus.definition.c0_30.classicube.response.ClassiCubeResponse; + +import javax.annotation.Nullable; +import java.util.Set; + +/** + * The class containing the response from the ClassiCube authentication service. + * Most fields, except for authenticated and errors, are null in the first request. + * As such, they are annotated as {@link Nullable}. + */ +public class ClassiCubeAuthenticationResponse extends ClassiCubeResponse { + @Nullable public final String token; + @Nullable public final String username; + public final boolean authenticated; + + public ClassiCubeAuthenticationResponse(@Nullable String token, @Nullable String username, boolean authenticated, Set errors) { + this.token = token; + this.username = username; + this.authenticated = authenticated; + this.errors = errors; + } + + public final Set errors; + + public boolean shouldError() { + return errors.size() > 0 && + errors.stream().anyMatch(e -> e.fatal); + } + + public String getErrorDisplay() { + final StringBuilder builder = new StringBuilder(); + + for (ClassiCubeError error : this.errors) { + builder.append(error.description) + .append("\n"); + } + + return builder.toString() + .trim(); + } + + public boolean mfaRequired() { + return this.errors.stream().anyMatch(e -> e == ClassiCubeError.LOGIN_CODE); + } + + public boolean isJustMfaError() { + return mfaRequired() && + this.errors.stream().anyMatch(e -> e != ClassiCubeError.LOGIN_CODE); + } + + public static ClassiCubeAuthenticationResponse fromJson(final String json) { + return GSON.fromJson(json, ClassiCubeAuthenticationResponse.class); + } +} diff --git a/src/main/java/de/florianmichael/viafabricplus/definition/c0_30/classicube/response/server/ClassiCubeServerInfoResponse.java b/src/main/java/de/florianmichael/viafabricplus/definition/c0_30/classicube/response/server/ClassiCubeServerInfoResponse.java new file mode 100644 index 00000000..f57d8dd0 --- /dev/null +++ b/src/main/java/de/florianmichael/viafabricplus/definition/c0_30/classicube/response/server/ClassiCubeServerInfoResponse.java @@ -0,0 +1,18 @@ +package de.florianmichael.viafabricplus.definition.c0_30.classicube.response.server; + +import de.florianmichael.viafabricplus.definition.c0_30.classicube.data.ClassiCubeServerInfo; +import de.florianmichael.viafabricplus.definition.c0_30.classicube.response.ClassiCubeResponse; + +import java.util.Set; + +public class ClassiCubeServerInfoResponse extends ClassiCubeResponse { + private final Set servers; + + public ClassiCubeServerInfoResponse(Set servers) { + this.servers = servers; + } + + public static ClassiCubeServerInfoResponse fromJson(final String json) { + return GSON.fromJson(json, ClassiCubeServerInfoResponse.class); + } +}