diff --git a/src/main/java/com/roxiun/mellow/api/seraph/SeraphApi.java b/src/main/java/com/roxiun/mellow/api/seraph/SeraphApi.java index e173077..72f53d6 100644 --- a/src/main/java/com/roxiun/mellow/api/seraph/SeraphApi.java +++ b/src/main/java/com/roxiun/mellow/api/seraph/SeraphApi.java @@ -4,124 +4,155 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; +import com.roxiun.mellow.Mellow; import com.roxiun.mellow.api.mojang.MojangApi; +import kotlin.Pair; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.URL; +import java.net.URLConnection; +import java.text.MessageFormat; import java.util.ArrayList; import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; +import java.util.UUID; import java.util.stream.Collectors; public class SeraphApi { private final MojangApi mojangApi; + /** + * TODO Properly implement Seraph authentication + * To implement at a later date - Use proper authentication which allows more granular access to specific endpoints. + * Use a placeholder now so the function is satisfied + */ + private final String seraphApiToken = ""; + public SeraphApi(MojangApi mojangApi) { this.mojangApi = mojangApi; } - public List fetchSeraphTags(String uuid, String seraphApiKey) - throws IOException { + /** + * Fetches tags for the player should always return the standard api format unless the API is down. + */ + public List fetchSeraphTags(@NotNull String uuidStr, @Nullable String seraphApiKey) throws IOException, IllegalArgumentException { try { - // If the UUID is invalid for any reason, throw an exception - if (uuid == null || uuid.equals("ERROR") || uuid.isEmpty()) { - throw new IOException("Invalid UUID provided."); + UUID uuid = UUID.fromString(uuidStr); + URL url = new URL(MessageFormat.format("https://api.seraph.si/cubelify/blacklist/{0}", uuid.toString())); + String jsonResponse = executeGetRequest(url, seraphApiKey, seraphApiToken); + return jsonResponse != null ? parseTags(jsonResponse) : new ArrayList<>(); + } catch (IllegalArgumentException | IOException e) { + // For compatibility with the existing codebase, we'll rethrow the IllegalArgumentException if it's thrown + if (e instanceof IllegalArgumentException) { + throw e; + } else { + throw new IOException("Failed to fetch Seraph tags for UUID: " + uuidStr, e); } + } + } - String apiUrl = "https://api.seraph.si/cubelify/blacklist/" + uuid; - if (seraphApiKey != null && !seraphApiKey.isEmpty()) { - apiUrl += "?key=" + seraphApiKey; + /** + * Fetches Client data that can be nullable if the player is not on a client. + * To be functional this requires a Seraph API Key with the "Admin" permission or an authorisation token with the "client" grant. + */ + public List fetchSeraphClientData(@NotNull UUID uuid, @Nullable String seraphApiKey) { + try { + URL queryUrl = new URL(MessageFormat.format("https://api.seraph.si/private-access/client/{0}", uuid.toString())); + String jsonResponse = executeGetRequest(queryUrl, seraphApiKey, seraphApiToken); + return jsonResponse != null ? parseTags(jsonResponse) : new ArrayList<>(); + } catch (IOException e) { + return new ArrayList<>(); + } + } + + /** + * Fetches the Mojang name and UUID for a player. + */ + public Pair fetchSeraphMojang(@NotNull String nameOrId) { + try { + URL queryUrl = new URL(MessageFormat.format("https://mowojang.seraph.si/{0}", nameOrId)); + String jsonResponse = executeGetRequest(queryUrl, null, null); + if (jsonResponse == null) return null; + JsonObject json = new JsonParser().parse(jsonResponse).getAsJsonObject(); + return new Pair<>(json.get("name").getAsString(), UUID.fromString(json.get("uuid").getAsString())); + } catch (IOException e) { + return null; + } + } + + /** + * Centralised request logic to a single function for easier maintainability. + */ + private @Nullable String executeGetRequest(@NotNull URL url, @Nullable String apiKey, @Nullable String seraphApiToken) throws IOException { + URLConnection urlObject = url.openConnection(); + if (apiKey != null && !apiKey.isEmpty()) { + try { + urlObject.setRequestProperty("seraph-api-key", UUID.fromString(apiKey).toString()); + } catch (IllegalArgumentException ignored) { + throw new IllegalArgumentException("Seraph API Key is not a valid UUID."); } + } + if (seraphApiToken != null && !seraphApiToken.isEmpty()) { + urlObject.setRequestProperty("Authorization", MessageFormat.format("Bearer {0}", seraphApiToken)); + } - URL url = new URL(apiUrl); - HttpURLConnection conn = (HttpURLConnection) url.openConnection(); - conn.setRequestMethod("GET"); - conn.setRequestProperty("User-Agent", "Mozilla/5.0"); - conn.setConnectTimeout(5000); - conn.setReadTimeout(5000); - - int responseCode = conn.getResponseCode(); - if (responseCode == HttpURLConnection.HTTP_OK) { - BufferedReader in = new BufferedReader( - new InputStreamReader(conn.getInputStream()) - ); - String response = in.lines().collect(Collectors.joining()); - in.close(); - return parseTags(response); - } else if (responseCode == HttpURLConnection.HTTP_NOT_FOUND) { - return new ArrayList<>(); // Player has no tags - } else { - throw new IOException( - "Seraph API request failed with code: " + responseCode - ); + HttpURLConnection conn = (HttpURLConnection) urlObject; + conn.setRequestMethod("GET"); + conn.setRequestProperty("User-Agent", MessageFormat.format("Mozilla/5.0 {0}/{1}", Mellow.NAME, Mellow.VERSION)); + conn.setConnectTimeout(5000); + conn.setReadTimeout(5000); + + // Use proper Java standards for handling HTTP responses + int responseCode = conn.getResponseCode(); + if (responseCode == HttpURLConnection.HTTP_OK) { + // Use a buffered reader to parse the response more efficiently rather than dumping it all into memory + try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(conn.getInputStream()))) { + return bufferedReader.lines().collect(Collectors.joining()); } - } catch (IOException e) { - // If request fails, return empty list - return new ArrayList<>(); + // Check for 404 since the Seraph Reporting API uses this to indicate that there aren't any reports against the player specified + } else if (responseCode == HttpURLConnection.HTTP_NOT_FOUND) { + return null; + } else { + throw new IOException("Seraph API error: " + responseCode); } } private List parseTags(String response) { + List tags = new ArrayList<>(); try { - JsonObject json = new JsonParser() - .parse(response) - .getAsJsonObject(); - if (json.has("tags")) { - JsonArray tagsArray = json.getAsJsonArray("tags"); - if (tagsArray.size() > 0) { - List tags = new ArrayList<>(); - for (JsonElement tagElement : tagsArray) { - JsonObject tagObj = tagElement.getAsJsonObject(); - - String icon = tagObj.has("icon") - ? tagObj.get("icon").getAsString() - : ""; - String tooltip = tagObj.has("tooltip") - ? tagObj.get("tooltip").getAsString() - : ""; - int color = tagObj.has("color") - ? tagObj.get("color").getAsInt() - : 0; - String tagName = tagObj.has("tag_name") - ? tagObj.get("tag_name").getAsString() - : ""; - String text = tagObj.has("text") - ? tagObj.get("text").getAsString() - : null; - int textColor = tagObj.has("textColor") - ? tagObj.get("textColor").getAsInt() - : 0; - - // Skip seraph.verified and seraph.advertisement tags - if ( - "seraph.verified".equals(tagName) || - "seraph.advertisement".equals(tagName) - ) { - continue; - } - - tags.add( - new SeraphTag( - icon, - tooltip, - color, - tagName, - text, - textColor - ) - ); - } - return tags; + JsonObject json = new JsonParser().parse(response).getAsJsonObject(); + if (!json.has("tags")) return tags; + + JsonArray tagsArray = json.getAsJsonArray("tags"); + for (JsonElement tagElement : tagsArray) { + JsonObject tagObj = tagElement.getAsJsonObject(); + String tagName = getStringOrDefault(tagObj, "tag_name", ""); + + // Filter out tags that we don't need; although an advertisement would be nice, I'll respect the decision to remove it + if ("seraph.verified".equals(tagName) || "seraph.advertisement".equals(tagName)) { + continue; } + + tags.add(new SeraphTag(getStringOrDefault(tagObj, "icon", ""), getStringOrDefault(tagObj, "tooltip", ""), getIntOrDefault(tagObj, "color", 0), tagName, getStringOrDefault(tagObj, "text", null), getIntOrDefault(tagObj, "textColor", 0))); } - } catch (Exception e) { - // If parsing fails, return empty list + } catch (Exception ignored) { + // I couldn't find a logger instance, so I'll leave this as a placeholder } - return new ArrayList<>(); + return tags; + } + + // Basic implementation of kotlin default or null helpers + private String getStringOrDefault(JsonObject obj, String propertyName, String fallback) { + return (obj.has(propertyName) && !obj.get(propertyName).isJsonNull()) ? obj.get(propertyName).getAsString() : fallback; + } + + // Set fallback to 0 by default? I wasn't sure what was preferred, so it's customisable for now + private int getIntOrDefault(JsonObject obj, String propertyName, int fallback) { + return (obj.has(propertyName) && !obj.get(propertyName).isJsonNull()) ? obj.get(propertyName).getAsInt() : fallback; } } diff --git a/src/main/java/com/roxiun/mellow/cache/PlayerCache.java b/src/main/java/com/roxiun/mellow/cache/PlayerCache.java index 31c6ccf..aa39649 100644 --- a/src/main/java/com/roxiun/mellow/cache/PlayerCache.java +++ b/src/main/java/com/roxiun/mellow/cache/PlayerCache.java @@ -11,11 +11,12 @@ import com.roxiun.mellow.data.PlayerProfile; import com.roxiun.mellow.util.ChatUtils; import com.roxiun.mellow.util.player.PlayerUtils; +import net.minecraft.client.Minecraft; + import java.io.IOException; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; -import net.minecraft.client.Minecraft; public class PlayerCache { @@ -29,13 +30,13 @@ public class PlayerCache { private final MellowOneConfig config; public PlayerCache( - MojangApi mojangApi, - StatsProvider statsProvider, - UrchinApi urchinApi, - SeraphApi seraphApi, - String urchinApiKey, - String seraphApiKey, - MellowOneConfig config + MojangApi mojangApi, + StatsProvider statsProvider, + UrchinApi urchinApi, + SeraphApi seraphApi, + String urchinApiKey, + String seraphApiKey, + MellowOneConfig config ) { this.mojangApi = mojangApi; this.statsProvider = statsProvider; @@ -60,7 +61,7 @@ public PlayerProfile getProfile(String playerName) { private PlayerProfile fetchAndCachePlayer(String playerName) { try { BedwarsPlayer bedwarsPlayer = statsProvider.fetchPlayerStats( - playerName + playerName ); if (bedwarsPlayer == null) { return null; @@ -75,17 +76,17 @@ private PlayerProfile fetchAndCachePlayer(String playerName) { if (config.urchin) { try { urchinTags = urchinApi.fetchUrchinTags( - uuid, - playerName, - urchinApiKey + uuid, + playerName, + urchinApiKey ); } catch (IOException e) { Minecraft.getMinecraft().addScheduledTask(() -> - ChatUtils.sendMessage( - "§cFailed to fetch Urchin tags for " + - playerName + - "." - ) + ChatUtils.sendMessage( + "§cFailed to fetch Urchin tags for " + + playerName + + "." + ) ); } } @@ -94,23 +95,31 @@ private PlayerProfile fetchAndCachePlayer(String playerName) { if (config.seraph) { try { seraphTags = seraphApi.fetchSeraphTags(uuid, seraphApiKey); - } catch (IOException e) { - Minecraft.getMinecraft().addScheduledTask(() -> - ChatUtils.sendMessage( - "§cFailed to fetch Seraph tags for " + - playerName + - "." - ) - ); + } catch (IOException | IllegalArgumentException e) { + if (e instanceof IllegalArgumentException) { + Minecraft.getMinecraft().addScheduledTask(() -> + ChatUtils.sendMessage( + "§cInvalid Seraph API key" + ) + ); + } else { + Minecraft.getMinecraft().addScheduledTask(() -> + ChatUtils.sendMessage( + "§cFailed to fetch Seraph tags for " + + playerName + + "." + ) + ); + } } } PlayerProfile newProfile = new PlayerProfile( - uuid, - playerName, - bedwarsPlayer, - urchinTags, - seraphTags + uuid, + playerName, + bedwarsPlayer, + urchinTags, + seraphTags ); cache.put(playerName.toLowerCase(), newProfile); return newProfile;