Skip to content

Commit 2b31428

Browse files
chore: make update-checker more configurable and link other download pages (#3091)
1 parent 09473c5 commit 2b31428

File tree

5 files changed

+237
-66
lines changed

5 files changed

+237
-66
lines changed

worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/WorldEditListener.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
package com.sk89q.worldedit.bukkit;
2323

24+
import com.fastasyncworldedit.core.configuration.Settings;
2425
import com.fastasyncworldedit.core.util.UpdateNotification;
2526
import com.sk89q.worldedit.LocalSession;
2627
import com.sk89q.worldedit.WorldEdit;
@@ -102,7 +103,9 @@ public void onJoin(PlayerJoinEvent event) {
102103
if ((session = WorldEdit.getInstance().getSessionManager().getIfPresent(player)) != null) {
103104
session.loadDefaults(player, true);
104105
}
105-
UpdateNotification.doUpdateNotification(player);
106+
if (Settings.settings().ENABLED_COMPONENTS.NOTIFY_UPDATE_INGAME) {
107+
UpdateNotification.doUpdateNotification(player);
108+
}
106109
}
107110
//FAWE end
108111

worldedit-core/src/main/java/com/fastasyncworldedit/core/configuration/Settings.java

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -289,8 +289,18 @@ public static final class ENABLED_COMPONENTS {
289289
@Comment({"Show additional information in console. It helps us at IntellectualSites to find out more about an issue.",
290290
"Leave it off if you don't need it, it can spam your console."})
291291
public boolean DEBUG = false;
292-
@Comment({"Whether or not FAWE should notify you on startup about new versions available."})
293-
public boolean UPDATE_NOTIFICATIONS = true;
292+
293+
@Migrate("enabled-components.update-notification")
294+
@Comment({"Whether or not FAWE should notify you on startup about new available snapshots."})
295+
public boolean SNAPSHOT_UPDATE_NOTIFICATIONS = true;
296+
297+
@Migrate("enabled-components.update-notification")
298+
@Comment({"Whether or not FAWE should notify you on startup about new releases."})
299+
public boolean RELEASE_UPDATE_NOTIFICATIONS = true;
300+
301+
@Migrate("enabled-components.update-notification")
302+
@Comment({"Whether or not FAWE should notify you for updates (snapshot / release) on join (with the required permission)"})
303+
public boolean NOTIFY_UPDATE_INGAME = true;
294304

295305
}
296306

worldedit-core/src/main/java/com/fastasyncworldedit/core/util/UpdateNotification.java

Lines changed: 189 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -4,82 +4,144 @@
44
import com.fastasyncworldedit.core.FaweVersion;
55
import com.fastasyncworldedit.core.configuration.Caption;
66
import com.fastasyncworldedit.core.configuration.Settings;
7+
import com.sk89q.util.StringUtil;
78
import com.sk89q.worldedit.extension.platform.Actor;
89
import com.sk89q.worldedit.internal.util.LogManagerCompat;
910
import com.sk89q.worldedit.util.formatting.text.TextComponent;
1011
import com.sk89q.worldedit.util.formatting.text.event.ClickEvent;
12+
import com.sk89q.worldedit.util.formatting.text.format.TextColor;
1113
import org.apache.logging.log4j.Logger;
12-
import org.w3c.dom.Document;
14+
import org.jetbrains.annotations.VisibleForTesting;
1315

14-
import javax.xml.XMLConstants;
15-
import javax.xml.parsers.DocumentBuilder;
16-
import javax.xml.parsers.DocumentBuilderFactory;
17-
import java.io.InputStream;
1816
import java.net.URI;
1917
import java.net.http.HttpClient;
2018
import java.net.http.HttpRequest;
2119
import java.net.http.HttpResponse;
22-
import java.time.Duration;
23-
import java.time.temporal.ChronoUnit;
20+
import java.util.Arrays;
21+
import java.util.List;
22+
import java.util.concurrent.CompletableFuture;
23+
import java.util.concurrent.TimeUnit;
24+
import java.util.regex.Matcher;
25+
import java.util.regex.Pattern;
2426

2527
public class UpdateNotification {
2628

29+
private static final String GITHUB_LAST_RELEASE = "https://api.github.com/repos/IntellectualSites/FastAsyncWorldEdit/releases/latest";
30+
private static final String JENKINS_LAST_BUILD = "https://ci.athion.net/job/FastAsyncWorldEdit/lastSuccessfulBuild/api/json";
31+
32+
private static final String LINK_DOWNLOAD_SPIGOTMC = "https://www.spigotmc.org/resources/13932";
33+
private static final String LINK_DOWNLOAD_JENKINS = "https://ci.athion.net/job/FastAsyncWorldEdit";
34+
private static final String LINK_DOWNLOAD_MODRINTH = "https://modrinth.com/plugin/fastasyncworldedit";
35+
private static final String LINK_DOWNLOAD_HANGAR = "https://hangar.papermc.io/IntellectualSites/FastAsyncWorldEdit";
36+
37+
private static final String CONSOLE_NOTIFICATION_OUTDATED_RELEASE = """
38+
A new release for FastAsyncWorldEdit is available: {}. You are currently on {}.
39+
Download from {}, {} or {}""";
40+
private static final String CONSOLE_NOTIFICATION_OUTDATED_BUILD = """
41+
An update for FastAsyncWorldEdit is available. You are {} build(s) out of date.
42+
You are running build {}, the latest version is build {}.
43+
Update at {}""";
44+
2745
private static final Logger LOGGER = LogManagerCompat.getLogger();
46+
private static final HttpClient HTTP_CLIENT = HttpClient.newHttpClient();
47+
private static final Pattern GITHUB_RESPONSE_TAG_NAME_PATTERN = Pattern.compile("\"tag_name\":\"([\\d.]+)\"");
48+
private static final Pattern JENKINS_RESPONSE_BUILD_PATTERN = Pattern.compile("\"number\":(\\d+)");
2849

29-
private static boolean hasUpdate;
30-
private static String faweVersion = "";
50+
private static volatile int[] lastRelease;
51+
private static volatile int lastBuild = -1;
3152

3253
/**
3354
* Check whether a new build with a higher build number than the current build is available.
3455
*/
3556
public static void doUpdateCheck() {
36-
if (Settings.settings().ENABLED_COMPONENTS.UPDATE_NOTIFICATIONS) {
37-
final HttpRequest request = HttpRequest
38-
.newBuilder()
39-
.uri(URI.create("https://ci.athion.net/job/FastAsyncWorldEdit/api/xml/"))
40-
.timeout(Duration.of(10L, ChronoUnit.SECONDS))
41-
.build();
42-
HttpClient.newHttpClient()
43-
.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream())
44-
.whenComplete((response, thrown) -> {
45-
if (thrown != null) {
46-
LOGGER.error("Update check failed: {} ", thrown.getMessage());
47-
}
48-
processResponseBody(response.body());
49-
});
50-
57+
if (hasUpdateInfo()) {
58+
return;
59+
}
60+
final FaweVersion installedVersion = Fawe.instance().getVersion();
61+
if (installedVersion == null || (installedVersion.build == 0 && installedVersion.snapshot)) {
62+
LOGGER.warn("You are using a snapshot or a custom version of FAWE. " +
63+
"This is not an official build distributed via https://www.spigotmc.org/resources/13932/");
64+
return;
65+
}
66+
if (Settings.settings().ENABLED_COMPONENTS.SNAPSHOT_UPDATE_NOTIFICATIONS) {
67+
checkLatestBuild().orTimeout(10, TimeUnit.SECONDS).whenComplete((build, throwable) -> {
68+
if (throwable != null) {
69+
LOGGER.error("Failed to check for latest build", throwable);
70+
return;
71+
}
72+
lastBuild = build;
73+
int difference = lastBuild - installedVersion.build;
74+
if (difference < 1) {
75+
return;
76+
}
77+
LOGGER.warn(CONSOLE_NOTIFICATION_OUTDATED_BUILD, difference, installedVersion.build, lastBuild, LINK_DOWNLOAD_JENKINS);
78+
});
79+
}
80+
if (Settings.settings().ENABLED_COMPONENTS.RELEASE_UPDATE_NOTIFICATIONS) {
81+
checkLatestRelease().orTimeout(10, TimeUnit.SECONDS).whenComplete((version, throwable) -> {
82+
if (throwable != null) {
83+
LOGGER.error("Failed to check for latest release", throwable);
84+
return;
85+
}
86+
lastRelease = version;
87+
if (installedVersion.semver != null && hasUpdateSemver(installedVersion.semver, version)) {
88+
LOGGER.warn(CONSOLE_NOTIFICATION_OUTDATED_RELEASE,
89+
StringUtil.joinString(lastRelease, ".", 0),
90+
StringUtil.joinString(installedVersion.semver, ".", 0),
91+
LINK_DOWNLOAD_MODRINTH, LINK_DOWNLOAD_HANGAR, LINK_DOWNLOAD_SPIGOTMC
92+
);
93+
}
94+
});
5195
}
5296
}
5397

54-
private static void processResponseBody(InputStream body) {
55-
try {
56-
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
57-
dbf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
58-
DocumentBuilder db = dbf.newDocumentBuilder();
59-
Document doc = db.parse(body);
60-
faweVersion = doc.getElementsByTagName("lastSuccessfulBuild").item(0).getFirstChild().getTextContent();
61-
FaweVersion faweVersion = Fawe.instance().getVersion();
62-
if (faweVersion.build == 0 && faweVersion.snapshot) {
63-
LOGGER.warn("You are using a snapshot or a custom version of FAWE. This is not an official build distributed " +
64-
"via https://www.spigotmc.org/resources/13932/");
65-
return;
98+
private static CompletableFuture<int[]> checkLatestRelease() {
99+
return HTTP_CLIENT.sendAsync(
100+
HttpRequest.newBuilder().GET().uri(URI.create(GITHUB_LAST_RELEASE)).build(),
101+
HttpResponse.BodyHandlers.ofString()
102+
).thenApply(response -> {
103+
if (response.statusCode() != 200) {
104+
throw new UpdateCheckException("GitHub returned status code " + response.statusCode());
66105
}
67-
if (faweVersion.snapshot && faweVersion.build < Integer.parseInt(UpdateNotification.faweVersion)) {
68-
hasUpdate = true;
69-
int versionDifference = Integer.parseInt(UpdateNotification.faweVersion) - faweVersion.build;
70-
LOGGER.warn(
71-
"""
72-
An update for FastAsyncWorldEdit is available. You are {} build(s) out of date.
73-
You are running build {}, the latest version is build {}.
74-
Update at https://www.spigotmc.org/resources/13932/""",
75-
versionDifference,
76-
faweVersion.build,
77-
UpdateNotification.faweVersion
78-
);
106+
return response.body();
107+
}).thenApply(body -> {
108+
final Matcher matcher = GITHUB_RESPONSE_TAG_NAME_PATTERN.matcher(body);
109+
if (!matcher.find()) {
110+
throw new UpdateCheckException("Couldn't find tag name in response");
79111
}
80-
} catch (Exception ignored) {
81-
LOGGER.error("Unable to check for updates. Skipping.");
82-
}
112+
try {
113+
return Arrays.stream(matcher.group(1).split("\\.")).toList().stream().mapToInt(Integer::parseInt).toArray();
114+
} catch (NumberFormatException e) {
115+
throw new UpdateCheckException("Couldn't parse version", e);
116+
}
117+
}).thenApply(version -> {
118+
if (version.length != 3) {
119+
throw new UpdateCheckException("Retrieved malformed version (%s)".formatted(Arrays.toString(version)));
120+
}
121+
return version;
122+
});
123+
}
124+
125+
private static CompletableFuture<Integer> checkLatestBuild() {
126+
return HTTP_CLIENT.sendAsync(
127+
HttpRequest.newBuilder().GET().uri(URI.create(JENKINS_LAST_BUILD)).build(),
128+
HttpResponse.BodyHandlers.ofString()
129+
).thenApply(response -> {
130+
if (response.statusCode() != 200) {
131+
throw new UpdateCheckException("Jenkins returned status code " + response.statusCode());
132+
}
133+
return response.body();
134+
}).thenApply(body -> {
135+
final Matcher matcher = JENKINS_RESPONSE_BUILD_PATTERN.matcher(body);
136+
if (!matcher.find()) {
137+
throw new UpdateCheckException("Couldn't find latest build in response");
138+
}
139+
try {
140+
return Integer.parseInt(matcher.group(1));
141+
} catch (NumberFormatException e) {
142+
throw new UpdateCheckException("Couldn't parse build", e);
143+
}
144+
});
83145
}
84146

85147
/**
@@ -88,21 +150,86 @@ private static void processResponseBody(InputStream body) {
88150
* @param actor The player to notify.
89151
*/
90152
public static void doUpdateNotification(Actor actor) {
91-
if (Settings.settings().ENABLED_COMPONENTS.UPDATE_NOTIFICATIONS) {
92-
if (actor.hasPermission("fawe.admin") && UpdateNotification.hasUpdate) {
93-
FaweVersion faweVersion = Fawe.instance().getVersion();
94-
int versionDifference = Integer.parseInt(UpdateNotification.faweVersion) - faweVersion.build;
153+
if (!isAnyUpdateCheckEnabled() || !actor.hasPermission("fawe.admin") || !hasUpdateInfo()) {
154+
return;
155+
}
156+
final FaweVersion installed = Fawe.instance().getVersion();
157+
if (installed == null) {
158+
return;
159+
}
160+
if (lastBuild != -1 && Settings.settings().ENABLED_COMPONENTS.SNAPSHOT_UPDATE_NOTIFICATIONS) {
161+
int difference = lastBuild - installed.build;
162+
if (difference > 0) {
163+
actor.print(Caption.of(
164+
"fawe.info.update-available.build",
165+
difference, installed.build, lastBuild,
166+
TextComponent.of(LINK_DOWNLOAD_JENKINS).clickEvent(ClickEvent.openUrl(LINK_DOWNLOAD_JENKINS))
167+
));
168+
}
169+
}
170+
if (installed.semver != null && lastRelease != null && Settings.settings().ENABLED_COMPONENTS.RELEASE_UPDATE_NOTIFICATIONS) {
171+
if (hasUpdateSemver(installed.semver, lastRelease)) {
95172
actor.print(Caption.of(
96-
"fawe.info.update-available",
97-
versionDifference,
98-
faweVersion.build,
99-
UpdateNotification.faweVersion,
100-
TextComponent
101-
.of("https://www.spigotmc.org/resources/13932/")
102-
.clickEvent(ClickEvent.openUrl("https://www.spigotmc.org/resources/13932/"))
173+
"fawe.info.update-available.release",
174+
StringUtil.joinString(lastRelease, ".", 0),
175+
StringUtil.joinString(installed.semver, ".", 0),
176+
TextComponent.empty().children(List.of(
177+
TextComponent
178+
.of("Modrinth")
179+
.color(TextColor.GREEN)
180+
.clickEvent(ClickEvent.openUrl(LINK_DOWNLOAD_MODRINTH)),
181+
TextComponent.empty().color(TextColor.GRAY)
182+
)),
183+
TextComponent.empty().children(List.of(
184+
TextComponent
185+
.of("Hangar")
186+
.color(TextColor.BLUE)
187+
.clickEvent(ClickEvent.openUrl(LINK_DOWNLOAD_HANGAR)),
188+
TextComponent.empty().color(TextColor.GRAY)
189+
)),
190+
TextComponent.empty().children(List.of(
191+
TextComponent
192+
.of("SpigotMC")
193+
.color(TextColor.GOLD)
194+
.clickEvent(ClickEvent.openUrl(LINK_DOWNLOAD_SPIGOTMC)),
195+
TextComponent.empty().color(TextColor.GRAY)
196+
))
103197
));
104198
}
105199
}
106200
}
107201

202+
@VisibleForTesting
203+
static boolean hasUpdateSemver(int[] installed, int[] latest) {
204+
for (int i = 0; i < Math.max(installed.length, latest.length); i++) {
205+
final int installedPart = i < installed.length ? installed[i] : 0;
206+
final int latestPart = i < latest.length ? latest[i] : 0;
207+
if (installedPart != latestPart) {
208+
return installedPart < latestPart;
209+
}
210+
}
211+
return false;
212+
}
213+
214+
private static boolean hasUpdateInfo() {
215+
return lastRelease != null || lastBuild != -1;
216+
}
217+
218+
private static boolean isAnyUpdateCheckEnabled() {
219+
return Settings.settings().ENABLED_COMPONENTS.RELEASE_UPDATE_NOTIFICATIONS
220+
|| Settings.settings().ENABLED_COMPONENTS.SNAPSHOT_UPDATE_NOTIFICATIONS;
221+
}
222+
223+
private static final class UpdateCheckException extends RuntimeException {
224+
225+
public UpdateCheckException(final String message) {
226+
super("Failed to check for update: " + message);
227+
}
228+
229+
public UpdateCheckException(final String message, final Throwable cause) {
230+
super("Failed to check for update: " + message, cause);
231+
}
232+
233+
}
234+
108235
}

worldedit-core/src/main/resources/lang/strings.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
"fawe.info.worldedit.oom.admin": "Possible options:\n - //fast\n - Do smaller edits\n - Allocate more memory\n - Disable `max-memory-percent`",
1515
"fawe.info.temporarily-not-working": "Temporarily not working",
1616
"fawe.info.light-blocks": "Light blocks are more reliable than light sources, please use the blocks. This command is deprecated and will be removed in a future version.",
17-
"fawe.info.update-available": "An update for FastAsyncWorldEdit is available. You are {0} build(s) out of date.\nYou are running build {1}, the latest version is build {2}.\nUpdate at {3}",
17+
"fawe.info.update-available.build": "An update for FastAsyncWorldEdit is available. You are {0} build(s) behind.\nYou are running build {1}, the latest build is {2}.\nUpdate at {3}",
18+
"fawe.info.update-available.release": "A new release for FastAsyncWorldEdit is available: {0}. You are currently on {1}. Download from {2}, {3} or {4}.",
1819
"fawe.web.generating.link": "Uploading {0}, please wait...",
1920
"fawe.web.generating.link.failed": "Failed to generate download link!",
2021
"fawe.web.download.link": "{0}",
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package com.fastasyncworldedit.core.util;
2+
3+
import org.junit.jupiter.params.ParameterizedTest;
4+
import org.junit.jupiter.params.provider.CsvSource;
5+
6+
import java.util.Arrays;
7+
8+
import static org.junit.jupiter.api.Assertions.*;
9+
10+
class UpdateNotificationTest {
11+
12+
@ParameterizedTest
13+
@CsvSource({"1.0.0,2.0.0", "1.0.0,1.1.0", "1.0.0,1.0.1", "1.0.0,1.0.0.1"})
14+
void hasUpdateSemver_true(String installed, String latest) {
15+
assertTrue(UpdateNotification.hasUpdateSemver(
16+
Arrays.stream(installed.split("\\.")).mapToInt(Integer::parseInt).toArray(),
17+
Arrays.stream(latest.split("\\.")).mapToInt(Integer::parseInt).toArray()
18+
));
19+
}
20+
21+
@ParameterizedTest
22+
@CsvSource({"1.0.0,1.0.0", "2.0.0,1.9.9", "1.0.0,1.0", "1.0,1.0.0"})
23+
void hasUpdateSemver_false(String installed, String latest) {
24+
assertFalse(UpdateNotification.hasUpdateSemver(
25+
Arrays.stream(installed.split("\\.")).mapToInt(Integer::parseInt).toArray(),
26+
Arrays.stream(latest.split("\\.")).mapToInt(Integer::parseInt).toArray()
27+
));
28+
}
29+
30+
}

0 commit comments

Comments
 (0)