Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
205 changes: 118 additions & 87 deletions src/main/java/com/roxiun/mellow/api/seraph/SeraphApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<SeraphTag> 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<SeraphTag> 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<SeraphTag> 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<String, UUID> 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<SeraphTag> parseTags(String response) {
List<SeraphTag> 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<SeraphTag> 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;
}
}
69 changes: 39 additions & 30 deletions src/main/java/com/roxiun/mellow/cache/PlayerCache.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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 +
"."
)
);
}
}
Expand All @@ -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;
Expand Down
Loading