wow! avatars!!

This commit is contained in:
Lee 2024-04-06 05:45:50 +01:00
parent 50265b6228
commit 0819b228ba
10 changed files with 206 additions and 9 deletions

@ -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;
}
}

@ -5,6 +5,7 @@ import lombok.Getter;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import lombok.experimental.Helper; import lombok.experimental.Helper;
import lombok.extern.log4j.Log4j2; import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;

@ -2,13 +2,15 @@ package cc.fascinated.api.controller;
import cc.fascinated.player.PlayerManagerService; import cc.fascinated.player.PlayerManagerService;
import cc.fascinated.player.impl.Player; 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.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@RestController @RestController
@RequestMapping(value = "/", produces = MediaType.APPLICATION_JSON_VALUE) @RequestMapping(value = "/")
public class PlayerController { public class PlayerController {
private final PlayerManagerService playerManagerService; private final PlayerManagerService playerManagerService;
@ -18,7 +20,7 @@ public class PlayerController {
this.playerManagerService = playerManagerService; this.playerManagerService = playerManagerService;
} }
@GetMapping("/{id}") @ResponseBody @GetMapping(value = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE) @ResponseBody
public ResponseEntity<Player> getPlayer(@PathVariable String id) { public ResponseEntity<Player> getPlayer(@PathVariable String id) {
Player player = playerManagerService.getPlayer(id); Player player = playerManagerService.getPlayer(id);
if (player == null) { if (player == null) {
@ -26,4 +28,17 @@ public class PlayerController {
} }
return ResponseEntity.ok(player); return ResponseEntity.ok(player);
} }
@GetMapping(value = "/avatar/{id}")
public ResponseEntity<byte[]> 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());
}
} }

@ -3,6 +3,7 @@ package cc.fascinated.mojang;
import cc.fascinated.Main; import cc.fascinated.Main;
import cc.fascinated.mojang.types.MojangApiProfile; import cc.fascinated.mojang.types.MojangApiProfile;
import cc.fascinated.mojang.types.MojangSessionServerProfile; import cc.fascinated.mojang.types.MojangSessionServerProfile;
import com.google.gson.reflect.TypeToken;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@ -35,7 +36,7 @@ public class MojangAPIService {
.build(); .build();
HttpResponse<String> response = Main.getCLIENT().send(request, HttpResponse.BodyHandlers.ofString()); HttpResponse<String> response = Main.getCLIENT().send(request, HttpResponse.BodyHandlers.ofString());
return Main.getGSON().fromJson(response.body(), MojangSessionServerProfile.class); return Main.getGSON().fromJson(response.body(), new TypeToken<MojangSessionServerProfile>(){}.getType());
} }
/** /**
@ -52,6 +53,6 @@ public class MojangAPIService {
.build(); .build();
HttpResponse<String> response = Main.getCLIENT().send(request, HttpResponse.BodyHandlers.ofString()); HttpResponse<String> response = Main.getCLIENT().send(request, HttpResponse.BodyHandlers.ofString());
return Main.getGSON().fromJson(response.body(), MojangApiProfile.class); return Main.getGSON().fromJson(response.body(), new TypeToken<MojangApiProfile>(){}.getType());
} }
} }

@ -10,4 +10,13 @@ public class MojangApiProfile {
private String name; private String name;
public MojangApiProfile() {} public MojangApiProfile() {}
/**
* Check if the profile is valid.
*
* @return if the profile is valid
*/
public boolean isValid() {
return id != null && name != null;
}
} }

@ -5,17 +5,21 @@ import cc.fascinated.mojang.types.MojangApiProfile;
import cc.fascinated.mojang.types.MojangSessionServerProfile; import cc.fascinated.mojang.types.MojangSessionServerProfile;
import cc.fascinated.player.impl.Player; import cc.fascinated.player.impl.Player;
import cc.fascinated.util.UUIDUtils; import cc.fascinated.util.UUIDUtils;
import lombok.extern.log4j.Log4j2;
import net.jodah.expiringmap.ExpirationPolicy; import net.jodah.expiringmap.ExpirationPolicy;
import net.jodah.expiringmap.ExpiringMap; import net.jodah.expiringmap.ExpiringMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@Service @Service @Log4j2
public class PlayerManagerService { public class PlayerManagerService {
private static final Logger log = LoggerFactory.getLogger(PlayerManagerService.class);
/** /**
* The cache of players. * The cache of players.
*/ */
@ -59,12 +63,13 @@ public class PlayerManagerService {
MojangSessionServerProfile profile = uuid == null ? null : mojangAPIService.getSessionServerProfile(uuid.toString()); MojangSessionServerProfile profile = uuid == null ? null : mojangAPIService.getSessionServerProfile(uuid.toString());
if (profile == null) { if (profile == null) {
MojangApiProfile apiProfile = mojangAPIService.getApiProfile(id); MojangApiProfile apiProfile = mojangAPIService.getApiProfile(id);
if (apiProfile == null) { if (apiProfile == null || !apiProfile.isValid()) {
return null; return null;
} }
profile = mojangAPIService.getSessionServerProfile(apiProfile.getId().length() == 32 ? UUIDUtils.addUUIDDashes(apiProfile.getId()) : apiProfile.getId()); 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 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; return null;
} }
Player player = new Player(profile); Player player = new Player(profile);

@ -1,11 +1,13 @@
package cc.fascinated.player.impl; package cc.fascinated.player.impl;
import cc.fascinated.Consts;
import cc.fascinated.Main; import cc.fascinated.Main;
import cc.fascinated.mojang.types.MojangSessionServerProfile; import cc.fascinated.mojang.types.MojangSessionServerProfile;
import cc.fascinated.mojang.types.MojangSessionServerProfileProperties; import cc.fascinated.mojang.types.MojangSessionServerProfileProperties;
import cc.fascinated.util.UUIDUtils; import cc.fascinated.util.UUIDUtils;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import lombok.Getter; import lombok.Getter;
import org.springframework.beans.factory.annotation.Value;
import java.util.UUID; import java.util.UUID;
@ -22,6 +24,11 @@ public class Player {
*/ */
private final String name; private final String name;
/**
* The avatar URL of the player
*/
private final String avatarUrl;
/** /**
* The skin of the player * The skin of the player
* <p> * <p>
@ -41,6 +48,7 @@ public class Player {
public Player(MojangSessionServerProfile profile) { public Player(MojangSessionServerProfile 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.avatarUrl = Consts.getSITE_URL() + "/avatar/" + this.uuid;
MojangSessionServerProfileProperties textureProperty = profile.getTextureProperty(); MojangSessionServerProfileProperties textureProperty = profile.getTextureProperty();
if (textureProperty == null) { if (textureProperty == null) {

@ -1,9 +1,16 @@
package cc.fascinated.player.impl; package cc.fascinated.player.impl;
import lombok.AllArgsConstructor; import cc.fascinated.Main;
import lombok.Getter; 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 { public class Skin {
/** /**
@ -15,4 +22,40 @@ public class Skin {
* The model of the skin * The model of the skin
*/ */
private final SkinType model; 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();
}
} }

@ -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;
}
}
}

@ -2,6 +2,8 @@ server:
address: 0.0.0.0 address: 0.0.0.0
port: 7500 port: 7500
public-url: http://localhost:7500
mojang: mojang:
session-server: https://sessionserver.mojang.com session-server: https://sessionserver.mojang.com
api: https://api.mojang.com api: https://api.mojang.com