From 141a45a8d23237bf23b3b7a14d447440a40e71ea Mon Sep 17 00:00:00 2001 From: Cow Date: Fri, 13 Aug 2021 10:48:19 +0200 Subject: Improved error messages for API errors --- .../cowtipper/cowlection/command/MooCommand.java | 31 +++++++++++++++--- .../cowlection/config/CredentialStorage.java | 4 +-- .../de/cowtipper/cowlection/data/HyApiKey.java | 27 ++++++++++++++-- .../cowlection/error/ApiAskPolitelyErrorEvent.java | 15 +++++++++ .../cowlection/error/ApiHttpErrorEvent.java | 30 ++++++++++++++++++ .../cowlection/error/ApiHttpErrorException.java | 16 ++++++++++ .../cowtipper/cowlection/event/ApiErrorEvent.java | 15 --------- .../cowlection/listener/ChatListener.java | 4 +-- .../cowlection/listener/PlayerListener.java | 21 ++++++++++-- .../de/cowtipper/cowlection/util/ApiUtils.java | 37 +++++++++++++++------- 10 files changed, 160 insertions(+), 40 deletions(-) create mode 100644 src/main/java/de/cowtipper/cowlection/error/ApiAskPolitelyErrorEvent.java create mode 100644 src/main/java/de/cowtipper/cowlection/error/ApiHttpErrorEvent.java create mode 100644 src/main/java/de/cowtipper/cowlection/error/ApiHttpErrorException.java delete mode 100644 src/main/java/de/cowtipper/cowlection/event/ApiErrorEvent.java (limited to 'src') diff --git a/src/main/java/de/cowtipper/cowlection/command/MooCommand.java b/src/main/java/de/cowtipper/cowlection/command/MooCommand.java index 43a1335..7a67a0d 100644 --- a/src/main/java/de/cowtipper/cowlection/command/MooCommand.java +++ b/src/main/java/de/cowtipper/cowlection/command/MooCommand.java @@ -1060,22 +1060,41 @@ public class MooCommand extends CommandBase { EnumChatFormatting color; EnumChatFormatting colorSecondary; if (CredentialStorage.isMooValid) { - firstSentence = "You already set your Hypixel API key."; + firstSentence = "[" + Cowlection.MODNAME + "] You already set your Hypixel API key. Run " + EnumChatFormatting.DARK_GREEN + "/" + getCommandName() + " apikey check " + EnumChatFormatting.GREEN + "to see usage statistics."; color = EnumChatFormatting.GREEN; colorSecondary = EnumChatFormatting.DARK_GREEN; } else { - firstSentence = "You haven't set your Hypixel API key yet or the API key is invalid."; + firstSentence = "[" + Cowlection.MODNAME + "] You haven't set your Hypixel API key yet or the API key is invalid."; color = EnumChatFormatting.RED; colorSecondary = EnumChatFormatting.DARK_RED; } main.getChatHelper().sendMessage(color, firstSentence + " Use " + colorSecondary + "/api new" + color + " to request a new API key from Hypixel or use " + colorSecondary + "/" + this.getCommandName() + " apikey " + color + " to manually set your existing API key."); } else { String key = args[1]; - if (Utils.isValidUuid(key)) { - main.getChatHelper().sendMessage(EnumChatFormatting.YELLOW, "Validating API key..."); + if ("check".equalsIgnoreCase(key)) { + if (StringUtils.isNotEmpty(CredentialStorage.moo)) { + ApiUtils.fetchApiKeyInfo(CredentialStorage.moo, hyApiKey -> { + if (hyApiKey != null && hyApiKey.isSuccess()) { + HyApiKey.Record apiKeyRecord = hyApiKey.getRecord(); + if (apiKeyRecord != null) { + main.getChatHelper().sendMessage(EnumChatFormatting.GREEN, "[" + Cowlection.MODNAME + "] Your Hypixel API key was used to execute a total of " + EnumChatFormatting.DARK_GREEN + Utils.formatNumber(apiKeyRecord.getTotalQueries()) + + EnumChatFormatting.GREEN + " API requests. In the last minute, " + EnumChatFormatting.DARK_GREEN + apiKeyRecord.getQueriesInPastMin() + EnumChatFormatting.GREEN + " out of maximum " + EnumChatFormatting.DARK_GREEN + apiKeyRecord.getLimit() + EnumChatFormatting.GREEN + " allowed requests were made."); + } else { + main.getChatHelper().sendMessage(EnumChatFormatting.GREEN, "[" + Cowlection.MODNAME + "] Your Hypixel API key seems to be valid, but processing usage statistics failed."); + } + } else { + String cause = hyApiKey != null ? hyApiKey.getCause() : null; + Cowlection.getInstance().getChatHelper().sendMessage(EnumChatFormatting.RED, "[" + Cowlection.MODNAME + "] Failed to check API key usage statistics: " + (cause != null ? cause : "unknown cause :c")); + } + }); + } else { + throw new MooCommandException("You haven't set your Hypixel API key yet. Use " + EnumChatFormatting.DARK_RED + "/api new" + EnumChatFormatting.RED + " to request a new API key from Hypixel or use " + EnumChatFormatting.DARK_RED + "/" + this.getCommandName() + " apikey " + EnumChatFormatting.RED + " to manually set your existing API key."); + } + } else if (Utils.isValidUuid(key)) { + main.getChatHelper().sendMessage(EnumChatFormatting.YELLOW, "[" + Cowlection.MODNAME + "] Validating API key..."); main.getMoo().setMooIfValid(key, true); } else { - throw new SyntaxErrorException("That doesn't look like a valid API key..."); + throw new SyntaxErrorException("[" + Cowlection.MODNAME + "] That doesn't look like a valid API key... Did you want check your API key usage statistics? Run /" + getCommandName() + " apikey check"); } } } @@ -1193,6 +1212,8 @@ public class MooCommand extends CommandBase { return getListOfStringsMatchingLastWord(args, "off", "on", "disable", "enable"); } else if (args.length == 2 && (args[0].equalsIgnoreCase("chestAnalyzer") || args[0].equalsIgnoreCase("chestAnalyser") || args[0].equalsIgnoreCase("analyzeChests") || args[0].equalsIgnoreCase("analyseChests"))) { return getListOfStringsMatchingLastWord(args, "stop"); + } else if (args.length == 2 && args[0].equalsIgnoreCase("apikey")) { + return getListOfStringsMatchingLastWord(args, "check"); } String commandArg = args[0].toLowerCase(); if (args.length == 2 && (commandArg.equals("s") || commandArg.equals("ss") || commandArg.equals("namechangecheck") || commandArg.contains("stalk") || commandArg.contains("askpolitely"))) { // stalk & stalkskyblock + namechangecheck diff --git a/src/main/java/de/cowtipper/cowlection/config/CredentialStorage.java b/src/main/java/de/cowtipper/cowlection/config/CredentialStorage.java index c7cf35f..f4e64f6 100644 --- a/src/main/java/de/cowtipper/cowlection/config/CredentialStorage.java +++ b/src/main/java/de/cowtipper/cowlection/config/CredentialStorage.java @@ -45,12 +45,12 @@ public class CredentialStorage { // api key is valid! Cowlection.getInstance().getMoo().setMoo(moo); if (commandTriggered) { - Cowlection.getInstance().getChatHelper().sendMessage(EnumChatFormatting.GREEN, "Successfully verified API key ✔"); + Cowlection.getInstance().getChatHelper().sendMessage(EnumChatFormatting.GREEN, "[" + Cowlection.MODNAME + "] Successfully verified API key ✔"); } } else if (commandTriggered) { // api key is invalid String cause = hyApiKey != null ? hyApiKey.getCause() : null; - Cowlection.getInstance().getChatHelper().sendMessage(EnumChatFormatting.RED, "Failed to verify API key: " + (cause != null ? cause : "unknown cause :c")); + Cowlection.getInstance().getChatHelper().sendMessage(EnumChatFormatting.RED, "[" + Cowlection.MODNAME + "] Failed to verify API key: " + (cause != null ? cause : "unknown cause :c")); } }); } diff --git a/src/main/java/de/cowtipper/cowlection/data/HyApiKey.java b/src/main/java/de/cowtipper/cowlection/data/HyApiKey.java index ca7152c..9552948 100644 --- a/src/main/java/de/cowtipper/cowlection/data/HyApiKey.java +++ b/src/main/java/de/cowtipper/cowlection/data/HyApiKey.java @@ -1,11 +1,12 @@ package de.cowtipper.cowlection.data; +@SuppressWarnings("unused") public class HyApiKey { - @SuppressWarnings("unused") private boolean success; - @SuppressWarnings("unused") private String cause; + private Record record; + /** * No-args constructor for GSON */ @@ -19,4 +20,26 @@ public class HyApiKey { public String getCause() { return cause; } + + public Record getRecord() { + return record; + } + + public class Record { + private int queriesInPastMin; + private int limit; + private long totalQueries; + + public int getQueriesInPastMin() { + return queriesInPastMin; + } + + public int getLimit() { + return limit; + } + + public long getTotalQueries() { + return totalQueries; + } + } } diff --git a/src/main/java/de/cowtipper/cowlection/error/ApiAskPolitelyErrorEvent.java b/src/main/java/de/cowtipper/cowlection/error/ApiAskPolitelyErrorEvent.java new file mode 100644 index 0000000..5da2dfb --- /dev/null +++ b/src/main/java/de/cowtipper/cowlection/error/ApiAskPolitelyErrorEvent.java @@ -0,0 +1,15 @@ +package de.cowtipper.cowlection.error; + +import net.minecraftforge.fml.common.eventhandler.Event; + +public class ApiAskPolitelyErrorEvent extends Event { + private final String playerName; + + public ApiAskPolitelyErrorEvent(String playerName) { + this.playerName = playerName; + } + + public String getPlayerName() { + return playerName; + } +} diff --git a/src/main/java/de/cowtipper/cowlection/error/ApiHttpErrorEvent.java b/src/main/java/de/cowtipper/cowlection/error/ApiHttpErrorEvent.java new file mode 100644 index 0000000..4b66bb6 --- /dev/null +++ b/src/main/java/de/cowtipper/cowlection/error/ApiHttpErrorEvent.java @@ -0,0 +1,30 @@ +package de.cowtipper.cowlection.error; + +import net.minecraftforge.fml.common.eventhandler.Event; + +public class ApiHttpErrorEvent extends Event { + private final String message; + private final String url; + + public ApiHttpErrorEvent(String message, String url) { + this.message = message; + this.url = url; + } + + public String getMessage() { + return message; + } + + public String getUrl() { + return url; + } + + public String getBaseUrl() { + int queryParamStart = url.indexOf('?', 10); + return queryParamStart > 0 ? url.substring(0, queryParamStart) : url; + } + + public boolean hasUrlKey() { + return url.contains("key="); + } +} diff --git a/src/main/java/de/cowtipper/cowlection/error/ApiHttpErrorException.java b/src/main/java/de/cowtipper/cowlection/error/ApiHttpErrorException.java new file mode 100644 index 0000000..ac0a6a4 --- /dev/null +++ b/src/main/java/de/cowtipper/cowlection/error/ApiHttpErrorException.java @@ -0,0 +1,16 @@ +package de.cowtipper.cowlection.error; + +import java.io.IOException; + +public class ApiHttpErrorException extends IOException { + private final String url; + + public ApiHttpErrorException(String message, String url) { + super(message); + this.url = url; + } + + public String getUrl() { + return url; + } +} diff --git a/src/main/java/de/cowtipper/cowlection/event/ApiErrorEvent.java b/src/main/java/de/cowtipper/cowlection/event/ApiErrorEvent.java deleted file mode 100644 index ccfc4d3..0000000 --- a/src/main/java/de/cowtipper/cowlection/event/ApiErrorEvent.java +++ /dev/null @@ -1,15 +0,0 @@ -package de.cowtipper.cowlection.event; - -import net.minecraftforge.fml.common.eventhandler.Event; - -public class ApiErrorEvent extends Event { - private final String playerName; - - public ApiErrorEvent(String playerName) { - this.playerName = playerName; - } - - public String getPlayerName() { - return playerName; - } -} diff --git a/src/main/java/de/cowtipper/cowlection/listener/ChatListener.java b/src/main/java/de/cowtipper/cowlection/listener/ChatListener.java index 00f9649..3b5a168 100644 --- a/src/main/java/de/cowtipper/cowlection/listener/ChatListener.java +++ b/src/main/java/de/cowtipper/cowlection/listener/ChatListener.java @@ -96,7 +96,7 @@ public class ChatListener { // Your new API key is 00000000-0000-0000-0000-000000000000 String moo = text.substring(20, 56); if (Utils.isValidUuid(moo)) { - main.getChatHelper().sendMessage(EnumChatFormatting.YELLOW, "Verifying the new API key..."); + main.getChatHelper().sendMessage(EnumChatFormatting.YELLOW, "[" + Cowlection.MODNAME + "] Verifying the new API key..."); main.getMoo().setMooIfValid(moo, true); } } @@ -254,7 +254,7 @@ public class ChatListener { String foundDungeonsSecrets = ""; if (hyPlayerData != null) { int foundSecrets = hyPlayerData.getAchievement("skyblock_treasure_hunter"); - foundDungeonsSecrets = "\n" + (outputAsChatMessages ? " " : "") + EnumChatFormatting.GRAY + "Found secrets: " + EnumChatFormatting.GOLD + foundSecrets; + foundDungeonsSecrets = "\n" + (outputAsChatMessages ? " " : "") + EnumChatFormatting.GRAY + "Found secrets: " + EnumChatFormatting.GOLD + Utils.formatNumber(foundSecrets); String averageSecretsPerCompletion = null; if (foundSecrets > 0 && totalDungeonCompletions > 0) { diff --git a/src/main/java/de/cowtipper/cowlection/listener/PlayerListener.java b/src/main/java/de/cowtipper/cowlection/listener/PlayerListener.java index cafa4a3..ff29669 100644 --- a/src/main/java/de/cowtipper/cowlection/listener/PlayerListener.java +++ b/src/main/java/de/cowtipper/cowlection/listener/PlayerListener.java @@ -3,7 +3,8 @@ package de.cowtipper.cowlection.listener; import de.cowtipper.cowlection.Cowlection; import de.cowtipper.cowlection.config.CredentialStorage; import de.cowtipper.cowlection.config.MooConfig; -import de.cowtipper.cowlection.event.ApiErrorEvent; +import de.cowtipper.cowlection.error.ApiAskPolitelyErrorEvent; +import de.cowtipper.cowlection.error.ApiHttpErrorEvent; import de.cowtipper.cowlection.listener.skyblock.DungeonsListener; import de.cowtipper.cowlection.listener.skyblock.SkyBlockListener; import de.cowtipper.cowlection.util.*; @@ -41,6 +42,7 @@ public class PlayerListener { private boolean isPlayerJoiningServer; private boolean isOnSkyBlock; private AbortableRunnable checkScoreboard; + private long nextApiErrorMessage; public PlayerListener(Cowlection main) { this.main = main; @@ -123,10 +125,25 @@ public class PlayerListener { } @SubscribeEvent - public void onApiError(ApiErrorEvent e) { + public void onApiAskPolitelyError(ApiAskPolitelyErrorEvent e) { main.getFriendsHandler().addErroredApiRequest(e.getPlayerName()); } + @SubscribeEvent + public void onApiHttpError(ApiHttpErrorEvent e) { + if (nextApiErrorMessage < System.currentTimeMillis() && Minecraft.getMinecraft().thePlayer != null) { + this.nextApiErrorMessage = System.currentTimeMillis() + 3000; + MooChatComponent hoverComponent = new MooChatComponent.KeyValueTooltipComponent("Click to visit", e.getBaseUrl()); + if (e.hasUrlKey()) { + String eyeCatcher = "" + EnumChatFormatting.LIGHT_PURPLE + EnumChatFormatting.OBFUSCATED + "#" + EnumChatFormatting.RESET + EnumChatFormatting.RED; + hoverComponent.appendFreshSibling(new MooChatComponent(eyeCatcher + " Warning! " + eyeCatcher + " If you're streaming or sharing your screen:").red() + .appendFreshSibling(new MooChatComponent("Clicking this will reveal your Hypixel " + EnumChatFormatting.DARK_RED + "API key" + EnumChatFormatting.RED + "!").red())); + } + main.getChatHelper().sendMessage(new MooChatComponent(e.getMessage()).red() + .setUrl(e.getUrl(), hoverComponent)); + } + } + @SubscribeEvent public void onWorldEnter(PlayerSetSpawnEvent e) { isPlayerJoiningServer = false; diff --git a/src/main/java/de/cowtipper/cowlection/util/ApiUtils.java b/src/main/java/de/cowtipper/cowlection/util/ApiUtils.java index 293c6ad..3a303e5 100644 --- a/src/main/java/de/cowtipper/cowlection/util/ApiUtils.java +++ b/src/main/java/de/cowtipper/cowlection/util/ApiUtils.java @@ -10,7 +10,9 @@ import de.cowtipper.cowlection.chesttracker.LowestBinsCache; import de.cowtipper.cowlection.command.exception.ThrowingConsumer; import de.cowtipper.cowlection.config.CredentialStorage; import de.cowtipper.cowlection.data.*; -import de.cowtipper.cowlection.event.ApiErrorEvent; +import de.cowtipper.cowlection.error.ApiAskPolitelyErrorEvent; +import de.cowtipper.cowlection.error.ApiHttpErrorEvent; +import de.cowtipper.cowlection.error.ApiHttpErrorException; import net.minecraftforge.common.MinecraftForge; import org.apache.http.HttpStatus; @@ -50,7 +52,7 @@ public class ApiUtils { return GsonUtils.fromJson(reader, Friend.class); } } catch (IOException | JsonSyntaxException e) { - e.printStackTrace(); + handleApiException(e); } return null; } @@ -70,7 +72,7 @@ public class ApiUtils { } } } catch (IOException | JsonSyntaxException e) { - e.printStackTrace(); + handleApiException(e); } return null; } @@ -85,7 +87,7 @@ public class ApiUtils { return GsonUtils.fromJson(reader, HyStalkingData.class); } } catch (IOException | JsonSyntaxException e) { - e.printStackTrace(); + handleApiException(e); } return null; } @@ -100,7 +102,7 @@ public class ApiUtils { return GsonUtils.fromJson(reader, HySkyBlockStats.class); } } catch (IOException | JsonSyntaxException e) { - e.printStackTrace(); + handleApiException(e); } return null; } @@ -115,7 +117,7 @@ public class ApiUtils { return GsonUtils.fromJson(reader, HyBazaarData.class); } } catch (IOException | JsonSyntaxException e) { - e.printStackTrace(); + handleApiException(e); } return null; } @@ -130,7 +132,7 @@ public class ApiUtils { return GsonUtils.fromJson(reader, LowestBinsCache.class); } } catch (IOException | JsonSyntaxException e) { - e.printStackTrace(); + handleApiException(e); } return new LowestBinsCache(); } @@ -145,9 +147,9 @@ public class ApiUtils { return GsonUtils.fromJson(reader, HyPlayerData.class); } } catch (IOException | JsonSyntaxException e) { - e.printStackTrace(); - ApiErrorEvent event = new ApiErrorEvent(stalkedPlayer.getName()); + ApiAskPolitelyErrorEvent event = new ApiAskPolitelyErrorEvent(stalkedPlayer.getName()); MinecraftForge.EVENT_BUS.post(event); + handleApiException(e); } return null; } @@ -162,11 +164,18 @@ public class ApiUtils { return GsonUtils.fromJson(reader, HyApiKey.class); } } catch (IOException | JsonSyntaxException e) { - e.printStackTrace(); + handleApiException(e); } return null; } + private static void handleApiException(Exception e) { + e.printStackTrace(); + if (e instanceof ApiHttpErrorException) { + MinecraftForge.EVENT_BUS.post(new ApiHttpErrorEvent(e.getMessage(), ((ApiHttpErrorException) e).getUrl())); + } + } + private static BufferedReader makeApiCall(String url) throws IOException { HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection(); connection.setConnectTimeout(5000); @@ -177,9 +186,13 @@ public class ApiUtils { if (connection.getResponseCode() == HttpStatus.SC_NO_CONTENT) { // http status 204 return null; } else if (connection.getResponseCode() == HttpStatus.SC_BAD_GATEWAY && url.startsWith("https://api.hypixel.net/")) { // http status 502 (cloudflare) - throw new IOException("Couldn't contact Hypixel API (502 Bad Gateway). API might be down, check https://status.hypixel.net for info."); + throw new ApiHttpErrorException("Couldn't contact Hypixel API (502 Bad Gateway). API might be down, check https://status.hypixel.net for info.", "https://status.hypixel.net"); + } else if (connection.getResponseCode() == HttpStatus.SC_SERVICE_UNAVAILABLE) { // http status 503 Service Unavailable + int queryParamStart = url.indexOf('?', 10); + String baseUrl = queryParamStart > 0 ? url.substring(0, queryParamStart) : url; + throw new ApiHttpErrorException("Couldn't contact the API (503 Service unavailable). API might be down, or you might be blocked by Cloudflare, check if you can reach: " + baseUrl, url); } else if (connection.getResponseCode() == HttpStatus.SC_BAD_GATEWAY && url.startsWith("https://moulberry.codes/")) { // http status 502 (cloudflare) - throw new IOException("Couldn't contact Moulberry's API (502 Bad Gateway). API might be down, check if " + LOWEST_BINS + " is reachable."); + throw new ApiHttpErrorException("Couldn't contact Moulberry's API (502 Bad Gateway). API might be down, check if " + LOWEST_BINS + " is reachable.", LOWEST_BINS); } else { BufferedReader reader; InputStream errorStream = connection.getErrorStream(); -- cgit