Skip to content

Commit

Permalink
Fix update checker making hundreds of API requests
Browse files Browse the repository at this point in the history
- Update checker now only makes a single Modrinth API request
- Added config for modpack authors to disable specific mods' update checkers
  • Loading branch information
Prospector committed Feb 17, 2023
1 parent 6b5cce0 commit bcb555c
Show file tree
Hide file tree
Showing 12 changed files with 137 additions and 96 deletions.
15 changes: 7 additions & 8 deletions src/main/java/com/terraformersmc/modmenu/ModMenu.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,15 @@
import org.slf4j.LoggerFactory;

import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.*;
import java.util.function.Supplier;

public class ModMenu implements ClientModInitializer {
public static final String MOD_ID = "modmenu";
public static final String GITHUB_REF = "TerraformersMC/ModMenu";
public static final Logger LOGGER = LoggerFactory.getLogger("Mod Menu");
public static final Gson GSON = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).setPrettyPrinting().create();
public static final Gson GSON_MINIFIED = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create();

public static final Map<String, Mod> MODS = new HashMap<>();
public static final Map<String, Mod> ROOT_MODS = new HashMap<>();
Expand All @@ -50,6 +46,7 @@ public class ModMenu implements ClientModInitializer {
private static List<Supplier<Map<String, ConfigScreenFactory<?>>>> dynamicScreenFactories = new ArrayList<>();

private static int cachedDisplayedModCount = -1;
public static boolean runningQuilt = false;

public static Screen getConfigScreen(String modid, Screen menuScreen) {
if (ModMenuConfig.HIDDEN_CONFIGS.getValue().contains(modid)) {
Expand Down Expand Up @@ -94,15 +91,17 @@ public void onInitializeClient() {
if (FabricLoader.getInstance().isModLoaded("quilt_loader")) {
QuiltMod mod = new QuiltMod(modContainer, modpackMods);
MODS.put(mod.getId(), mod);
ModrinthUtil.checkForUpdates(mod);
} else {
FabricMod mod = new FabricMod(modContainer, modpackMods);
MODS.put(mod.getId(), mod);
ModrinthUtil.checkForUpdates(mod);
}
}
}

if (ModMenuConfig.UPDATE_CHECKER.getValue()) {
ModrinthUtil.checkForUpdates();
}

Map<String, Mod> dummyParents = new HashMap<>();

// Initialize parent map
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,9 @@ public class ModMenuConfig {
public static final BooleanConfigOption DISABLE_DRAG_AND_DROP = new BooleanConfigOption("disable_drag_and_drop", false);
public static final StringSetConfigOption HIDDEN_MODS = new StringSetConfigOption("hidden_mods", new HashSet<>());
public static final StringSetConfigOption HIDDEN_CONFIGS = new StringSetConfigOption("hidden_configs", new HashSet<>());
public static final StringSetConfigOption DISABLE_UPDATE_CHECKER = new StringSetConfigOption("disable_update_checker", new HashSet<>());
public static final BooleanConfigOption UPDATE_CHECKER = new BooleanConfigOption("update_checker", true);
public static final BooleanConfigOption UPDATE_BADGE = new BooleanConfigOption("update_badge", true);
public static final BooleanConfigOption BUTTON_UPDATE_BADGE = new BooleanConfigOption("button_update_badge", true);

public static SimpleOption<?>[] asOptions() {
ArrayList<SimpleOption<?>> options = new ArrayList<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,14 +81,20 @@ public void render(MatrixStack matrices, int mouseX, int mouseY, float delta) {
}
}

if (mod.getModrinthData() != null) {
children().add(new DescriptionEntry(OrderedText.EMPTY, this));
children().add(new DescriptionEntry(Text.translatable("modmenu.hasUpdate").asOrderedText(), this).setUpdateTextEntry());
children().add(new LinkEntry(
Text.translatable("modmenu.updateText", mod.getModrinthData().versionNumber(), Text.translatable("modmenu.modrinth"))
.formatted(Formatting.BLUE)
.formatted(Formatting.UNDERLINE)
.asOrderedText(), "https://modrinth.com/mod/%s/version/%s".formatted(mod.getModrinthData().projectId(), mod.getModrinthData().versionId()), this, 8));
if (ModMenuConfig.UPDATE_CHECKER.getValue() && !ModMenuConfig.DISABLE_UPDATE_CHECKER.getValue().contains(mod.getId())) {
if (mod.getModrinthData() != null) {
children().add(new DescriptionEntry(OrderedText.EMPTY, this));
children().add(new DescriptionEntry(Text.translatable("modmenu.hasUpdate").asOrderedText(), this).setUpdateTextEntry());
children().add(new LinkEntry(
Text.translatable("modmenu.updateText", mod.getModrinthData().versionNumber(), Text.translatable("modmenu.modrinth"))
.formatted(Formatting.BLUE)
.formatted(Formatting.UNDERLINE)
.asOrderedText(), "https://modrinth.com/project/%s/version/%s".formatted(mod.getModrinthData().projectId(), mod.getModrinthData().versionId()), this, 8));
}
if (mod.getChildHasUpdate()) {
children().add(new DescriptionEntry(OrderedText.EMPTY, this));
children().add(new DescriptionEntry(Text.translatable("modmenu.childHasUpdate").asOrderedText(), this).setUpdateTextEntry());
}
}

Map<String, String> links = mod.getLinks();
Expand Down Expand Up @@ -268,7 +274,7 @@ public void render(MatrixStack matrices, int index, int y, int x, int itemWidth,
}
if (updateTextEntry) {
UpdateAvailableBadge.renderBadge(matrices, x + indent, y);
x += 10;
x += 11;
}
textRenderer.drawWithShadow(matrices, text, x + indent, y, 0xAAAAAA);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@

public class ModListWidget extends AlwaysSelectedEntryListWidget<ModListEntry> implements AutoCloseable {
public static final boolean DEBUG = Boolean.getBoolean("modmenu.debug");

private final ModsScreen parent;
private List<Mod> mods = null;
private final Set<Mod> addedMods = new HashSet<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public ModMenuButtonWidget(int x, int y, int width, int height, Text text, Scree
@Override
public void render(MatrixStack matrices, int mouseX, int mouseY, float delta) {
super.render(matrices, mouseX, mouseY, delta);
if (ModMenuConfig.UPDATE_BADGE.getValue() && ModMenu.modUpdateAvailable) {
if (ModMenuConfig.BUTTON_UPDATE_BADGE.getValue() && ModMenu.modUpdateAvailable) {
UpdateAvailableBadge.renderBadge(matrices, this.width + this.getX() - 16, this.height / 2 + this.getY() - 4);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public UpdateCheckerTexturedButtonWidget(int x, int y, int width, int height, in
@Override
public void renderButton(MatrixStack matrices, int mouseX, int mouseY, float delta) {
super.renderButton(matrices, mouseX, mouseY, delta);
if (ModMenuConfig.UPDATE_BADGE.getValue() && ModMenu.modUpdateAvailable) {
if (ModMenuConfig.BUTTON_UPDATE_BADGE.getValue() && ModMenu.modUpdateAvailable) {
UpdateAvailableBadge.renderBadge(matrices, this.getX() + this.width - 5, this.getY() - 3);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,9 @@ public void render(MatrixStack matrices, int index, int y, int x, int rowWidth,
}
font.draw(matrices, Language.getInstance().reorder(trimmedName), x + iconSize + 3, y + 1, 0xFFFFFF);
var updateBadgeXOffset = 0;
if (ModMenuConfig.UPDATE_CHECKER.getValue() && mod.getModrinthData() != null) {
if (ModMenuConfig.UPDATE_CHECKER.getValue() && !ModMenuConfig.DISABLE_UPDATE_CHECKER.getValue().contains(mod.getId()) && (mod.getModrinthData() != null || mod.getChildHasUpdate())) {
UpdateAvailableBadge.renderBadge(matrices, x + iconSize + 3 + font.getWidth(name) + 2, y);
updateBadgeXOffset = 10;
updateBadgeXOffset = 11;
}
if (!ModMenuConfig.HIDE_BADGES.getValue()) {
new ModBadgeRenderer(x + iconSize + 3 + font.getWidth(name) + 2 + updateBadgeXOffset, y, x + rowWidth, mod, list.getParent()).draw(matrices, mouseX, mouseY);
Expand Down
142 changes: 75 additions & 67 deletions src/main/java/com/terraformersmc/modmenu/util/ModrinthUtil.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.terraformersmc.modmenu.util;

import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.annotations.SerializedName;
import com.terraformersmc.modmenu.ModMenu;
import com.terraformersmc.modmenu.config.ModMenuConfig;
import com.terraformersmc.modmenu.util.mod.Mod;
Expand All @@ -11,94 +13,86 @@
import net.minecraft.client.toast.SystemToast;
import net.minecraft.text.Text;
import net.minecraft.util.Util;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import java.util.*;

public class ModrinthUtil {
public static final Logger LOGGER = LoggerFactory.getLogger("Mod Menu/Update Checker");

private static final HttpClient client = HttpClient.newHttpClient();
private static boolean apiV2Deprecated = false;

public static @Nullable void checkForUpdates(Mod mod) {
if (!mod.allowsUpdateChecks() || !ModMenuConfig.UPDATE_CHECKER.getValue() || apiV2Deprecated) {
return;
}
public static void checkForUpdates() {
Util.getMainWorkerExecutor().execute(() -> {
LOGGER.info("Checking mod updates...");
Map<String, Set<Mod>> HASH_TO_MOD = new HashMap<>();
new ArrayList<>(ModMenu.MODS.values()).stream().filter(mod -> mod.allowsUpdateChecks() &&
ModMenuConfig.UPDATE_CHECKER.getValue() &&
!ModMenuConfig.DISABLE_UPDATE_CHECKER.getValue().contains(mod.getId()) &&
!apiV2Deprecated)
.forEach(mod -> {
try {
String hash = mod.getSha512Hash();
if (hash != null) {
LOGGER.debug("Hash for {} is {}", mod.getId(), hash);
HASH_TO_MOD.putIfAbsent(hash, new HashSet<>());
HASH_TO_MOD.get(hash).add(mod);
}
} catch (IOException e) {
LOGGER.error("Error checking for updates: ", e);
}
});
final var userAgent = "%s/%s".formatted(
ModMenu.GITHUB_REF,
FabricLoader.getInstance().getModContainer(ModMenu.MOD_ID)
.get().getMetadata().getVersion().getFriendlyString());
String body = ModMenu.GSON_MINIFIED.toJson(new LatestVersionsFromHashesBody(HASH_TO_MOD.keySet(), ModMenu.runningQuilt ? "quilt" : "fabric", SharedConstants.getGameVersion().getName()));
LOGGER.debug("Body: " + body);
var latestVersionsRequest = HttpRequest.newBuilder()
.POST(HttpRequest.BodyPublishers.ofString(body))
.header("User-Agent", userAgent)
.header("Content-Type", "application/json")
.uri(URI.create("https://api.modrinth.com/v2/version_files/update"))
.build();

try {
var localHash = mod.getSha512Hash();
if (localHash == null) {
LOGGER.debug("Unable to check for updates of '{}@{}' without local hash.", mod.getId(), mod.getVersion());
return;
}
var userAgent = "%s/%s".formatted(
ModMenu.GITHUB_REF,
FabricLoader.getInstance().getModContainer(ModMenu.MOD_ID)
.get().getMetadata().getVersion().getFriendlyString());
var versionReq = HttpRequest.newBuilder()
.GET()
.header("User-Agent", userAgent)
.uri(URI.create("https://api.modrinth.com/v2/version_file/%s?algorithm=sha512".formatted(localHash)))
.build();
try {
var versionRsp = client.send(versionReq, HttpResponse.BodyHandlers.ofString());
if (versionRsp.statusCode() == 404) {
LOGGER.debug("Unable to find a Modrinth version that matches local file hash of '{}@{}'", mod.getId(), mod.getVersion());
} else if (versionRsp.statusCode() == 410) {
apiV2Deprecated = true;
LOGGER.warn("Modrinth API v2 is deprecated, unable to check for mod updates.");
} else if (versionRsp.statusCode() == 200) {
LOGGER.debug("Found matching version file hash on Modrinth for '{}@{}'.", mod.getId(), mod.getVersion());
// https://docs.modrinth.com/api-spec/#tag/version-files/operation/versionFromHash
var versionObj = JsonParser.parseString(versionRsp.body()).getAsJsonObject();
var modrinthVersion = versionObj.get("version_number").getAsString();
var projectId = versionObj.get("project_id").getAsString();
var latestVersionsResponse = client.send(latestVersionsRequest, HttpResponse.BodyHandlers.ofString());

var latestReq = HttpRequest.newBuilder()
.GET()
.header("User-Agent", userAgent)
.uri(URI.create("https://api.modrinth.com/v2/project/%s/version?loaders=%s&game_versions=%s".formatted(
projectId,
URLEncoder.encode("[\"%s\"]".formatted(FabricLoader.getInstance().isModLoaded("quilt_loader") ? "quilt" : "fabric"), StandardCharsets.UTF_8),
URLEncoder.encode("[\"%s\"]".formatted(SharedConstants.getGameVersion().getName()), StandardCharsets.UTF_8)
)))
.build();
var latestRsp = client.send(latestReq, HttpResponse.BodyHandlers.ofString());
if (latestRsp.statusCode() == 404) {
// This probably won't happen since we check earlier but better safe than sorry.
LOGGER.debug("Unable to find versions for '{}@{}'", mod.getId(), mod.getVersion());
} else if (latestRsp.statusCode() == 200) {
LOGGER.debug("Getting latest version from Modrinth.");
var versions = JsonParser.parseString(latestRsp.body()).getAsJsonArray();
var latestObj = versions.get(0).getAsJsonObject();
var latestVersion = latestObj.get("version_number").getAsString();
var latestId = latestObj.get("id").getAsString();
var latestHash = latestObj.get("files").getAsJsonArray().asList()
.stream().filter(file -> file.getAsJsonObject().get("primary").getAsBoolean()).findFirst()
.get().getAsJsonObject().get("hashes").getAsJsonObject().get("sha512").getAsString();
if (!Objects.equals(latestHash, localHash)) {
// hashes different, there's an update.
LOGGER.info("Update available for '{}@{}', ({} -> {})", mod.getId(), mod.getVersion(), modrinthVersion, latestVersion);
mod.setModrinthData(new ModrinthData(projectId, latestId, latestVersion));
int status = latestVersionsResponse.statusCode();
LOGGER.debug("Status: " + status);
if (status == 410) {
apiV2Deprecated = true;
LOGGER.warn("Modrinth API v2 is deprecated, unable to check for mod updates.");
} else if (status == 200) {
JsonObject responseObject = JsonParser.parseString(latestVersionsResponse.body()).getAsJsonObject();
LOGGER.debug(String.valueOf(responseObject));
responseObject.asMap().forEach((lookupHash, versionJson) -> {
var versionObj = versionJson.getAsJsonObject();
var projectId = versionObj.get("project_id").getAsString();
var versionNumber = versionObj.get("version_number").getAsString();
var versionId = versionObj.get("id").getAsString();
var versionHash = versionObj.get("files").getAsJsonArray().asList()
.stream().filter(file -> file.getAsJsonObject().get("primary").getAsBoolean()).findFirst()
.get().getAsJsonObject().get("hashes").getAsJsonObject().get("sha512").getAsString();
if (!Objects.equals(versionHash, lookupHash)) {
// hashes different, there's an update.
HASH_TO_MOD.get(lookupHash).forEach(mod -> {
LOGGER.info("Update available for '{}@{}', (-> {})", mod.getId(), mod.getVersion(), versionNumber);
mod.setModrinthData(new ModrinthData(projectId, versionId, versionNumber));
ModMenu.modUpdateAvailable = true;
}
});
}
}
} catch (IOException | InterruptedException e) {
LOGGER.error("Error contacting Modrinth for {}@{}.", mod.getId(), mod.getVersion(), e);
});
}
} catch (IOException e) {
LOGGER.warn("Failed sha512 hash for {}, skipping update check.", mod.getId(), e);
} catch (IOException | InterruptedException e) {
LOGGER.error("Error checking for updates: ", e);
}
});
}
Expand All @@ -112,4 +106,18 @@ public static void triggerV2DeprecatedToast() {
));
}
}

public static class LatestVersionsFromHashesBody {
public Collection<String> hashes;
public String algorithm = "sha512";
public Collection<String> loaders;
@SerializedName("game_versions")
public Collection<String> gameVersions;

public LatestVersionsFromHashesBody(Collection<String> hashes, String loader, String mcVersion) {
this.hashes = hashes;
this.loaders = Set.of(loader);
this.gameVersions = Set.of(mcVersion);
}
}
}
4 changes: 4 additions & 0 deletions src/main/java/com/terraformersmc/modmenu/util/mod/Mod.java
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ default String getTranslatedDescription() {

void setModrinthData(ModrinthData modrinthData);

void setChildHasUpdate();

boolean getChildHasUpdate();

enum Badge {
LIBRARY("modmenu.badge.library", 0xff107454, 0xff093929, "library"),
CLIENT("modmenu.badge.clientsideOnly", 0xff2b4b7c, 0xff0e2a55, null),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
public class FabricDummyParentMod implements Mod {
private final String id;
private final FabricMod host;
private boolean childHasUpdate;

public FabricDummyParentMod(FabricMod host, String id) {
this.host = host;
Expand Down Expand Up @@ -168,4 +169,14 @@ public void setModrinthData(ModrinthData modrinthData) {
public boolean allowsUpdateChecks() {
return false;
}

@Override
public boolean getChildHasUpdate() {
return childHasUpdate;
}

@Override
public void setChildHasUpdate() {
this.childHasUpdate = true;
}
}
Loading

0 comments on commit bcb555c

Please sign in to comment.