mirror of
https://github.com/FalsePattern/FalsePatternLib.git
synced 2026-11-11 17:17:32 +00:00
V2 update checker API. No more manual bumps!
This commit is contained in:
5 files changed
+289
-54
No files matched your search
@@ -43,7 +43,7 @@ import cpw.mods.fml.common.network.simpleimpl.SimpleNetworkWrapper;
|
||||
guiFactory = Tags.GROUPNAME + ".internal.config.LibraryGuiFactory",
|
||||
acceptableRemoteVersions = "*")
|
||||
public class FalsePatternLib {
|
||||
public static final String UPDATE_URL = "https://falsepattern.com/mc/versions.json";
|
||||
public static final String UPDATE_URL = "https://falsepattern.com/mc/api/v2/versions.json";
|
||||
public static final SimpleNetworkWrapper NETWORK = NetworkRegistry.INSTANCE.newSimpleChannel(Tags.MODID);
|
||||
|
||||
@SidedProxy(clientSide = Tags.GROUPNAME + ".internal.proxy.ClientProxy",
|
||||
|
||||
@@ -32,6 +32,8 @@ import java.io.OutputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
@@ -39,13 +41,40 @@ import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class Internet {
|
||||
private static final Map<String, String> NO_HEADERS = Collections.emptyMap();
|
||||
public static void connect(URL URL, Consumer<Exception> onError, Consumer<InputStream> onSuccess) {
|
||||
connect(URL, NO_HEADERS, onError, onSuccess);
|
||||
}
|
||||
|
||||
public static Map<String, String> constructHeaders(String... entries) {
|
||||
if (entries.length % 2 != 0) {
|
||||
throw new IllegalArgumentException("Entries must be in pairs");
|
||||
}
|
||||
val map = new java.util.HashMap<String, String>();
|
||||
for (int i = 0; i < entries.length; i += 2) {
|
||||
map.put(entries[i], entries[i + 1]);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
public static void connect(URL URL, Map<String, String> headers, Consumer<Exception> onError, Consumer<InputStream> onSuccess) {
|
||||
try {
|
||||
val connection = (HttpURLConnection) URL.openConnection();
|
||||
connection.setConnectTimeout(3500);
|
||||
connection.setReadTimeout(5000);
|
||||
connection.setRequestProperty("User-Agent", Tags.MODNAME + " " + Tags.VERSION + " Internet Connector" +
|
||||
" (https://github.com/FalsePattern/FalsePatternLib)");
|
||||
for (val header: headers.entrySet()) {
|
||||
val key = header.getKey();
|
||||
val value = header.getValue();
|
||||
if (key == null || value == null) {
|
||||
throw new IllegalArgumentException("Null key or value");
|
||||
}
|
||||
if (key.isEmpty()) {
|
||||
throw new IllegalArgumentException("Empty key");
|
||||
}
|
||||
connection.setRequestProperty(key, value);
|
||||
}
|
||||
if (connection.getResponseCode() != 200) {
|
||||
onError.accept(new Exception("HTTP response code " + connection.getResponseCode()));
|
||||
} else {
|
||||
|
||||
@@ -28,14 +28,17 @@ import com.falsepattern.lib.updates.ModUpdateInfo;
|
||||
import com.falsepattern.lib.updates.UpdateCheckException;
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import lombok.val;
|
||||
import lombok.var;
|
||||
|
||||
import net.minecraft.client.resources.I18n;
|
||||
import net.minecraft.event.ClickEvent;
|
||||
import net.minecraft.util.IChatComponent;
|
||||
import cpw.mods.fml.common.Loader;
|
||||
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
@@ -43,11 +46,9 @@ import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
public final class UpdateCheckerImpl {
|
||||
private static final AtomicBoolean jsonLibraryLoaded = new AtomicBoolean(false);
|
||||
|
||||
public static List<IChatComponent> updateListToChatMessages(String initiator, List<ModUpdateInfo> updates) {
|
||||
if (updates == null || updates.size() == 0) {
|
||||
return null;
|
||||
@@ -84,32 +85,37 @@ public final class UpdateCheckerImpl {
|
||||
return updateText;
|
||||
}
|
||||
|
||||
private static JsonArray fetchRootListShared(String url) {
|
||||
if (!LibraryConfig.ENABLE_UPDATE_CHECKER) {
|
||||
throw new CompletionException(new UpdateCheckException("Update checker is disabled in config!"));
|
||||
}
|
||||
URL URL;
|
||||
try {
|
||||
URL = new URL(url);
|
||||
} catch (MalformedURLException e) {
|
||||
throw new CompletionException(new UpdateCheckException("Invalid URL: " + url, e));
|
||||
}
|
||||
JsonElement parsed;
|
||||
try {
|
||||
parsed = new JsonParser().parse(Internet.download(URL).thenApply(String::new).join());
|
||||
} catch (CompletionException e) {
|
||||
throw new CompletionException(new UpdateCheckException("Failed to download update checker JSON file!",
|
||||
e.getCause() == null ? e : e.getCause()));
|
||||
}
|
||||
JsonArray modList;
|
||||
if (parsed.isJsonArray()) {
|
||||
modList = parsed.getAsJsonArray();
|
||||
} else {
|
||||
modList = new JsonArray();
|
||||
modList.add(parsed);
|
||||
}
|
||||
return modList;
|
||||
}
|
||||
|
||||
public static CompletableFuture<List<ModUpdateInfo>> fetchUpdatesAsync(String url) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
if (!LibraryConfig.ENABLE_UPDATE_CHECKER) {
|
||||
throw new CompletionException(new UpdateCheckException("Update checker is disabled in config!"));
|
||||
}
|
||||
URL URL;
|
||||
try {
|
||||
URL = new URL(url);
|
||||
} catch (MalformedURLException e) {
|
||||
throw new CompletionException(new UpdateCheckException("Invalid URL: " + url, e));
|
||||
}
|
||||
val modList = fetchRootListShared(url);
|
||||
val result = new ArrayList<ModUpdateInfo>();
|
||||
JsonElement parsed;
|
||||
try {
|
||||
parsed = new JsonParser().parse(Internet.download(URL).thenApply(String::new).join());
|
||||
} catch (CompletionException e) {
|
||||
throw new CompletionException(new UpdateCheckException("Failed to download update checker JSON file!",
|
||||
e.getCause() == null ? e : e.getCause()));
|
||||
}
|
||||
JsonArray modList;
|
||||
if (parsed.isJsonArray()) {
|
||||
modList = parsed.getAsJsonArray();
|
||||
} else {
|
||||
modList = new JsonArray();
|
||||
modList.add(parsed);
|
||||
}
|
||||
val installedMods = Loader.instance().getIndexedModList();
|
||||
for (val node : modList) {
|
||||
if (!node.isJsonObject()) {
|
||||
@@ -170,4 +176,159 @@ public final class UpdateCheckerImpl {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static CompletableFuture<List<ModUpdateInfo>> fetchUpdatesAsyncV2(String url) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
val modList = fetchRootListShared(url);
|
||||
val result = new ArrayList<ModUpdateInfo>();
|
||||
val installedMods = Loader.instance().getIndexedModList();
|
||||
for (val node : modList) {
|
||||
if (!node.isJsonObject()) {
|
||||
continue;
|
||||
}
|
||||
val obj = node.getAsJsonObject();
|
||||
if (!obj.has("modid")) {
|
||||
continue;
|
||||
}
|
||||
if (!obj.has("versions")) {
|
||||
continue;
|
||||
}
|
||||
val modid = obj.get("modid").getAsString();
|
||||
if (!installedMods.containsKey(modid)) {
|
||||
continue;
|
||||
}
|
||||
val mod = installedMods.get(modid);
|
||||
var versions = obj.get("versions");
|
||||
if (!versions.isJsonArray()) {
|
||||
if (!versions.isJsonObject()) {
|
||||
continue;
|
||||
}
|
||||
val ver = versions.getAsJsonObject();
|
||||
versions = new JsonArray();
|
||||
versions.getAsJsonArray().add(ver);
|
||||
}
|
||||
val parsedVersions = parseVersionSpecs(versions.getAsJsonArray()).join();
|
||||
val latestVersions = new ArrayList<String>();
|
||||
for (val version : parsedVersions) {
|
||||
val versionObj = version.getAsJsonObject();
|
||||
latestVersions.add(versionObj.get("version").getAsString());
|
||||
}
|
||||
val currentVersion = mod.getVersion();
|
||||
if (latestVersions.contains(currentVersion)) {
|
||||
continue;
|
||||
}
|
||||
val latest = parsedVersions.get(0).getAsJsonObject();
|
||||
result.add(new ModUpdateInfo(modid, currentVersion, latest.get("version").getAsString(), latest.get("url").getAsString()));
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
private static String getString(JsonObject object, String entry) {
|
||||
if (!object.has(entry)) {
|
||||
return null;
|
||||
}
|
||||
val element = object.get(entry);
|
||||
if (element.isJsonPrimitive() && element.getAsJsonPrimitive().isString()) {
|
||||
return element.getAsString();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static CompletableFuture<JsonArray> parseVersionSpecs(JsonArray versions) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
val result = new JsonArray();
|
||||
for (val entry: versions) {
|
||||
if (!entry.isJsonObject()) {
|
||||
continue;
|
||||
}
|
||||
val obj = entry.getAsJsonObject();
|
||||
val type = getString(obj, "type");
|
||||
if (type == null) {
|
||||
continue;
|
||||
}
|
||||
switch (type) {
|
||||
case "raw": {
|
||||
val version = getString(obj, "version");
|
||||
val url = getString(obj, "updateURL");
|
||||
if (version == null || url == null) {
|
||||
continue;
|
||||
}
|
||||
val res = new JsonObject();
|
||||
res.addProperty("version", version);
|
||||
res.addProperty("url", url);
|
||||
result.add(res);
|
||||
}
|
||||
case "github": {
|
||||
val repo = getString(obj, "repo");
|
||||
if (repo == null) {
|
||||
continue;
|
||||
}
|
||||
JsonObject response;
|
||||
try {
|
||||
response = parseLatestGithubVersion(repo);
|
||||
} catch (MalformedURLException e) {
|
||||
e.printStackTrace();
|
||||
continue;
|
||||
}
|
||||
if (response == null) {
|
||||
continue;
|
||||
}
|
||||
result.add(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
private static JsonObject parseLatestGithubVersion(String repo) throws MalformedURLException {
|
||||
val parts = repo.split("/");
|
||||
if (parts.length != 2) {
|
||||
return null;
|
||||
}
|
||||
val owner = parts[0];
|
||||
val name = parts[1];
|
||||
val response = new AtomicReference<JsonObject>();
|
||||
val result = new JsonObject();
|
||||
Internet.connect(new URL("https://api.github.com/repos/" + owner + "/" + name + "/releases/latest"),
|
||||
Internet.constructHeaders("Accept", "application/vnd.github+json",
|
||||
"X-GitHub-Api-Version", "2022-11-28"),
|
||||
(e) -> {throw new CompletionException(e);
|
||||
},
|
||||
(stream) -> {
|
||||
val parser = new JsonParser();
|
||||
val json = parser.parse(new InputStreamReader(stream));
|
||||
if (!json.isJsonObject()) {
|
||||
return;
|
||||
}
|
||||
response.set(json.getAsJsonObject());
|
||||
});
|
||||
if (response.get() == null) {
|
||||
return null;
|
||||
}
|
||||
val payload = response.get();
|
||||
val url = getString(payload, "html_url");
|
||||
val version = getString(payload, "tag_name");
|
||||
if (version == null) {
|
||||
return null;
|
||||
}
|
||||
result.addProperty("version", version);
|
||||
result.addProperty("url", url == null ? "" : url);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static List<ModUpdateInfo> fetchUpdatesV2(String url) throws UpdateCheckException {
|
||||
try {
|
||||
return fetchUpdatesAsyncV2(url).join();
|
||||
} catch (CompletionException e) {
|
||||
try {
|
||||
throw e.getCause();
|
||||
} catch (UpdateCheckException e1) {
|
||||
throw e1;
|
||||
} catch (Throwable e1) {
|
||||
throw new UpdateCheckException("Failed to check for updates!", e1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -58,7 +58,7 @@ public class CommonProxy {
|
||||
CommonEventHandlerPre.registerBus();
|
||||
if (LibraryConfig.ENABLE_UPDATE_CHECKER) {
|
||||
Share.LOG.info("Launching asynchronous update check.");
|
||||
updatesFuture = UpdateChecker.fetchUpdatesAsync(FalsePatternLib.UPDATE_URL).thenApplyAsync(updates -> {
|
||||
updatesFuture = UpdateChecker.fetchUpdatesAsyncV2(FalsePatternLib.UPDATE_URL).thenApplyAsync(updates -> {
|
||||
if (updates == null) {
|
||||
updates = Collections.emptyList();
|
||||
}
|
||||
|
||||
@@ -31,36 +31,81 @@ import java.util.concurrent.CompletableFuture;
|
||||
@StableAPI(since = "0.8.0")
|
||||
public final class UpdateChecker {
|
||||
/**
|
||||
* Checks for updates. The URL should be a JSON file that contains a list of mods, each with a mod ID, one or more
|
||||
* versions, and a URL for the user to check for updates in case the current and latest versions are different.
|
||||
* The JSON file must have the following format:
|
||||
* <pre>{@code
|
||||
* [
|
||||
* {
|
||||
* "modID": "modid",
|
||||
* "latestVersion": ["1.0.0", "1.0.0-foo"],
|
||||
* "updateURL": "https://example.com/mods/mymod"
|
||||
* },
|
||||
* {
|
||||
* "modID": "modid2",
|
||||
* "latestVersion": ["0.2.0", "0.3.0-alpha"],
|
||||
* "updateURL": "https://example.com/mods/mymod2"
|
||||
* },
|
||||
* ...etc, one json object per mod.
|
||||
* ]
|
||||
* }</pre>
|
||||
*
|
||||
* @param url The URL to check
|
||||
*
|
||||
* @return A list of mods that were both available on the URL and installed
|
||||
* DEPRECATED: Use the V2 API instead. This is kept for backwards compatibility.
|
||||
*/
|
||||
@StableAPI.Expose
|
||||
@Deprecated
|
||||
public static CompletableFuture<List<ModUpdateInfo>> fetchUpdatesAsync(String url) {
|
||||
return UpdateCheckerImpl.fetchUpdatesAsync(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Same this as {@link #fetchUpdatesAsync(String)}, but returns the result in a blocking fashion.
|
||||
* Checks for updates. The URL should be a JSON file that contains a list of mods, each with a mod ID, and one or more
|
||||
* versions. The JSON file must have the following format:
|
||||
* <pre>{@code
|
||||
* [
|
||||
* {
|
||||
* "modid": "modid",
|
||||
* "versions": [
|
||||
* {
|
||||
* ...version object...
|
||||
* },
|
||||
* {
|
||||
* ...version object...
|
||||
* }
|
||||
* ]
|
||||
* },
|
||||
* {
|
||||
* "modid": "modid2",
|
||||
* "versions": {
|
||||
* ...version object...
|
||||
* },
|
||||
* "updateURL": "https://example.com/mods/mymod2"
|
||||
* },
|
||||
* ...etc, one json object per mod.
|
||||
* ]
|
||||
* }</pre>
|
||||
* The currently supported version object formats are:
|
||||
* <p>
|
||||
* Raw version entry:
|
||||
* <pre>{@code
|
||||
* {
|
||||
* "type": "raw",
|
||||
* "version": "1.0.0",
|
||||
* "updateURL": "https://example.com/mods/mymod"
|
||||
* }
|
||||
* }</pre>
|
||||
*
|
||||
* GitHub version entry:
|
||||
* <pre>{@code
|
||||
* {
|
||||
* "type": "github",
|
||||
* "repo": "Example/ExampleMod"
|
||||
* }
|
||||
* }</pre>
|
||||
*
|
||||
* The GitHub version entry will automatically fetch the latest release from the specified repository using the GitHub API.
|
||||
*
|
||||
* @param url The URL to check
|
||||
*
|
||||
* @return A list of mods that were both available on the URL and installed
|
||||
*/
|
||||
@StableAPI.Expose(since = "0.11.0")
|
||||
public static CompletableFuture<List<ModUpdateInfo>> fetchUpdatesAsyncV2(String url) {
|
||||
return UpdateCheckerImpl.fetchUpdatesAsyncV2(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* DEPRECATED: Use the V2 API instead. This is kept for backwards compatibility.
|
||||
*/
|
||||
@StableAPI.Expose
|
||||
@Deprecated
|
||||
public static List<ModUpdateInfo> fetchUpdates(String url) throws UpdateCheckException {
|
||||
return UpdateCheckerImpl.fetchUpdates(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Same this as {@link #fetchUpdatesAsyncV2(String)}, but returns the result in a blocking fashion.
|
||||
*
|
||||
* @param url The URL to check
|
||||
*
|
||||
@@ -68,9 +113,9 @@ public final class UpdateChecker {
|
||||
*
|
||||
* @throws UpdateCheckException If the update checker is disabled in config, the URL is invalid, or
|
||||
*/
|
||||
@StableAPI.Expose
|
||||
public static List<ModUpdateInfo> fetchUpdates(String url) throws UpdateCheckException {
|
||||
return UpdateCheckerImpl.fetchUpdates(url);
|
||||
@StableAPI.Expose(since = "0.11.0")
|
||||
public static List<ModUpdateInfo> fetchUpdatesV2(String url) throws UpdateCheckException {
|
||||
return UpdateCheckerImpl.fetchUpdatesV2(url);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user