commit f68d941dc86d6a9200fbd6bf2c5a2aa28e5c5d48 Author: Liam Date: Sat Apr 6 04:10:15 2024 +0100 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..97845ff --- /dev/null +++ b/.gitignore @@ -0,0 +1,143 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store### Intellij template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Java template +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* +replay_pid* + +### Minecraft Helper ### +application.yml diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..aa00ffa --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..010b430 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,14 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/uiDesigner.xml b/.idea/uiDesigner.xml new file mode 100644 index 0000000..2b63946 --- /dev/null +++ b/.idea/uiDesigner.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..45acea1 --- /dev/null +++ b/pom.xml @@ -0,0 +1,67 @@ + + + 4.0.0 + + cc.fascinated + Minecraft-Helper + 1.0-SNAPSHOT + + + 17 + 17 + UTF-8 + + + + org.springframework.boot + spring-boot-starter-parent + 3.2.4 + + + + + + org.projectlombok + lombok + 1.18.32 + provided + + + + org.apache.logging.log4j + log4j-api + 2.20.0 + compile + + + org.apache.logging.log4j + log4j-core + 2.20.0 + compile + + + org.yaml + snakeyaml + 2.2 + compile + + + com.google.code.gson + gson + 2.10.1 + compile + + + org.springframework.boot + spring-boot-starter-web + + + net.jodah + expiringmap + 0.5.11 + + + + \ No newline at end of file diff --git a/src/main/java/cc/fascinated/Main.java b/src/main/java/cc/fascinated/Main.java new file mode 100644 index 0000000..47d2997 --- /dev/null +++ b/src/main/java/cc/fascinated/Main.java @@ -0,0 +1,35 @@ +package cc.fascinated; + +import com.google.gson.Gson; +import lombok.Getter; +import lombok.SneakyThrows; +import lombok.extern.log4j.Log4j2; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +import java.io.File; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.util.Objects; + +@SpringBootApplication @Log4j2 +public class Main { + + @Getter + private static final Gson GSON = new Gson(); + + @SneakyThrows + public static void main(String[] args) { + File config = new File("application.yml"); + if (!config.exists()) { // Saving the default config if it doesn't exist locally + Files.copy(Objects.requireNonNull(Main.class.getResourceAsStream("/application.yml")), config.toPath(), StandardCopyOption.REPLACE_EXISTING); + log.info("Saved the default configuration to '{}', please re-launch the application", // Log the default config being saved + config.getAbsolutePath() + ); + return; + } + log.info("Found configuration at '{}'", config.getAbsolutePath()); // Log the found config + + SpringApplication.run(Main.class, args); + } +} \ No newline at end of file diff --git a/src/main/java/cc/fascinated/api/controller/PlayerController.java b/src/main/java/cc/fascinated/api/controller/PlayerController.java new file mode 100644 index 0000000..f0900bc --- /dev/null +++ b/src/main/java/cc/fascinated/api/controller/PlayerController.java @@ -0,0 +1,29 @@ +package cc.fascinated.api.controller; + +import cc.fascinated.player.PlayerManagerService; +import cc.fascinated.player.impl.Player; +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) +public class PlayerController { + + private final PlayerManagerService playerManagerService; + + @Autowired + public PlayerController(PlayerManagerService playerManagerService) { + this.playerManagerService = playerManagerService; + } + + @GetMapping("/{id}") @ResponseBody + public ResponseEntity getPlayer(@PathVariable String id) { + Player player = playerManagerService.getPlayer(id); + if (player == null) { + return ResponseEntity.notFound().build(); + } + return ResponseEntity.ok(player); + } +} 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..ec42b49 --- /dev/null +++ b/src/main/java/cc/fascinated/mojang/MojangAPIService.java @@ -0,0 +1,59 @@ +package cc.fascinated.mojang; + +import cc.fascinated.Main; +import cc.fascinated.mojang.types.MojangApiProfile; +import cc.fascinated.mojang.types.MojangSessionServerProfile; +import lombok.SneakyThrows; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; + +@Service +public class MojangAPIService { + + private static final HttpClient CLIENT = HttpClient.newHttpClient(); + + @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 + */ + @SneakyThrows + public MojangSessionServerProfile getSessionServerProfile(String id) { + HttpRequest request = HttpRequest.newBuilder() + .uri(new URI(mojangSessionServerUrl + "/session/minecraft/profile/" + id)) + .GET() + .build(); + + HttpResponse response = CLIENT.send(request, HttpResponse.BodyHandlers.ofString()); + return Main.getGSON().fromJson(response.body(), MojangSessionServerProfile.class); + } + + /** + * Gets the Mojang API profile of the player with the given UUID. + * + * @param id the name of the player + * @return the profile + */ + @SneakyThrows + public MojangApiProfile getApiProfile(String id) { + HttpRequest request = HttpRequest.newBuilder() + .uri(new URI(mojangApiUrl + "/users/profiles/minecraft/" + id)) + .GET() + .build(); + + HttpResponse response = CLIENT.send(request, HttpResponse.BodyHandlers.ofString()); + return Main.getGSON().fromJson(response.body(), 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..7705dcb --- /dev/null +++ b/src/main/java/cc/fascinated/mojang/types/MojangApiProfile.java @@ -0,0 +1,12 @@ +package cc.fascinated.mojang.types; + +import lombok.Getter; + +@Getter +public class MojangApiProfile { + + private String id; + private String name; + + public MojangApiProfile() {} +} 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/PlayerManagerService.java b/src/main/java/cc/fascinated/player/PlayerManagerService.java new file mode 100644 index 0000000..2251b4d --- /dev/null +++ b/src/main/java/cc/fascinated/player/PlayerManagerService.java @@ -0,0 +1,72 @@ +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 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 +public class PlayerManagerService { + + /** + * 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 PlayerManagerService(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; + if (id.length() == 32 || id.length() == 36) { + uuid = UUID.fromString(id.length() == 32 ? UUIDUtils.addUUIDDashes(id) : id); + } else { + uuid = playerNameToUUIDCache.get(id.toUpperCase()); + } + + if (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) { + return null; + } + profile = mojangAPIService.getSessionServerProfile(apiProfile.getId().length() == 32 ? UUIDUtils.addUUIDDashes(apiProfile.getId()) : apiProfile.getId()); + } + 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/Player.java b/src/main/java/cc/fascinated/player/impl/Player.java new file mode 100644 index 0000000..f653d59 --- /dev/null +++ b/src/main/java/cc/fascinated/player/impl/Player.java @@ -0,0 +1,57 @@ +package cc.fascinated.player.impl; + +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 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; + + 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 textures = json.getAsJsonObject("textures"); + JsonObject skin = textures.getAsJsonObject("SKIN"); + JsonObject metadata = skin.get("metadata").getAsJsonObject(); + + String url = skin.get("url").getAsString(); + SkinType model = SkinType.fromString(metadata.get("model").getAsString()); + + this.skin = new Skin(url, model); + } + +} 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..c049a3b --- /dev/null +++ b/src/main/java/cc/fascinated/player/impl/Skin.java @@ -0,0 +1,18 @@ +package cc.fascinated.player.impl; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter @AllArgsConstructor +public class Skin { + + /** + * The URL of the skin + */ + private final String url; + + /** + * The model of the skin + */ + private final SkinType model; +} diff --git a/src/main/java/cc/fascinated/player/impl/SkinType.java b/src/main/java/cc/fascinated/player/impl/SkinType.java new file mode 100644 index 0000000..334057d --- /dev/null +++ b/src/main/java/cc/fascinated/player/impl/SkinType.java @@ -0,0 +1,22 @@ +package cc.fascinated.player.impl; + +public enum SkinType { + + DEFAULT, + SLIM; + + /** + * Get the skin type from a string + * + * @param string the string + * @return the skin type + */ + public static SkinType fromString(String string) { + for (SkinType type : values()) { + if (type.name().equalsIgnoreCase(string)) { + return type; + } + } + return null; + } +} diff --git a/src/main/java/cc/fascinated/util/UUIDUtils.java b/src/main/java/cc/fascinated/util/UUIDUtils.java new file mode 100644 index 0000000..d8aed39 --- /dev/null +++ b/src/main/java/cc/fascinated/util/UUIDUtils.java @@ -0,0 +1,19 @@ +package cc.fascinated.util; + +public class UUIDUtils { + + /** + * Add dashes to a UUID. + * + * @param idNoDashes the UUID without dashes + * @return the UUID with dashes + */ + public static String addUUIDDashes(String idNoDashes) { + StringBuilder idBuff = new StringBuilder(idNoDashes); + idBuff.insert(20, '-'); + idBuff.insert(16, '-'); + idBuff.insert(12, '-'); + idBuff.insert(8, '-'); + return idBuff.toString(); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..e60941d --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,7 @@ +server: + address: 0.0.0.0 + port: 7500 + +mojang: + session-server: https://sessionserver.mojang.com + api: https://api.mojang.com \ No newline at end of file