diff --git a/application.yml b/application.yml new file mode 100644 index 0000000..e52f767 --- /dev/null +++ b/application.yml @@ -0,0 +1,12 @@ +server: + address: 0.0.0.0 + port: 80 + error: + whitelabel: + enabled: false + +public-url: http://localhost:80 + +mojang: + session-server: https://sessionserver.mojang.com + api: https://api.mojang.com \ No newline at end of file diff --git a/src/main/java/cc/fascinated/model/ErrorResponse.java b/src/main/java/cc/fascinated/model/ErrorResponse.java new file mode 100644 index 0000000..3f83888 --- /dev/null +++ b/src/main/java/cc/fascinated/model/ErrorResponse.java @@ -0,0 +1,40 @@ +package cc.fascinated.model; + +import com.fasterxml.jackson.annotation.JsonFormat; +import io.micrometer.common.lang.NonNull; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; +import org.springframework.http.HttpStatus; + +import java.util.Date; + +@NoArgsConstructor +@Setter +@Getter +@ToString +public final class ErrorResponse { + /** + * The status code of this error. + */ + @NonNull + private HttpStatus status; + + /** + * The message of this error. + */ + @NonNull private String message; + + /** + * The timestamp this error occurred. + */ + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "dd-MM-yyyy hh:mm:ss") + private Date timestamp; + + public ErrorResponse(@NonNull HttpStatus status, @NonNull String message) { + this.status = status; + this.message = message; + timestamp = new Date(); + } +} \ No newline at end of file diff --git a/src/main/java/cc/fascinated/mojang/MojangAPIService.java b/src/main/java/cc/fascinated/mojang/MojangAPIService.java new file mode 100644 index 0000000..50da6b7 --- /dev/null +++ b/src/main/java/cc/fascinated/mojang/MojangAPIService.java @@ -0,0 +1,38 @@ +package cc.fascinated.mojang; + +import cc.fascinated.mojang.types.MojangApiProfile; +import cc.fascinated.mojang.types.MojangSessionServerProfile; +import cc.fascinated.util.WebRequest; +import lombok.extern.log4j.Log4j2; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +@Service @Log4j2 +public class MojangAPIService { + + @Value("${mojang.session-server}") + private String mojangSessionServerUrl; + + @Value("${mojang.api}") + private String mojangApiUrl; + + /** + * Gets the Session Server profile of the player with the given UUID. + * + * @param id the uuid or name of the player + * @return the profile + */ + public MojangSessionServerProfile getSessionServerProfile(String id) { + return WebRequest.get(mojangSessionServerUrl + "/session/minecraft/profile/" + id, MojangSessionServerProfile.class); + } + + /** + * Gets the Mojang API profile of the player with the given UUID. + * + * @param id the name of the player + * @return the profile + */ + public MojangApiProfile getApiProfile(String id) { + return WebRequest.get(mojangApiUrl + "/users/profiles/minecraft/" + id, MojangApiProfile.class); + } +} diff --git a/src/main/java/cc/fascinated/mojang/types/MojangApiProfile.java b/src/main/java/cc/fascinated/mojang/types/MojangApiProfile.java new file mode 100644 index 0000000..89656f0 --- /dev/null +++ b/src/main/java/cc/fascinated/mojang/types/MojangApiProfile.java @@ -0,0 +1,22 @@ +package cc.fascinated.mojang.types; + +import lombok.Getter; +import lombok.ToString; + +@Getter @ToString +public class MojangApiProfile { + + private String id; + 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/mojang/types/MojangSessionServerProfile.java b/src/main/java/cc/fascinated/mojang/types/MojangSessionServerProfile.java new file mode 100644 index 0000000..a153813 --- /dev/null +++ b/src/main/java/cc/fascinated/mojang/types/MojangSessionServerProfile.java @@ -0,0 +1,42 @@ +package cc.fascinated.mojang.types; + +import lombok.Getter; +import lombok.ToString; + +import java.util.ArrayList; +import java.util.List; + +@Getter @ToString +public class MojangSessionServerProfile { + + /** + * The UUID of the player. + */ + private String id; + + /** + * The name of the player. + */ + private String name; + + /** + * The properties for the player. + */ + private final List properties = new ArrayList<>(); + + public MojangSessionServerProfile() {} + + /** + * Get the texture property for the player. + * + * @return the texture property + */ + public MojangSessionServerProfileProperties getTextureProperty() { + for (MojangSessionServerProfileProperties property : properties) { + if (property.getName().equals("textures")) { + return property; + } + } + return null; + } +} diff --git a/src/main/java/cc/fascinated/mojang/types/MojangSessionServerProfileProperties.java b/src/main/java/cc/fascinated/mojang/types/MojangSessionServerProfileProperties.java new file mode 100644 index 0000000..6df0197 --- /dev/null +++ b/src/main/java/cc/fascinated/mojang/types/MojangSessionServerProfileProperties.java @@ -0,0 +1,12 @@ +package cc.fascinated.mojang.types; + +import lombok.Getter; +import lombok.ToString; + +@Getter @ToString +public class MojangSessionServerProfileProperties { + private String name; + private String value; + + public MojangSessionServerProfileProperties() {} +} diff --git a/src/main/java/cc/fascinated/player/PlayerService.java b/src/main/java/cc/fascinated/player/PlayerService.java new file mode 100644 index 0000000..7f3c312 --- /dev/null +++ b/src/main/java/cc/fascinated/player/PlayerService.java @@ -0,0 +1,79 @@ +package cc.fascinated.player; + +import cc.fascinated.mojang.MojangAPIService; +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.springframework.stereotype.Service; + +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +@Service @Log4j2 +public class PlayerService { + + /** + * The cache of players. + */ + private final Map players = ExpiringMap.builder() + .expiration(1, TimeUnit.HOURS) + .expirationPolicy(ExpirationPolicy.CREATED) + .build(); + + /** + * The cache of player names to UUIDs. + */ + private final Map playerNameToUUIDCache = ExpiringMap.builder() + .expiration(1, TimeUnit.DAYS) + .expirationPolicy(ExpirationPolicy.CREATED) + .build(); + + private final MojangAPIService mojangAPIService; + + public PlayerService(MojangAPIService mojangAPIService) { + this.mojangAPIService = mojangAPIService; + } + + /** + * Gets a player by their UUID. + * + * @param id the uuid or name of the player + * @return the player or null if the player does not exist + */ + public Player getPlayer(String id) { + UUID uuid = null; + if (id.length() == 32 || id.length() == 36) { + try { + uuid = UUID.fromString(id.length() == 32 ? UUIDUtils.addUUIDDashes(id) : id); + } catch (Exception ignored) {} + } else { + uuid = playerNameToUUIDCache.get(id.toUpperCase()); + } + + if (uuid != null && players.containsKey(uuid)) { + return players.get(uuid); + } + + MojangSessionServerProfile profile = uuid == null ? null : mojangAPIService.getSessionServerProfile(uuid.toString()); + if (profile == null) { + MojangApiProfile apiProfile = mojangAPIService.getApiProfile(id); + 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); + players.put(player.getUuid(), player); + playerNameToUUIDCache.put(player.getName().toUpperCase(), player.getUuid()); + return player; + } +} diff --git a/src/main/java/cc/fascinated/player/impl/Cape.java b/src/main/java/cc/fascinated/player/impl/Cape.java new file mode 100644 index 0000000..92eed7c --- /dev/null +++ b/src/main/java/cc/fascinated/player/impl/Cape.java @@ -0,0 +1,13 @@ +package cc.fascinated.player.impl; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter @AllArgsConstructor +public class Cape { + + /** + * The URL of the cape + */ + private final String url; +} diff --git a/src/main/java/cc/fascinated/player/impl/Player.java b/src/main/java/cc/fascinated/player/impl/Player.java new file mode 100644 index 0000000..153d5df --- /dev/null +++ b/src/main/java/cc/fascinated/player/impl/Player.java @@ -0,0 +1,65 @@ +package cc.fascinated.player.impl; + +import cc.fascinated.Main; +import cc.fascinated.config.Config; +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 java.util.UUID; + +@Getter +public class Player { + + /** + * The UUID of the player + */ + private final UUID uuid; + + /** + * The name of the player + */ + private final String name; + + /** + * The skin of the player + *

+ * This will be null if the player does not have a skin. + *

+ */ + private Skin skin; + + /** + * The cape of the player + *

+ * This will be null if the player does not have a cape. + *

+ */ + private Cape cape; + + public Player(MojangSessionServerProfile profile) { + this.uuid = UUID.fromString(UUIDUtils.addUUIDDashes(profile.getId())); + this.name = profile.getName(); + + MojangSessionServerProfileProperties textureProperty = profile.getTextureProperty(); + if (textureProperty == null) { + return; + } + + // Decode the texture property + String decoded = new String(java.util.Base64.getDecoder().decode(textureProperty.getValue())); + + // Parse the decoded JSON + JsonObject json = Main.getGSON().fromJson(decoded, JsonObject.class); + JsonObject texturesJson = json.getAsJsonObject("textures"); + JsonObject skinJson = texturesJson.getAsJsonObject("SKIN"); + JsonObject capeJson = texturesJson.getAsJsonObject("CAPE"); + JsonObject metadataJson = skinJson.get("metadata").getAsJsonObject(); + + this.skin = new Skin(this.uuid.toString(), skinJson.get("url").getAsString(), + Skin.SkinType.valueOf(metadataJson.get("model").getAsString().toUpperCase())); + this.cape = new Cape(capeJson.get("url").getAsString()); + } +} diff --git a/src/main/java/cc/fascinated/player/impl/Skin.java b/src/main/java/cc/fascinated/player/impl/Skin.java new file mode 100644 index 0000000..58eb331 --- /dev/null +++ b/src/main/java/cc/fascinated/player/impl/Skin.java @@ -0,0 +1,136 @@ +package cc.fascinated.player.impl; + +import cc.fascinated.Main; +import cc.fascinated.config.Config; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.SneakyThrows; +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.Map; + +@Getter @Log4j2 +public class Skin { + + /** + * The URL of the skin + */ + private final String url; + + /** + * The model of the skin + */ + private final SkinType model; + + /** + * The bytes of the skin + */ + @JsonIgnore + private final byte[] skinBytes; + + /** + * The skin parts for this skin + */ + @JsonIgnore + private final Map parts = new HashMap<>(); + + @JsonProperty("parts") + private final Map partUrls = new HashMap<>(); + + public Skin(String playerUuid, String url, SkinType model) { + this.url = url; + this.model = model; + this.skinBytes = this.getSkinData(); + + // The skin parts + this.parts.put(SkinPartEnum.HEAD, new SkinPart(this.skinBytes, SkinPartEnum.HEAD)); + + for (Map.Entry 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. + * + * @return the default head + */ + public static SkinPart getDefaultHead() { + try (InputStream stream = Main.class.getClassLoader().getResourceAsStream("images/default_head.png")) { + 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; + } + } + + /** + * 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(); + } + + /** + * 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())); + } + + /** + * The skin part enum that contains the + * information about the part. + */ + @Getter @AllArgsConstructor + public enum SkinPartEnum { + + HEAD(8, 8, 8, 8, 250); + + /** + * 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; + } + + /** + * The type of the skin. + */ + public enum SkinType { + DEFAULT, + SLIM + } +} 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..78e6a92 --- /dev/null +++ b/src/main/java/cc/fascinated/player/impl/SkinPart.java @@ -0,0 +1,69 @@ +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.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; + } + } +} diff --git a/target/classes/application.yml b/target/classes/application.yml new file mode 100644 index 0000000..e52f767 --- /dev/null +++ b/target/classes/application.yml @@ -0,0 +1,12 @@ +server: + address: 0.0.0.0 + port: 80 + error: + whitelabel: + enabled: false + +public-url: http://localhost:80 + +mojang: + session-server: https://sessionserver.mojang.com + api: https://api.mojang.com \ No newline at end of file diff --git a/target/classes/images/default_head.png b/target/classes/images/default_head.png new file mode 100644 index 0000000..32b3889 Binary files /dev/null and b/target/classes/images/default_head.png differ diff --git a/target/classes/public/favicon.ico b/target/classes/public/favicon.ico new file mode 100644 index 0000000..daa4531 Binary files /dev/null and b/target/classes/public/favicon.ico differ diff --git a/target/classes/templates/error.html b/target/classes/templates/error.html new file mode 100644 index 0000000..7263d08 --- /dev/null +++ b/target/classes/templates/error.html @@ -0,0 +1,23 @@ + + + + Minecraft Utilities + + + + + + + + + + + + +

Oh, no!

+

You have encountered an error.

+ + Error Gif + + \ No newline at end of file diff --git a/target/classes/templates/index.html b/target/classes/templates/index.html new file mode 100644 index 0000000..c338ca2 --- /dev/null +++ b/target/classes/templates/index.html @@ -0,0 +1,26 @@ + + + + Minecraft Utilities + + + + + + + + + + + + +

Hello!!!

+

Wrapper for the Minecraft APIs to make them easier to use.

+ +
+

Player Data: ???

+

Avatar Url: ???

+
+ + \ No newline at end of file diff --git a/target/maven-archiver/pom.properties b/target/maven-archiver/pom.properties new file mode 100644 index 0000000..af29734 --- /dev/null +++ b/target/maven-archiver/pom.properties @@ -0,0 +1,3 @@ +artifactId=Minecraft-Helper +groupId=cc.fascinated +version=1.0-SNAPSHOT diff --git a/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst new file mode 100644 index 0000000..9f1cbaf --- /dev/null +++ b/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst @@ -0,0 +1,16 @@ +cc\fascinated\player\impl\Skin.class +cc\fascinated\Main.class +cc\fascinated\player\impl\SkinPart.class +cc\fascinated\mojang\types\MojangSessionServerProfileProperties.class +cc\fascinated\mojang\MojangAPIService$1.class +cc\fascinated\mojang\types\MojangApiProfile.class +cc\fascinated\player\impl\Player.class +cc\fascinated\mojang\MojangAPIService$2.class +cc\fascinated\player\impl\SkinType.class +cc\fascinated\player\impl\Cape.class +cc\fascinated\player\PlayerManagerService.class +cc\fascinated\util\UUIDUtils.class +cc\fascinated\mojang\types\MojangSessionServerProfile.class +cc\fascinated\mojang\MojangAPIService.class +cc\fascinated\Consts.class +cc\fascinated\api\controller\PlayerController.class diff --git a/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst b/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst new file mode 100644 index 0000000..18ecb82 --- /dev/null +++ b/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst @@ -0,0 +1,14 @@ +C:\Users\Liam\Desktop\Projects\Minecraft-Helper\src\main\java\cc\fascinated\mojang\types\MojangSessionServerProfileProperties.java +C:\Users\Liam\Desktop\Projects\Minecraft-Helper\src\main\java\cc\fascinated\player\impl\Player.java +C:\Users\Liam\Desktop\Projects\Minecraft-Helper\src\main\java\cc\fascinated\player\impl\SkinType.java +C:\Users\Liam\Desktop\Projects\Minecraft-Helper\src\main\java\cc\fascinated\player\impl\Cape.java +C:\Users\Liam\Desktop\Projects\Minecraft-Helper\src\main\java\cc\fascinated\player\impl\Skin.java +C:\Users\Liam\Desktop\Projects\Minecraft-Helper\src\main\java\cc\fascinated\mojang\types\MojangSessionServerProfile.java +C:\Users\Liam\Desktop\Projects\Minecraft-Helper\src\main\java\cc\fascinated\Main.java +C:\Users\Liam\Desktop\Projects\Minecraft-Helper\src\main\java\cc\fascinated\player\PlayerManagerService.java +C:\Users\Liam\Desktop\Projects\Minecraft-Helper\src\main\java\cc\fascinated\mojang\types\MojangApiProfile.java +C:\Users\Liam\Desktop\Projects\Minecraft-Helper\src\main\java\cc\fascinated\util\UUIDUtils.java +C:\Users\Liam\Desktop\Projects\Minecraft-Helper\src\main\java\cc\fascinated\Consts.java +C:\Users\Liam\Desktop\Projects\Minecraft-Helper\src\main\java\cc\fascinated\player\impl\SkinPart.java +C:\Users\Liam\Desktop\Projects\Minecraft-Helper\src\main\java\cc\fascinated\mojang\MojangAPIService.java +C:\Users\Liam\Desktop\Projects\Minecraft-Helper\src\main\java\cc\fascinated\api\controller\PlayerController.java diff --git a/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/createdFiles.lst b/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/createdFiles.lst new file mode 100644 index 0000000..e69de29 diff --git a/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/inputFiles.lst b/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/inputFiles.lst new file mode 100644 index 0000000..e69de29