Skip to content

Commit 6edb48b

Browse files
committed
V2 update checker API. No more manual bumps!
1 parent ce28e3e commit 6edb48b

File tree

5 files changed

+277
-42
lines changed

5 files changed

+277
-42
lines changed

src/main/java/com/falsepattern/lib/internal/FalsePatternLib.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
guiFactory = Tags.GROUPNAME + ".internal.config.LibraryGuiFactory",
4444
acceptableRemoteVersions = "*")
4545
public class FalsePatternLib {
46-
public static final String UPDATE_URL = "https://falsepattern.com/mc/versions.json";
46+
public static final String UPDATE_URL = "https://falsepattern.com/mc/api/v2/versions.json";
4747
public static final SimpleNetworkWrapper NETWORK = NetworkRegistry.INSTANCE.newSimpleChannel(Tags.MODID);
4848

4949
@SidedProxy(clientSide = Tags.GROUPNAME + ".internal.proxy.ClientProxy",

src/main/java/com/falsepattern/lib/internal/Internet.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,20 +32,49 @@
3232
import java.net.HttpURLConnection;
3333
import java.net.URL;
3434
import java.nio.file.Files;
35+
import java.util.Collections;
36+
import java.util.Map;
3537
import java.util.concurrent.CompletableFuture;
3638
import java.util.concurrent.CompletionException;
3739
import java.util.concurrent.atomic.AtomicBoolean;
3840
import java.util.concurrent.atomic.AtomicReference;
3941
import java.util.function.Consumer;
4042

4143
public class Internet {
44+
private static final Map<String, String> NO_HEADERS = Collections.emptyMap();
4245
public static void connect(URL URL, Consumer<Exception> onError, Consumer<InputStream> onSuccess) {
46+
connect(URL, NO_HEADERS, onError, onSuccess);
47+
}
48+
49+
public static Map<String, String> constructHeaders(String... entries) {
50+
if (entries.length % 2 != 0) {
51+
throw new IllegalArgumentException("Entries must be in pairs");
52+
}
53+
val map = new java.util.HashMap<String, String>();
54+
for (int i = 0; i < entries.length; i += 2) {
55+
map.put(entries[i], entries[i + 1]);
56+
}
57+
return map;
58+
}
59+
60+
public static void connect(URL URL, Map<String, String> headers, Consumer<Exception> onError, Consumer<InputStream> onSuccess) {
4361
try {
4462
val connection = (HttpURLConnection) URL.openConnection();
4563
connection.setConnectTimeout(3500);
4664
connection.setReadTimeout(5000);
4765
connection.setRequestProperty("User-Agent", Tags.MODNAME + " " + Tags.VERSION + " Internet Connector" +
4866
" (https://github.com/FalsePattern/FalsePatternLib)");
67+
for (val header: headers.entrySet()) {
68+
val key = header.getKey();
69+
val value = header.getValue();
70+
if (key == null || value == null) {
71+
throw new IllegalArgumentException("Null key or value");
72+
}
73+
if (key.isEmpty()) {
74+
throw new IllegalArgumentException("Empty key");
75+
}
76+
connection.setRequestProperty(key, value);
77+
}
4978
if (connection.getResponseCode() != 200) {
5079
onError.accept(new Exception("HTTP response code " + connection.getResponseCode()));
5180
} else {

src/main/java/com/falsepattern/lib/internal/impl/updates/UpdateCheckerImpl.java

Lines changed: 187 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -28,26 +28,27 @@
2828
import com.falsepattern.lib.updates.UpdateCheckException;
2929
import com.google.gson.JsonArray;
3030
import com.google.gson.JsonElement;
31+
import com.google.gson.JsonObject;
3132
import com.google.gson.JsonParser;
3233
import lombok.val;
34+
import lombok.var;
3335

3436
import net.minecraft.client.resources.I18n;
3537
import net.minecraft.event.ClickEvent;
3638
import net.minecraft.util.IChatComponent;
3739
import cpw.mods.fml.common.Loader;
3840

41+
import java.io.InputStreamReader;
3942
import java.net.MalformedURLException;
4043
import java.net.URL;
4144
import java.util.ArrayList;
4245
import java.util.Collections;
4346
import java.util.List;
4447
import java.util.concurrent.CompletableFuture;
4548
import java.util.concurrent.CompletionException;
46-
import java.util.concurrent.atomic.AtomicBoolean;
49+
import java.util.concurrent.atomic.AtomicReference;
4750

4851
public final class UpdateCheckerImpl {
49-
private static final AtomicBoolean jsonLibraryLoaded = new AtomicBoolean(false);
50-
5152
public static List<IChatComponent> updateListToChatMessages(String initiator, List<ModUpdateInfo> updates) {
5253
if (updates == null || updates.size() == 0) {
5354
return null;
@@ -84,32 +85,37 @@ public static List<IChatComponent> updateListToChatMessages(String initiator, Li
8485
return updateText;
8586
}
8687

88+
private static JsonArray fetchRootListShared(String url) {
89+
if (!LibraryConfig.ENABLE_UPDATE_CHECKER) {
90+
throw new CompletionException(new UpdateCheckException("Update checker is disabled in config!"));
91+
}
92+
URL URL;
93+
try {
94+
URL = new URL(url);
95+
} catch (MalformedURLException e) {
96+
throw new CompletionException(new UpdateCheckException("Invalid URL: " + url, e));
97+
}
98+
JsonElement parsed;
99+
try {
100+
parsed = new JsonParser().parse(Internet.download(URL).thenApply(String::new).join());
101+
} catch (CompletionException e) {
102+
throw new CompletionException(new UpdateCheckException("Failed to download update checker JSON file!",
103+
e.getCause() == null ? e : e.getCause()));
104+
}
105+
JsonArray modList;
106+
if (parsed.isJsonArray()) {
107+
modList = parsed.getAsJsonArray();
108+
} else {
109+
modList = new JsonArray();
110+
modList.add(parsed);
111+
}
112+
return modList;
113+
}
114+
87115
public static CompletableFuture<List<ModUpdateInfo>> fetchUpdatesAsync(String url) {
88116
return CompletableFuture.supplyAsync(() -> {
89-
if (!LibraryConfig.ENABLE_UPDATE_CHECKER) {
90-
throw new CompletionException(new UpdateCheckException("Update checker is disabled in config!"));
91-
}
92-
URL URL;
93-
try {
94-
URL = new URL(url);
95-
} catch (MalformedURLException e) {
96-
throw new CompletionException(new UpdateCheckException("Invalid URL: " + url, e));
97-
}
117+
val modList = fetchRootListShared(url);
98118
val result = new ArrayList<ModUpdateInfo>();
99-
JsonElement parsed;
100-
try {
101-
parsed = new JsonParser().parse(Internet.download(URL).thenApply(String::new).join());
102-
} catch (CompletionException e) {
103-
throw new CompletionException(new UpdateCheckException("Failed to download update checker JSON file!",
104-
e.getCause() == null ? e : e.getCause()));
105-
}
106-
JsonArray modList;
107-
if (parsed.isJsonArray()) {
108-
modList = parsed.getAsJsonArray();
109-
} else {
110-
modList = new JsonArray();
111-
modList.add(parsed);
112-
}
113119
val installedMods = Loader.instance().getIndexedModList();
114120
for (val node : modList) {
115121
if (!node.isJsonObject()) {
@@ -170,4 +176,159 @@ public static List<ModUpdateInfo> fetchUpdates(String url) throws UpdateCheckExc
170176
}
171177
}
172178
}
179+
180+
public static CompletableFuture<List<ModUpdateInfo>> fetchUpdatesAsyncV2(String url) {
181+
return CompletableFuture.supplyAsync(() -> {
182+
val modList = fetchRootListShared(url);
183+
val result = new ArrayList<ModUpdateInfo>();
184+
val installedMods = Loader.instance().getIndexedModList();
185+
for (val node : modList) {
186+
if (!node.isJsonObject()) {
187+
continue;
188+
}
189+
val obj = node.getAsJsonObject();
190+
if (!obj.has("modid")) {
191+
continue;
192+
}
193+
if (!obj.has("versions")) {
194+
continue;
195+
}
196+
val modid = obj.get("modid").getAsString();
197+
if (!installedMods.containsKey(modid)) {
198+
continue;
199+
}
200+
val mod = installedMods.get(modid);
201+
var versions = obj.get("versions");
202+
if (!versions.isJsonArray()) {
203+
if (!versions.isJsonObject()) {
204+
continue;
205+
}
206+
val ver = versions.getAsJsonObject();
207+
versions = new JsonArray();
208+
versions.getAsJsonArray().add(ver);
209+
}
210+
val parsedVersions = parseVersionSpecs(versions.getAsJsonArray()).join();
211+
val latestVersions = new ArrayList<String>();
212+
for (val version : parsedVersions) {
213+
val versionObj = version.getAsJsonObject();
214+
latestVersions.add(versionObj.get("version").getAsString());
215+
}
216+
val currentVersion = mod.getVersion();
217+
if (latestVersions.contains(currentVersion)) {
218+
continue;
219+
}
220+
val latest = parsedVersions.get(0).getAsJsonObject();
221+
result.add(new ModUpdateInfo(modid, currentVersion, latest.get("version").getAsString(), latest.get("url").getAsString()));
222+
}
223+
return result;
224+
});
225+
}
226+
227+
private static String getString(JsonObject object, String entry) {
228+
if (!object.has(entry)) {
229+
return null;
230+
}
231+
val element = object.get(entry);
232+
if (element.isJsonPrimitive() && element.getAsJsonPrimitive().isString()) {
233+
return element.getAsString();
234+
}
235+
return null;
236+
}
237+
238+
private static CompletableFuture<JsonArray> parseVersionSpecs(JsonArray versions) {
239+
return CompletableFuture.supplyAsync(() -> {
240+
val result = new JsonArray();
241+
for (val entry: versions) {
242+
if (!entry.isJsonObject()) {
243+
continue;
244+
}
245+
val obj = entry.getAsJsonObject();
246+
val type = getString(obj, "type");
247+
if (type == null) {
248+
continue;
249+
}
250+
switch (type) {
251+
case "raw": {
252+
val version = getString(obj, "version");
253+
val url = getString(obj, "updateURL");
254+
if (version == null || url == null) {
255+
continue;
256+
}
257+
val res = new JsonObject();
258+
res.addProperty("version", version);
259+
res.addProperty("url", url);
260+
result.add(res);
261+
}
262+
case "github": {
263+
val repo = getString(obj, "repo");
264+
if (repo == null) {
265+
continue;
266+
}
267+
JsonObject response;
268+
try {
269+
response = parseLatestGithubVersion(repo);
270+
} catch (MalformedURLException e) {
271+
e.printStackTrace();
272+
continue;
273+
}
274+
if (response == null) {
275+
continue;
276+
}
277+
result.add(response);
278+
}
279+
}
280+
}
281+
return result;
282+
});
283+
}
284+
285+
private static JsonObject parseLatestGithubVersion(String repo) throws MalformedURLException {
286+
val parts = repo.split("/");
287+
if (parts.length != 2) {
288+
return null;
289+
}
290+
val owner = parts[0];
291+
val name = parts[1];
292+
val response = new AtomicReference<JsonObject>();
293+
val result = new JsonObject();
294+
Internet.connect(new URL("https://api.github.com/repos/" + owner + "/" + name + "/releases/latest"),
295+
Internet.constructHeaders("Accept", "application/vnd.github+json",
296+
"X-GitHub-Api-Version", "2022-11-28"),
297+
(e) -> {throw new CompletionException(e);
298+
},
299+
(stream) -> {
300+
val parser = new JsonParser();
301+
val json = parser.parse(new InputStreamReader(stream));
302+
if (!json.isJsonObject()) {
303+
return;
304+
}
305+
response.set(json.getAsJsonObject());
306+
});
307+
if (response.get() == null) {
308+
return null;
309+
}
310+
val payload = response.get();
311+
val url = getString(payload, "html_url");
312+
val version = getString(payload, "tag_name");
313+
if (version == null) {
314+
return null;
315+
}
316+
result.addProperty("version", version);
317+
result.addProperty("url", url == null ? "" : url);
318+
return result;
319+
}
320+
321+
public static List<ModUpdateInfo> fetchUpdatesV2(String url) throws UpdateCheckException {
322+
try {
323+
return fetchUpdatesAsyncV2(url).join();
324+
} catch (CompletionException e) {
325+
try {
326+
throw e.getCause();
327+
} catch (UpdateCheckException e1) {
328+
throw e1;
329+
} catch (Throwable e1) {
330+
throw new UpdateCheckException("Failed to check for updates!", e1);
331+
}
332+
}
333+
}
173334
}

src/main/java/com/falsepattern/lib/internal/proxy/CommonProxy.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ public void preInit(FMLPreInitializationEvent e) {
5858
CommonEventHandlerPre.registerBus();
5959
if (LibraryConfig.ENABLE_UPDATE_CHECKER) {
6060
Share.LOG.info("Launching asynchronous update check.");
61-
updatesFuture = UpdateChecker.fetchUpdatesAsync(FalsePatternLib.UPDATE_URL).thenApplyAsync(updates -> {
61+
updatesFuture = UpdateChecker.fetchUpdatesAsyncV2(FalsePatternLib.UPDATE_URL).thenApplyAsync(updates -> {
6262
if (updates == null) {
6363
updates = Collections.emptyList();
6464
}

0 commit comments

Comments
 (0)