From 677f8b02b090018375ae1e352ceacad4cdfbcbc1 Mon Sep 17 00:00:00 2001 From: Technofied <40795318+Technofied@users.noreply.github.com> Date: Tue, 27 Jan 2026 17:09:01 +0800 Subject: [PATCH] feat: alternative minetools API provider --- config/cli/config.json | 1 + config/plugin/config.yml | 1 + src/main/java/de/pdinklag/mcstats/Config.java | 10 +++ .../MinetoolsAPIPlayerProfileProvider.java | 35 +++++++++ .../java/de/pdinklag/mcstats/Updater.java | 7 +- .../pdinklag/mcstats/bukkit/BukkitConfig.java | 1 + .../de/pdinklag/mcstats/cli/JSONConfig.java | 1 + .../de/pdinklag/mcstats/minetools/API.java | 71 +++++++++++++++++++ .../minetools/APIRequestException.java | 21 ++++++ .../minetools/EmptyResponseException.java | 9 +++ 10 files changed, 156 insertions(+), 1 deletion(-) create mode 100644 src/main/java/de/pdinklag/mcstats/MinetoolsAPIPlayerProfileProvider.java create mode 100644 src/main/java/de/pdinklag/mcstats/minetools/API.java create mode 100644 src/main/java/de/pdinklag/mcstats/minetools/APIRequestException.java create mode 100644 src/main/java/de/pdinklag/mcstats/minetools/EmptyResponseException.java diff --git a/config/cli/config.json b/config/cli/config.json index 574678a1..179278b0 100644 --- a/config/cli/config.json +++ b/config/cli/config.json @@ -28,6 +28,7 @@ "inactiveDays": 7, "updateInactive": false, "profileUpdateInterval": 3, + "profileAPI": "mojang", "minPlaytime": 60, "excludeBanned": true, "excludeOps": false, diff --git a/config/plugin/config.yml b/config/plugin/config.yml index 936e80c3..7e3ece85 100644 --- a/config/plugin/config.yml +++ b/config/plugin/config.yml @@ -19,6 +19,7 @@ players: inactiveDays: 90 updateInactive: true profileUpdateInterval: 3 + profileAPI: "mojang" minPlaytime: 60 excludeBanned: true excludeOps: false diff --git a/src/main/java/de/pdinklag/mcstats/Config.java b/src/main/java/de/pdinklag/mcstats/Config.java index b3119b24..81c40457 100644 --- a/src/main/java/de/pdinklag/mcstats/Config.java +++ b/src/main/java/de/pdinklag/mcstats/Config.java @@ -31,6 +31,8 @@ public class Config { private int playerCacheUUIDPrefix = 2; private String defaultLanguage = "en"; + private String profileAPI = "mojang"; + public Config() { } @@ -177,4 +179,12 @@ public Path getEventsPath() { public void setEventsPath(Path eventsPath) { this.eventsPath = eventsPath; } + + public String getProfileAPI() { + return profileAPI; + } + + public void setProfileAPI(String profileAPI) { + this.profileAPI = profileAPI; + } } diff --git a/src/main/java/de/pdinklag/mcstats/MinetoolsAPIPlayerProfileProvider.java b/src/main/java/de/pdinklag/mcstats/MinetoolsAPIPlayerProfileProvider.java new file mode 100644 index 00000000..d4437863 --- /dev/null +++ b/src/main/java/de/pdinklag/mcstats/MinetoolsAPIPlayerProfileProvider.java @@ -0,0 +1,35 @@ +package de.pdinklag.mcstats; + +import de.pdinklag.mcstats.minetools.API; +import de.pdinklag.mcstats.minetools.APIRequestException; +import de.pdinklag.mcstats.minetools.EmptyResponseException; + +/** + * Provides player profiles via the Minetools.eu API. + */ +public class MinetoolsAPIPlayerProfileProvider implements PlayerProfileProvider { + private final LogWriter log; + + /** + * Constructs a new provider. + */ + public MinetoolsAPIPlayerProfileProvider(LogWriter log) { + this.log = log; + } + + @Override + public PlayerProfile getPlayerProfile(Player player) { + if (player.getAccountType().maybeMojangAccount()) { + try { + PlayerProfile profile = API.requestPlayerProfile(player.getUuid()); + player.setAccountType(AccountType.MOJANG); + return profile; + } catch (EmptyResponseException e) { + player.setAccountType(AccountType.OFFLINE); + } catch (APIRequestException e) { + log.writeError("Minetools.eu API profile request for player failed: " + player.getUuid(), e); + } + } + return player.getProfile(); + } +} diff --git a/src/main/java/de/pdinklag/mcstats/Updater.java b/src/main/java/de/pdinklag/mcstats/Updater.java index c6760248..e447f75b 100644 --- a/src/main/java/de/pdinklag/mcstats/Updater.java +++ b/src/main/java/de/pdinklag/mcstats/Updater.java @@ -60,7 +60,12 @@ public abstract class Updater { private final Path dbPlayerlistPath; protected PlayerProfileProvider getAuthenticProfileProvider() { - return new MojangAPIPlayerProfileProvider(log); + final String api = config.getProfileAPI(); + if ("minetools".equalsIgnoreCase(api)) { + return new MinetoolsAPIPlayerProfileProvider(log); + } else { + return new MojangAPIPlayerProfileProvider(log); + } } protected void gatherLocalProfileProviders(PlayerProfileProviderList providers) { diff --git a/src/main/java/de/pdinklag/mcstats/bukkit/BukkitConfig.java b/src/main/java/de/pdinklag/mcstats/bukkit/BukkitConfig.java index 3acd443e..d6cd6d01 100644 --- a/src/main/java/de/pdinklag/mcstats/bukkit/BukkitConfig.java +++ b/src/main/java/de/pdinklag/mcstats/bukkit/BukkitConfig.java @@ -38,6 +38,7 @@ public BukkitConfig(Plugin plugin) { setMinPlaytime(bukkitConfig.getInt("players.minPlaytime", getMinPlaytime())); setUpdateInactive(bukkitConfig.getBoolean("players.updateInactive", isUpdateInactive())); setProfileUpdateInterval(bukkitConfig.getInt("players.profileUpdateInterval", getProfileUpdateInterval())); + setProfileAPI(bukkitConfig.getString("players.profileAPI", getProfileAPI())); setExcludeBanned(bukkitConfig.getBoolean("players.excludeBanned", isExcludeBanned())); setExcludeOps(bukkitConfig.getBoolean("players.excludeOps", isExcludeOps())); diff --git a/src/main/java/de/pdinklag/mcstats/cli/JSONConfig.java b/src/main/java/de/pdinklag/mcstats/cli/JSONConfig.java index f26de97d..3be4c5eb 100644 --- a/src/main/java/de/pdinklag/mcstats/cli/JSONConfig.java +++ b/src/main/java/de/pdinklag/mcstats/cli/JSONConfig.java @@ -44,6 +44,7 @@ public JSONConfig(Path jsonPath) throws IOException, JSONException { setMinPlaytime(players.optInt("minPlaytime", getMinPlaytime())); setUpdateInactive(players.optBoolean("updateInactive", isUpdateInactive())); setProfileUpdateInterval(players.optInt("profileUpdateInterval", getProfileUpdateInterval())); + setProfileAPI(players.optString("profileAPI", getProfileAPI())); setExcludeBanned(players.optBoolean("excludeBanned", isExcludeBanned())); setExcludeOps(players.optBoolean("excludeOps", isExcludeOps())); diff --git a/src/main/java/de/pdinklag/mcstats/minetools/API.java b/src/main/java/de/pdinklag/mcstats/minetools/API.java new file mode 100644 index 00000000..fcf403f7 --- /dev/null +++ b/src/main/java/de/pdinklag/mcstats/minetools/API.java @@ -0,0 +1,71 @@ +package de.pdinklag.mcstats.minetools; + +import java.net.URL; + +import javax.net.ssl.HttpsURLConnection; + +import org.json.JSONObject; + +import de.pdinklag.mcstats.PlayerProfile; +import de.pdinklag.mcstats.util.StreamUtils; + +/** + * Minetools.eu API. + */ +public class API { + private static final String API_URL = "https://api.minetools.eu/profile/"; + private static final String SKIN_URL = "http://textures.minecraft.net/texture/"; + + /** + * Requests a player profile from the Minetools.eu API. + * + * @param uuid the UUID of the player in question + * @return the player profile associated to the given UUID. + * @throws EmptyResponseException in case the Minetools.eu API gives an empty response + * @throws APIRequestException in case any error occurs trying to request the + * profile + */ + public static PlayerProfile requestPlayerProfile(String uuid) throws EmptyResponseException, APIRequestException { + try { + final String response; + { + URL url = new URL(API_URL + uuid); + HttpsURLConnection conn = (HttpsURLConnection) url.openConnection(); + response = StreamUtils.readStreamFully(conn.getInputStream()); + conn.disconnect(); + } + + if (!response.isEmpty()) { + JSONObject obj = new JSONObject(response); + + // Check if the response has the decoded profile + if (!obj.has("decoded")) { + throw new EmptyResponseException(); + } + + JSONObject decoded = obj.getJSONObject("decoded"); + String name = decoded.getString("profileName"); + + // Extract skin URL from decoded textures + JSONObject textures = decoded.getJSONObject("textures"); + if (!textures.has("SKIN")) { + throw new EmptyResponseException(); + } + + String skinUrl = textures.getJSONObject("SKIN").getString("url"); + String skin = skinUrl.substring(SKIN_URL.length()); + + // Use timestamp from decoded profile, or current time if not available + long timestamp = decoded.optLong("timestamp", System.currentTimeMillis()); + + return new PlayerProfile(name, skin, timestamp); + } else { + throw new EmptyResponseException(); + } + } catch (EmptyResponseException e) { + throw e; // nb: delegate + } catch (Exception e) { + throw new APIRequestException(e); + } + } +} diff --git a/src/main/java/de/pdinklag/mcstats/minetools/APIRequestException.java b/src/main/java/de/pdinklag/mcstats/minetools/APIRequestException.java new file mode 100644 index 00000000..4928bb18 --- /dev/null +++ b/src/main/java/de/pdinklag/mcstats/minetools/APIRequestException.java @@ -0,0 +1,21 @@ +package de.pdinklag.mcstats.minetools; + +/** + * An exception raised while processing a Minetools.eu API request. + */ +public class APIRequestException extends RuntimeException { + APIRequestException() { + } + + APIRequestException(String message) { + super(message); + } + + APIRequestException(Throwable cause) { + super(cause); + } + + APIRequestException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/de/pdinklag/mcstats/minetools/EmptyResponseException.java b/src/main/java/de/pdinklag/mcstats/minetools/EmptyResponseException.java new file mode 100644 index 00000000..b3ffffe8 --- /dev/null +++ b/src/main/java/de/pdinklag/mcstats/minetools/EmptyResponseException.java @@ -0,0 +1,9 @@ +package de.pdinklag.mcstats.minetools; + +/** + * Indicates an empty response from the Minetools.eu API. + */ +public class EmptyResponseException extends RuntimeException { + EmptyResponseException() { + } +}