add server pinger
Some checks failed
ci / deploy (push) Failing after 1m3s

This commit is contained in:
Lee
2024-04-10 07:43:38 +01:00
parent 25c69e11e1
commit fcb8ef0357
40 changed files with 1751 additions and 0 deletions

View File

@ -0,0 +1,31 @@
package cc.fascinated.model.cache;
import cc.fascinated.model.server.MinecraftServer;
import lombok.*;
import org.springframework.data.annotation.Id;
import org.springframework.data.redis.core.RedisHash;
import java.io.Serializable;
/**
* @author Braydon
*/
@AllArgsConstructor @Setter @Getter @ToString
@RedisHash(value = "server", timeToLive = 60L) // 1 minute (in seconds)
public final class CachedMinecraftServer implements Serializable {
/**
* The id of this cached server.
*/
@Id @NonNull private transient final String id;
/**
* The cached server.
*/
@NonNull private final MinecraftServer value;
/**
* The unix timestamp of when this
* server was cached, -1 if not cached.
*/
private long cached;
}

View File

@ -0,0 +1,34 @@
package cc.fascinated.model.cache;
import cc.fascinated.model.mojang.MojangProfile;
import cc.fascinated.model.player.Cape;
import cc.fascinated.model.player.Player;
import cc.fascinated.model.player.Skin;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.data.redis.core.RedisHash;
import java.io.Serializable;
import java.util.UUID;
/**
* A cacheable {@link Player}.
*
* @author Braydon
*/
@Setter @Getter
@ToString(callSuper = true)
@RedisHash(value = "player", timeToLive = 60L * 60L) // 1 hour (in seconds)
public final class CachedPlayer extends Player implements Serializable {
/**
* The unix timestamp of when this
* player was cached, -1 if not cached.
*/
private long cached;
public CachedPlayer(UUID uuid, String username, Skin skin, Cape cape, long cached) {
super(uuid, username, skin, cape);
this.cached = cached;
}
}

View File

@ -0,0 +1,27 @@
package cc.fascinated.model.cache;
import lombok.*;
import org.springframework.data.annotation.Id;
import org.springframework.data.redis.core.RedisHash;
import java.util.UUID;
/**
* @author Braydon
*/
@AllArgsConstructor
@Getter
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
@ToString
@RedisHash(value = "playerName", timeToLive = 60L * 60L) // 1 hour (in seconds)
public final class CachedPlayerName {
/**
* The username of the player.
*/
@Id @NonNull private String username;
/**
* The unique id of the player.
*/
@NonNull private UUID uniqueId;
}

View File

@ -0,0 +1,13 @@
package cc.fascinated.model.mojang;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.ToString;
/**
* @author Braydon
*/
@AllArgsConstructor @Getter @ToString
public final class JavaServerStatusToken {
private final String description;
}

View File

@ -0,0 +1,111 @@
package cc.fascinated.model.mojang;
import cc.fascinated.Main;
import cc.fascinated.common.Tuple;
import cc.fascinated.common.UUIDUtils;
import cc.fascinated.model.player.Cape;
import cc.fascinated.model.player.Skin;
import com.google.gson.JsonObject;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
@Getter @NoArgsConstructor
public class MojangProfile {
/**
* The UUID of the player.
*/
private String id;
/**
* The name of the player.
*/
private String name;
/**
* The properties of the player.
*/
private final List<ProfileProperty> properties = new ArrayList<>();
/**
* Get the skin and cape of the player.
*
* @return the skin and cape of the player
*/
public Tuple<Skin, Cape> getSkinAndCape() {
ProfileProperty textureProperty = getProfileProperty("textures");
if (textureProperty == null) {
return null;
}
JsonObject json = Main.GSON.fromJson(textureProperty.getDecodedValue(), JsonObject.class); // Decode the texture property
JsonObject texturesJson = json.getAsJsonObject("textures"); // Parse the decoded JSON and get the textures object
return new Tuple<>(Skin.fromJson(texturesJson.getAsJsonObject("SKIN")).populatePartUrls(this.getFormattedUuid()),
Cape.fromJson(texturesJson.getAsJsonObject("CAPE")));
}
/**
* Gets the formatted UUID of the player.
*
* @return the formatted UUID
*/
public String getFormattedUuid() {
return id.length() == 32 ? UUIDUtils.addDashes(id).toString() : id;
}
/**
* Get a profile property for the player
*
* @return the profile property
*/
public ProfileProperty getProfileProperty(String name) {
for (ProfileProperty property : properties) {
if (property.getName().equals(name)) {
return property;
}
}
return null;
}
@Getter @AllArgsConstructor
public static class ProfileProperty {
/**
* The name of the property.
*/
private String name;
/**
* The base64 value of the property.
*/
private String value;
/**
* The signature of the property.
*/
private String signature;
/**
* Decodes the value for this property.
*
* @return the decoded value
*/
public String getDecodedValue() {
return new String(Base64.getDecoder().decode(this.value));
}
/**
* Check if the property is signed.
*
* @return true if the property is signed, false otherwise
*/
public boolean isSigned() {
return signature != null;
}
}
}

View File

@ -0,0 +1,27 @@
package cc.fascinated.model.mojang;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Getter @NoArgsConstructor
public class MojangUsernameToUuid {
/**
* The UUID of the player.
*/
private String id;
/**
* The name of the player.
*/
private String name;
/**
* Check if the profile is valid.
*
* @return if the profile is valid
*/
public boolean isValid() {
return id != null && name != null;
}
}

View File

@ -0,0 +1,27 @@
package cc.fascinated.model.player;
import com.google.gson.JsonObject;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter @AllArgsConstructor
public class Cape {
/**
* The URL of the cape
*/
private final String url;
/**
* Gets the cape from a {@link JsonObject}.
*
* @param json the JSON object
* @return the cape
*/
public static Cape fromJson(JsonObject json) {
if (json == null) {
return null;
}
return new Cape(json.get("url").getAsString());
}
}

View File

@ -0,0 +1,49 @@
package cc.fascinated.model.player;
import cc.fascinated.common.Tuple;
import cc.fascinated.common.UUIDUtils;
import cc.fascinated.model.mojang.MojangProfile;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.data.annotation.Id;
import java.util.UUID;
@Getter @AllArgsConstructor
public class Player {
/**
* The UUID of the player
*/
@Id private final UUID uuid;
/**
* The username of the player
*/
private final String username;
/**
* The skin of the player, null if the
* player does not have a skin
*/
private Skin skin;
/**
* The cape of the player, null if the
* player does not have a cape
*/
private Cape cape;
public Player(MojangProfile profile) {
this.uuid = UUIDUtils.addDashes(profile.getId());
this.username = profile.getName();
// Get the skin and cape
Tuple<Skin, Cape> skinAndCape = profile.getSkinAndCape();
if (skinAndCape != null) {
this.skin = skinAndCape.getLeft();
this.cape = skinAndCape.getRight();
}
}
}

View File

@ -0,0 +1,158 @@
package cc.fascinated.model.player;
import cc.fascinated.common.PlayerUtils;
import cc.fascinated.config.Config;
import cc.fascinated.exception.impl.BadRequestException;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.gson.JsonObject;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.extern.log4j.Log4j2;
import java.awt.image.BufferedImage;
import java.util.HashMap;
import java.util.Map;
@AllArgsConstructor @NoArgsConstructor
@Getter @Log4j2
public class Skin {
/**
* The default skin, usually used when the skin is not found.
*/
public static final Skin DEFAULT_SKIN = new Skin("http://textures.minecraft.net/texture/60a5bd016b3c9a1b9272e4929e30827a67be4ebb219017adbbc4a4d22ebd5b1",
Model.DEFAULT);
/**
* The URL for the skin
*/
private String url;
/**
* The model for the skin
*/
private Model model;
/**
* The skin image for the skin
*/
@JsonIgnore
private byte[] skinImage;
/**
* The part URLs of the skin
*/
@JsonProperty("parts")
private Map<String, String> partUrls = new HashMap<>();
public Skin(String url, Model model) {
this.url = url;
this.model = model;
this.skinImage = PlayerUtils.getSkinImage(url);
}
/**
* Gets the skin from a {@link JsonObject}.
*
* @param json the JSON object
* @return the skin
*/
public static Skin fromJson(JsonObject json) {
if (json == null) {
return null;
}
String url = json.get("url").getAsString();
JsonObject metadata = json.getAsJsonObject("metadata");
Model model = Model.fromName(metadata == null ? "slim" : // Fall back to slim if the model is not found
metadata.get("model").getAsString());
return new Skin(url, model);
}
/**
* Populates the part URLs for the skin.
*
* @param playerUuid the player's UUID
*/
public Skin populatePartUrls(String playerUuid) {
for (Parts part : Parts.values()) {
String partName = part.name().toLowerCase();
this.partUrls.put(partName, Config.INSTANCE.getWebPublicUrl() + "/player/" + partName + "/" + playerUuid + "?size=" + part.getDefaultSize());
}
return this;
}
/**
* The skin part enum that contains the
* information about the part.
*/
@Getter @AllArgsConstructor
public enum Parts {
HEAD(8, 8, 8, 8, 256);
/**
* The x and y position of the part.
*/
private final int x, y;
/**
* The width and height of the part.
*/
private final int width, height;
/**
* The scale of the part.
*/
private final int defaultSize;
/**
* Gets the name of the part.
*
* @return the name of the part
*/
public String getName() {
return this.name().toLowerCase();
}
/**
* Gets the skin part from its name.
*
* @param name the name of the part
* @return the skin part
* @throws BadRequestException if the part is not found
*/
public static Parts fromName(String name) throws BadRequestException {
for (Parts part : values()) {
if (part.name().equalsIgnoreCase(name)) {
return part;
}
}
throw new BadRequestException("Invalid part name: " + name);
}
}
/**
* The model of the skin.
*/
public enum Model {
DEFAULT,
SLIM;
/**
* Gets the model from its name.
*
* @param name the name of the model
* @return the model
*/
public static Model fromName(String name) {
for (Model model : values()) {
if (model.name().equalsIgnoreCase(name)) {
return model;
}
}
return null;
}
}
}

View File

@ -0,0 +1,40 @@
package cc.fascinated.model.response;
import io.micrometer.common.lang.NonNull;
import lombok.Getter;
import lombok.ToString;
import org.springframework.http.HttpStatus;
import java.util.Date;
@Getter
@ToString
public class ErrorResponse {
/**
* The status code of this error.
*/
@NonNull
private final HttpStatus status;
/**
* The HTTP code of this error.
*/
private final int code;
/**
* The message of this error.
*/
@NonNull private final String message;
/**
* The timestamp this error occurred.
*/
@NonNull private final Date timestamp;
public ErrorResponse(@NonNull HttpStatus status, @NonNull String message) {
this.status = status;
code = status.value();
this.message = message;
timestamp = new Date();
}
}

View File

@ -0,0 +1,10 @@
package cc.fascinated.model.server;
/**
* @author Braydon
*/
public final class JavaMinecraftServer extends MinecraftServer {
public JavaMinecraftServer(String hostname, String ip, int port, String motd) {
super(hostname, ip, port, motd);
}
}

View File

@ -0,0 +1,42 @@
package cc.fascinated.model.server;
import cc.fascinated.service.pinger.MinecraftServerPinger;
import cc.fascinated.service.pinger.impl.JavaMinecraftServerPinger;
import io.micrometer.common.lang.NonNull;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.ToString;
/**
* @author Braydon
*/
@AllArgsConstructor @Getter @ToString
public class MinecraftServer {
private final String hostname;
private final String ip;
private final int port;
private final String motd;
/**
* A platform a Minecraft
* server can operate on.
*/
@AllArgsConstructor @Getter
public enum Platform {
/**
* The Java edition of Minecraft.
*/
JAVA(new JavaMinecraftServerPinger(), 25565);
/**
* The server pinger for this platform.
*/
@NonNull
private final MinecraftServerPinger<?> pinger;
/**
* The default server port for this platform.
*/
private final int defaultPort;
}
}