aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/main/java/io/github/moulberry/notenoughupdates/NEUManager.java2
-rw-r--r--src/main/java/io/github/moulberry/notenoughupdates/options/customtypes/NEUDebugFlag.java4
-rw-r--r--src/main/java/io/github/moulberry/notenoughupdates/options/seperateSections/ApiData.java9
-rw-r--r--src/main/java/io/github/moulberry/notenoughupdates/profileviewer/ProfileViewer.java10
-rw-r--r--src/main/java/io/github/moulberry/notenoughupdates/profileviewer/SkyblockProfiles.java30
-rw-r--r--src/main/java/io/github/moulberry/notenoughupdates/util/ApiUtil.java48
-rw-r--r--src/main/java/io/github/moulberry/notenoughupdates/util/NEUDebugLogger.java3
-rw-r--r--src/main/java/io/github/moulberry/notenoughupdates/util/ProfileApiSyncer.java15
-rw-r--r--src/main/java/io/github/moulberry/notenoughupdates/util/SBInfo.java2
-rw-r--r--src/main/kotlin/io/github/moulberry/notenoughupdates/commands/dev/DevTestCommand.kt16
-rw-r--r--src/main/kotlin/io/github/moulberry/notenoughupdates/commands/misc/ProfileViewerCommands.kt7
-rw-r--r--src/main/kotlin/io/github/moulberry/notenoughupdates/util/HttpStatusCodeException.kt28
-rw-r--r--src/main/kotlin/io/github/moulberry/notenoughupdates/util/UrsaClient.kt201
-rw-r--r--src/main/kotlin/io/github/moulberry/notenoughupdates/util/hypixelapi/Collection.kt2
-rw-r--r--src/main/kotlin/io/github/moulberry/notenoughupdates/util/kotlin/Coroutines.kt20
15 files changed, 351 insertions, 46 deletions
diff --git a/src/main/java/io/github/moulberry/notenoughupdates/NEUManager.java b/src/main/java/io/github/moulberry/notenoughupdates/NEUManager.java
index cbc3ce3b..c60e7317 100644
--- a/src/main/java/io/github/moulberry/notenoughupdates/NEUManager.java
+++ b/src/main/java/io/github/moulberry/notenoughupdates/NEUManager.java
@@ -39,6 +39,7 @@ import io.github.moulberry.notenoughupdates.util.ApiUtil;
import io.github.moulberry.notenoughupdates.util.Constants;
import io.github.moulberry.notenoughupdates.util.ItemResolutionQuery;
import io.github.moulberry.notenoughupdates.util.ItemUtils;
+import io.github.moulberry.notenoughupdates.util.UrsaClient;
import io.github.moulberry.notenoughupdates.util.Utils;
import net.minecraft.client.Minecraft;
import net.minecraft.client.settings.KeyBinding;
@@ -130,6 +131,7 @@ public class NEUManager {
public long viewItemAttemptTime = 0;
public final ApiUtil apiUtils = new ApiUtil();
+ public final UrsaClient ursaClient = new UrsaClient(apiUtils);
private final Map<String, ItemStack> itemstackCache = new HashMap<>();
diff --git a/src/main/java/io/github/moulberry/notenoughupdates/options/customtypes/NEUDebugFlag.java b/src/main/java/io/github/moulberry/notenoughupdates/options/customtypes/NEUDebugFlag.java
index ab1a6516..ba638b9b 100644
--- a/src/main/java/io/github/moulberry/notenoughupdates/options/customtypes/NEUDebugFlag.java
+++ b/src/main/java/io/github/moulberry/notenoughupdates/options/customtypes/NEUDebugFlag.java
@@ -19,7 +19,9 @@
package io.github.moulberry.notenoughupdates.options.customtypes;
+import io.github.moulberry.notenoughupdates.util.MinecraftExecutor;
import io.github.moulberry.notenoughupdates.util.NEUDebugLogger;
+import net.minecraft.client.Minecraft;
import java.util.Arrays;
import java.util.List;
@@ -46,7 +48,7 @@ public enum NEUDebugFlag {
}
public void log(String message) {
- NEUDebugLogger.log(this, message);
+ NEUDebugLogger.log(this, message);
}
public boolean isSet() {
diff --git a/src/main/java/io/github/moulberry/notenoughupdates/options/seperateSections/ApiData.java b/src/main/java/io/github/moulberry/notenoughupdates/options/seperateSections/ApiData.java
index 6d93b98d..ef871b83 100644
--- a/src/main/java/io/github/moulberry/notenoughupdates/options/seperateSections/ApiData.java
+++ b/src/main/java/io/github/moulberry/notenoughupdates/options/seperateSections/ApiData.java
@@ -127,6 +127,15 @@ public class ApiData {
@ConfigEditorText
public String moulberryCodesApi = "moulberry.codes";
+
+ @Expose
+ @ConfigOption(
+ name = "Ursa Minor Proxy",
+ desc = "§4Do §lNOT §r§4change this, unless you know exactly what you are doing"
+ )
+ @ConfigEditorText
+ public String ursaApi = "https://ursa.notenoughupdates.org/";
+
public String getCommitApiUrl() {
return String.format("https://api.github.com/repos/%s/%s/commits/%s", repoUser, repoName, repoBranch);
}
diff --git a/src/main/java/io/github/moulberry/notenoughupdates/profileviewer/ProfileViewer.java b/src/main/java/io/github/moulberry/notenoughupdates/profileviewer/ProfileViewer.java
index a81956f2..ed3cf8ef 100644
--- a/src/main/java/io/github/moulberry/notenoughupdates/profileviewer/ProfileViewer.java
+++ b/src/main/java/io/github/moulberry/notenoughupdates/profileviewer/ProfileViewer.java
@@ -22,6 +22,7 @@ package io.github.moulberry.notenoughupdates.profileviewer;
import com.google.gson.JsonObject;
import io.github.moulberry.notenoughupdates.NEUManager;
import io.github.moulberry.notenoughupdates.NotEnoughUpdates;
+import io.github.moulberry.notenoughupdates.util.UrsaClient;
import io.github.moulberry.notenoughupdates.util.Utils;
import lombok.Getter;
import net.minecraft.init.Blocks;
@@ -474,7 +475,7 @@ public class ProfileViewer {
updatingResourceCollection.set(true);
NotEnoughUpdates.INSTANCE.manager.apiUtils
- .newHypixelApiRequest("resources/skyblock/collections")
+ .newAnonymousHypixelApiRequest("resources/skyblock/collections")
.requestJson()
.thenAccept(jsonObject -> {
updatingResourceCollection.set(false);
@@ -528,11 +529,8 @@ public class ProfileViewer {
callback.accept(null);
} else {
if (!uuidToHypixelProfile.containsKey(uuid)) {
- manager.apiUtils
- .newHypixelApiRequest("player")
- .queryArgument("uuid", uuid)
- .maxCacheAge(Duration.ofSeconds(30))
- .requestJson()
+ manager.ursaClient
+ .get(UrsaClient.player(Utils.parseDashlessUUID(uuid)))
.thenAccept(playerJson -> {
if (
playerJson != null &&
diff --git a/src/main/java/io/github/moulberry/notenoughupdates/profileviewer/SkyblockProfiles.java b/src/main/java/io/github/moulberry/notenoughupdates/profileviewer/SkyblockProfiles.java
index 88251d32..61872980 100644
--- a/src/main/java/io/github/moulberry/notenoughupdates/profileviewer/SkyblockProfiles.java
+++ b/src/main/java/io/github/moulberry/notenoughupdates/profileviewer/SkyblockProfiles.java
@@ -28,6 +28,7 @@ import io.github.moulberry.notenoughupdates.profileviewer.bestiary.BestiaryData;
import io.github.moulberry.notenoughupdates.profileviewer.weight.senither.SenitherWeight;
import io.github.moulberry.notenoughupdates.profileviewer.weight.weight.Weight;
import io.github.moulberry.notenoughupdates.util.Constants;
+import io.github.moulberry.notenoughupdates.util.UrsaClient;
import io.github.moulberry.notenoughupdates.util.Utils;
import io.github.moulberry.notenoughupdates.util.hypixelapi.ProfileCollectionInfo;
import lombok.Getter;
@@ -84,6 +85,7 @@ public class SkyblockProfiles {
"social"
);
private final ProfileViewer profileViewer;
+ // TODO: replace with UUID type
private final String uuid;
private final AtomicBoolean updatingSkyblockProfilesState = new AtomicBoolean(false);
private final AtomicBoolean updatingGuildInfoState = new AtomicBoolean(false);
@@ -119,10 +121,8 @@ public class SkyblockProfiles {
lastStatusInfoState = currentTime;
updatingPlayerStatusState.set(true);
- profileViewer.getManager().apiUtils
- .newHypixelApiRequest("status")
- .queryArgument("uuid", uuid)
- .requestJson()
+ profileViewer.getManager().ursaClient
+ .get(UrsaClient.status(Utils.parseDashlessUUID(uuid)))
.handle((jsonObject, ex) -> {
updatingPlayerStatusState.set(false);
@@ -143,10 +143,8 @@ public class SkyblockProfiles {
lastBingoInfoState = currentTime;
updatingBingoInfo.set(true);
- NotEnoughUpdates.INSTANCE.manager.apiUtils
- .newHypixelApiRequest("skyblock/bingo")
- .queryArgument("uuid", uuid)
- .requestJson()
+ NotEnoughUpdates.INSTANCE.manager.ursaClient
+ .get(UrsaClient.bingo(Utils.parseDashlessUUID(uuid)))
.handle(((jsonObject, throwable) -> {
updatingBingoInfo.set(false);
@@ -317,10 +315,8 @@ public class SkyblockProfiles {
lastPlayerInfoState = currentTime;
updatingSkyblockProfilesState.set(true);
- profileViewer.getManager().apiUtils
- .newHypixelApiRequest("skyblock/profiles")
- .queryArgument("uuid", uuid)
- .requestJson()
+ profileViewer.getManager().ursaClient
+ .get(UrsaClient.profiles(Utils.parseDashlessUUID(uuid)))
.handle((profilesJson, throwable) -> {
if (profilesJson != null && profilesJson.has("success")
&& profilesJson.get("success").getAsBoolean() && profilesJson.has("profiles")) {
@@ -388,10 +384,8 @@ public class SkyblockProfiles {
lastGuildInfoState = currentTime;
updatingGuildInfoState.set(true);
- profileViewer.getManager().apiUtils
- .newHypixelApiRequest("guild")
- .queryArgument("player", uuid)
- .requestJson()
+ profileViewer.getManager().ursaClient
+ .get(UrsaClient.guild(Utils.parseDashlessUUID(uuid)))
.handle((jsonObject, ex) -> {
updatingGuildInfoState.set(false);
@@ -649,9 +643,7 @@ public class SkyblockProfiles {
updatingMuseumData.set(true);
String profileId = getOuterProfileJson().get("profile_id").getAsString();
- profileViewer.getManager().apiUtils.newHypixelApiRequest("skyblock/museum")
- .queryArgument("profile", profileId)
- .requestJson()
+ profileViewer.getManager().ursaClient.get(UrsaClient.museumForProfile(profileId))
.handle((museumJson, throwable) -> {
if (museumJson != null && museumJson.has("success")
&& museumJson.get("success").getAsBoolean() && museumJson.has("members")) {
diff --git a/src/main/java/io/github/moulberry/notenoughupdates/util/ApiUtil.java b/src/main/java/io/github/moulberry/notenoughupdates/util/ApiUtil.java
index 4cb1abc8..3b6aed84 100644
--- a/src/main/java/io/github/moulberry/notenoughupdates/util/ApiUtil.java
+++ b/src/main/java/io/github/moulberry/notenoughupdates/util/ApiUtil.java
@@ -58,10 +58,12 @@ import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
+import java.util.stream.Collectors;
import java.util.zip.GZIPInputStream;
public class ApiUtil {
@@ -90,16 +92,17 @@ public class ApiUtil {
try {
KeyStore letsEncryptStore = KeyStore.getInstance("JKS");
letsEncryptStore.load(ApiUtil.class.getResourceAsStream("/neukeystore.jks"), "neuneu".toCharArray());
- ctx = SSLContext.getInstance("TLS");
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
kmf.init(letsEncryptStore, null);
tmf.init(letsEncryptStore);
+ ctx = SSLContext.getInstance("TLS");
ctx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
} catch (KeyStoreException | NoSuchAlgorithmException | KeyManagementException | UnrecoverableKeyException |
IOException | CertificateException e) {
System.out.println("Failed to load NEU keystore. A lot of API requests won't work");
e.printStackTrace();
+ ctx = null;
}
}
@@ -129,6 +132,8 @@ public class ApiUtil {
private Duration maxCacheAge = Duration.ofSeconds(500);
private String method = "GET";
private String postData = null;
+ private final Map<String, String> headers = new HashMap<>();
+ private Map<String, List<String>> responseHeaders = new HashMap<>();
private String postContentType = null;
public Request method(String method) {
@@ -136,6 +141,11 @@ public class ApiUtil {
return this;
}
+ public Request header(String key, String value) {
+ this.headers.put(key, value);
+ return this;
+ }
+
/**
* Specify a cache timeout of {@code null} to signify an uncacheable request.
* Non {@code GET} requests are always uncacheable.
@@ -196,9 +206,17 @@ public class ApiUtil {
return new ApiCache.CacheKey(baseUrl, queryArguments, shouldGunzip);
}
+ /**
+ * Note: This map may be empty on cache hits.
+ */
+ public Map<String, List<String>> getResponseHeaders() {
+ return responseHeaders;
+ }
+
private CompletableFuture<String> requestString0() {
return buildUrl().thenApplyAsync(url -> {
InputStream inputStream = null;
+ int httpStatusCode = 200;
URLConnection conn = null;
try {
try {
@@ -212,6 +230,9 @@ public class ApiUtil {
conn.setConnectTimeout(10000);
conn.setReadTimeout(10000);
conn.setRequestProperty("User-Agent", getUserAgent());
+ for (Map.Entry<String, String> header : headers.entrySet()) {
+ conn.setRequestProperty(header.getKey(), header.getValue());
+ }
if (this.postContentType != null) {
conn.setRequestProperty("Content-Type", this.postContentType);
}
@@ -228,16 +249,33 @@ public class ApiUtil {
}
}
- inputStream = conn.getInputStream();
+ if (conn instanceof HttpURLConnection) {
+ HttpURLConnection httpConn = (HttpURLConnection) conn;
+ httpStatusCode = httpConn.getResponseCode();
+ if (httpStatusCode >= 400) {
+ inputStream = httpConn.getErrorStream();
+ }
+ }
+ if (inputStream == null)
+ inputStream = conn.getInputStream();
if (shouldGunzip || "gzip".equals(conn.getContentEncoding())) {
inputStream = new GZIPInputStream(inputStream);
}
+ responseHeaders = conn.getHeaderFields().entrySet().stream()
+ .collect(Collectors.toMap(
+ it -> it.getKey() == null ? null : it.getKey().toLowerCase(Locale.ROOT),
+ it -> new ArrayList<>(it.getValue())
+ ));
// While the assumption of UTF8 isn't always true; it *should* always be true.
// Not in the sense that this will hold in most cases (although that as well),
// but in the sense that any violation of this better have a good reason.
- return IOUtils.toString(inputStream, StandardCharsets.UTF_8);
+ String response = IOUtils.toString(inputStream, StandardCharsets.UTF_8);
+ if (httpStatusCode >= 400) {
+ throw new HttpStatusCodeException(url, httpStatusCode, response);
+ }
+ return response;
} finally {
try {
if (inputStream != null) {
@@ -298,13 +336,15 @@ public class ApiUtil {
}
public static void patchHttpsRequest(HttpsURLConnection connection) {
- connection.setSSLSocketFactory(ctx.getSocketFactory());
+ if (ctx != null && connection != null)
+ connection.setSSLSocketFactory(ctx.getSocketFactory());
}
public Request request() {
return new Request();
}
+ @Deprecated
public Request newHypixelApiRequest(String apiPath) {
return newAnonymousHypixelApiRequest(apiPath)
.queryArgument("key", NotEnoughUpdates.INSTANCE.config.apiData.apiKey);
diff --git a/src/main/java/io/github/moulberry/notenoughupdates/util/NEUDebugLogger.java b/src/main/java/io/github/moulberry/notenoughupdates/util/NEUDebugLogger.java
index e5d00a66..d73a500c 100644
--- a/src/main/java/io/github/moulberry/notenoughupdates/util/NEUDebugLogger.java
+++ b/src/main/java/io/github/moulberry/notenoughupdates/util/NEUDebugLogger.java
@@ -22,7 +22,6 @@ package io.github.moulberry.notenoughupdates.util;
import io.github.moulberry.notenoughupdates.NotEnoughUpdates;
import io.github.moulberry.notenoughupdates.options.customtypes.NEUDebugFlag;
import net.minecraft.client.Minecraft;
-import net.minecraft.util.ChatComponentText;
import net.minecraft.util.EnumChatFormatting;
import java.util.function.Consumer;
@@ -34,7 +33,7 @@ public class NEUDebugLogger {
public static boolean allFlagsEnabled = false;
private static void chatLogger(String message) {
- Utils.addChatMessage(EnumChatFormatting.YELLOW + "[NEU DEBUG] " + message);
+ mc.addScheduledTask(() -> Utils.addChatMessage(EnumChatFormatting.YELLOW + "[NEU DEBUG] " + message));
}
public static boolean isFlagEnabled(NEUDebugFlag flag) {
diff --git a/src/main/java/io/github/moulberry/notenoughupdates/util/ProfileApiSyncer.java b/src/main/java/io/github/moulberry/notenoughupdates/util/ProfileApiSyncer.java
index 67ed7c7b..ed88a6a5 100644
--- a/src/main/java/io/github/moulberry/notenoughupdates/util/ProfileApiSyncer.java
+++ b/src/main/java/io/github/moulberry/notenoughupdates/util/ProfileApiSyncer.java
@@ -19,6 +19,7 @@
package io.github.moulberry.notenoughupdates.util;
+import com.google.gson.JsonObject;
import io.github.moulberry.notenoughupdates.NotEnoughUpdates;
import io.github.moulberry.notenoughupdates.profileviewer.SkyblockProfiles;
import net.minecraft.client.Minecraft;
@@ -89,9 +90,15 @@ public class ProfileApiSyncer {
if (Minecraft.getMinecraft().thePlayer == null) return;
String uuid = Minecraft.getMinecraft().thePlayer.getUniqueID().toString().replace("-", "");
- NotEnoughUpdates.profileViewer.getOrLoadSkyblockProfiles(uuid, (profile) -> {
- for (Consumer<SkyblockProfiles> c : finishSyncCallbacks.values()) c.accept(profile);
- finishSyncCallbacks.clear();
- });
+ NotEnoughUpdates.INSTANCE.manager.apiUtils
+ .newHypixelApiRequest("/skyblock/profiles")
+ .queryArgument("uuid", uuid)
+ .requestJson()
+ .thenAcceptAsync((profile) -> {
+ SkyblockProfiles skyblockProfiles = new SkyblockProfiles(NotEnoughUpdates.profileViewer, uuid);
+ for (Consumer<SkyblockProfiles> c : finishSyncCallbacks.values())
+ c.accept((skyblockProfiles));
+ finishSyncCallbacks.clear();
+ }, MinecraftExecutor.OnThread);
}
}
diff --git a/src/main/java/io/github/moulberry/notenoughupdates/util/SBInfo.java b/src/main/java/io/github/moulberry/notenoughupdates/util/SBInfo.java
index 2d637ab8..e5a0e1b8 100644
--- a/src/main/java/io/github/moulberry/notenoughupdates/util/SBInfo.java
+++ b/src/main/java/io/github/moulberry/notenoughupdates/util/SBInfo.java
@@ -459,7 +459,7 @@ public class SBInfo {
public void updateMayor() {
NotEnoughUpdates.INSTANCE.manager.apiUtils
- .newHypixelApiRequest("resources/skyblock/election")
+ .newAnonymousHypixelApiRequest("resources/skyblock/election")
.requestJson()
.thenAccept(newJson -> mayorJson = newJson);
}
diff --git a/src/main/kotlin/io/github/moulberry/notenoughupdates/commands/dev/DevTestCommand.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/commands/dev/DevTestCommand.kt
index e72a1ed4..8ad765eb 100644
--- a/src/main/kotlin/io/github/moulberry/notenoughupdates/commands/dev/DevTestCommand.kt
+++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/commands/dev/DevTestCommand.kt
@@ -19,7 +19,9 @@
package io.github.moulberry.notenoughupdates.commands.dev
+import com.google.gson.JsonObject
import com.mojang.brigadier.arguments.StringArgumentType
+import com.mojang.brigadier.arguments.StringArgumentType.string
import io.github.moulberry.notenoughupdates.BuildFlags
import io.github.moulberry.notenoughupdates.NotEnoughUpdates
import io.github.moulberry.notenoughupdates.autosubscribe.NEUAutoSubscribe
@@ -36,7 +38,6 @@ import io.github.moulberry.notenoughupdates.util.brigadier.*
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.GuiScreen
import net.minecraft.command.ICommandSender
-import net.minecraft.entity.item.EntityArmorStand
import net.minecraft.entity.player.EntityPlayer
import net.minecraft.launchwrapper.Launch
import net.minecraft.util.ChatComponentText
@@ -200,6 +201,16 @@ class DevTestCommand {
}
}
}
+ thenLiteral("callUrsa") {
+ thenArgument("path", RestArgumentType) { path ->
+ thenExecute {
+ NotEnoughUpdates.INSTANCE.manager.ursaClient.getString(this[path])
+ .thenAccept {
+ reply(it.toString())
+ }
+ }
+ }.withHelp("Send an authenticated request to the current ursa server")
+ }
thenLiteralExecute("dev") {
NotEnoughUpdates.INSTANCE.config.hidden.dev = !NotEnoughUpdates.INSTANCE.config.hidden.dev
reply("Dev mode " + if (NotEnoughUpdates.INSTANCE.config.hidden.dev) "§aenabled" else "§cdisabled")
@@ -210,7 +221,8 @@ class DevTestCommand {
}.withHelp("Force sync the config to disk")
thenLiteralExecute("clearapicache") {
ApiCache.clear()
- reply("Cleared API cache")
+ NotEnoughUpdates.INSTANCE.manager.ursaClient.clearToken()
+ reply("Cleared API cache and reset ursa token")
}.withHelp("Clear the API cache")
thenLiteralExecute("searchmode") {
NotEnoughUpdates.INSTANCE.config.hidden.firstTimeSearchFocus = true
diff --git a/src/main/kotlin/io/github/moulberry/notenoughupdates/commands/misc/ProfileViewerCommands.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/commands/misc/ProfileViewerCommands.kt
index d25e880b..fac673b4 100644
--- a/src/main/kotlin/io/github/moulberry/notenoughupdates/commands/misc/ProfileViewerCommands.kt
+++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/commands/misc/ProfileViewerCommands.kt
@@ -45,16 +45,11 @@ class ProfileViewerCommands {
reply("${RED}Some parts of the profile viewer do not work with OptiFine Fast Render. Go to ESC > Options > Video Settings > Performance > Fast Render to disable it.")
}
- if (NotEnoughUpdates.INSTANCE.config.apiData.apiKey.isNullOrBlank()) {
- reply("${RED}Can't view profile, an API key is not set. Run /api new and put the result in settings.")
- return
- }
-
NotEnoughUpdates.profileViewer.loadPlayerByName(
name ?: Minecraft.getMinecraft().thePlayer.name
) { profile ->
if (profile == null) {
- reply("${RED}Invalid player name/API key. Maybe the API is down? Try /api new.")
+ reply("${RED}Invalid player name. Maybe the API is down? Try again later.")
} else {
profile.resetCache()
ProfileViewerUtils.saveSearch(name)
diff --git a/src/main/kotlin/io/github/moulberry/notenoughupdates/util/HttpStatusCodeException.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/util/HttpStatusCodeException.kt
new file mode 100644
index 00000000..bd38706f
--- /dev/null
+++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/util/HttpStatusCodeException.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2023 NotEnoughUpdates contributors
+ *
+ * This file is part of NotEnoughUpdates.
+ *
+ * NotEnoughUpdates is free software: you can redistribute it
+ * and/or modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation, either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * NotEnoughUpdates is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with NotEnoughUpdates. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package io.github.moulberry.notenoughupdates.util
+
+import java.net.URL
+
+data class HttpStatusCodeException(
+ val url: URL,
+ val statusCode: Int,
+ val serverMessage: String,
+) : RuntimeException("Server returned HTTP Status Code: $statusCode:\n$serverMessage")
diff --git a/src/main/kotlin/io/github/moulberry/notenoughupdates/util/UrsaClient.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/util/UrsaClient.kt
new file mode 100644
index 00000000..2320163d
--- /dev/null
+++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/util/UrsaClient.kt
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2023 NotEnoughUpdates contributors
+ *
+ * This file is part of NotEnoughUpdates.
+ *
+ * NotEnoughUpdates is free software: you can redistribute it
+ * and/or modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation, either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * NotEnoughUpdates is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with NotEnoughUpdates. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package io.github.moulberry.notenoughupdates.util
+
+import com.google.gson.JsonObject
+import io.github.moulberry.notenoughupdates.NotEnoughUpdates
+import io.github.moulberry.notenoughupdates.autosubscribe.NEUAutoSubscribe
+import io.github.moulberry.notenoughupdates.options.customtypes.NEUDebugFlag
+import io.github.moulberry.notenoughupdates.util.kotlin.Coroutines.await
+import io.github.moulberry.notenoughupdates.util.kotlin.Coroutines.continueOn
+import io.github.moulberry.notenoughupdates.util.kotlin.Coroutines.launchCoroutine
+import net.minecraft.client.Minecraft
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
+import net.minecraftforge.fml.common.gameevent.TickEvent
+import java.time.Duration
+import java.time.Instant
+import java.util.*
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.ConcurrentLinkedQueue
+
+class UrsaClient(val apiUtil: ApiUtil) {
+ private data class Token(
+ val validUntil: Instant,
+ val token: String,
+ val obtainedFrom: String,
+ ) {
+ val isValid get() = Instant.now().plusSeconds(60) < validUntil
+ }
+
+ val logger = NEUDebugFlag.API_CACHE
+
+ // Needs synchronized access
+ private var token: Token? = null
+ private var isPollingForToken = false
+
+ private data class Request<T>(
+ val path: String,
+ val objectMapping: Class<T>?,
+ val consumer: CompletableFuture<T>,
+ )
+
+ private val queue = ConcurrentLinkedQueue<Request<*>>()
+ private val ursaRoot
+ get() = NotEnoughUpdates.INSTANCE.config.apiData.ursaApi.removeSuffix("/").takeIf { it.isNotBlank() }
+ ?: "https://ursa.notenoughupdates.org"
+
+ private suspend fun authorizeRequest(usedUrsaRoot: String, connection: ApiUtil.Request, t: Token?) {
+ if (t != null && t.obtainedFrom == usedUrsaRoot) {
+ logger.log("Authorizing request using token")
+ connection.header("x-ursa-token", t.token)
+ } else {
+ logger.log("Authorizing request using username and serverId")
+ val serverId = UUID.randomUUID().toString()
+ val session = Minecraft.getMinecraft().session
+ val name = session.username
+ connection.header("x-ursa-username", name).header("x-ursa-serverid", serverId)
+ continueOn(MinecraftExecutor.OffThread)
+ Minecraft.getMinecraft().sessionService.joinServer(session.profile, session.token, serverId)
+ logger.log("Authorizing request using username and serverId complete")
+ }
+ }
+
+ private suspend fun saveToken(usedUrsaRoot: String, connection: ApiUtil.Request) {
+ logger.log("Attempting to save token")
+ val token =
+ connection.responseHeaders["x-ursa-token"]?.firstOrNull()
+ val validUntil = connection.responseHeaders["x-ursa-expires"]
+ ?.firstOrNull()
+ ?.toLongOrNull()
+ ?.let { Instant.ofEpochMilli(it) } ?: (Instant.now() + Duration.ofMinutes(55))
+ continueOn(MinecraftExecutor.OnThread)
+ if (token == null) {
+ isPollingForToken = false
+ logger.log("No token found. Marking as non polling")
+ } else {
+ this.token = Token(validUntil, token, usedUrsaRoot)
+ isPollingForToken = false
+ logger.log("Token saving successful")
+ }
+ }
+
+ private suspend fun <T> performRequest(request: Request<T>, token: Token?) {
+ val usedUrsaRoot = ursaRoot
+ val apiRequest = apiUtil.request().url("$usedUrsaRoot/${request.path}")
+ try {
+ logger.log("Ursa Request started")
+ authorizeRequest(usedUrsaRoot, apiRequest, token)
+ val response =
+ if (request.objectMapping == null)
+ (apiRequest.requestString().await() as T)
+ else
+ (apiRequest.requestJson(request.objectMapping).await() as T)
+ logger.log("Request completed")
+ saveToken(usedUrsaRoot, apiRequest)
+ request.consumer.complete(response)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ logger.log("Request failed")
+ continueOn(MinecraftExecutor.OnThread)
+ isPollingForToken = false
+ request.consumer.completeExceptionally(e)
+ }
+ }
+
+ private fun bumpRequests() {
+ while (!queue.isEmpty()) {
+ if (isPollingForToken) return
+ val nextRequest = queue.poll()
+ if (nextRequest == null) {
+ logger.log("No request to bump found")
+ return
+ }
+ logger.log("Request found")
+ var t = token
+ if (!(t != null && t.isValid && t.obtainedFrom == ursaRoot)) {
+ isPollingForToken = true
+ t = null
+ if (token != null) {
+ logger.log("Disposing old invalid ursa token.")
+ token = null
+ }
+ logger.log("No token saved. Marking this request as a token poll request")
+ }
+ launchCoroutine { performRequest(nextRequest, t) }
+ }
+ }
+
+
+ fun clearToken() {
+ synchronized(this) {
+ token = null
+ }
+ }
+
+ fun <T> get(path: String, clazz: Class<T>): CompletableFuture<T> {
+ val c = CompletableFuture<T>()
+ queue.add(Request(path, clazz, c))
+ return c
+ }
+
+
+ fun getString(path: String): CompletableFuture<String> {
+ val c = CompletableFuture<String>()
+ queue.add(Request(path, null, c))
+ return c
+ }
+
+ fun <T> get(knownRequest: KnownRequest<T>): CompletableFuture<T> {
+ return get(knownRequest.path, knownRequest.type)
+ }
+
+ data class KnownRequest<T>(val path: String, val type: Class<T>) {
+ fun <N> typed(newType: Class<N>) = KnownRequest(path, newType)
+ inline fun <reified N> typed() = typed(N::class.java)
+ }
+
+ @NEUAutoSubscribe
+ object TickHandler {
+ @SubscribeEvent
+ fun onTick(event: TickEvent) {
+ NotEnoughUpdates.INSTANCE.manager.ursaClient.bumpRequests()
+ }
+ }
+
+ companion object {
+ @JvmStatic
+ fun profiles(uuid: UUID) = KnownRequest("v1/hypixel/profiles/${uuid}", JsonObject::class.java)
+
+ @JvmStatic
+ fun player(uuid: UUID) = KnownRequest("v1/hypixel/player/${uuid}", JsonObject::class.java)
+
+ @JvmStatic
+ fun guild(uuid: UUID) = KnownRequest("v1/hypixel/guild/${uuid}", JsonObject::class.java)
+
+ @JvmStatic
+ fun bingo(uuid: UUID) = KnownRequest("v1/hypixel/bingo/${uuid}", JsonObject::class.java)
+
+ @JvmStatic
+ fun museumForProfile(profileUuid: String) = KnownRequest("v1/hypixel/museum/${profileUuid}", JsonObject::class.java)
+
+ @JvmStatic
+ fun status(uuid: UUID) = KnownRequest("v1/hypixel/status/${uuid}", JsonObject::class.java)
+ }
+}
diff --git a/src/main/kotlin/io/github/moulberry/notenoughupdates/util/hypixelapi/Collection.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/util/hypixelapi/Collection.kt
index b2c7fcec..e5c7263c 100644
--- a/src/main/kotlin/io/github/moulberry/notenoughupdates/util/hypixelapi/Collection.kt
+++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/util/hypixelapi/Collection.kt
@@ -95,7 +95,7 @@ data class ProfileCollectionInfo(
val hypixelCollectionInfo: CompletableFuture<CollectionMetadata> by lazy {
NotEnoughUpdates.INSTANCE.manager.apiUtils
- .newHypixelApiRequest("resources/skyblock/collections")
+ .newAnonymousHypixelApiRequest("resources/skyblock/collections")
.requestJson()
.thenApply {
NotEnoughUpdates.INSTANCE.manager.gson.fromJson(it, CollectionMetadata::class.java)
diff --git a/src/main/kotlin/io/github/moulberry/notenoughupdates/util/kotlin/Coroutines.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/util/kotlin/Coroutines.kt
index 4bcf0c61..d84bf082 100644
--- a/src/main/kotlin/io/github/moulberry/notenoughupdates/util/kotlin/Coroutines.kt
+++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/util/kotlin/Coroutines.kt
@@ -24,6 +24,7 @@ import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
import net.minecraftforge.fml.common.gameevent.TickEvent
import java.util.concurrent.CompletableFuture
import java.util.concurrent.Executor
+import java.util.concurrent.ForkJoinPool
import kotlin.coroutines.*
@NEUAutoSubscribe
@@ -54,6 +55,13 @@ object Coroutines {
}
}
+ fun <T> launchCoroutine(block: suspend () -> T): CompletableFuture<T> {
+ return launchCoroutineOnCurrentThread {
+ continueOn(ForkJoinPool.commonPool())
+ block()
+ }
+ }
+
private data class DelayedTask(val contination: () -> Unit, var tickDelay: Int)
private val tasks = mutableListOf<DelayedTask>()
@@ -76,6 +84,18 @@ object Coroutines {
}
}
+
+ suspend fun <T> CompletableFuture<T>.await(): T {
+ return suspendCoroutine { cont ->
+ handle { res, ex ->
+ if (ex != null)
+ cont.resumeWithException(ex)
+ else
+ cont.resume(res)
+ }
+ }
+ }
+
suspend fun waitTicks(tickCount: Int) {
suspendCoroutine {
synchronized(tasks) {