From 1b4890a60ac3aa420dd6a8873f6eadda9e154dfe Mon Sep 17 00:00:00 2001 From: Cyborger1 <45152844+Cyborger1@users.noreply.github.com> Date: Tue, 14 Nov 2023 14:54:15 -0500 Subject: [PATCH 01/10] First WIP on API-V2 --- .../botdetector/http/BotDetectorClient.java | 81 ++++++++++--------- 1 file changed, 44 insertions(+), 37 deletions(-) diff --git a/src/main/java/com/botdetector/http/BotDetectorClient.java b/src/main/java/com/botdetector/http/BotDetectorClient.java index ed00e175..fb3b556d 100644 --- a/src/main/java/com/botdetector/http/BotDetectorClient.java +++ b/src/main/java/com/botdetector/http/BotDetectorClient.java @@ -85,22 +85,24 @@ public class BotDetectorClient private static final String API_VERSION_FALLBACK_WORD = "latest"; private static final HttpUrl BASE_HTTP_URL = HttpUrl.parse( System.getProperty("BotDetectorAPIPath", "https://api.prd.osrsbotdetector.com")); + private static final HttpUrl BASE_HTTP_URL_V2 = HttpUrl.parse( + System.getProperty("BotDetectorAPIPathV2", "https://api-v2.prd.osrsbotdetector.com")); private static final Supplier CURRENT_EPOCH_SUPPLIER = () -> String.valueOf(Instant.now().getEpochSecond()); @Getter @AllArgsConstructor private enum ApiPath { - DETECTION("v1/report"), - PLAYER_STATS_PASSIVE("v1/report/count"), - PLAYER_STATS_MANUAL("v1/report/manual/count"), - PLAYER_STATS_FEEDBACK("v1/feedback/count"), - PREDICTION("v1/prediction"), - FEEDBACK("v1/feedback/"), - VERIFY_DISCORD("site/discord_user/") + DETECTION("v2/report", true), + PLAYER_STATS_REPORTS("v2/player/report/score", true), + PLAYER_STATS_FEEDBACK("v2/player/feedback/score", true), + PREDICTION("v2/player/prediction", true), + FEEDBACK("v1/feedback/", false), + VERIFY_DISCORD("site/discord_user/", false) ; final String path; + final boolean v2; } public OkHttpClient okHttpClient; @@ -123,7 +125,7 @@ private enum ApiPath */ private HttpUrl getUrl(ApiPath path, boolean addVersion) { - HttpUrl.Builder builder = BASE_HTTP_URL.newBuilder(); + HttpUrl.Builder builder = (path.isV2() ? BASE_HTTP_URL_V2 : BASE_HTTP_URL).newBuilder(); if (addVersion) { @@ -398,7 +400,15 @@ public void onResponse(Call call, Response response) { try { - future.complete(processResponse(gson, response, Prediction.class)); + Collection preds = processResponse(gson, response, new TypeToken>() + { + }.getType()); + if (preds != null) + { + future.complete(preds.stream().findFirst().orElse(null)); + return; + } + future.complete(null); } catch (IOException e) { @@ -422,18 +432,8 @@ public void onResponse(Call call, Response response) */ public CompletableFuture> requestPlayerStats(String playerName) { - Gson bdGson = gson.newBuilder() - .registerTypeAdapter(boolean.class, new BooleanToZeroOneConverter()) - .create(); - - Request requestP = new Request.Builder() - .url(getUrl(ApiPath.PLAYER_STATS_PASSIVE).newBuilder() - .addQueryParameter("name", playerName) - .build()) - .build(); - - Request requestM = new Request.Builder() - .url(getUrl(ApiPath.PLAYER_STATS_MANUAL).newBuilder() + Request requestR = new Request.Builder() + .url(getUrl(ApiPath.PLAYER_STATS_REPORTS).newBuilder() .addQueryParameter("name", playerName) .build()) .build(); @@ -444,18 +444,16 @@ public CompletableFuture> requestPlayerStats(S .build()) .build(); - CompletableFuture> passiveFuture = new CompletableFuture<>(); - CompletableFuture> manualFuture = new CompletableFuture<>(); + CompletableFuture> reportsFuture = new CompletableFuture<>(); CompletableFuture> feedbackFuture = new CompletableFuture<>(); - okHttpClient.newCall(requestP).enqueue(new PlayerStatsCallback(passiveFuture, bdGson)); - okHttpClient.newCall(requestM).enqueue(new PlayerStatsCallback(manualFuture, bdGson)); - okHttpClient.newCall(requestF).enqueue(new PlayerStatsCallback(feedbackFuture, bdGson)); + okHttpClient.newCall(requestR).enqueue(new PlayerStatsCallback(reportsFuture, gson)); + okHttpClient.newCall(requestF).enqueue(new PlayerStatsCallback(feedbackFuture, gson)); CompletableFuture> finalFuture = new CompletableFuture<>(); - // Doing this so we log only the first future failing, not all 3 within the callback. - CompletableFuture.allOf(passiveFuture, manualFuture, feedbackFuture).whenComplete((v, e) -> + // Doing this so we log only the first future failing, not all 2 within the callback. + CompletableFuture.allOf(reportsFuture, feedbackFuture).whenComplete((v, e) -> { if (e != null) { @@ -465,8 +463,7 @@ public CompletableFuture> requestPlayerStats(S } else { - finalFuture.complete(processPlayerStats( - passiveFuture.join(), manualFuture.join(), feedbackFuture.join())); + finalFuture.complete(processPlayerStats(reportsFuture.join(), feedbackFuture.join())); } }); @@ -592,20 +589,21 @@ private IOException getIOException(Response response) /** * Collects the given {@link PlayerStatsAPIItem} into a combined map that the plugin expects. - * @param passive The passive usage stats from the API. - * @param manual The manual flagging stats from the API. + * @param reports The reports usage stats from the API. * @param feedback The feedback stats from the API. * @return The combined processed map expected by the plugin. */ - private Map processPlayerStats(Collection passive, Collection manual, Collection feedback) + private Map processPlayerStats(Collection reports, Collection feedback) { - if (passive == null || manual == null || feedback == null) + if (reports == null || feedback == null) { return null; } - PlayerStats passiveStats = countStats(passive, false); - PlayerStats manualStats = countStats(manual, true); + PlayerStats passiveStats = countStats(reports.stream().filter( + r -> r.getManual() != null && !r.getManual()).collect(Collectors.toList()), false); + PlayerStats manualStats = countStats(reports.stream().filter( + r -> r.getManual() != null && r.getManual()).collect(Collectors.toList()), true); PlayerStats feedbackStats = countStats(feedback, false); PlayerStats totalStats = PlayerStats.builder() @@ -623,7 +621,7 @@ private Map processPlayerStats(Collection Date: Thu, 1 Feb 2024 19:40:10 -0500 Subject: [PATCH 02/10] Lowercase placeholder labels --- src/main/java/com/botdetector/ui/BotDetectorPanel.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/botdetector/ui/BotDetectorPanel.java b/src/main/java/com/botdetector/ui/BotDetectorPanel.java index 846dec80..0e160703 100644 --- a/src/main/java/com/botdetector/ui/BotDetectorPanel.java +++ b/src/main/java/com/botdetector/ui/BotDetectorPanel.java @@ -160,9 +160,9 @@ public enum WarningLabel private static final int MAX_FEEDBACK_TEXT_CHARS = 250; private static final Dimension FEEDBACK_TEXTBOX_PREFERRED_SIZE = new Dimension(0, 75); - private static final FeedbackPredictionLabel UNSURE_PREDICTION_LABEL = new FeedbackPredictionLabel("Unsure", null, FeedbackValue.NEUTRAL); - private static final FeedbackPredictionLabel SOMETHING_ELSE_PREDICTION_LABEL = new FeedbackPredictionLabel("Something_else", null, FeedbackValue.NEGATIVE); - private static final FeedbackPredictionLabel CORRECT_FALLBACK_PREDICTION_LABEL = new FeedbackPredictionLabel("Correct", null, FeedbackValue.POSITIVE); + private static final FeedbackPredictionLabel UNSURE_PREDICTION_LABEL = new FeedbackPredictionLabel("unsure", null, FeedbackValue.NEUTRAL); + private static final FeedbackPredictionLabel SOMETHING_ELSE_PREDICTION_LABEL = new FeedbackPredictionLabel("something_else", null, FeedbackValue.NEGATIVE); + private static final FeedbackPredictionLabel CORRECT_FALLBACK_PREDICTION_LABEL = new FeedbackPredictionLabel("correct", null, FeedbackValue.POSITIVE); private static final PlayerStatsType[] PLAYER_STAT_TYPES = { PlayerStatsType.TOTAL, PlayerStatsType.PASSIVE, PlayerStatsType.MANUAL From c66be9c3c177281a200c433744b9a2118dd95644 Mon Sep 17 00:00:00 2001 From: Cyborger1 <45152844+Cyborger1@users.noreply.github.com> Date: Thu, 1 Feb 2024 19:40:40 -0500 Subject: [PATCH 03/10] Use v2/feeback, remove v2 api switch --- .../com/botdetector/http/BotDetectorClient.java | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/botdetector/http/BotDetectorClient.java b/src/main/java/com/botdetector/http/BotDetectorClient.java index fb3b556d..a40bceea 100644 --- a/src/main/java/com/botdetector/http/BotDetectorClient.java +++ b/src/main/java/com/botdetector/http/BotDetectorClient.java @@ -85,24 +85,21 @@ public class BotDetectorClient private static final String API_VERSION_FALLBACK_WORD = "latest"; private static final HttpUrl BASE_HTTP_URL = HttpUrl.parse( System.getProperty("BotDetectorAPIPath", "https://api.prd.osrsbotdetector.com")); - private static final HttpUrl BASE_HTTP_URL_V2 = HttpUrl.parse( - System.getProperty("BotDetectorAPIPathV2", "https://api-v2.prd.osrsbotdetector.com")); private static final Supplier CURRENT_EPOCH_SUPPLIER = () -> String.valueOf(Instant.now().getEpochSecond()); @Getter @AllArgsConstructor private enum ApiPath { - DETECTION("v2/report", true), - PLAYER_STATS_REPORTS("v2/player/report/score", true), - PLAYER_STATS_FEEDBACK("v2/player/feedback/score", true), - PREDICTION("v2/player/prediction", true), - FEEDBACK("v1/feedback/", false), - VERIFY_DISCORD("site/discord_user/", false) + DETECTION("v2/report"), + PLAYER_STATS_REPORTS("v2/player/report/score"), + PLAYER_STATS_FEEDBACK("v2/player/feedback/score"), + PREDICTION("v2/player/prediction"), + FEEDBACK("v2/feedback"), + VERIFY_DISCORD("site/discord_user") ; final String path; - final boolean v2; } public OkHttpClient okHttpClient; @@ -125,7 +122,7 @@ private enum ApiPath */ private HttpUrl getUrl(ApiPath path, boolean addVersion) { - HttpUrl.Builder builder = (path.isV2() ? BASE_HTTP_URL_V2 : BASE_HTTP_URL).newBuilder(); + HttpUrl.Builder builder = BASE_HTTP_URL.newBuilder(); if (addVersion) { From c0c48e5ef40d4bdd3e31a618963d52f6a5629f76 Mon Sep 17 00:00:00 2001 From: Cyborger1 <45152844+Cyborger1@users.noreply.github.com> Date: Mon, 26 Feb 2024 17:15:02 -0500 Subject: [PATCH 04/10] Revert to v1/feedback for now --- src/main/java/com/botdetector/http/BotDetectorClient.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/botdetector/http/BotDetectorClient.java b/src/main/java/com/botdetector/http/BotDetectorClient.java index a40bceea..c775e989 100644 --- a/src/main/java/com/botdetector/http/BotDetectorClient.java +++ b/src/main/java/com/botdetector/http/BotDetectorClient.java @@ -95,7 +95,7 @@ private enum ApiPath PLAYER_STATS_REPORTS("v2/player/report/score"), PLAYER_STATS_FEEDBACK("v2/player/feedback/score"), PREDICTION("v2/player/prediction"), - FEEDBACK("v2/feedback"), + FEEDBACK("v1/feedback/"), // Remove last forward slash when using v2! VERIFY_DISCORD("site/discord_user") ; From c1e3ce7673906b8035aacaec31de7cb102dd19b4 Mon Sep 17 00:00:00 2001 From: Cyborger1 <45152844+Cyborger1@users.noreply.github.com> Date: Tue, 29 Jul 2025 16:22:05 -0400 Subject: [PATCH 05/10] Turn v2 feedback back on --- src/main/java/com/botdetector/http/BotDetectorClient.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/botdetector/http/BotDetectorClient.java b/src/main/java/com/botdetector/http/BotDetectorClient.java index c775e989..13c9f653 100644 --- a/src/main/java/com/botdetector/http/BotDetectorClient.java +++ b/src/main/java/com/botdetector/http/BotDetectorClient.java @@ -65,6 +65,7 @@ import lombok.Value; import lombok.extern.slf4j.Slf4j; import net.runelite.api.kit.KitType; +import okhttp3.Cache; import okhttp3.Call; import okhttp3.Callback; import okhttp3.HttpUrl; @@ -95,7 +96,7 @@ private enum ApiPath PLAYER_STATS_REPORTS("v2/player/report/score"), PLAYER_STATS_FEEDBACK("v2/player/feedback/score"), PREDICTION("v2/player/prediction"), - FEEDBACK("v1/feedback/"), // Remove last forward slash when using v2! + FEEDBACK("v2/feedback"), VERIFY_DISCORD("site/discord_user") ; From d39336792249964e07f48736d7b8ca322bb5b20c Mon Sep 17 00:00:00 2001 From: Cyborger1 <45152844+Cyborger1@users.noreply.github.com> Date: Tue, 29 Jul 2025 18:08:24 -0400 Subject: [PATCH 06/10] Fix Feedback Send button getting smooshed in the side panel --- src/main/java/com/botdetector/ui/BotDetectorPanel.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/botdetector/ui/BotDetectorPanel.java b/src/main/java/com/botdetector/ui/BotDetectorPanel.java index 0e160703..5d78541c 100644 --- a/src/main/java/com/botdetector/ui/BotDetectorPanel.java +++ b/src/main/java/com/botdetector/ui/BotDetectorPanel.java @@ -783,7 +783,7 @@ private JPanel predictionFeedbackPanel() feedbackLabelComboBox.setRenderer(new ComboBoxSelfTextTooltipListRenderer<>()); c.gridy++; c.gridx = 0; - c.weightx = 2.0 / 3; + c.weightx = 1; c.gridwidth = 2; panel.add(feedbackLabelComboBox, c); @@ -794,7 +794,7 @@ private JPanel predictionFeedbackPanel() feedbackSendButton.addActionListener(l -> sendFeedbackToClient((FeedbackPredictionLabel)feedbackLabelComboBox.getSelectedItem())); feedbackSendButton.setFocusable(false); c.gridx = 2; - c.weightx = 1.0 / 3; + c.weightx = 0; c.gridwidth = 1; panel.add(feedbackSendButton, c); From fce88d4fd0cfcc551b5ebe760691c09a2eccb620 Mon Sep 17 00:00:00 2001 From: Cyborger1 <45152844+Cyborger1@users.noreply.github.com> Date: Tue, 29 Jul 2025 18:16:55 -0400 Subject: [PATCH 07/10] Introduce Labels endpoint to predictions breakdown and lowercase all labels --- .../botdetector/http/BotDetectorClient.java | 129 ++++++++++++++++-- 1 file changed, 121 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/botdetector/http/BotDetectorClient.java b/src/main/java/com/botdetector/http/BotDetectorClient.java index 13c9f653..050c633c 100644 --- a/src/main/java/com/botdetector/http/BotDetectorClient.java +++ b/src/main/java/com/botdetector/http/BotDetectorClient.java @@ -50,8 +50,10 @@ import com.google.inject.Singleton; import java.io.IOException; import java.lang.reflect.Type; +import java.time.Duration; import java.time.Instant; import java.util.Collection; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -88,6 +90,8 @@ public class BotDetectorClient System.getProperty("BotDetectorAPIPath", "https://api.prd.osrsbotdetector.com")); private static final Supplier CURRENT_EPOCH_SUPPLIER = () -> String.valueOf(Instant.now().getEpochSecond()); + private static final long LABELS_CACHE_SECONDS = 60 * 60; // One hour + @Getter @AllArgsConstructor private enum ApiPath @@ -97,6 +101,7 @@ private enum ApiPath PLAYER_STATS_FEEDBACK("v2/player/feedback/score"), PREDICTION("v2/player/prediction"), FEEDBACK("v2/feedback"), + LABELS("v2/labels"), VERIFY_DISCORD("site/discord_user") ; @@ -115,6 +120,9 @@ private enum ApiPath private final Supplier pluginVersionSupplier = () -> (pluginVersion != null && !pluginVersion.isEmpty()) ? pluginVersion : API_VERSION_FALLBACK_WORD; + private Collection cachedLabels = null; + private Instant lastTimeCachedLabels = Instant.MIN; + /** * Constructs a base URL for the given {@code path}. * @param path The path to get the base URL for. @@ -376,21 +384,21 @@ public CompletableFuture requestPrediction(String playerName) */ public CompletableFuture requestPrediction(String playerName, boolean receiveBreakdownOnSpecialCases) { - Request request = new Request.Builder() + Request request_pred = new Request.Builder() .url(getUrl(ApiPath.PREDICTION).newBuilder() .addQueryParameter("name", playerName) .addQueryParameter("breakdown", Boolean.toString(receiveBreakdownOnSpecialCases)) .build()) .build(); - CompletableFuture future = new CompletableFuture<>(); - okHttpClient.newCall(request).enqueue(new Callback() + CompletableFuture predFuture = new CompletableFuture<>(); + okHttpClient.newCall(request_pred).enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { log.warn("Error obtaining player prediction data", e); - future.completeExceptionally(e); + predFuture.completeExceptionally(e); } @Override @@ -403,15 +411,15 @@ public void onResponse(Call call, Response response) }.getType()); if (preds != null) { - future.complete(preds.stream().findFirst().orElse(null)); + predFuture.complete(preds.stream().findFirst().orElse(null)); return; } - future.complete(null); + predFuture.complete(null); } catch (IOException e) { log.warn("Error obtaining player prediction data", e); - future.completeExceptionally(e); + predFuture.completeExceptionally(e); } finally { @@ -420,7 +428,105 @@ public void onResponse(Call call, Response response) } }); - return future; + CompletableFuture> labelsFuture = new CompletableFuture<>(); + + Instant now = Instant.now(); + if (Duration.between(lastTimeCachedLabels, now).getSeconds() <= LABELS_CACHE_SECONDS) + { + labelsFuture.complete(cachedLabels); + } + else + { + Request request_labels = new Request.Builder() + .url(getUrl(ApiPath.LABELS).newBuilder() + .build()) + .build(); + + okHttpClient.newCall(request_labels).enqueue(new Callback() + { + @Override + public void onFailure(Call call, IOException e) + { + log.warn("Error obtaining labels data", e); + labelsFuture.completeExceptionally(e); + } + + @Override + public void onResponse(Call call, Response response) + { + try + { + Collection labels = processResponse(gson, response, new TypeToken>() + { + }.getType()); + if (labels != null) + { + cachedLabels = labels; + lastTimeCachedLabels = now; + } + labelsFuture.complete(labels); + } + catch (IOException e) + { + log.warn("Error obtaining player labels data", e); + labelsFuture.completeExceptionally(e); + } + finally + { + response.close(); + } + } + }); + } + + CompletableFuture finalFuture = new CompletableFuture<>(); + + // Doing this so we log only the first future failing, not all 2 within the callback. + CompletableFuture.allOf(predFuture, labelsFuture).whenComplete((v, e) -> + { + if (e != null) + { + // allOf will send a CompletionException when one of the futures fail, just get the cause. + log.warn("Error obtaining player prediction data", e.getCause()); + finalFuture.completeExceptionally(e.getCause()); + } + else + { + Prediction pred = predFuture.join(); + Collection labels = labelsFuture.join(); + + // Re-add predictions as lowercase + Map newBreakdown = new HashMap<>(); + if (pred.getPredictionBreakdown() != null) + { + for (Map.Entry entry : pred.getPredictionBreakdown().entrySet()) { + newBreakdown.put(entry.getKey().toLowerCase(), entry.getValue()); + } + } + + // Add labels that may not be in the breakdown + if (labels != null) + { + for (LabelAPIItem label : labels) + { + newBreakdown.putIfAbsent(label.getLabel().toLowerCase(), 0.0); + } + } + + // Build a new copy of the prediction object with normalized labels + finalFuture.complete( + Prediction.builder() + .playerName(pred.getPlayerName()) + .playerId(pred.getPlayerId()) + .confidence(pred.getConfidence()) + .predictionBreakdown(newBreakdown) + .predictionLabel(pred.getPredictionLabel().toLowerCase()) + .build() + ); + } + }); + + return finalFuture; } /** @@ -719,6 +825,13 @@ private static class PlayerStatsAPIItem Long vote; } + @Value + private static class LabelAPIItem + { + int id; + String label; + } + /** * Wrapper around the {@link PlayerSighting}'s json serializer. * Adds the reporter name as an element on the same level as the {@link PlayerSighting}'s fields. From c9abc2a2c884a315cad5312a2791f29842e51e7a Mon Sep 17 00:00:00 2001 From: Cyborger1 <45152844+Cyborger1@users.noreply.github.com> Date: Tue, 29 Jul 2025 18:48:32 -0400 Subject: [PATCH 08/10] Handle v2 Feedback's duplicate record warning a bit more gracefully, if a bit jank however --- .../botdetector/http/BotDetectorClient.java | 15 ++++++++--- .../com/botdetector/ui/BotDetectorPanel.java | 27 ++++++++++++++----- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/botdetector/http/BotDetectorClient.java b/src/main/java/com/botdetector/http/BotDetectorClient.java index 050c633c..2bdb7ff2 100644 --- a/src/main/java/com/botdetector/http/BotDetectorClient.java +++ b/src/main/java/com/botdetector/http/BotDetectorClient.java @@ -306,7 +306,7 @@ public void onResponse(Call call, Response response) } /** - * Sends a feedback to the API for the given prediction. + * Sends a feedback to the API for the given prediction. If a feedback is duplicated, the future will return false. * @param pred The prediction object to give a feedback for. * @param uploaderName The user's player name (See {@link BotDetectorPlugin#getUploaderName()}). * @param proposedLabel The user's proposed label and feedback. @@ -343,12 +343,21 @@ public void onResponse(Call call, Response response) { try { + boolean duplicated = false; + if (!response.isSuccessful()) { - throw getIOException(response); + IOException ioe = getIOException(response); + // If the error is because of being a duplicate record, do not throw + // Instead return false and let the caller handle it + if (!ioe.getMessage().contains("duplicate_record")) + { + throw ioe; + } + duplicated = true; } - future.complete(true); + future.complete(!duplicated); } catch (IOException e) { diff --git a/src/main/java/com/botdetector/ui/BotDetectorPanel.java b/src/main/java/com/botdetector/ui/BotDetectorPanel.java index 5d78541c..8b5f9f17 100644 --- a/src/main/java/com/botdetector/ui/BotDetectorPanel.java +++ b/src/main/java/com/botdetector/ui/BotDetectorPanel.java @@ -1358,19 +1358,32 @@ private void sendFeedbackToClient(FeedbackPredictionLabel proposedLabel) feedbackHeaderLabel.setIcon(Icons.LOADING_SPINNER); feedbackHeaderLabel.setToolTipText(null); detectorClient.sendFeedback(lastPrediction, lastPredictionUploaderName, proposedLabel, feedbackText) - .whenComplete((b, ex) -> + .whenComplete((successful, ex) -> { boolean stillSame = lastPrediction != null && wrappedName.equals(normalizeAndWrapPlayerName(lastPrediction.getPlayerName())); String message; - if (ex == null && b) + if (ex == null) { - message = "Thank you for your prediction feedback for '%s'!"; - if (stillSame) + if (successful) { - feedbackHeaderLabel.setIcon(null); - feedbackHeaderLabel.setToolTipText(null); + message = "Thank you for your prediction feedback for '%s'!"; + if (stillSame) + { + feedbackHeaderLabel.setIcon(null); + feedbackHeaderLabel.setToolTipText(null); + } + } + // Failure is due to duplicate record on server side + else + { + message = "Sorry, but your feedback for '%s' was rejected as it already exists on the server."; + if (stillSame) + { + feedbackHeaderLabel.setIcon(Icons.WARNING_ICON); + feedbackHeaderLabel.setToolTipText("The server rejected your feedback as it already exists for this player"); + } } } else @@ -1382,7 +1395,7 @@ private void sendFeedbackToClient(FeedbackPredictionLabel proposedLabel) { resetFeedbackPanel(false); feedbackHeaderLabel.setIcon(Icons.ERROR_ICON); - feedbackHeaderLabel.setToolTipText(ex != null ? ex.getMessage() : "Unknown error"); + feedbackHeaderLabel.setToolTipText(ex.getMessage()); } } From e24f975b2443dda87e71b3d080ce072fcc0c3abc Mon Sep 17 00:00:00 2001 From: Cyborger1 <45152844+Cyborger1@users.noreply.github.com> Date: Tue, 29 Jul 2025 19:04:25 -0400 Subject: [PATCH 09/10] Remove unused import --- src/main/java/com/botdetector/http/BotDetectorClient.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/com/botdetector/http/BotDetectorClient.java b/src/main/java/com/botdetector/http/BotDetectorClient.java index 2bdb7ff2..15b09681 100644 --- a/src/main/java/com/botdetector/http/BotDetectorClient.java +++ b/src/main/java/com/botdetector/http/BotDetectorClient.java @@ -67,7 +67,6 @@ import lombok.Value; import lombok.extern.slf4j.Slf4j; import net.runelite.api.kit.KitType; -import okhttp3.Cache; import okhttp3.Call; import okhttp3.Callback; import okhttp3.HttpUrl; From bc3ebbd20663a07dc9e64b6fff9d4abebf5f80f6 Mon Sep 17 00:00:00 2001 From: Cyborger1 <45152844+Cyborger1@users.noreply.github.com> Date: Wed, 17 Sep 2025 12:35:48 -0400 Subject: [PATCH 10/10] Fix NPE in predictions/labels combination step when predictions are null (e.g. 404 from API) --- .../botdetector/http/BotDetectorClient.java | 59 ++++++++++--------- 1 file changed, 32 insertions(+), 27 deletions(-) diff --git a/src/main/java/com/botdetector/http/BotDetectorClient.java b/src/main/java/com/botdetector/http/BotDetectorClient.java index 15b09681..b8204478 100644 --- a/src/main/java/com/botdetector/http/BotDetectorClient.java +++ b/src/main/java/com/botdetector/http/BotDetectorClient.java @@ -497,41 +497,46 @@ public void onResponse(Call call, Response response) // allOf will send a CompletionException when one of the futures fail, just get the cause. log.warn("Error obtaining player prediction data", e.getCause()); finalFuture.completeExceptionally(e.getCause()); + return; } - else + + Prediction pred = predFuture.join(); + if (pred == null) { - Prediction pred = predFuture.join(); - Collection labels = labelsFuture.join(); + finalFuture.complete(null); + return; + } - // Re-add predictions as lowercase - Map newBreakdown = new HashMap<>(); - if (pred.getPredictionBreakdown() != null) - { - for (Map.Entry entry : pred.getPredictionBreakdown().entrySet()) { - newBreakdown.put(entry.getKey().toLowerCase(), entry.getValue()); - } + Collection labels = labelsFuture.join(); + + // Re-add predictions as lowercase + Map newBreakdown = new HashMap<>(); + if (pred.getPredictionBreakdown() != null) + { + for (Map.Entry entry : pred.getPredictionBreakdown().entrySet()) { + newBreakdown.put(entry.getKey().toLowerCase(), entry.getValue()); } + } - // Add labels that may not be in the breakdown - if (labels != null) + // Add labels that may not be in the breakdown + if (labels != null) + { + for (LabelAPIItem label : labels) { - for (LabelAPIItem label : labels) - { - newBreakdown.putIfAbsent(label.getLabel().toLowerCase(), 0.0); - } + newBreakdown.putIfAbsent(label.getLabel().toLowerCase(), 0.0); } - - // Build a new copy of the prediction object with normalized labels - finalFuture.complete( - Prediction.builder() - .playerName(pred.getPlayerName()) - .playerId(pred.getPlayerId()) - .confidence(pred.getConfidence()) - .predictionBreakdown(newBreakdown) - .predictionLabel(pred.getPredictionLabel().toLowerCase()) - .build() - ); } + + // Build a new copy of the prediction object with normalized labels + finalFuture.complete( + Prediction.builder() + .playerName(pred.getPlayerName()) + .playerId(pred.getPlayerId()) + .confidence(pred.getConfidence()) + .predictionBreakdown(newBreakdown) + .predictionLabel(pred.getPredictionLabel().toLowerCase()) + .build() + ); }); return finalFuture;