more cleanup

This commit is contained in:
Lee 2024-04-08 06:13:03 +01:00
parent 1f45d26f53
commit 4cdffd47fd
10 changed files with 186 additions and 183 deletions

@ -1,7 +1,6 @@
package cc.fascinated; package cc.fascinated;
import com.google.gson.Gson; import com.google.gson.Gson;
import lombok.Getter;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import lombok.extern.log4j.Log4j2; import lombok.extern.log4j.Log4j2;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
@ -16,11 +15,8 @@ import java.util.Objects;
@SpringBootApplication @Log4j2 @SpringBootApplication @Log4j2
public class Main { public class Main {
@Getter public static final Gson GSON = new Gson();
private static final Gson GSON = new Gson(); public static final HttpClient HTTP_CLIENT = HttpClient.newHttpClient();
@Getter
private static final HttpClient CLIENT = HttpClient.newHttpClient();
@SneakyThrows @SneakyThrows
public static void main(String[] args) { public static void main(String[] args) {

@ -3,8 +3,7 @@ package cc.fascinated.controller;
import cc.fascinated.service.PlayerService; import cc.fascinated.service.PlayerService;
import cc.fascinated.model.player.Player; import cc.fascinated.model.player.Player;
import cc.fascinated.model.player.Skin; import cc.fascinated.model.player.Skin;
import cc.fascinated.model.player.SkinPart; import cc.fascinated.util.PlayerUtils;
import lombok.NonNull;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.CacheControl; import org.springframework.http.CacheControl;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
@ -13,7 +12,6 @@ import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@RestController @RestController
@ -21,9 +19,6 @@ import java.util.concurrent.TimeUnit;
public class PlayerController { public class PlayerController {
private final CacheControl cacheControl = CacheControl.maxAge(1, TimeUnit.HOURS).cachePublic(); private final CacheControl cacheControl = CacheControl.maxAge(1, TimeUnit.HOURS).cachePublic();
@NonNull
private final SkinPart defaultHead = Objects.requireNonNull(Skin.getDefaultHead(), "Default head is null");
private final PlayerService playerManagerService; private final PlayerService playerManagerService;
@Autowired @Autowired
@ -48,22 +43,20 @@ public class PlayerController {
@PathVariable String id, @PathVariable String id,
@RequestParam(required = false, defaultValue = "250") int size) { @RequestParam(required = false, defaultValue = "250") int size) {
Player player = playerManagerService.getPlayer(id); Player player = playerManagerService.getPlayer(id);
byte[] headBytes = new byte[0]; byte[] partBytes = new byte[0];
if (player != null) { // The player exists if (player != null) { // The player exists
Skin skin = player.getSkin(); Skin skin = player.getSkin();
SkinPart skinPart = skin.getPart(part); Skin.Parts skinPart = Skin.Parts.fromName(part);
if (skinPart != null) { partBytes = PlayerUtils.getSkinPartBytes(skin, skinPart, size);
headBytes = skinPart.getPartData(size);
}
} }
if (headBytes == null) { // Fallback to the default head if (partBytes == null) { // Fallback to the default head
headBytes = defaultHead.getPartData(size); partBytes = PlayerUtils.getSkinPartBytes(Skin.DEFAULT_SKIN, Skin.Parts.HEAD, size);
} }
return ResponseEntity.ok() return ResponseEntity.ok()
.cacheControl(cacheControl) .cacheControl(cacheControl)
.contentType(MediaType.IMAGE_PNG) .contentType(MediaType.IMAGE_PNG)
.body(headBytes); .body(partBytes);
} }
} }

@ -1,5 +1,6 @@
package cc.fascinated.model.player; package cc.fascinated.model.player;
import com.google.gson.JsonObject;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Getter; import lombok.Getter;
@ -10,4 +11,17 @@ public class Cape {
* The URL of the cape * The URL of the cape
*/ */
private final String url; 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());
}
} }

@ -5,7 +5,6 @@ import cc.fascinated.util.Tuple;
import cc.fascinated.util.UUIDUtils; import cc.fascinated.util.UUIDUtils;
import lombok.Getter; import lombok.Getter;
import java.util.List;
import java.util.UUID; import java.util.UUID;
@Getter @Getter
@ -33,15 +32,9 @@ public class Player {
*/ */
private Cape cape; private Cape cape;
/**
* The raw properties of the player
*/
private final List<MojangProfile.ProfileProperty> rawProperties;
public Player(MojangProfile profile) { public Player(MojangProfile profile) {
this.uuid = UUID.fromString(UUIDUtils.addUUIDDashes(profile.getId())); this.uuid = UUID.fromString(UUIDUtils.addUuidDashes(profile.getId()));
this.name = profile.getName(); this.name = profile.getName();
this.rawProperties = profile.getProperties();
// Get the skin and cape // Get the skin and cape
Tuple<Skin, Cape> skinAndCape = profile.getSkinAndCape(); Tuple<Skin, Cape> skinAndCape = profile.getSkinAndCape();

@ -1,23 +1,22 @@
package cc.fascinated.model.player; package cc.fascinated.model.player;
import cc.fascinated.Main;
import cc.fascinated.config.Config; import cc.fascinated.config.Config;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.gson.JsonObject;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Getter; import lombok.Getter;
import lombok.SneakyThrows;
import lombok.extern.log4j.Log4j2; import lombok.extern.log4j.Log4j2;
import java.io.InputStream;
import java.net.URI;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
@Getter @Log4j2 @Getter @Log4j2
public class Skin { 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 of the skin * The URL of the skin
@ -27,78 +26,44 @@ public class Skin {
/** /**
* The model of the skin * The model of the skin
*/ */
private final SkinType model; private final Model model;
/**
* The bytes of the skin
*/
@JsonIgnore
private final byte[] skinBytes;
/**
* The skin parts for this skin
*/
@JsonIgnore
private final Map<SkinPartEnum, SkinPart> parts = new HashMap<>();
@JsonProperty("parts") @JsonProperty("parts")
private final Map<String, String> partUrls = new HashMap<>(); private final Map<String, String> partUrls = new HashMap<>();
public Skin(String playerUuid, String url, SkinType model) { public Skin(String url, Model model) {
this.url = url; this.url = url;
this.model = model; this.model = model;
this.skinBytes = this.getSkinData();
// The skin parts
this.parts.put(SkinPartEnum.HEAD, new SkinPart(this.skinBytes, SkinPartEnum.HEAD));
for (Map.Entry<SkinPartEnum, SkinPart> entry : this.parts.entrySet()) {
String partName = entry.getKey().name().toLowerCase();
this.partUrls.put(partName, Config.INSTANCE.getWebPublicUrl() + "/player/" + partName + "/" + playerUuid + "?size=250");
}
} }
/** /**
* Gets the default/fallback head. * Gets the skin from a {@link JsonObject}.
* *
* @return the default head * @param json the JSON object
* @return the skin
*/ */
public static SkinPart getDefaultHead() { public static Skin fromJson(JsonObject json) {
try (InputStream stream = Main.class.getClassLoader().getResourceAsStream("images/default_head.png")) { if (json == null) {
if (stream == null) {
return null;
}
byte[] bytes = stream.readAllBytes();
return new SkinPart(bytes, SkinPartEnum.HEAD);
} catch (Exception ex) {
log.warn("Failed to load default head", ex);
return 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);
} }
/** /**
* Gets the skin data from the URL. * Populates the part URLs for the skin.
* *
* @return the skin data * @param playerUuid the player's UUID
*/ */
@SneakyThrows @JsonIgnore public Skin populatePartUrls(String playerUuid) {
public byte[] getSkinData() { for (Parts part : Parts.values()) {
HttpRequest request = HttpRequest.newBuilder() String partName = part.name().toLowerCase();
.uri(new URI(this.url)) this.partUrls.put(partName, Config.INSTANCE.getWebPublicUrl() + "/player/" + partName + "/" + playerUuid + "?size=250");
.GET() }
.build(); return this;
return Main.getCLIENT().send(request, HttpResponse.BodyHandlers.ofByteArray()).body();
}
/**
* Gets a part from the skin.
*
* @param part the part name
* @return the part
*/
public SkinPart getPart(String part) {
return this.parts.get(SkinPartEnum.valueOf(part.toUpperCase()));
} }
/** /**
@ -106,7 +71,7 @@ public class Skin {
* information about the part. * information about the part.
*/ */
@Getter @AllArgsConstructor @Getter @AllArgsConstructor
public enum SkinPartEnum { public enum Parts {
HEAD(8, 8, 8, 8, 250); HEAD(8, 8, 8, 8, 250);
@ -124,13 +89,43 @@ public class Skin {
* The scale of the part. * The scale of the part.
*/ */
private final int defaultSize; private final int defaultSize;
/**
* Gets the skin part from its name.
*
* @param name the name of the part
* @return the skin part
*/
public static Parts fromName(String name) {
for (Parts part : values()) {
if (part.name().equalsIgnoreCase(name)) {
return part;
}
}
return null;
}
} }
/** /**
* The type of the skin. * The model of the skin.
*/ */
public enum SkinType { public enum Model {
DEFAULT, DEFAULT,
SLIM 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;
}
} }
} }

@ -1,69 +0,0 @@
package cc.fascinated.model.player;
import lombok.Getter;
import lombok.extern.log4j.Log4j2;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
@Getter @Log4j2
public class SkinPart {
/**
* The whole skin data.
*/
private final byte[] data;
/**
* The information about the part.
*/
private final Skin.SkinPartEnum skinPartEnum;
/**
* The part data from the skin.
*/
private byte[] partBytes;
public SkinPart(byte[] data, Skin.SkinPartEnum skinPartEnum) {
this.data = data;
this.skinPartEnum = skinPartEnum;
}
/**
* Gets the part data from the skin.
*
* @return the part data
*/
public byte[] getPartData(int size) {
if (size == -1) {
size = this.skinPartEnum.getDefaultSize();
}
try {
BufferedImage image = ImageIO.read(new ByteArrayInputStream(this.data));
if (image == null) {
return null;
}
// Get the part of the image (e.g. the head)
BufferedImage partImage = image.getSubimage(this.skinPartEnum.getX(), this.skinPartEnum.getY(), this.skinPartEnum.getWidth(), this.skinPartEnum.getHeight());
// Scale the image
BufferedImage scaledImage = new BufferedImage(size, size, partImage.getType());
Graphics2D graphics2D = scaledImage.createGraphics();
graphics2D.drawImage(partImage, 0, 0, size, size, null);
graphics2D.dispose();
partImage = scaledImage;
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ImageIO.write(partImage, "png", byteArrayOutputStream);
this.partBytes = byteArrayOutputStream.toByteArray();
return this.partBytes;
} catch (Exception ex) {
log.error("Failed to read image from skin data.", ex);
return null;
}
}
}

@ -49,7 +49,7 @@ public class PlayerService {
UUID uuid = null; UUID uuid = null;
if (id.length() == 32 || id.length() == 36) { // Check if the id is a UUID if (id.length() == 32 || id.length() == 36) { // Check if the id is a UUID
try { try {
uuid = UUID.fromString(id.length() == 32 ? UUIDUtils.addUUIDDashes(id) : id); uuid = UUID.fromString(id.length() == 32 ? UUIDUtils.addUuidDashes(id) : id);
} catch (Exception ignored) {} } catch (Exception ignored) {}
} else { // Check if the id is a name } else { // Check if the id is a name
uuid = playerNameToUUIDCache.get(id.toUpperCase()); uuid = playerNameToUUIDCache.get(id.toUpperCase());
@ -68,7 +68,7 @@ public class PlayerService {
} }
// Get the profile of the player using their UUID // Get the profile of the player using their UUID
profile = mojangAPIService.getProfile(apiProfile.getId().length() == 32 ? profile = mojangAPIService.getProfile(apiProfile.getId().length() == 32 ?
UUIDUtils.addUUIDDashes(apiProfile.getId()) : apiProfile.getId()); UUIDUtils.addUuidDashes(apiProfile.getId()) : apiProfile.getId());
} }
if (profile == null) { // The player cannot be found using their name or UUID if (profile == null) { // The player cannot be found using their name or UUID
log.info("Player with id {} could not be found", id); log.info("Player with id {} could not be found", id);

@ -4,12 +4,14 @@ import cc.fascinated.Main;
import cc.fascinated.model.player.Cape; import cc.fascinated.model.player.Cape;
import cc.fascinated.model.player.Skin; import cc.fascinated.model.player.Skin;
import cc.fascinated.util.Tuple; import cc.fascinated.util.Tuple;
import cc.fascinated.util.UUIDUtils;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Base64;
import java.util.List; import java.util.List;
@Getter @NoArgsConstructor @Getter @NoArgsConstructor
@ -36,36 +38,35 @@ public class MojangProfile {
* @return the skin and cape of the player * @return the skin and cape of the player
*/ */
public Tuple<Skin, Cape> getSkinAndCape() { public Tuple<Skin, Cape> getSkinAndCape() {
ProfileProperty textureProperty = getTextureProperty(); ProfileProperty textureProperty = getProfileProperty("textures");
if (textureProperty == null) { if (textureProperty == null) {
return null; return null;
} }
// Decode the texture property JsonObject json = Main.GSON.fromJson(textureProperty.getDecodedValue(), JsonObject.class); // Decode the texture property
String decoded = new String(java.util.Base64.getDecoder().decode(textureProperty.getValue())); JsonObject texturesJson = json.getAsJsonObject("textures"); // Parse the decoded JSON and get the textures object
// Parse the decoded JSON return new Tuple<>(Skin.fromJson(texturesJson.getAsJsonObject("SKIN")).populatePartUrls(this.getFormattedUuid()),
JsonObject json = Main.getGSON().fromJson(decoded, JsonObject.class); Cape.fromJson(texturesJson.getAsJsonObject("CAPE")));
JsonObject texturesJson = json.getAsJsonObject("textures");
JsonObject skinJson = texturesJson.getAsJsonObject("SKIN");
JsonObject capeJson = texturesJson.getAsJsonObject("CAPE");
JsonObject metadataJson = skinJson.get("metadata").getAsJsonObject();
Skin skin = new Skin(id, skinJson.get("url").getAsString(),
Skin.SkinType.valueOf(metadataJson.get("model").getAsString().toUpperCase()));
Cape cape = new Cape(capeJson.get("url").getAsString());
return new Tuple<>(skin, cape);
} }
/** /**
* Get the texture property of the player. * Gets the formatted UUID of the player.
* *
* @return the texture property * @return the formatted UUID
*/ */
public ProfileProperty getTextureProperty() { public String getFormattedUuid() {
return id.length() == 32 ? UUIDUtils.addUuidDashes(id) : id;
}
/**
* Get a profile property for the player
*
* @return the profile property
*/
public ProfileProperty getProfileProperty(String name) {
for (ProfileProperty property : properties) { for (ProfileProperty property : properties) {
if (property.getName().equals("textures")) { if (property.getName().equals(name)) {
return property; return property;
} }
} }
@ -89,6 +90,15 @@ public class MojangProfile {
*/ */
private String signature; 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. * Check if the property is signed.
* *

@ -0,0 +1,71 @@
package cc.fascinated.util;
import cc.fascinated.Main;
import cc.fascinated.model.player.Skin;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.SneakyThrows;
import lombok.experimental.UtilityClass;
import lombok.extern.log4j.Log4j2;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.net.URI;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
@UtilityClass @Log4j2
public class PlayerUtils {
/**
* Gets the skin data from the URL.
*
* @return the skin data
*/
@SneakyThrows
@JsonIgnore
public static byte[] getSkinData(String url) {
HttpRequest request = HttpRequest.newBuilder()
.uri(new URI(url))
.GET()
.build();
return Main.HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofByteArray()).body();
}
/**
* Gets the part data from the skin.
*
* @return the part data
*/
public static byte[] getSkinPartBytes(Skin skin, Skin.Parts part, int size) {
if (size == -1) {
size = part.getDefaultSize();
}
try {
BufferedImage image = ImageIO.read(new ByteArrayInputStream(PlayerUtils.getSkinData(skin.getUrl())));
if (image == null) {
return null;
}
// Get the part of the image (e.g. the head)
BufferedImage partImage = image.getSubimage(part.getX(), part.getY(), part.getWidth(), part.getHeight());
// Scale the image
BufferedImage scaledImage = new BufferedImage(size, size, partImage.getType());
Graphics2D graphics2D = scaledImage.createGraphics();
graphics2D.drawImage(partImage, 0, 0, size, size, null);
graphics2D.dispose();
partImage = scaledImage;
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ImageIO.write(partImage, "png", byteArrayOutputStream);
return byteArrayOutputStream.toByteArray();
} catch (Exception ex) {
log.error("Failed to get {} part bytes for {}", part.name(), skin.getUrl(), ex);
return null;
}
}
}

@ -11,7 +11,7 @@ public class UUIDUtils {
* @param idNoDashes the UUID without dashes * @param idNoDashes the UUID without dashes
* @return the UUID with dashes * @return the UUID with dashes
*/ */
public static String addUUIDDashes(String idNoDashes) { public static String addUuidDashes(String idNoDashes) {
StringBuilder idBuff = new StringBuilder(idNoDashes); StringBuilder idBuff = new StringBuilder(idNoDashes);
idBuff.insert(20, '-'); idBuff.insert(20, '-');
idBuff.insert(16, '-'); idBuff.insert(16, '-');