diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 00000000..9d9c0493 Binary files /dev/null and b/.DS_Store differ 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/PlayerDBAPIPlayerProfileProvider.java b/src/main/java/de/pdinklag/mcstats/PlayerDBAPIPlayerProfileProvider.java new file mode 100644 index 00000000..10dc16b0 --- /dev/null +++ b/src/main/java/de/pdinklag/mcstats/PlayerDBAPIPlayerProfileProvider.java @@ -0,0 +1,35 @@ +package de.pdinklag.mcstats; + +import de.pdinklag.mcstats.playerdb.API; +import de.pdinklag.mcstats.playerdb.APIRequestException; +import de.pdinklag.mcstats.playerdb.EmptyResponseException; + +/** + * Provides player profiles via the PlayerDB API. + */ +public class PlayerDBAPIPlayerProfileProvider implements PlayerProfileProvider { + private final LogWriter log; + + /** + * Constructs a new provider. + */ + public PlayerDBAPIPlayerProfileProvider(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("PlayerDB 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..189738ca 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 ("playerdb".equalsIgnoreCase(api)) { + return new PlayerDBAPIPlayerProfileProvider(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/playerdb/API.java b/src/main/java/de/pdinklag/mcstats/playerdb/API.java new file mode 100644 index 00000000..2b205cb7 --- /dev/null +++ b/src/main/java/de/pdinklag/mcstats/playerdb/API.java @@ -0,0 +1,70 @@ +package de.pdinklag.mcstats.playerdb; + +import java.net.URI; + +import javax.net.ssl.HttpsURLConnection; + +import org.json.JSONObject; + +import de.pdinklag.mcstats.PlayerProfile; +import de.pdinklag.mcstats.util.StreamUtils; + +/** + * PlayerDB API. + */ +public class API { + private static final String API_URL = "https://playerdb.co/api/player/minecraft/"; + private static final String TEXTURES_URL_PREFIX = "https://textures.minecraft.net/texture/"; + private static final String USER_AGENT = "MinecraftStats"; + + /** + * Requests a player profile from the PlayerDB API. + * + * @param id the UUID or username of the player in question + * @return the player profile associated to the given id + * @throws EmptyResponseException in case the PlayerDB API gives an empty response + * @throws APIRequestException in case any error occurs trying to request the + * profile + */ + public static PlayerProfile requestPlayerProfile(String id) throws EmptyResponseException, APIRequestException { + try { + final String response; + { + URI uri = URI.create(API_URL + id); + java.net.URL url = uri.toURL(); + HttpsURLConnection conn = (HttpsURLConnection) url.openConnection(); + conn.setRequestProperty("User-Agent", USER_AGENT); + response = StreamUtils.readStreamFully(conn.getInputStream()); + conn.disconnect(); + } + + if (response.isEmpty()) { + throw new EmptyResponseException(); + } + + JSONObject root = new JSONObject(response); + JSONObject data = root.optJSONObject("data"); + JSONObject player = (data == null) ? null : data.optJSONObject("player"); + if (player == null) { + throw new EmptyResponseException(); + } + + String name = player.optString("username", ""); + if (name.isEmpty()) { + throw new EmptyResponseException(); + } + + String skin = null; + String skinTexture = player.optString("skin_texture", ""); + if (skinTexture.startsWith(TEXTURES_URL_PREFIX) && skinTexture.length() > TEXTURES_URL_PREFIX.length()) { + skin = skinTexture.substring(TEXTURES_URL_PREFIX.length()); + } + + return new PlayerProfile(name, skin, System.currentTimeMillis()); + } catch (EmptyResponseException e) { + throw e; // nb: delegate + } catch (Exception e) { + throw new APIRequestException(e); + } + } +} diff --git a/src/main/java/de/pdinklag/mcstats/playerdb/APIRequestException.java b/src/main/java/de/pdinklag/mcstats/playerdb/APIRequestException.java new file mode 100644 index 00000000..4b5a2ac4 --- /dev/null +++ b/src/main/java/de/pdinklag/mcstats/playerdb/APIRequestException.java @@ -0,0 +1,21 @@ +package de.pdinklag.mcstats.playerdb; + +/** + * An exception raised while processing a PlayerDB 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/playerdb/EmptyResponseException.java b/src/main/java/de/pdinklag/mcstats/playerdb/EmptyResponseException.java new file mode 100644 index 00000000..a457dc12 --- /dev/null +++ b/src/main/java/de/pdinklag/mcstats/playerdb/EmptyResponseException.java @@ -0,0 +1,9 @@ +package de.pdinklag.mcstats.playerdb; + +/** + * Indicates an empty response from the PlayerDB API. + */ +public class EmptyResponseException extends RuntimeException { + EmptyResponseException() { + } +}