diff --git a/src/main/java/cc/fascinated/Consts.java b/src/main/java/cc/fascinated/Consts.java new file mode 100644 index 0000000..09c39f3 --- /dev/null +++ b/src/main/java/cc/fascinated/Consts.java @@ -0,0 +1,18 @@ +package cc.fascinated; + +import lombok.Getter; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; +import org.springframework.stereotype.Component; + +@Component +public class Consts { + + @Getter + private static String SITE_URL; + + @Value("${site-url}") + public void setSiteUrl(String name) { + SITE_URL = name; + } +} diff --git a/src/main/java/cc/fascinated/Main.java b/src/main/java/cc/fascinated/Main.java index 5116594..eeda6a9 100644 --- a/src/main/java/cc/fascinated/Main.java +++ b/src/main/java/cc/fascinated/Main.java @@ -5,6 +5,7 @@ import lombok.Getter; import lombok.SneakyThrows; import lombok.experimental.Helper; import lombok.extern.log4j.Log4j2; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; diff --git a/src/main/java/cc/fascinated/api/controller/PlayerController.java b/src/main/java/cc/fascinated/api/controller/PlayerController.java index f0900bc..001a63f 100644 --- a/src/main/java/cc/fascinated/api/controller/PlayerController.java +++ b/src/main/java/cc/fascinated/api/controller/PlayerController.java @@ -2,13 +2,15 @@ package cc.fascinated.api.controller; import cc.fascinated.player.PlayerManagerService; import cc.fascinated.player.impl.Player; +import cc.fascinated.player.impl.Skin; +import cc.fascinated.player.impl.SkinPart; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @RestController -@RequestMapping(value = "/", produces = MediaType.APPLICATION_JSON_VALUE) +@RequestMapping(value = "/") public class PlayerController { private final PlayerManagerService playerManagerService; @@ -18,7 +20,7 @@ public class PlayerController { this.playerManagerService = playerManagerService; } - @GetMapping("/{id}") @ResponseBody + @GetMapping(value = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE) @ResponseBody public ResponseEntity getPlayer(@PathVariable String id) { Player player = playerManagerService.getPlayer(id); if (player == null) { @@ -26,4 +28,17 @@ public class PlayerController { } return ResponseEntity.ok(player); } + + @GetMapping(value = "/avatar/{id}") + public ResponseEntity getPlayerHead(@PathVariable String id) { + Player player = playerManagerService.getPlayer(id); + if (player == null) { + return null; + } + Skin skin = player.getSkin(); + SkinPart head = skin.getHead(); + return ResponseEntity.ok() + .contentType(MediaType.IMAGE_PNG) + .body(head.getPartData()); + } } diff --git a/src/main/java/cc/fascinated/mojang/MojangAPIService.java b/src/main/java/cc/fascinated/mojang/MojangAPIService.java index e20cd02..6ec5243 100644 --- a/src/main/java/cc/fascinated/mojang/MojangAPIService.java +++ b/src/main/java/cc/fascinated/mojang/MojangAPIService.java @@ -3,6 +3,7 @@ package cc.fascinated.mojang; import cc.fascinated.Main; import cc.fascinated.mojang.types.MojangApiProfile; import cc.fascinated.mojang.types.MojangSessionServerProfile; +import com.google.gson.reflect.TypeToken; import lombok.SneakyThrows; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; @@ -35,7 +36,7 @@ public class MojangAPIService { .build(); HttpResponse response = Main.getCLIENT().send(request, HttpResponse.BodyHandlers.ofString()); - return Main.getGSON().fromJson(response.body(), MojangSessionServerProfile.class); + return Main.getGSON().fromJson(response.body(), new TypeToken(){}.getType()); } /** @@ -52,6 +53,6 @@ public class MojangAPIService { .build(); HttpResponse response = Main.getCLIENT().send(request, HttpResponse.BodyHandlers.ofString()); - return Main.getGSON().fromJson(response.body(), MojangApiProfile.class); + return Main.getGSON().fromJson(response.body(), new TypeToken(){}.getType()); } } diff --git a/src/main/java/cc/fascinated/mojang/types/MojangApiProfile.java b/src/main/java/cc/fascinated/mojang/types/MojangApiProfile.java index 7dceb79..89656f0 100644 --- a/src/main/java/cc/fascinated/mojang/types/MojangApiProfile.java +++ b/src/main/java/cc/fascinated/mojang/types/MojangApiProfile.java @@ -10,4 +10,13 @@ public class MojangApiProfile { private String name; public MojangApiProfile() {} + + /** + * Check if the profile is valid. + * + * @return if the profile is valid + */ + public boolean isValid() { + return id != null && name != null; + } } diff --git a/src/main/java/cc/fascinated/player/PlayerManagerService.java b/src/main/java/cc/fascinated/player/PlayerManagerService.java index b92eb0f..ce238da 100644 --- a/src/main/java/cc/fascinated/player/PlayerManagerService.java +++ b/src/main/java/cc/fascinated/player/PlayerManagerService.java @@ -5,17 +5,21 @@ import cc.fascinated.mojang.types.MojangApiProfile; import cc.fascinated.mojang.types.MojangSessionServerProfile; import cc.fascinated.player.impl.Player; import cc.fascinated.util.UUIDUtils; +import lombok.extern.log4j.Log4j2; import net.jodah.expiringmap.ExpirationPolicy; import net.jodah.expiringmap.ExpiringMap; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import java.util.Map; import java.util.UUID; import java.util.concurrent.TimeUnit; -@Service +@Service @Log4j2 public class PlayerManagerService { + private static final Logger log = LoggerFactory.getLogger(PlayerManagerService.class); /** * The cache of players. */ @@ -59,12 +63,13 @@ public class PlayerManagerService { MojangSessionServerProfile profile = uuid == null ? null : mojangAPIService.getSessionServerProfile(uuid.toString()); if (profile == null) { MojangApiProfile apiProfile = mojangAPIService.getApiProfile(id); - if (apiProfile == null) { + if (apiProfile == null || !apiProfile.isValid()) { return null; } profile = mojangAPIService.getSessionServerProfile(apiProfile.getId().length() == 32 ? UUIDUtils.addUUIDDashes(apiProfile.getId()) : apiProfile.getId()); } if (profile == null) { // The player cannot be found using their name or UUID + log.info("Player with id {} could not be found", id); return null; } Player player = new Player(profile); diff --git a/src/main/java/cc/fascinated/player/impl/Player.java b/src/main/java/cc/fascinated/player/impl/Player.java index 3dd924c..3ebd7f0 100644 --- a/src/main/java/cc/fascinated/player/impl/Player.java +++ b/src/main/java/cc/fascinated/player/impl/Player.java @@ -1,11 +1,13 @@ package cc.fascinated.player.impl; +import cc.fascinated.Consts; import cc.fascinated.Main; import cc.fascinated.mojang.types.MojangSessionServerProfile; import cc.fascinated.mojang.types.MojangSessionServerProfileProperties; import cc.fascinated.util.UUIDUtils; import com.google.gson.JsonObject; import lombok.Getter; +import org.springframework.beans.factory.annotation.Value; import java.util.UUID; @@ -22,6 +24,11 @@ public class Player { */ private final String name; + /** + * The avatar URL of the player + */ + private final String avatarUrl; + /** * The skin of the player *

@@ -41,6 +48,7 @@ public class Player { public Player(MojangSessionServerProfile profile) { this.uuid = UUID.fromString(UUIDUtils.addUUIDDashes(profile.getId())); this.name = profile.getName(); + this.avatarUrl = Consts.getSITE_URL() + "/avatar/" + this.uuid; MojangSessionServerProfileProperties textureProperty = profile.getTextureProperty(); if (textureProperty == null) { diff --git a/src/main/java/cc/fascinated/player/impl/Skin.java b/src/main/java/cc/fascinated/player/impl/Skin.java index c049a3b..34d1cea 100644 --- a/src/main/java/cc/fascinated/player/impl/Skin.java +++ b/src/main/java/cc/fascinated/player/impl/Skin.java @@ -1,9 +1,16 @@ package cc.fascinated.player.impl; -import lombok.AllArgsConstructor; -import lombok.Getter; +import cc.fascinated.Main; +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.*; +import org.springframework.stereotype.Service; +import org.springframework.web.bind.annotation.RequestParam; -@Getter @AllArgsConstructor +import java.net.URI; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; + +@Getter public class Skin { /** @@ -15,4 +22,40 @@ public class Skin { * The model of the skin */ private final SkinType model; + + /** + * The bytes of the skin + */ + @JsonIgnore + private final byte[] skinBytes; + + /** + * The head of the skin + */ + @JsonIgnore + private final SkinPart head; + + public Skin(String url, SkinType model) { + this.url = url; + this.model = model; + this.skinBytes = this.getSkinData(); + + // The skin parts + this.head = new SkinPart(this.skinBytes, 8, 8, 8, 8, 20); + } + + /** + * Gets the skin data from the URL. + * + * @return the skin data + */ + @SneakyThrows @JsonIgnore + public byte[] getSkinData() { + HttpRequest request = HttpRequest.newBuilder() + .uri(new URI(this.url)) + .GET() + .build(); + + return Main.getCLIENT().send(request, HttpResponse.BodyHandlers.ofByteArray()).body(); + } } diff --git a/src/main/java/cc/fascinated/player/impl/SkinPart.java b/src/main/java/cc/fascinated/player/impl/SkinPart.java new file mode 100644 index 0000000..36d5064 --- /dev/null +++ b/src/main/java/cc/fascinated/player/impl/SkinPart.java @@ -0,0 +1,95 @@ +package cc.fascinated.player.impl; + +import lombok.Getter; +import lombok.extern.log4j.Log4j2; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferByte; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; + +@Getter @Log4j2 +public class SkinPart { + + /** + * The whole skin data. + */ + private final byte[] data; + + /** + * The X coordinate of the part. + */ + private final int x; + + /** + * The Y coordinate of the part. + */ + private final int y; + + /** + * The width of the part. + */ + private final int width; + + /** + * The height of the part. + */ + private final int height; + + /** + * The scale of the part output. + */ + private final int scale; + + /** + * The part data from the skin. + */ + private byte[] partBytes; + + public SkinPart(byte[] data, int x, int y, int width, int height, int scale) { + this.data = data; + this.x = x; + this.y = y; + this.width = width; + this.height = height; + this.scale = scale; + } + + /** + * Gets the part data from the skin. + * + * @return the part data + */ + public byte[] getPartData() { + if (this.partBytes != null) { + return this.partBytes; + } + + try { + BufferedImage image = ImageIO.read(new ByteArrayInputStream(this.data)); + if (image == null) { + return null; + } + BufferedImage partImage = image.getSubimage(this.x, this.y, this.width, this.height); + + // Scale the image + int width = partImage.getWidth() * this.scale; + int height = partImage.getHeight() * this.scale; + BufferedImage scaledImage = new BufferedImage(width, height, partImage.getType()); + Graphics2D graphics2D = scaledImage.createGraphics(); + graphics2D.drawImage(partImage, 0, 0, width, height, 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; + } + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index e60941d..ffe25c8 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -2,6 +2,8 @@ server: address: 0.0.0.0 port: 7500 +public-url: http://localhost:7500 + mojang: session-server: https://sessionserver.mojang.com api: https://api.mojang.com \ No newline at end of file