From a1e3c6ae5f2f71cfa3e229316d11fdd635178b65 Mon Sep 17 00:00:00 2001 From: Veyronity Date: Sat, 28 Feb 2026 13:51:34 +0200 Subject: [PATCH 1/7] Add server sent events Also improve API version handling Events: - NewDay - NationCreated, NationDeleted, NationKingChanged, NationMerged - TownCreated, TownDeleted, TownRenamed, TownMayorChanged, TownMerged, TownRuined, TownReclaimed --- src/main/java/net/earthmc/emcapi/EMCAPI.java | 13 ++ .../emcapi/manager/EndpointManager.java | 34 ++-- .../net/earthmc/emcapi/sse/SSEManager.java | 49 ++++++ .../sse/listeners/TownySSEListeners.java | 146 ++++++++++++++++++ .../earthmc/emcapi/util/EndpointUtils.java | 7 + .../net/earthmc/emcapi/util/JSONUtil.java | 12 ++ 6 files changed, 244 insertions(+), 17 deletions(-) create mode 100644 src/main/java/net/earthmc/emcapi/sse/SSEManager.java create mode 100644 src/main/java/net/earthmc/emcapi/sse/listeners/TownySSEListeners.java diff --git a/src/main/java/net/earthmc/emcapi/EMCAPI.java b/src/main/java/net/earthmc/emcapi/EMCAPI.java index 8ce11a9..c1e20e1 100644 --- a/src/main/java/net/earthmc/emcapi/EMCAPI.java +++ b/src/main/java/net/earthmc/emcapi/EMCAPI.java @@ -6,6 +6,8 @@ import jakarta.servlet.http.HttpServletResponse; import net.earthmc.emcapi.integration.Integrations; import net.earthmc.emcapi.manager.EndpointManager; +import net.earthmc.emcapi.sse.SSEManager; +import net.earthmc.emcapi.sse.listeners.TownySSEListeners; import net.earthmc.emcapi.util.EndpointUtils; import net.earthmc.emcapi.command.OptOutCommand; import org.bukkit.command.PluginCommand; @@ -25,6 +27,7 @@ public final class EMCAPI extends JavaPlugin { public static EMCAPI instance; private Javalin javalin; private Integrations pluginIntegrations; + private SSEManager sseManager; @Override public void onLoad() { @@ -58,6 +61,10 @@ public void onEnable() { } catch (IOException e) { getLogger().warning("IOException while loading opted-out players: " + e); } + + sseManager = new SSEManager(this); + sseManager.loadSSE(); + getServer().getPluginManager().registerEvents(new TownySSEListeners(sseManager), this); } @Override @@ -68,6 +75,7 @@ public void onDisable() { } catch (IOException e) { getLogger().warning("IOException while saving opted-out players: " + e); } + sseManager.shutdown(); } private void initialiseJavalin() { @@ -120,4 +128,9 @@ public Javalin getJavalin() { public Integrations integrations() { return this.pluginIntegrations; } + + public String getURLPath() { + String version = getConfig().getString("networking.api_version", "3"); + return "v" + version + "/" + getConfig().getString("networking.url_path"); + } } diff --git a/src/main/java/net/earthmc/emcapi/manager/EndpointManager.java b/src/main/java/net/earthmc/emcapi/manager/EndpointManager.java index 58e8ac0..d218f9a 100644 --- a/src/main/java/net/earthmc/emcapi/manager/EndpointManager.java +++ b/src/main/java/net/earthmc/emcapi/manager/EndpointManager.java @@ -24,12 +24,12 @@ public class EndpointManager { private final EMCAPI plugin; private final Javalin javalin; - private final String v3URLPath; + private final String URLPath; public EndpointManager(EMCAPI plugin) { this.plugin = plugin; this.javalin = plugin.getJavalin(); - this.v3URLPath = "v3/" + plugin.getConfig().getString("networking.url_path"); + this.URLPath = plugin.getURLPath(); } public void loadEndpoints() { @@ -37,10 +37,10 @@ public void loadEndpoints() { javalin.get("/", ctx -> ctx.json(documentationEndpoint.lookup())); ServerEndpoint serverEndpoint = new ServerEndpoint(plugin); - javalin.get(v3URLPath, ctx -> ctx.json(serverEndpoint.lookup())); + javalin.get(URLPath, ctx -> ctx.json(serverEndpoint.lookup())); MysteryMasterEndpoint mysteryMasterEndpoint = new MysteryMasterEndpoint(plugin); - javalin.get(v3URLPath + "/mm", ctx -> { + javalin.get(URLPath + "/mm", ctx -> { plugin.integrations().mysteryMasterIntegration().throwIfDisabled(); ctx.json(mysteryMasterEndpoint.lookup()); }); @@ -80,10 +80,10 @@ private Pair parseBody(String body) { private void loadPlayersEndpoint() { PlayersListEndpoint ple = new PlayersListEndpoint(); - javalin.get(v3URLPath + "/players", ctx -> ctx.json(ple.lookup())); + javalin.get(URLPath + "/players", ctx -> ctx.json(ple.lookup())); PlayersEndpoint playersEndpoint = new PlayersEndpoint(); - javalin.post(v3URLPath + "/players", ctx -> { + javalin.post(URLPath + "/players", ctx -> { Pair parsedBody = parseBody(ctx.body()); ctx.json(playersEndpoint.lookup(parsedBody.getFirst(), parsedBody.getSecond())); }); @@ -91,10 +91,10 @@ private void loadPlayersEndpoint() { private void loadTownsEndpoint() { TownsListEndpoint tle = new TownsListEndpoint(); - javalin.get(v3URLPath + "/towns", ctx -> ctx.json(tle.lookup())); + javalin.get(URLPath + "/towns", ctx -> ctx.json(tle.lookup())); TownsEndpoint townsEndpoint = new TownsEndpoint(plugin); - javalin.post(v3URLPath + "/towns", ctx -> { + javalin.post(URLPath + "/towns", ctx -> { Pair parsedBody = parseBody(ctx.body()); ctx.json(townsEndpoint.lookup(parsedBody.getFirst(), parsedBody.getSecond())); }); @@ -102,10 +102,10 @@ private void loadTownsEndpoint() { private void loadNationsEndpoint() { NationsListEndpoint nle = new NationsListEndpoint(); - javalin.get(v3URLPath + "/nations", ctx -> ctx.json(nle.lookup())); + javalin.get(URLPath + "/nations", ctx -> ctx.json(nle.lookup())); NationsEndpoint nationsEndpoint = new NationsEndpoint(); - javalin.post(v3URLPath + "/nations", ctx -> { + javalin.post(URLPath + "/nations", ctx -> { Pair parsedBody = parseBody(ctx.body()); ctx.json(nationsEndpoint.lookup(parsedBody.getFirst(), parsedBody.getSecond())); }); @@ -115,13 +115,13 @@ private void loadQuartersEndpoint() { QuartersIntegration quartersIntegration = plugin.integrations().quartersIntegration(); QuartersListEndpoint qle = new QuartersListEndpoint(quartersIntegration); - javalin.get(v3URLPath + "/quarters", ctx -> { + javalin.get(URLPath + "/quarters", ctx -> { quartersIntegration.throwIfDisabled(); ctx.json(qle.lookup()); }); QuartersEndpoint quartersEndpoint = new QuartersEndpoint(); - javalin.post(v3URLPath + "/quarters", ctx -> { + javalin.post(URLPath + "/quarters", ctx -> { quartersIntegration.throwIfDisabled(); Pair parsedBody = parseBody(ctx.body()); ctx.json(quartersEndpoint.lookup(parsedBody.getFirst(), parsedBody.getSecond())); @@ -130,7 +130,7 @@ private void loadQuartersEndpoint() { private void loadLocationEndpoint() { LocationEndpoint locationEndpoint = new LocationEndpoint(); - javalin.post(v3URLPath + "/location", ctx -> { + javalin.post(URLPath + "/location", ctx -> { Pair parsedBody = parseBody(ctx.body()); ctx.json(locationEndpoint.lookup(parsedBody.getFirst(), parsedBody.getSecond())); }); @@ -138,7 +138,7 @@ private void loadLocationEndpoint() { private void loadNearbyEndpoint() { NearbyEndpoint nearbyEndpoint = new NearbyEndpoint(); - javalin.post(v3URLPath + "/nearby", ctx -> { + javalin.post(URLPath + "/nearby", ctx -> { Pair parsedBody = parseBody(ctx.body()); ctx.json(nearbyEndpoint.lookup(parsedBody.getFirst(), parsedBody.getSecond())); }); @@ -148,7 +148,7 @@ private void loadDiscordEndpoint() { DiscordEndpoint discordEndpoint = new DiscordEndpoint(); final DiscordIntegration discordIntegration = plugin.integrations().discordIntegration(); - javalin.post(v3URLPath + "/discord", ctx -> { + javalin.post(URLPath + "/discord", ctx -> { discordIntegration.throwIfDisabled(); Pair parsedBody = parseBody(ctx.body()); @@ -159,14 +159,14 @@ private void loadDiscordEndpoint() { private void loadPlayerStatsEndpoint() { PlayerStatsEndpoint playerStatsEndpoint = new PlayerStatsEndpoint(this.plugin); playerStatsEndpoint.initialize(); - javalin.get(v3URLPath + "/player-stats", ctx -> { + javalin.get(URLPath + "/player-stats", ctx -> { ctx.json(playerStatsEndpoint.latestCachedStatistics()); }); } private void loadOnlinePlayersEndpoint() { OnlineEndpoint onlineEndpoint = new OnlineEndpoint(); - javalin.get(v3URLPath + "/online", ctx -> { + javalin.get(URLPath + "/online", ctx -> { ctx.json(onlineEndpoint.lookup()); }); } diff --git a/src/main/java/net/earthmc/emcapi/sse/SSEManager.java b/src/main/java/net/earthmc/emcapi/sse/SSEManager.java new file mode 100644 index 0000000..340af4b --- /dev/null +++ b/src/main/java/net/earthmc/emcapi/sse/SSEManager.java @@ -0,0 +1,49 @@ +package net.earthmc.emcapi.sse; + +import com.google.gson.JsonObject; +import io.javalin.Javalin; +import io.javalin.http.sse.SseClient; +import net.earthmc.emcapi.EMCAPI; + +import java.time.Instant; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +public class SSEManager { + private final EMCAPI plugin; + private final Javalin javalin; + private static final Set clients = ConcurrentHashMap.newKeySet(); + + public SSEManager(EMCAPI plugin) { + this.plugin = plugin; + this.javalin = plugin.getJavalin(); + } + + public void loadSSE() { + javalin.sse(plugin.getURLPath() + "/events", client -> { + client.keepAlive(); + client.sendEvent("open", "Connected to the EarthMC API."); + client.onClose(() -> clients.remove(client)); + clients.add(client); + }); + } + + public void shutdown() { + for (SseClient client : clients) { + plugin.getServer().getAsyncScheduler().runNow(plugin, t -> { + client.sendEvent("close", "Disconnected from the EarthMC API."); + client.close(); + }); + } + } + + public void sendEvent(String event, JsonObject data) { + long timestamp = Instant.now().getEpochSecond(); + data.addProperty("timestamp", timestamp); + String message = data.toString(); + + for (SseClient client : clients) { + plugin.getServer().getAsyncScheduler().runNow(plugin, t -> client.sendEvent(event, message)); + } + } +} diff --git a/src/main/java/net/earthmc/emcapi/sse/listeners/TownySSEListeners.java b/src/main/java/net/earthmc/emcapi/sse/listeners/TownySSEListeners.java new file mode 100644 index 0000000..b455767 --- /dev/null +++ b/src/main/java/net/earthmc/emcapi/sse/listeners/TownySSEListeners.java @@ -0,0 +1,146 @@ +package net.earthmc.emcapi.sse.listeners; + +import com.google.gson.JsonObject; +import com.palmergames.bukkit.towny.event.DeleteTownEvent; +import com.palmergames.bukkit.towny.event.NewDayEvent; +import com.palmergames.bukkit.towny.event.NewNationEvent; +import com.palmergames.bukkit.towny.event.NewTownEvent; +import com.palmergames.bukkit.towny.event.PreDeleteNationEvent; +import com.palmergames.bukkit.towny.event.RenameNationEvent; +import com.palmergames.bukkit.towny.event.RenameTownEvent; +import com.palmergames.bukkit.towny.event.nation.NationKingChangeEvent; +import com.palmergames.bukkit.towny.event.nation.NationMergeEvent; +import com.palmergames.bukkit.towny.event.town.TownMayorChangedEvent; +import com.palmergames.bukkit.towny.event.town.TownMergeEvent; +import com.palmergames.bukkit.towny.event.town.TownPreRuinedEvent; +import com.palmergames.bukkit.towny.event.town.TownReclaimedEvent; +import com.palmergames.bukkit.towny.object.Nation; +import net.earthmc.emcapi.sse.SSEManager; +import net.earthmc.emcapi.util.EndpointUtils; +import net.earthmc.emcapi.util.JSONUtil; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; + +public class TownySSEListeners implements Listener { + private final SSEManager sse; + + public TownySSEListeners(SSEManager sse) { + this.sse = sse; + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onNewDay(NewDayEvent event) { + JsonObject message = new JsonObject(); + message.add("fallenTowns", JSONUtil.getJsonArrayFromStringList(event.getFallenTowns())); + message.add("fallenNations", JSONUtil.getJsonArrayFromStringList(event.getFallenNations())); + sse.sendEvent("NewDay", message); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onNewNation(NewNationEvent event) { + JsonObject message = new JsonObject(); + Nation nation = event.getNation(); + message.add("nation", EndpointUtils.getNationJsonObject(nation)); + message.add("king", EndpointUtils.getResidentJsonObject(nation.getKing())); + message.add("capital", EndpointUtils.getTownJsonObject(nation.getCapital())); + sse.sendEvent("NationCreated", message); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onDeleteNation(PreDeleteNationEvent event) { + JsonObject message = new JsonObject(); + Nation nation = event.getNation(); + message.add("nation", EndpointUtils.generateNameUUIDJsonObject(nation.getName(), nation.getUUID())); + message.add("king", EndpointUtils.getResidentJsonObject(nation.getKing())); + message.add("capital", EndpointUtils.getTownJsonObject(nation.getCapital())); + sse.sendEvent("NationDeleted", message); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onRenameNation(RenameNationEvent event) { + JsonObject message = new JsonObject(); + message.add("nation", EndpointUtils.getNationJsonObject(event.getNation())); + message.addProperty("oldName", event.getOldName()); + sse.sendEvent("NationRenamed", message); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onNationKingChange(NationKingChangeEvent event) { + JsonObject message = new JsonObject(); + message.add("nation", EndpointUtils.getNationJsonObject(event.getNation())); + message.add("newKing", EndpointUtils.getResidentJsonObject(event.getNewKing())); + message.add("oldKing", EndpointUtils.getResidentJsonObject(event.getOldKing())); + message.addProperty("isCapitalChange", event.isCapitalChange()); + if (event.isCapitalChange()) { + message.add("newCapital", EndpointUtils.getTownJsonObject(event.getNewKing().getTownOrNull())); + message.add("oldCapital", EndpointUtils.getTownJsonObject(event.getOldKing().getTownOrNull())); + } + sse.sendEvent("NationKingChanged", message); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onNationMerge(NationMergeEvent event) { + JsonObject message = new JsonObject(); + message.add("oldNation", EndpointUtils.getNationJsonObject(event.getNation())); + message.add("remainingNation", EndpointUtils.getNationJsonObject(event.getRemainingnation())); + sse.sendEvent("NationMerged", message); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onNewTown(NewTownEvent event) { + JsonObject message = new JsonObject(); + message.add("town", EndpointUtils.getTownJsonObject(event.getTown())); + message.add("mayor", EndpointUtils.getResidentJsonObject(event.getTown().getMayor())); + sse.sendEvent("TownCreated", message); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onDeleteTown(DeleteTownEvent event) { + JsonObject message = new JsonObject(); + message.add("town", EndpointUtils.generateNameUUIDJsonObject(event.getTownName(), event.getTownUUID())); + message.add("mayor", EndpointUtils.getResidentJsonObject(event.getMayor())); + sse.sendEvent("TownDeleted", message); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onRenameTown(RenameTownEvent event) { + JsonObject message = new JsonObject(); + message.add("town", EndpointUtils.getTownJsonObject(event.getTown())); + message.addProperty("oldName", event.getOldName()); + sse.sendEvent("TownRenamed", message); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onTownMayorChanged(TownMayorChangedEvent event) { + JsonObject message = new JsonObject(); + message.add("town", EndpointUtils.getTownJsonObject(event.getTown())); + message.add("newMayor", EndpointUtils.getResidentJsonObject(event.getNewMayor())); + message.add("oldMayor", EndpointUtils.getResidentJsonObject(event.getOldMayor())); + sse.sendEvent("TownMayorChanged", message); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onTownMerge(TownMergeEvent event) { + JsonObject message = new JsonObject(); + message.add("oldTown", EndpointUtils.generateNameUUIDJsonObject(event.getSuccumbingTownName(), event.getSuccumbingTownUUID())); + message.add("remainingTown", EndpointUtils.getTownJsonObject(event.getRemainingTown())); + sse.sendEvent("TownMerged", message); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onTownRuined(TownPreRuinedEvent event) { + JsonObject message = new JsonObject(); + message.add("town", EndpointUtils.getTownJsonObject(event.getTown())); + message.add("oldMayor", EndpointUtils.getResidentJsonObject(event.getTown().getMayor())); + sse.sendEvent("TownRuined", message); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onTownReclaimed(TownReclaimedEvent event) { + JsonObject message = new JsonObject(); + message.add("town", EndpointUtils.getTownJsonObject(event.getTown())); + message.add("newMayor", EndpointUtils.getResidentJsonObject(event.getResident())); + sse.sendEvent("TownReclaimed", message); + } +} diff --git a/src/main/java/net/earthmc/emcapi/util/EndpointUtils.java b/src/main/java/net/earthmc/emcapi/util/EndpointUtils.java index 4afdb27..c2fe088 100644 --- a/src/main/java/net/earthmc/emcapi/util/EndpointUtils.java +++ b/src/main/java/net/earthmc/emcapi/util/EndpointUtils.java @@ -214,4 +214,11 @@ public static void saveOptOut(Path path) throws IOException { Files.write(path.resolve(optOutFile), lines, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); } + + public static JsonObject generateNameUUIDJsonObject(String name, UUID uuid) { + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("name", name); + jsonObject.addProperty("uuid", uuid.toString()); + return jsonObject; + } } diff --git a/src/main/java/net/earthmc/emcapi/util/JSONUtil.java b/src/main/java/net/earthmc/emcapi/util/JSONUtil.java index 7257c59..38acc3f 100644 --- a/src/main/java/net/earthmc/emcapi/util/JSONUtil.java +++ b/src/main/java/net/earthmc/emcapi/util/JSONUtil.java @@ -3,6 +3,8 @@ import com.google.gson.*; import io.javalin.http.BadRequestResponse; +import java.util.List; + public class JSONUtil { public static JsonObject getJsonObjectFromString(String string) { @@ -13,6 +15,16 @@ public static JsonObject getJsonObjectFromString(String string) { } } + public static JsonArray getJsonArrayFromStringList(List stringList) { + JsonArray jsonArray = new JsonArray(); + if (stringList == null) return jsonArray; + + for (String item : stringList) { + jsonArray.add(item); + } + return jsonArray; + } + public static String getJsonElementAsStringOrNull(JsonElement element) { if (element == null) return null; From a0d8ab6e5e22bb4cabf1dd0d10e7d787ff24d7ad Mon Sep 17 00:00:00 2001 From: Veyronity Date: Sat, 28 Feb 2026 18:37:57 +0200 Subject: [PATCH 2/7] Remove broken shutdown function - Update plugin version - Update config.yml --- gradle.properties | 2 +- src/main/java/net/earthmc/emcapi/EMCAPI.java | 1 - src/main/java/net/earthmc/emcapi/sse/SSEManager.java | 9 --------- src/main/resources/config.yml | 5 ++++- 4 files changed, 5 insertions(+), 12 deletions(-) diff --git a/gradle.properties b/gradle.properties index 52d728a..be58298 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ group = net.earthmc.emcapi -version = 3.0.0-SNAPSHOT +version = 3.1.0-SNAPSHOT description = EMCAPI org.gradle.configuration-cache=true diff --git a/src/main/java/net/earthmc/emcapi/EMCAPI.java b/src/main/java/net/earthmc/emcapi/EMCAPI.java index c1e20e1..c9bc198 100644 --- a/src/main/java/net/earthmc/emcapi/EMCAPI.java +++ b/src/main/java/net/earthmc/emcapi/EMCAPI.java @@ -75,7 +75,6 @@ public void onDisable() { } catch (IOException e) { getLogger().warning("IOException while saving opted-out players: " + e); } - sseManager.shutdown(); } private void initialiseJavalin() { diff --git a/src/main/java/net/earthmc/emcapi/sse/SSEManager.java b/src/main/java/net/earthmc/emcapi/sse/SSEManager.java index 340af4b..32a6511 100644 --- a/src/main/java/net/earthmc/emcapi/sse/SSEManager.java +++ b/src/main/java/net/earthmc/emcapi/sse/SSEManager.java @@ -28,15 +28,6 @@ public void loadSSE() { }); } - public void shutdown() { - for (SseClient client : clients) { - plugin.getServer().getAsyncScheduler().runNow(plugin, t -> { - client.sendEvent("close", "Disconnected from the EarthMC API."); - client.close(); - }); - } - } - public void sendEvent(String event, JsonObject data) { long timestamp = Instant.now().getEpochSecond(); data.addProperty("timestamp", timestamp); diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 0dbb4a2..c026564 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -7,7 +7,10 @@ networking: # Port the API will listen on port: 8080 - # The URL path, for instance https://example.com/v3/aurora where aurora is the string written below, version numbers are always present + # The version number for endpoints + api_version: 3 + + # The URL path, formatted as: https://example.com/{api_version}/{url_path} url_path: aurora behaviour: From c5cbd72dc494a85eee20a386c0ee118cb2eae078 Mon Sep 17 00:00:00 2001 From: Veyronity Date: Fri, 6 Mar 2026 13:40:25 +0200 Subject: [PATCH 3/7] Add API keys, Shop integration, and allow specifying events --- build.gradle.kts | 7 + gradle/libs.versions.toml | 2 + src/main/java/net/earthmc/emcapi/EMCAPI.java | 27 +++- .../earthmc/emcapi/command/ApiCommand.java | 121 ++++++++++++++++++ .../earthmc/emcapi/command/OptOutCommand.java | 59 --------- .../net/earthmc/emcapi/sse/SSEManager.java | 116 ++++++++++++++++- .../sse/listeners/AbstractSSEListener.java | 12 ++ .../emcapi/sse/listeners/ShopSSEListener.java | 68 ++++++++++ ...SEListeners.java => TownySSEListener.java} | 55 ++++++-- .../earthmc/emcapi/util/EndpointUtils.java | 66 ++++++++++ 10 files changed, 457 insertions(+), 76 deletions(-) create mode 100644 src/main/java/net/earthmc/emcapi/command/ApiCommand.java delete mode 100644 src/main/java/net/earthmc/emcapi/command/OptOutCommand.java create mode 100644 src/main/java/net/earthmc/emcapi/sse/listeners/AbstractSSEListener.java create mode 100644 src/main/java/net/earthmc/emcapi/sse/listeners/ShopSSEListener.java rename src/main/java/net/earthmc/emcapi/sse/listeners/{TownySSEListeners.java => TownySSEListener.java} (75%) diff --git a/build.gradle.kts b/build.gradle.kts index 62c2c4b..e9ff33c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -23,6 +23,10 @@ repositories { maven("https://repo.earthmc.net/public/") { mavenContent { includeGroupAndSubgroups("net.earthmc") } } + + maven("https://repo.codemc.io/repository/maven-public/") { + mavenContent { includeGroup("org.maxgamer") } + } } dependencies { @@ -33,6 +37,9 @@ dependencies { compileOnly(libs.discordsrv) compileOnly(libs.superbvote) compileOnly(libs.mysterymaster.api) + compileOnly(libs.quickshop) { + exclude("*") + } } tasks { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6bc1adf..969fd0b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,6 +7,7 @@ towny = "0.102.0.0" mysterymaster-api = "1.0.0" superbvote = "0.6.0" conventions = "1.0.8" +quickshop = "5.1.2.5-SNAPSHOT" [libraries] discordsrv = { group = "com.discordsrv", name = "discordsrv", version.ref = "discordsrv" } @@ -16,6 +17,7 @@ javalin = { group = "io.javalin", name = "javalin", version.ref = "javalin" } paper = { group = "io.papermc.paper", name = "paper-api", version.ref = "paper" } mysterymaster-api = { group = "net.earthmc.mysterymaster", name = "mysterymaster-api", version.ref = "mysterymaster-api" } superbvote = { group = "net.earthmc.superbvote", name = "SuperbVote", version.ref = "superbvote" } +quickshop = { group = "org.maxgamer", name = "QuickShop", version.ref = "quickshop" } [plugins] conventions-java = { id = "net.earthmc.conventions.java", version.ref = "conventions" } diff --git a/src/main/java/net/earthmc/emcapi/EMCAPI.java b/src/main/java/net/earthmc/emcapi/EMCAPI.java index c9bc198..1531b1e 100644 --- a/src/main/java/net/earthmc/emcapi/EMCAPI.java +++ b/src/main/java/net/earthmc/emcapi/EMCAPI.java @@ -7,10 +7,12 @@ import net.earthmc.emcapi.integration.Integrations; import net.earthmc.emcapi.manager.EndpointManager; import net.earthmc.emcapi.sse.SSEManager; -import net.earthmc.emcapi.sse.listeners.TownySSEListeners; +import net.earthmc.emcapi.sse.listeners.ShopSSEListener; +import net.earthmc.emcapi.sse.listeners.TownySSEListener; import net.earthmc.emcapi.util.EndpointUtils; -import net.earthmc.emcapi.command.OptOutCommand; +import net.earthmc.emcapi.command.ApiCommand; import org.bukkit.command.PluginCommand; +import org.bukkit.plugin.PluginManager; import org.bukkit.plugin.java.JavaPlugin; import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.HttpConnectionFactory; @@ -52,7 +54,7 @@ public void onEnable() { if (apiCommand == null) { getLogger().warning("API command not found."); } else { - OptOutCommand cmd = new OptOutCommand(); + ApiCommand cmd = new ApiCommand(); apiCommand.setExecutor(cmd); apiCommand.setTabCompleter(cmd); } @@ -64,7 +66,18 @@ public void onEnable() { sseManager = new SSEManager(this); sseManager.loadSSE(); - getServer().getPluginManager().registerEvents(new TownySSEListeners(sseManager), this); + PluginManager pm = getServer().getPluginManager(); + if (pm.isPluginEnabled("Towny")) { + pm.registerEvents(new TownySSEListener(sseManager), this); + } + if (pm.isPluginEnabled("QuickShop")) { + pm.registerEvents(new ShopSSEListener(sseManager), this); + } + try { + EndpointUtils.loadApiKeys(getDataFolder().toPath()); + } catch (IOException e) { + getSLF4JLogger().warn("IOException while loading API keys: ", e); + } } @Override @@ -75,6 +88,12 @@ public void onDisable() { } catch (IOException e) { getLogger().warning("IOException while saving opted-out players: " + e); } + sseManager.shutdown(); + try { + EndpointUtils.saveApiKeys(getDataFolder().toPath()); + } catch (IOException e) { + getSLF4JLogger().warn("IOException while saving API keys: ", e); + } } private void initialiseJavalin() { diff --git a/src/main/java/net/earthmc/emcapi/command/ApiCommand.java b/src/main/java/net/earthmc/emcapi/command/ApiCommand.java new file mode 100644 index 0000000..ff48b0a --- /dev/null +++ b/src/main/java/net/earthmc/emcapi/command/ApiCommand.java @@ -0,0 +1,121 @@ +package net.earthmc.emcapi.command; + +import net.earthmc.emcapi.sse.SSEManager; +import net.earthmc.emcapi.util.EndpointUtils; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextDecoration; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabExecutor; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.UUID; +import java.util.stream.Stream; + +public class ApiCommand implements TabExecutor { + private static final Component infoMessage = Component.text() + .append(Component.text("- The API provides real-time information about players, towns, and nations. The API can be accessed ", NamedTextColor.AQUA)) + .append(Component.text("here", NamedTextColor.AQUA, TextDecoration.UNDERLINED).clickEvent(ClickEvent.openUrl("https://api.earthmc.net/"))) + .appendNewline() + .append(Component.text("- Read the docs ", NamedTextColor.GREEN)) + .append(Component.text("here", NamedTextColor.GREEN, TextDecoration.UNDERLINED).clickEvent(ClickEvent.openUrl("https://earthmc.net/docs/api"))) + .appendNewline() + .append(Component.text("- If you'd like to connect to the API's Server-Sent-Events endpoint, please create an API key using /api key create", NamedTextColor.GREEN)) + .appendNewline() + .append(Component.text("- If you'd like to opt out of your information being public on the API, you can use /api opt-out", NamedTextColor.RED)) + .build(); + + @Override + public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s, @NotNull String[] args) { + if (!(commandSender instanceof Player player)) { + commandSender.sendMessage(Component.text("Only players may use this command.", NamedTextColor.RED)); + return true; + } + + if (args.length < 1) { + player.sendMessage(infoMessage); + return true; + } + String action = args[0].toLowerCase(); + switch (action) { + case "opt-in" -> { + player.sendMessage(Component.text("You have opted back in for your information being public on the API.", NamedTextColor.GREEN)); + EndpointUtils.setOptedOut(player.getUniqueId(), false); + } + case "opt-out" -> { + player.sendMessage(Component.text("You have opted out of your information being public on the API", NamedTextColor.RED)); + EndpointUtils.setOptedOut(player.getUniqueId(), true); + } + case "key" -> handleKey(player, args); + default -> player.sendMessage(Component.text("Usage: /api [opt-in|opt-out|key]", NamedTextColor.RED)); + } + + return true; + } + + @Override + public @Nullable List onTabComplete(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s, @NotNull String[] args) { + if (args.length == 1) { + return Stream.of("opt-in", "opt-out", "key").filter(str -> str.toLowerCase().startsWith(args[0].toLowerCase())).toList(); + } + if (args.length == 2 && args[0].equalsIgnoreCase("key")) { + return Stream.of("create", "delete", "copy").filter(str -> str.toLowerCase().startsWith(args[1].toLowerCase())).toList(); + } + + return List.of(); + } + + private void handleKey(Player player, String[] args) { + if (!player.hasPermission("emcapi.key")) { + player.sendMessage(Component.text("You do not have permission to use this command", NamedTextColor.RED)); + return; + } + + UUID playerID = player.getUniqueId(); + UUID key; + if (args.length == 1) { + key = EndpointUtils.getPlayerKey(playerID); + if (key != null) { + player.sendMessage(Component.text("Click to copy your API key.", NamedTextColor.GREEN).clickEvent(ClickEvent.copyToClipboard(key.toString()))); + } else { + player.sendMessage(Component.text("You do not have an API key. Use /api key create to create one.", NamedTextColor.RED)); + } + return; + } + String action = args[1].toLowerCase(); + switch (action) { + case "create" -> { + if (EndpointUtils.getPlayerKey(playerID) != null) { + player.sendMessage(Component.text("You already have an API key! Use /api key to get it.", NamedTextColor.RED)); + } else { + key = EndpointUtils.createApiKey(playerID); + player.sendMessage(Component.text("Key created! Click to copy.", NamedTextColor.GREEN).clickEvent(ClickEvent.copyToClipboard(key.toString()))); + } + } + case "delete" -> { + key = EndpointUtils.getPlayerKey(playerID); + if (key != null) { + SSEManager.deleteKey(key); + EndpointUtils.deletePlayerKey(playerID); + player.sendMessage(Component.text("Successfully deleted your API key", NamedTextColor.GREEN)); + } else { + player.sendMessage(Component.text("You do not have an API key.", NamedTextColor.RED)); + } + } + case "copy" -> { + key = EndpointUtils.getPlayerKey(playerID); + if (key != null) { + player.sendMessage(Component.text("Click to copy your API key.", NamedTextColor.GREEN).clickEvent(ClickEvent.copyToClipboard(key.toString()))); + } else { + player.sendMessage(Component.text("You do not have an API key. Use /api key create to create one.", NamedTextColor.RED)); + } + } + default -> player.sendMessage(Component.text("Usage: /api key ", NamedTextColor.RED)); + } + } +} diff --git a/src/main/java/net/earthmc/emcapi/command/OptOutCommand.java b/src/main/java/net/earthmc/emcapi/command/OptOutCommand.java deleted file mode 100644 index 1d6f9d3..0000000 --- a/src/main/java/net/earthmc/emcapi/command/OptOutCommand.java +++ /dev/null @@ -1,59 +0,0 @@ -package net.earthmc.emcapi.command; - -import net.earthmc.emcapi.util.EndpointUtils; -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.event.ClickEvent; -import net.kyori.adventure.text.format.NamedTextColor; -import net.kyori.adventure.text.format.TextDecoration; -import org.bukkit.command.Command; -import org.bukkit.command.CommandSender; -import org.bukkit.command.TabExecutor; -import org.bukkit.entity.Player; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.List; -import java.util.stream.Stream; - -public class OptOutCommand implements TabExecutor { - private static final Component infoMessage = Component.text("The API provides real-time information about players, towns, and nations. The API can be accessed ", NamedTextColor.BLUE) - .append(Component.text("here", NamedTextColor.BLUE, TextDecoration.UNDERLINED).clickEvent(ClickEvent.openUrl("https://api.earthmc.net/v3/aurora/"))) - .appendNewline() - .append(Component.text("Read the docs ", NamedTextColor.GREEN)) - .append(Component.text("here", NamedTextColor.GREEN, TextDecoration.UNDERLINED).clickEvent(ClickEvent.openUrl("https://earthmc.net/docs/api"))) - .appendNewline() - .append(Component.text("If you'd like to opt out of your information being public on the API, you can use /api opt-out", NamedTextColor.DARK_RED)); - - @Override - public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s, @NotNull String[] strings) { - if (!(commandSender instanceof Player player)) { - commandSender.sendMessage(Component.text("Only players may use this command.", NamedTextColor.RED)); - return true; - } - - if (strings.length < 1) { - player.sendMessage(infoMessage); - return true; - } - if (strings[0].equalsIgnoreCase("opt-in")) { - player.sendMessage(Component.text("You have opted back in for your information being public on the API.", NamedTextColor.GREEN)); - EndpointUtils.setOptedOut(player.getUniqueId(), false); - } else if (strings[0].equalsIgnoreCase("opt-out")){ - player.sendMessage(Component.text("You have opted out of your information being public on the API", NamedTextColor.RED)); - EndpointUtils.setOptedOut(player.getUniqueId(), true); - } else { - player.sendMessage(Component.text("Usage: /api [opt-in|opt-out]", NamedTextColor.RED)); - } - - return true; - } - - @Override - public @Nullable List onTabComplete(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s, @NotNull String[] strings) { - if (strings.length == 1) { - return Stream.of("opt-in", "opt-out").filter(str -> str.startsWith(strings[0])).toList(); - } - - return List.of(); - } -} diff --git a/src/main/java/net/earthmc/emcapi/sse/SSEManager.java b/src/main/java/net/earthmc/emcapi/sse/SSEManager.java index 32a6511..3166308 100644 --- a/src/main/java/net/earthmc/emcapi/sse/SSEManager.java +++ b/src/main/java/net/earthmc/emcapi/sse/SSEManager.java @@ -2,17 +2,33 @@ import com.google.gson.JsonObject; import io.javalin.Javalin; +import io.javalin.http.Context; import io.javalin.http.sse.SseClient; import net.earthmc.emcapi.EMCAPI; +import net.earthmc.emcapi.util.EndpointUtils; +import javax.annotation.Nullable; import java.time.Instant; +import java.util.HashSet; +import java.util.Map; import java.util.Set; +import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Predicate; public class SSEManager { private final EMCAPI plugin; private final Javalin javalin; + private static final Map clientsMap = new ConcurrentHashMap<>(); private static final Set clients = ConcurrentHashMap.newKeySet(); + private static final Set ALLOWED_EVENTS = Set.of( + "NewDay", + "NationCreated", "NationDeleted", "NationRenamed", "NationKingChanged", "NationMerged", + "TownCreated", "TownDeleted", "TownRenamed", "TownMayorChanged", "TownMerged", "TownRuined", "TownReclaimed", + "TownJoinedNation", "TownLeftNation", + "ResidentJoinedTown", "ResidentLeftTown", + "ShopSoldItem", "ShopBoughtItem", "ShopOutOfStock", "ShopOutOfSpace", "ShopOutOfGold" + ); public SSEManager(EMCAPI plugin) { this.plugin = plugin; @@ -21,20 +37,114 @@ public SSEManager(EMCAPI plugin) { public void loadSSE() { javalin.sse(plugin.getURLPath() + "/events", client -> { + Context ctx = client.ctx(); + String auth = ctx.header("Authorization"); + + if (auth == null || !auth.startsWith("Bearer ")) { + ctx.status(401).result("Missing API key"); + client.close(); + return; + } + + UUID key; + try { + key = UUID.fromString(auth.substring("Bearer ".length())); + } catch (IllegalArgumentException ignored) { + ctx.status(401).result("Invalid API key format"); + client.close(); + return; + } + + UUID owner = EndpointUtils.getKeyOwner(key); + if (owner == null) { + ctx.status(403).result("Invalid API key"); + client.close(); + return; + } + if (clientsMap.containsKey(key)) { + ctx.status(403).result("This API key is already in use."); + client.close(); + return; + } + + Set events = new HashSet<>(); + Set invalid = new HashSet<>(); + + String listenStr = ctx.queryParam("listen"); + if (listenStr != null) { + for (String event : listenStr.split(",")) { + if (ALLOWED_EVENTS.contains(event)) { + events.add(event); + } else { + invalid.add(event); + } + } + if (events.isEmpty()) { + ctx.status(400).result("No valid events specified"); + client.close(); + return; + } + } else { + events.addAll(new HashSet<>(ALLOWED_EVENTS)); + } + ClientData data = new ClientData(client, events, owner); client.keepAlive(); client.sendEvent("open", "Connected to the EarthMC API."); - client.onClose(() -> clients.remove(client)); + client.sendEvent("listening", "Listening to the following events: " + String.join(", ", events)); + if (!invalid.isEmpty()) { + client.sendEvent("invalid", "The following events are invalid: " + String.join(", ", invalid)); + } + client.onClose(() -> { + clients.remove(client); + clientsMap.remove(key, data); + }); + clients.add(client); + clientsMap.put(key, data); }); } + public void shutdown() { + for (SseClient client : clients) { + client.sendEvent("close", "EarthMC API shut down."); + client.close(); + } + clientsMap.clear(); + } + public void sendEvent(String event, JsonObject data) { + sendEvent(event, data, (Predicate) null); + } + + public void sendEvent(String event, JsonObject data, UUID targetPlayerID) { + sendEvent(event, data, client -> client.playerID.equals(targetPlayerID)); + } + + public void sendEvent(String event, JsonObject data, @Nullable Predicate predicate) { long timestamp = Instant.now().getEpochSecond(); data.addProperty("timestamp", timestamp); String message = data.toString(); - for (SseClient client : clients) { - plugin.getServer().getAsyncScheduler().runNow(plugin, t -> client.sendEvent(event, message)); + plugin.getServer().getAsyncScheduler().runNow(plugin, t -> { + for (ClientData clientData : clientsMap.values()) { + if (!clientData.events.contains(event)) { + continue; + } + if (predicate != null && !predicate.test(clientData)) + continue; + clientData.client.sendEvent(event, message); + } + }); + } + + public static void deleteKey(UUID uuid) { + ClientData data = clientsMap.remove(uuid); + if (data != null) { + SseClient client = data.client; + client.sendEvent("disconnected", "This API key was deleted by the owner"); + client.close(); } } + + public record ClientData(SseClient client, Set events, UUID playerID) {} } diff --git a/src/main/java/net/earthmc/emcapi/sse/listeners/AbstractSSEListener.java b/src/main/java/net/earthmc/emcapi/sse/listeners/AbstractSSEListener.java new file mode 100644 index 0000000..5b3c3e1 --- /dev/null +++ b/src/main/java/net/earthmc/emcapi/sse/listeners/AbstractSSEListener.java @@ -0,0 +1,12 @@ +package net.earthmc.emcapi.sse.listeners; + +import net.earthmc.emcapi.sse.SSEManager; +import org.bukkit.event.Listener; + +public abstract class AbstractSSEListener implements Listener { + protected final SSEManager sse; + + protected AbstractSSEListener(SSEManager sseManager) { + this.sse = sseManager; + } +} diff --git a/src/main/java/net/earthmc/emcapi/sse/listeners/ShopSSEListener.java b/src/main/java/net/earthmc/emcapi/sse/listeners/ShopSSEListener.java new file mode 100644 index 0000000..cd5ed26 --- /dev/null +++ b/src/main/java/net/earthmc/emcapi/sse/listeners/ShopSSEListener.java @@ -0,0 +1,68 @@ +package net.earthmc.emcapi.sse.listeners; + +import com.google.gson.JsonObject; +import com.palmergames.bukkit.towny.TownyAPI; +import com.palmergames.bukkit.towny.object.Resident; +import net.earthmc.emcapi.sse.SSEManager; +import net.earthmc.emcapi.util.EndpointUtils; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.maxgamer.quickshop.api.event.ShopSuccessPurchaseEvent; +import org.maxgamer.quickshop.api.shop.Shop; + +import java.util.UUID; + +public class ShopSSEListener extends AbstractSSEListener { + + public ShopSSEListener(SSEManager sseManager) { + super(sseManager); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onShopPurchase(ShopSuccessPurchaseEvent event) { + Shop shop = event.getShop(); + UUID owner = shop.getOwner(); + + boolean isSelling = shop.isSelling(); + String purchaser = getPlayerName(event.getPurchaser()); + JsonObject saleMessage = new JsonObject(); + saleMessage.add("shop", EndpointUtils.getShopObject(shop)); + if (isSelling) { + saleMessage.addProperty("buyer", purchaser); + sse.sendEvent("ShopSoldItem", saleMessage, owner); + } else { + saleMessage.addProperty("seller", purchaser); + sse.sendEvent("ShopBoughtItem", saleMessage, owner); + } + + checkOwnerBalance(owner); + checkShopOut(shop); + } + + private void checkOwnerBalance(UUID owner) { + Resident res = TownyAPI.getInstance().getResident(owner); + if (res == null || res.getAccount().getHoldingBalance() > 0) return; + + sse.sendEvent("ShopOutOfGold", new JsonObject(), owner); + } + + private void checkShopOut(Shop shop) { + boolean isSelling = shop.isSelling(); + if (isSelling && shop.getRemainingStock() > 0 || !isSelling && shop.getRemainingSpace() > 0) { + return; + } + JsonObject alertMessage = new JsonObject(); + alertMessage.addProperty("action", isSelling ? "out_of_stock" : "out_of_space"); + alertMessage.add("shop", EndpointUtils.getShopObject(shop)); + sse.sendEvent("ShopOutOf" + (isSelling ? "Stock " : "Space"), alertMessage, shop.getOwner()); + } + + private String getPlayerName(UUID uuid) { + Resident res = TownyAPI.getInstance().getResident(uuid); + if (res != null) { + return res.getName(); + } else { + return "Unknown player `(" + uuid + ")`"; + } + } +} diff --git a/src/main/java/net/earthmc/emcapi/sse/listeners/TownySSEListeners.java b/src/main/java/net/earthmc/emcapi/sse/listeners/TownySSEListener.java similarity index 75% rename from src/main/java/net/earthmc/emcapi/sse/listeners/TownySSEListeners.java rename to src/main/java/net/earthmc/emcapi/sse/listeners/TownySSEListener.java index b455767..2f4b885 100644 --- a/src/main/java/net/earthmc/emcapi/sse/listeners/TownySSEListeners.java +++ b/src/main/java/net/earthmc/emcapi/sse/listeners/TownySSEListener.java @@ -2,12 +2,16 @@ import com.google.gson.JsonObject; import com.palmergames.bukkit.towny.event.DeleteTownEvent; +import com.palmergames.bukkit.towny.event.NationAddTownEvent; +import com.palmergames.bukkit.towny.event.NationRemoveTownEvent; import com.palmergames.bukkit.towny.event.NewDayEvent; import com.palmergames.bukkit.towny.event.NewNationEvent; import com.palmergames.bukkit.towny.event.NewTownEvent; import com.palmergames.bukkit.towny.event.PreDeleteNationEvent; import com.palmergames.bukkit.towny.event.RenameNationEvent; import com.palmergames.bukkit.towny.event.RenameTownEvent; +import com.palmergames.bukkit.towny.event.TownAddResidentEvent; +import com.palmergames.bukkit.towny.event.TownRemoveResidentEvent; import com.palmergames.bukkit.towny.event.nation.NationKingChangeEvent; import com.palmergames.bukkit.towny.event.nation.NationMergeEvent; import com.palmergames.bukkit.towny.event.town.TownMayorChangedEvent; @@ -15,26 +19,21 @@ import com.palmergames.bukkit.towny.event.town.TownPreRuinedEvent; import com.palmergames.bukkit.towny.event.town.TownReclaimedEvent; import com.palmergames.bukkit.towny.object.Nation; +import com.palmergames.bukkit.towny.object.Town; import net.earthmc.emcapi.sse.SSEManager; import net.earthmc.emcapi.util.EndpointUtils; -import net.earthmc.emcapi.util.JSONUtil; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; -import org.bukkit.event.Listener; -public class TownySSEListeners implements Listener { - private final SSEManager sse; +public class TownySSEListener extends AbstractSSEListener { - public TownySSEListeners(SSEManager sse) { - this.sse = sse; + public TownySSEListener(SSEManager sse) { + super(sse); } @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) public void onNewDay(NewDayEvent event) { - JsonObject message = new JsonObject(); - message.add("fallenTowns", JSONUtil.getJsonArrayFromStringList(event.getFallenTowns())); - message.add("fallenNations", JSONUtil.getJsonArrayFromStringList(event.getFallenNations())); - sse.sendEvent("NewDay", message); + sse.sendEvent("NewDay", new JsonObject()); } @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) @@ -143,4 +142,40 @@ public void onTownReclaimed(TownReclaimedEvent event) { message.add("newMayor", EndpointUtils.getResidentJsonObject(event.getResident())); sse.sendEvent("TownReclaimed", message); } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onTownJoin(NationAddTownEvent event) { + Nation nation = event.getNation(); + JsonObject message = new JsonObject(); + message.add("nation", EndpointUtils.getNationJsonObject(nation)); + message.add("town", EndpointUtils.getTownJsonObject(event.getTown())); + sse.sendEvent("TownJoinedNation", message, nation.getKing().getUUID()); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onTownLeave(NationRemoveTownEvent event) { + Nation nation = event.getNation(); + JsonObject message = new JsonObject(); + message.add("nation", EndpointUtils.getNationJsonObject(nation)); + message.add("town", EndpointUtils.getTownJsonObject(event.getTown())); + sse.sendEvent("TownLeftNation", message, nation.getKing().getUUID()); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onTownJoin(TownAddResidentEvent event) { + Town town = event.getTown(); + JsonObject message = new JsonObject(); + message.add("town", EndpointUtils.getTownJsonObject(town)); + message.add("resident", EndpointUtils.getResidentJsonObject(event.getResident())); + sse.sendEvent("ResidentJoinedTown", message, town.getMayor().getUUID()); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onTownLeave(TownRemoveResidentEvent event) { + Town town = event.getTown(); + JsonObject message = new JsonObject(); + message.add("town", EndpointUtils.getTownJsonObject(town)); + message.add("resident", EndpointUtils.getResidentJsonObject(event.getResident())); + sse.sendEvent("ResidentLeftTown", message, town.getMayor().getUUID()); + } } diff --git a/src/main/java/net/earthmc/emcapi/util/EndpointUtils.java b/src/main/java/net/earthmc/emcapi/util/EndpointUtils.java index c2fe088..f9b412c 100644 --- a/src/main/java/net/earthmc/emcapi/util/EndpointUtils.java +++ b/src/main/java/net/earthmc/emcapi/util/EndpointUtils.java @@ -10,11 +10,14 @@ import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; +import org.maxgamer.quickshop.api.shop.Shop; import javax.annotation.Nullable; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.UUID; import java.util.Set; import java.util.HashSet; @@ -22,10 +25,15 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; +import java.util.concurrent.ConcurrentHashMap; public class EndpointUtils { private static final Set optedOut = new HashSet<>(); private static final String optOutFile = "opt-out.txt"; + private static final Map playerKeyMap = new ConcurrentHashMap<>(); + private static final Map keyPlayerMap = new ConcurrentHashMap<>(); + private static final Set apiKeys = ConcurrentHashMap.newKeySet(); + private static final String apiKeyFile = "api_keys.txt"; public static int getNumOnlineNomads() { int numOnlineNomads = 0; @@ -215,10 +223,68 @@ public static void saveOptOut(Path path) throws IOException { Files.write(path.resolve(optOutFile), lines, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); } + public static void loadApiKeys(Path path) throws IOException { + final Path file = path.resolve(apiKeyFile); + if (!Files.exists(file)) { + return; + } + + Files.readAllLines(file).forEach(result -> { + try { + String[] split = result.split(","); + if (split.length != 2) return; + UUID player = UUID.fromString(split[0]); + UUID key = UUID.fromString(split[1]); + playerKeyMap.put(player, key); + keyPlayerMap.put(key, player); + apiKeys.add(key); + } catch (IllegalArgumentException ignored) {} + }); + } + + public static void saveApiKeys(Path path) throws IOException { + final List lines = playerKeyMap.entrySet().stream().map(entry -> entry.getKey() + "," + entry.getValue()).toList(); + + Files.write(path.resolve(apiKeyFile), lines, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); + } + + public static @Nullable UUID getPlayerKey(UUID player) { + return playerKeyMap.get(player); + } + + public static @NotNull UUID createApiKey(UUID player) { + UUID newID = UUID.randomUUID(); + playerKeyMap.put(player, newID); + keyPlayerMap.put(newID, player); + apiKeys.add(newID); + return newID; + } + + public static void deletePlayerKey(UUID player) { + UUID key = playerKeyMap.remove(player); + if (key != null) { + keyPlayerMap.remove(key); + apiKeys.remove(key); + } + } + + public static UUID getKeyOwner(UUID key) { + return keyPlayerMap.get(key); + } + public static JsonObject generateNameUUIDJsonObject(String name, UUID uuid) { JsonObject jsonObject = new JsonObject(); jsonObject.addProperty("name", name); jsonObject.addProperty("uuid", uuid.toString()); return jsonObject; } + + public static JsonObject getShopObject(Shop shop) { + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("item", shop.getItem().getType().name()); + jsonObject.addProperty("amount", shop.getItem().getAmount()); + jsonObject.addProperty("type", shop.isSelling() ? "selling" : "buying"); + jsonObject.addProperty("stock", shop.isSelling() ? shop.getRemainingStock() : shop.getRemainingSpace()); + return jsonObject; + } } From c728ce674acf603fb151c2a52823eefc09af1b14 Mon Sep 17 00:00:00 2001 From: Veyronity Date: Fri, 6 Mar 2026 15:23:26 +0200 Subject: [PATCH 4/7] Update to v4 - Move deprecated endpoints to /legacy/ - Update endpoints to accept an API key, currently only the player & shop endpoints use it - Add QuickShop integratoin to allow players to lookup their own shops - Allow players to use their API keys to query their resident status, even if they have opted out - Add permissions for api command & key management --- gradle.properties | 2 +- src/main/java/net/earthmc/emcapi/EMCAPI.java | 7 +- .../emcapi/endpoint/DiscordEndpoint.java | 5 +- .../emcapi/endpoint/LocationEndpoint.java | 7 +- .../emcapi/endpoint/NearbyEndpoint.java | 6 +- .../earthmc/emcapi/endpoint/ShopEndpoint.java | 52 ++++++ .../{ => legacy}/DocumentationEndpoint.java | 2 +- .../endpoint/{ => legacy}/MudkipEndpoint.java | 2 +- .../{ => legacy}/PlayerStatsEndpoint.java | 3 +- .../endpoint/towny/NationsEndpoint.java | 5 +- .../endpoint/towny/PlayersEndpoint.java | 9 +- .../endpoint/towny/QuartersEndpoint.java | 5 +- .../emcapi/endpoint/towny/TownsEndpoint.java | 5 +- .../emcapi/integration/Integrations.java | 6 + .../integration/QuickShopIntegration.java | 25 +++ .../emcapi/manager/EndpointManager.java | 110 ++++++----- .../emcapi/manager/LegacyEndpointManager.java | 176 ++++++++++++++++++ .../emcapi/object/endpoint/PostEndpoint.java | 26 ++- .../earthmc/emcapi/util/EndpointUtils.java | 2 + .../net/earthmc/emcapi/util/JSONUtil.java | 10 - src/main/resources/config.yml | 3 + src/main/resources/plugin.yml | 13 +- 22 files changed, 386 insertions(+), 95 deletions(-) create mode 100644 src/main/java/net/earthmc/emcapi/endpoint/ShopEndpoint.java rename src/main/java/net/earthmc/emcapi/endpoint/{ => legacy}/DocumentationEndpoint.java (96%) rename src/main/java/net/earthmc/emcapi/endpoint/{ => legacy}/MudkipEndpoint.java (99%) rename src/main/java/net/earthmc/emcapi/endpoint/{ => legacy}/PlayerStatsEndpoint.java (98%) create mode 100644 src/main/java/net/earthmc/emcapi/integration/QuickShopIntegration.java create mode 100644 src/main/java/net/earthmc/emcapi/manager/LegacyEndpointManager.java diff --git a/gradle.properties b/gradle.properties index be58298..8050299 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ group = net.earthmc.emcapi -version = 3.1.0-SNAPSHOT +version = 4.0.0-SNAPSHOT description = EMCAPI org.gradle.configuration-cache=true diff --git a/src/main/java/net/earthmc/emcapi/EMCAPI.java b/src/main/java/net/earthmc/emcapi/EMCAPI.java index 1531b1e..4f5a295 100644 --- a/src/main/java/net/earthmc/emcapi/EMCAPI.java +++ b/src/main/java/net/earthmc/emcapi/EMCAPI.java @@ -6,6 +6,7 @@ import jakarta.servlet.http.HttpServletResponse; import net.earthmc.emcapi.integration.Integrations; import net.earthmc.emcapi.manager.EndpointManager; +import net.earthmc.emcapi.manager.LegacyEndpointManager; import net.earthmc.emcapi.sse.SSEManager; import net.earthmc.emcapi.sse.listeners.ShopSSEListener; import net.earthmc.emcapi.sse.listeners.TownySSEListener; @@ -47,8 +48,10 @@ public void onEnable() { this.pluginIntegrations = new Integrations(this); getServer().getPluginManager().registerEvents(this.pluginIntegrations, this); - EndpointManager endpointManager = new EndpointManager(this); - endpointManager.loadEndpoints(); + if (getConfig().getBoolean("behavior.load_legacy")) { + new LegacyEndpointManager(this).loadEndpoints(); // Load retired endpoints and still serve current endpoints at /v3/aurora/ + } + new EndpointManager(this).loadEndpoints(); PluginCommand apiCommand = getCommand("api"); if (apiCommand == null) { diff --git a/src/main/java/net/earthmc/emcapi/endpoint/DiscordEndpoint.java b/src/main/java/net/earthmc/emcapi/endpoint/DiscordEndpoint.java index 0dddcff..fcd5730 100644 --- a/src/main/java/net/earthmc/emcapi/endpoint/DiscordEndpoint.java +++ b/src/main/java/net/earthmc/emcapi/endpoint/DiscordEndpoint.java @@ -11,6 +11,7 @@ import net.earthmc.emcapi.util.HttpExceptions; import net.earthmc.emcapi.util.JSONUtil; +import javax.annotation.Nullable; import java.util.UUID; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -21,7 +22,7 @@ public class DiscordEndpoint extends PostEndpoint { private static final BadRequestResponse INVALID_TYPE_TARGET = new BadRequestResponse("Your JSON query has an invalid type or target"); @Override - public DiscordContext getObjectOrNull(JsonElement element) { + public DiscordContext getObjectOrNull(JsonElement element, @Nullable UUID key) { JsonObject jsonObject = JSONUtil.getJsonElementAsJsonObjectOrNull(element); if (jsonObject == null) { throw HttpExceptions.NOT_A_JSON_OBJECT; @@ -60,7 +61,7 @@ public DiscordContext getObjectOrNull(JsonElement element) { } @Override - public JsonElement getJsonElement(DiscordContext context) { + public JsonElement getJsonElement(DiscordContext context, @Nullable UUID key) { DiscordType type = context.getType(); String target = context.getTarget(); diff --git a/src/main/java/net/earthmc/emcapi/endpoint/LocationEndpoint.java b/src/main/java/net/earthmc/emcapi/endpoint/LocationEndpoint.java index d01d78d..201d547 100644 --- a/src/main/java/net/earthmc/emcapi/endpoint/LocationEndpoint.java +++ b/src/main/java/net/earthmc/emcapi/endpoint/LocationEndpoint.java @@ -13,10 +13,13 @@ import org.bukkit.Bukkit; import org.bukkit.Location; +import javax.annotation.Nullable; +import java.util.UUID; + public class LocationEndpoint extends PostEndpoint> { @Override - public Pair getObjectOrNull(JsonElement element) { + public Pair getObjectOrNull(JsonElement element, @Nullable UUID key) { JsonArray jsonArray = JSONUtil.getJsonElementAsJsonArrayOrNull(element); if (jsonArray == null) throw new BadRequestResponse("Your query contains a value that is not a JSON array"); @@ -40,7 +43,7 @@ public Pair getObjectOrNull(JsonElement element) { } @Override - public JsonElement getJsonElement(Pair pair) { + public JsonElement getJsonElement(Pair pair, @Nullable UUID key) { int x = pair.getFirst(); int z = pair.getSecond(); diff --git a/src/main/java/net/earthmc/emcapi/endpoint/NearbyEndpoint.java b/src/main/java/net/earthmc/emcapi/endpoint/NearbyEndpoint.java index 83c533c..3b3ef7a 100644 --- a/src/main/java/net/earthmc/emcapi/endpoint/NearbyEndpoint.java +++ b/src/main/java/net/earthmc/emcapi/endpoint/NearbyEndpoint.java @@ -19,13 +19,15 @@ import org.bukkit.Bukkit; import org.bukkit.Location; +import javax.annotation.Nullable; import java.util.ArrayList; import java.util.List; +import java.util.UUID; public class NearbyEndpoint extends PostEndpoint { @Override - public NearbyContext getObjectOrNull(JsonElement element) { + public NearbyContext getObjectOrNull(JsonElement element, @Nullable UUID key) { JsonObject jsonObject = JSONUtil.getJsonElementAsJsonObjectOrNull(element); if (jsonObject == null) throw new BadRequestResponse("Your query contains a value that is not a JSON object"); @@ -64,7 +66,7 @@ public NearbyContext getObjectOrNull(JsonElement element) { } @Override - public JsonElement getJsonElement(NearbyContext context) { + public JsonElement getJsonElement(NearbyContext context, @Nullable UUID key) { NearbyType targetType = context.getTargetType(); int radius = context.getRadius(); switch (targetType) { diff --git a/src/main/java/net/earthmc/emcapi/endpoint/ShopEndpoint.java b/src/main/java/net/earthmc/emcapi/endpoint/ShopEndpoint.java new file mode 100644 index 0000000..8718b67 --- /dev/null +++ b/src/main/java/net/earthmc/emcapi/endpoint/ShopEndpoint.java @@ -0,0 +1,52 @@ +package net.earthmc.emcapi.endpoint; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import io.javalin.http.BadRequestResponse; +import net.earthmc.emcapi.EMCAPI; +import net.earthmc.emcapi.integration.QuickShopIntegration; +import net.earthmc.emcapi.object.endpoint.PostEndpoint; +import net.earthmc.emcapi.util.EndpointUtils; +import net.earthmc.emcapi.util.JSONUtil; +import org.jspecify.annotations.Nullable; +import org.maxgamer.quickshop.api.shop.Shop; + +import java.util.List; +import java.util.UUID; + +public class ShopEndpoint extends PostEndpoint> { + private final QuickShopIntegration integration; + + public ShopEndpoint(EMCAPI plugin) { + this.integration = plugin.integrations().quickShopIntegration(); + } + + @Override + public List getObjectOrNull(JsonElement element, @Nullable UUID key) { + String string = JSONUtil.getJsonElementAsStringOrNull(element); + if (string == null) throw new BadRequestResponse("Your query contains a value that is not a string"); + + UUID player; + try { + player = UUID.fromString(string); + } catch (IllegalArgumentException ignored) { + return null; + } + + integration.throwIfDisabled(); + return integration.getPlayerShops(player, key); + } + + @Override + public JsonElement getJsonElement(List object, @Nullable UUID key) { + JsonObject shopsObject = new JsonObject(); + int counter = 1; + UUID keyOwner = EndpointUtils.getKeyOwner(key); + for (Shop shop : object) { + if (!shop.getOwner().equals(keyOwner)) continue; + shopsObject.add(String.valueOf(counter++), EndpointUtils.getShopObject(shop)); + } + + return shopsObject; + } +} diff --git a/src/main/java/net/earthmc/emcapi/endpoint/DocumentationEndpoint.java b/src/main/java/net/earthmc/emcapi/endpoint/legacy/DocumentationEndpoint.java similarity index 96% rename from src/main/java/net/earthmc/emcapi/endpoint/DocumentationEndpoint.java rename to src/main/java/net/earthmc/emcapi/endpoint/legacy/DocumentationEndpoint.java index bba2703..f673011 100644 --- a/src/main/java/net/earthmc/emcapi/endpoint/DocumentationEndpoint.java +++ b/src/main/java/net/earthmc/emcapi/endpoint/legacy/DocumentationEndpoint.java @@ -1,4 +1,4 @@ -package net.earthmc.emcapi.endpoint; +package net.earthmc.emcapi.endpoint.legacy; import com.google.gson.JsonObject; import net.earthmc.emcapi.object.endpoint.GetEndpoint; diff --git a/src/main/java/net/earthmc/emcapi/endpoint/MudkipEndpoint.java b/src/main/java/net/earthmc/emcapi/endpoint/legacy/MudkipEndpoint.java similarity index 99% rename from src/main/java/net/earthmc/emcapi/endpoint/MudkipEndpoint.java rename to src/main/java/net/earthmc/emcapi/endpoint/legacy/MudkipEndpoint.java index 0e5e00c..efcece2 100644 --- a/src/main/java/net/earthmc/emcapi/endpoint/MudkipEndpoint.java +++ b/src/main/java/net/earthmc/emcapi/endpoint/legacy/MudkipEndpoint.java @@ -1,4 +1,4 @@ -package net.earthmc.emcapi.endpoint; +package net.earthmc.emcapi.endpoint.legacy; public class MudkipEndpoint { diff --git a/src/main/java/net/earthmc/emcapi/endpoint/PlayerStatsEndpoint.java b/src/main/java/net/earthmc/emcapi/endpoint/legacy/PlayerStatsEndpoint.java similarity index 98% rename from src/main/java/net/earthmc/emcapi/endpoint/PlayerStatsEndpoint.java rename to src/main/java/net/earthmc/emcapi/endpoint/legacy/PlayerStatsEndpoint.java index 2bdd10c..e5bcb73 100644 --- a/src/main/java/net/earthmc/emcapi/endpoint/PlayerStatsEndpoint.java +++ b/src/main/java/net/earthmc/emcapi/endpoint/legacy/PlayerStatsEndpoint.java @@ -1,4 +1,4 @@ -package net.earthmc.emcapi.endpoint; +package net.earthmc.emcapi.endpoint.legacy; import com.google.gson.JsonObject; import io.papermc.paper.threadedregions.scheduler.ScheduledTask; @@ -16,7 +16,6 @@ import java.nio.file.StandardOpenOption; import java.util.HashMap; import java.util.Map; -import java.util.concurrent.TimeUnit; public class PlayerStatsEndpoint { private final EMCAPI plugin; diff --git a/src/main/java/net/earthmc/emcapi/endpoint/towny/NationsEndpoint.java b/src/main/java/net/earthmc/emcapi/endpoint/towny/NationsEndpoint.java index 8a76ba9..b25e655 100644 --- a/src/main/java/net/earthmc/emcapi/endpoint/towny/NationsEndpoint.java +++ b/src/main/java/net/earthmc/emcapi/endpoint/towny/NationsEndpoint.java @@ -13,13 +13,14 @@ import net.earthmc.emcapi.util.EndpointUtils; import net.earthmc.emcapi.util.JSONUtil; +import javax.annotation.Nullable; import java.util.List; import java.util.UUID; public class NationsEndpoint extends PostEndpoint { @Override - public Nation getObjectOrNull(JsonElement element) { + public Nation getObjectOrNull(JsonElement element, @Nullable UUID key) { String string = JSONUtil.getJsonElementAsStringOrNull(element); if (string == null) throw new BadRequestResponse("Your query contains a value that is not a string"); @@ -34,7 +35,7 @@ public Nation getObjectOrNull(JsonElement element) { } @Override - public JsonElement getJsonElement(Nation nation) { + public JsonElement getJsonElement(Nation nation, @Nullable UUID key) { JsonObject nationObject = new JsonObject(); nationObject.addProperty("name", nation.getName()); diff --git a/src/main/java/net/earthmc/emcapi/endpoint/towny/PlayersEndpoint.java b/src/main/java/net/earthmc/emcapi/endpoint/towny/PlayersEndpoint.java index a4c232d..d0a6e00 100644 --- a/src/main/java/net/earthmc/emcapi/endpoint/towny/PlayersEndpoint.java +++ b/src/main/java/net/earthmc/emcapi/endpoint/towny/PlayersEndpoint.java @@ -10,6 +10,7 @@ import net.earthmc.emcapi.object.endpoint.PostEndpoint; import net.earthmc.emcapi.util.EndpointUtils; import net.earthmc.emcapi.util.JSONUtil; +import org.jetbrains.annotations.Nullable; import java.util.List; import java.util.UUID; @@ -17,7 +18,7 @@ public class PlayersEndpoint extends PostEndpoint { @Override - public Resident getObjectOrNull(JsonElement element) { + public Resident getObjectOrNull(JsonElement element, @Nullable UUID key) { String string = JSONUtil.getJsonElementAsStringOrNull(element); if (string == null) throw new BadRequestResponse("Your query contains a value that is not a string"); @@ -28,7 +29,9 @@ public Resident getObjectOrNull(JsonElement element) { resident = TownyAPI.getInstance().getResident(string); } - if (resident != null && EndpointUtils.playerOptedOut(resident.getUUID())) { + if (resident != null && EndpointUtils.playerOptedOut(resident.getUUID()) + && !resident.getUUID().equals(EndpointUtils.getKeyOwner(key)) + ) { return null; } @@ -36,7 +39,7 @@ public Resident getObjectOrNull(JsonElement element) { } @Override - public JsonElement getJsonElement(Resident resident) { + public JsonElement getJsonElement(Resident resident, @Nullable UUID key) { JsonObject playerObject = new JsonObject(); playerObject.addProperty("name", resident.getName()); diff --git a/src/main/java/net/earthmc/emcapi/endpoint/towny/QuartersEndpoint.java b/src/main/java/net/earthmc/emcapi/endpoint/towny/QuartersEndpoint.java index 9c58dbf..8b48a4a 100644 --- a/src/main/java/net/earthmc/emcapi/endpoint/towny/QuartersEndpoint.java +++ b/src/main/java/net/earthmc/emcapi/endpoint/towny/QuartersEndpoint.java @@ -12,13 +12,14 @@ import net.earthmc.emcapi.util.JSONUtil; import org.bukkit.Location; +import javax.annotation.Nullable; import java.awt.*; import java.util.UUID; public class QuartersEndpoint extends PostEndpoint { @Override - public Quarter getObjectOrNull(JsonElement element) { + public Quarter getObjectOrNull(JsonElement element, @Nullable UUID key) { String string = JSONUtil.getJsonElementAsStringOrNull(element); if (string == null) throw new BadRequestResponse("Your query contains a value that is not a string"); @@ -33,7 +34,7 @@ public Quarter getObjectOrNull(JsonElement element) { } @Override - public JsonElement getJsonElement(Quarter quarter) { + public JsonElement getJsonElement(Quarter quarter, @Nullable UUID key) { JsonObject quarterObject = new JsonObject(); quarterObject.addProperty("name", quarter.getName()); diff --git a/src/main/java/net/earthmc/emcapi/endpoint/towny/TownsEndpoint.java b/src/main/java/net/earthmc/emcapi/endpoint/towny/TownsEndpoint.java index 50e7aa9..b943d00 100644 --- a/src/main/java/net/earthmc/emcapi/endpoint/towny/TownsEndpoint.java +++ b/src/main/java/net/earthmc/emcapi/endpoint/towny/TownsEndpoint.java @@ -16,6 +16,7 @@ import net.earthmc.emcapi.util.EndpointUtils; import net.earthmc.emcapi.util.JSONUtil; +import javax.annotation.Nullable; import java.util.UUID; public class TownsEndpoint extends PostEndpoint { @@ -26,7 +27,7 @@ public TownsEndpoint(EMCAPI plugin) { } @Override - public Town getObjectOrNull(JsonElement element) { + public Town getObjectOrNull(JsonElement element, @Nullable UUID key) { String string = JSONUtil.getJsonElementAsStringOrNull(element); if (string == null) throw new BadRequestResponse("Your query contains a value that is not a string"); @@ -41,7 +42,7 @@ public Town getObjectOrNull(JsonElement element) { } @Override - public JsonElement getJsonElement(Town town) { + public JsonElement getJsonElement(Town town, @Nullable UUID key) { JsonObject townObject = new JsonObject(); townObject.addProperty("name", town.getName()); diff --git a/src/main/java/net/earthmc/emcapi/integration/Integrations.java b/src/main/java/net/earthmc/emcapi/integration/Integrations.java index 8b383c6..075ef3c 100644 --- a/src/main/java/net/earthmc/emcapi/integration/Integrations.java +++ b/src/main/java/net/earthmc/emcapi/integration/Integrations.java @@ -17,6 +17,7 @@ public class Integrations implements Listener { private final QuartersIntegration quartersIntegration; private final SuperbVoteIntegration superbVoteIntegration; private final MysteryMasterIntegration mysteryMasterIntegration; + private final QuickShopIntegration quickShopIntegration; public Integrations(final EMCAPI plugin) { this.plugin = plugin; @@ -25,6 +26,7 @@ public Integrations(final EMCAPI plugin) { this.quartersIntegration = addIntegration(new QuartersIntegration()); this.superbVoteIntegration = addIntegration(new SuperbVoteIntegration()); this.mysteryMasterIntegration = addIntegration(new MysteryMasterIntegration()); + this.quickShopIntegration = addIntegration(new QuickShopIntegration()); } private T addIntegration(final T integration) { @@ -50,6 +52,10 @@ public MysteryMasterIntegration mysteryMasterIntegration() { return this.mysteryMasterIntegration; } + public QuickShopIntegration quickShopIntegration() { + return quickShopIntegration; + } + @EventHandler public void onPluginEnable(final PluginEnableEvent event) { final Integration integration = integrations.get(event.getPlugin().getName()); diff --git a/src/main/java/net/earthmc/emcapi/integration/QuickShopIntegration.java b/src/main/java/net/earthmc/emcapi/integration/QuickShopIntegration.java new file mode 100644 index 0000000..d4e65d3 --- /dev/null +++ b/src/main/java/net/earthmc/emcapi/integration/QuickShopIntegration.java @@ -0,0 +1,25 @@ +package net.earthmc.emcapi.integration; + +import net.earthmc.emcapi.util.EndpointUtils; +import org.maxgamer.quickshop.QuickShop; +import org.maxgamer.quickshop.api.shop.Shop; + +import java.util.List; +import java.util.UUID; + +public class QuickShopIntegration extends Integration { + + public QuickShopIntegration() { + super("QuickShop"); + } + + public List getPlayerShops(UUID player, UUID key) { + if (!isEnabled()) { + return List.of(); + } + if (!player.equals(EndpointUtils.getKeyOwner(key))) { + return List.of(); + } + return QuickShop.getInstance().getShopManager().getPlayerAllShops(player); + } +} diff --git a/src/main/java/net/earthmc/emcapi/manager/EndpointManager.java b/src/main/java/net/earthmc/emcapi/manager/EndpointManager.java index d218f9a..4522f66 100644 --- a/src/main/java/net/earthmc/emcapi/manager/EndpointManager.java +++ b/src/main/java/net/earthmc/emcapi/manager/EndpointManager.java @@ -5,9 +5,14 @@ import com.google.gson.JsonObject; import io.javalin.Javalin; import io.javalin.http.BadRequestResponse; -import kotlin.Pair; import net.earthmc.emcapi.EMCAPI; -import net.earthmc.emcapi.endpoint.*; +import net.earthmc.emcapi.endpoint.DiscordEndpoint; +import net.earthmc.emcapi.endpoint.LocationEndpoint; +import net.earthmc.emcapi.endpoint.MysteryMasterEndpoint; +import net.earthmc.emcapi.endpoint.NearbyEndpoint; +import net.earthmc.emcapi.endpoint.OnlineEndpoint; +import net.earthmc.emcapi.endpoint.ServerEndpoint; +import net.earthmc.emcapi.endpoint.ShopEndpoint; import net.earthmc.emcapi.endpoint.towny.NationsEndpoint; import net.earthmc.emcapi.endpoint.towny.PlayersEndpoint; import net.earthmc.emcapi.endpoint.towny.QuartersEndpoint; @@ -17,9 +22,14 @@ import net.earthmc.emcapi.endpoint.towny.list.QuartersListEndpoint; import net.earthmc.emcapi.endpoint.towny.list.TownsListEndpoint; import net.earthmc.emcapi.integration.DiscordIntegration; +import net.earthmc.emcapi.integration.MysteryMasterIntegration; import net.earthmc.emcapi.integration.QuartersIntegration; +import net.earthmc.emcapi.integration.QuickShopIntegration; import net.earthmc.emcapi.util.JSONUtil; +import javax.annotation.Nullable; +import java.util.UUID; + public class EndpointManager { private final EMCAPI plugin; @@ -33,24 +43,9 @@ public EndpointManager(EMCAPI plugin) { } public void loadEndpoints() { - DocumentationEndpoint documentationEndpoint = new DocumentationEndpoint(); - javalin.get("/", ctx -> ctx.json(documentationEndpoint.lookup())); - ServerEndpoint serverEndpoint = new ServerEndpoint(plugin); javalin.get(URLPath, ctx -> ctx.json(serverEndpoint.lookup())); - MysteryMasterEndpoint mysteryMasterEndpoint = new MysteryMasterEndpoint(plugin); - javalin.get(URLPath + "/mm", ctx -> { - plugin.integrations().mysteryMasterIntegration().throwIfDisabled(); - ctx.json(mysteryMasterEndpoint.lookup()); - }); - - MudkipEndpoint mudkipEndpoint = new MudkipEndpoint(); - javalin.get("/mudkip", ctx -> { - ctx.contentType("text/plain; charset=UTF-8"); - ctx.result(mudkipEndpoint.lookup()); - }); - loadPlayersEndpoint(); loadTownsEndpoint(); loadNationsEndpoint(); @@ -58,11 +53,12 @@ public void loadEndpoints() { loadLocationEndpoint(); loadNearbyEndpoint(); loadDiscordEndpoint(); - loadPlayerStatsEndpoint(); loadOnlinePlayersEndpoint(); + loadMysteryMasterEndpoint(); + loadShopsEndpoint(); } - private Pair parseBody(String body) { + private QueryBody parseBody(String body) { JsonObject jsonObject = JSONUtil.getJsonObjectFromString(body); JsonElement queryElement = jsonObject.get("query"); @@ -71,21 +67,30 @@ private Pair parseBody(String body) { JsonArray queryArray = queryElement.getAsJsonArray(); JsonElement templateElement = jsonObject.get("template"); - JsonObject templateObject = templateElement != null && templateElement.isJsonObject() - ? templateElement.getAsJsonObject() - : null; - - return new Pair<>(queryArray, templateObject); + JsonObject templateObject = templateElement != null && templateElement.isJsonObject() ? templateElement.getAsJsonObject() : null; + + JsonElement keyElement = jsonObject.get("key"); + String keyValue = keyElement != null && keyElement.isJsonPrimitive() ? keyElement.getAsString() : null; + UUID key = null; + if (keyValue != null) { + try { + key = UUID.fromString(keyValue); + } catch (IllegalArgumentException ignored) {} + } + + return new QueryBody(queryArray, templateObject, key); } + private record QueryBody(JsonArray query, @Nullable JsonObject template, @Nullable UUID key) {} + private void loadPlayersEndpoint() { PlayersListEndpoint ple = new PlayersListEndpoint(); javalin.get(URLPath + "/players", ctx -> ctx.json(ple.lookup())); PlayersEndpoint playersEndpoint = new PlayersEndpoint(); javalin.post(URLPath + "/players", ctx -> { - Pair parsedBody = parseBody(ctx.body()); - ctx.json(playersEndpoint.lookup(parsedBody.getFirst(), parsedBody.getSecond())); + QueryBody parsedBody = parseBody(ctx.body()); + ctx.json(playersEndpoint.lookup(parsedBody.query, parsedBody.template, parsedBody.key)); }); } @@ -95,8 +100,8 @@ private void loadTownsEndpoint() { TownsEndpoint townsEndpoint = new TownsEndpoint(plugin); javalin.post(URLPath + "/towns", ctx -> { - Pair parsedBody = parseBody(ctx.body()); - ctx.json(townsEndpoint.lookup(parsedBody.getFirst(), parsedBody.getSecond())); + QueryBody parsedBody = parseBody(ctx.body()); + ctx.json(townsEndpoint.lookup(parsedBody.query, parsedBody.template, parsedBody.key)); }); } @@ -106,8 +111,8 @@ private void loadNationsEndpoint() { NationsEndpoint nationsEndpoint = new NationsEndpoint(); javalin.post(URLPath + "/nations", ctx -> { - Pair parsedBody = parseBody(ctx.body()); - ctx.json(nationsEndpoint.lookup(parsedBody.getFirst(), parsedBody.getSecond())); + QueryBody parsedBody = parseBody(ctx.body()); + ctx.json(nationsEndpoint.lookup(parsedBody.query, parsedBody.template, parsedBody.key)); }); } @@ -123,24 +128,24 @@ private void loadQuartersEndpoint() { QuartersEndpoint quartersEndpoint = new QuartersEndpoint(); javalin.post(URLPath + "/quarters", ctx -> { quartersIntegration.throwIfDisabled(); - Pair parsedBody = parseBody(ctx.body()); - ctx.json(quartersEndpoint.lookup(parsedBody.getFirst(), parsedBody.getSecond())); + QueryBody parsedBody = parseBody(ctx.body()); + ctx.json(quartersEndpoint.lookup(parsedBody.query, parsedBody.template, parsedBody.key)); }); } private void loadLocationEndpoint() { LocationEndpoint locationEndpoint = new LocationEndpoint(); javalin.post(URLPath + "/location", ctx -> { - Pair parsedBody = parseBody(ctx.body()); - ctx.json(locationEndpoint.lookup(parsedBody.getFirst(), parsedBody.getSecond())); + QueryBody parsedBody = parseBody(ctx.body()); + ctx.json(locationEndpoint.lookup(parsedBody.query, parsedBody.template, parsedBody.key)); }); } private void loadNearbyEndpoint() { NearbyEndpoint nearbyEndpoint = new NearbyEndpoint(); javalin.post(URLPath + "/nearby", ctx -> { - Pair parsedBody = parseBody(ctx.body()); - ctx.json(nearbyEndpoint.lookup(parsedBody.getFirst(), parsedBody.getSecond())); + QueryBody parsedBody = parseBody(ctx.body()); + ctx.json(nearbyEndpoint.lookup(parsedBody.query, parsedBody.template, parsedBody.key)); }); } @@ -151,23 +156,34 @@ private void loadDiscordEndpoint() { javalin.post(URLPath + "/discord", ctx -> { discordIntegration.throwIfDisabled(); - Pair parsedBody = parseBody(ctx.body()); - ctx.json(discordEndpoint.lookup(parsedBody.getFirst(), parsedBody.getSecond())); + QueryBody parsedBody = parseBody(ctx.body()); + ctx.json(discordEndpoint.lookup(parsedBody.query, parsedBody.template, parsedBody.key)); }); } - private void loadPlayerStatsEndpoint() { - PlayerStatsEndpoint playerStatsEndpoint = new PlayerStatsEndpoint(this.plugin); - playerStatsEndpoint.initialize(); - javalin.get(URLPath + "/player-stats", ctx -> { - ctx.json(playerStatsEndpoint.latestCachedStatistics()); + private void loadOnlinePlayersEndpoint() { + OnlineEndpoint onlineEndpoint = new OnlineEndpoint(); + javalin.get(URLPath + "/online", ctx -> ctx.json(onlineEndpoint.lookup())); + } + + private void loadMysteryMasterEndpoint() { + MysteryMasterEndpoint mysteryMasterEndpoint = new MysteryMasterEndpoint(plugin); + MysteryMasterIntegration mysteryMasterIntegration = plugin.integrations().mysteryMasterIntegration(); + javalin.get(URLPath + "/mm", ctx -> { + mysteryMasterIntegration.throwIfDisabled(); + + ctx.json(mysteryMasterEndpoint.lookup()); }); } - private void loadOnlinePlayersEndpoint() { - OnlineEndpoint onlineEndpoint = new OnlineEndpoint(); - javalin.get(URLPath + "/online", ctx -> { - ctx.json(onlineEndpoint.lookup()); + private void loadShopsEndpoint() { + ShopEndpoint shopEndpoint = new ShopEndpoint(plugin); + QuickShopIntegration quickShopIntegration = plugin.integrations().quickShopIntegration();; + javalin.post(URLPath + "/shop", ctx -> { + quickShopIntegration.throwIfDisabled(); + + QueryBody parsedBody = parseBody(ctx.body()); + ctx.json(shopEndpoint.lookup(parsedBody.query, parsedBody.template, parsedBody.key)); }); } } diff --git a/src/main/java/net/earthmc/emcapi/manager/LegacyEndpointManager.java b/src/main/java/net/earthmc/emcapi/manager/LegacyEndpointManager.java new file mode 100644 index 0000000..206a80e --- /dev/null +++ b/src/main/java/net/earthmc/emcapi/manager/LegacyEndpointManager.java @@ -0,0 +1,176 @@ +package net.earthmc.emcapi.manager; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import io.javalin.Javalin; +import io.javalin.http.BadRequestResponse; +import kotlin.Pair; +import net.earthmc.emcapi.EMCAPI; +import net.earthmc.emcapi.endpoint.DiscordEndpoint; +import net.earthmc.emcapi.endpoint.legacy.DocumentationEndpoint; +import net.earthmc.emcapi.endpoint.LocationEndpoint; +import net.earthmc.emcapi.endpoint.legacy.MudkipEndpoint; +import net.earthmc.emcapi.endpoint.MysteryMasterEndpoint; +import net.earthmc.emcapi.endpoint.NearbyEndpoint; +import net.earthmc.emcapi.endpoint.OnlineEndpoint; +import net.earthmc.emcapi.endpoint.legacy.PlayerStatsEndpoint; +import net.earthmc.emcapi.endpoint.ServerEndpoint; +import net.earthmc.emcapi.endpoint.towny.NationsEndpoint; +import net.earthmc.emcapi.endpoint.towny.PlayersEndpoint; +import net.earthmc.emcapi.endpoint.towny.QuartersEndpoint; +import net.earthmc.emcapi.endpoint.towny.TownsEndpoint; +import net.earthmc.emcapi.endpoint.towny.list.NationsListEndpoint; +import net.earthmc.emcapi.endpoint.towny.list.PlayersListEndpoint; +import net.earthmc.emcapi.endpoint.towny.list.QuartersListEndpoint; +import net.earthmc.emcapi.endpoint.towny.list.TownsListEndpoint; +import net.earthmc.emcapi.integration.DiscordIntegration; +import net.earthmc.emcapi.integration.QuartersIntegration; +import net.earthmc.emcapi.util.JSONUtil; + +public class LegacyEndpointManager { + + private final EMCAPI plugin; + private final Javalin javalin; + private final String URLPath = "v3/aurora"; + + public LegacyEndpointManager(EMCAPI plugin) { + this.plugin = plugin; + this.javalin = plugin.getJavalin(); + } + + public void loadEndpoints() { + DocumentationEndpoint documentationEndpoint = new DocumentationEndpoint(); + javalin.get("/", ctx -> ctx.json(documentationEndpoint.lookup())); + + ServerEndpoint serverEndpoint = new ServerEndpoint(plugin); + javalin.get(URLPath, ctx -> ctx.json(serverEndpoint.lookup())); + + MysteryMasterEndpoint mysteryMasterEndpoint = new MysteryMasterEndpoint(plugin); + javalin.get(URLPath + "/mm", ctx -> { + plugin.integrations().mysteryMasterIntegration().throwIfDisabled(); + ctx.json(mysteryMasterEndpoint.lookup()); + }); + + MudkipEndpoint mudkipEndpoint = new MudkipEndpoint(); + javalin.get("/mudkip", ctx -> { + ctx.contentType("text/plain; charset=UTF-8"); + ctx.result(mudkipEndpoint.lookup()); + }); + + loadPlayersEndpoint(); + loadTownsEndpoint(); + loadNationsEndpoint(); + loadQuartersEndpoint(); + loadLocationEndpoint(); + loadNearbyEndpoint(); + loadDiscordEndpoint(); + loadPlayerStatsEndpoint(); + loadOnlinePlayersEndpoint(); + } + + private Pair parseBody(String body) { + JsonObject jsonObject = JSONUtil.getJsonObjectFromString(body); + + JsonElement queryElement = jsonObject.get("query"); + if (queryElement == null) throw new BadRequestResponse("No query array provided"); + if (!queryElement.isJsonArray()) throw new BadRequestResponse("Provided query is not an array"); + JsonArray queryArray = queryElement.getAsJsonArray(); + + JsonElement templateElement = jsonObject.get("template"); + JsonObject templateObject = templateElement != null && templateElement.isJsonObject() + ? templateElement.getAsJsonObject() + : null; + + return new Pair<>(queryArray, templateObject); + } + + private void loadPlayersEndpoint() { + PlayersListEndpoint ple = new PlayersListEndpoint(); + javalin.get(URLPath + "/players", ctx -> ctx.json(ple.lookup())); + + PlayersEndpoint playersEndpoint = new PlayersEndpoint(); + javalin.post(URLPath + "/players", ctx -> { + Pair parsedBody = parseBody(ctx.body()); + ctx.json(playersEndpoint.lookup(parsedBody.getFirst(), parsedBody.getSecond(), null)); + }); + } + + private void loadTownsEndpoint() { + TownsListEndpoint tle = new TownsListEndpoint(); + javalin.get(URLPath + "/towns", ctx -> ctx.json(tle.lookup())); + + TownsEndpoint townsEndpoint = new TownsEndpoint(plugin); + javalin.post(URLPath + "/towns", ctx -> { + Pair parsedBody = parseBody(ctx.body()); + ctx.json(townsEndpoint.lookup(parsedBody.getFirst(), parsedBody.getSecond(), null)); + }); + } + + private void loadNationsEndpoint() { + NationsListEndpoint nle = new NationsListEndpoint(); + javalin.get(URLPath + "/nations", ctx -> ctx.json(nle.lookup())); + + NationsEndpoint nationsEndpoint = new NationsEndpoint(); + javalin.post(URLPath + "/nations", ctx -> { + Pair parsedBody = parseBody(ctx.body()); + ctx.json(nationsEndpoint.lookup(parsedBody.getFirst(), parsedBody.getSecond(), null)); + }); + } + + private void loadQuartersEndpoint() { + QuartersIntegration quartersIntegration = plugin.integrations().quartersIntegration(); + QuartersListEndpoint qle = new QuartersListEndpoint(quartersIntegration); + + javalin.get(URLPath + "/quarters", ctx -> { + quartersIntegration.throwIfDisabled(); + ctx.json(qle.lookup()); + }); + + QuartersEndpoint quartersEndpoint = new QuartersEndpoint(); + javalin.post(URLPath + "/quarters", ctx -> { + quartersIntegration.throwIfDisabled(); + Pair parsedBody = parseBody(ctx.body()); + ctx.json(quartersEndpoint.lookup(parsedBody.getFirst(), parsedBody.getSecond(), null)); + }); + } + + private void loadLocationEndpoint() { + LocationEndpoint locationEndpoint = new LocationEndpoint(); + javalin.post(URLPath + "/location", ctx -> { + Pair parsedBody = parseBody(ctx.body()); + ctx.json(locationEndpoint.lookup(parsedBody.getFirst(), parsedBody.getSecond(), null)); + }); + } + + private void loadNearbyEndpoint() { + NearbyEndpoint nearbyEndpoint = new NearbyEndpoint(); + javalin.post(URLPath + "/nearby", ctx -> { + Pair parsedBody = parseBody(ctx.body()); + ctx.json(nearbyEndpoint.lookup(parsedBody.getFirst(), parsedBody.getSecond(), null)); + }); + } + + private void loadDiscordEndpoint() { + DiscordEndpoint discordEndpoint = new DiscordEndpoint(); + final DiscordIntegration discordIntegration = plugin.integrations().discordIntegration(); + + javalin.post(URLPath + "/discord", ctx -> { + discordIntegration.throwIfDisabled(); + + Pair parsedBody = parseBody(ctx.body()); + ctx.json(discordEndpoint.lookup(parsedBody.getFirst(), parsedBody.getSecond(), null)); + }); + } + + private void loadPlayerStatsEndpoint() { + PlayerStatsEndpoint playerStatsEndpoint = new PlayerStatsEndpoint(this.plugin); + playerStatsEndpoint.initialize(); + javalin.get(URLPath + "/player-stats", ctx -> ctx.json(playerStatsEndpoint.latestCachedStatistics())); + } + + private void loadOnlinePlayersEndpoint() { + OnlineEndpoint onlineEndpoint = new OnlineEndpoint(); + javalin.get(URLPath + "/online", ctx -> ctx.json(onlineEndpoint.lookup())); + } +} diff --git a/src/main/java/net/earthmc/emcapi/object/endpoint/PostEndpoint.java b/src/main/java/net/earthmc/emcapi/object/endpoint/PostEndpoint.java index cdd4202..1e4a3a6 100644 --- a/src/main/java/net/earthmc/emcapi/object/endpoint/PostEndpoint.java +++ b/src/main/java/net/earthmc/emcapi/object/endpoint/PostEndpoint.java @@ -6,37 +6,35 @@ import com.google.gson.JsonPrimitive; import net.earthmc.emcapi.EMCAPI; +import javax.annotation.Nullable; import java.util.Map; +import java.util.UUID; public abstract class PostEndpoint { - public String lookup(JsonArray queryArray, JsonObject template) { + public String lookup(JsonArray queryArray, @Nullable JsonObject template, @Nullable UUID key) { JsonArray jsonArray = new JsonArray(); int numLoops = Math.min(EMCAPI.instance.getConfig().getInt("behaviour.max_lookup_size"), queryArray.size()); for (int i = 0; i < numLoops; i++) { JsonElement element = queryArray.get(i); - T object = getObjectOrNull(element); + T object = getObjectOrNull(element, key); - JsonElement innerObject; if (object == null) { continue; - } else { - innerObject = getTemplateJsonElement(object, template); } - - jsonArray.add(innerObject); + jsonArray.add(getTemplateJsonElement(object, template, key)); } return jsonArray.toString(); } - public abstract T getObjectOrNull(JsonElement element); + public abstract T getObjectOrNull(JsonElement element, @Nullable UUID key); - public abstract JsonElement getJsonElement(T object); + public abstract JsonElement getJsonElement(T object, @Nullable UUID key); - public JsonElement getTemplateJsonElement(T object, JsonObject template) { - JsonElement fullJson = getJsonElement(object); + public JsonElement getTemplateJsonElement(T object, JsonObject template, @Nullable UUID key) { + JsonElement fullJson = getJsonElement(object, key); if (!(fullJson instanceof JsonObject) || template == null || template.entrySet().isEmpty()) { return fullJson; @@ -46,9 +44,9 @@ public JsonElement getTemplateJsonElement(T object, JsonObject template) { JsonObject filteredJson = new JsonObject(); for (Map.Entry entry : template.entrySet()) { - String key = entry.getKey(); - if (entry.getValue() instanceof JsonPrimitive primitive && primitive.getAsBoolean() && fullJsonObject.has(key)) { - filteredJson.add(key, fullJsonObject.get(key)); + String entryKey = entry.getKey(); + if (entry.getValue() instanceof JsonPrimitive primitive && primitive.getAsBoolean() && fullJsonObject.has(entryKey)) { + filteredJson.add(entryKey, fullJsonObject.get(entryKey)); } } diff --git a/src/main/java/net/earthmc/emcapi/util/EndpointUtils.java b/src/main/java/net/earthmc/emcapi/util/EndpointUtils.java index f9b412c..ca163b7 100644 --- a/src/main/java/net/earthmc/emcapi/util/EndpointUtils.java +++ b/src/main/java/net/earthmc/emcapi/util/EndpointUtils.java @@ -269,6 +269,7 @@ public static void deletePlayerKey(UUID player) { } public static UUID getKeyOwner(UUID key) { + if (key == null) return null; return keyPlayerMap.get(key); } @@ -282,6 +283,7 @@ public static JsonObject generateNameUUIDJsonObject(String name, UUID uuid) { public static JsonObject getShopObject(Shop shop) { JsonObject jsonObject = new JsonObject(); jsonObject.addProperty("item", shop.getItem().getType().name()); + jsonObject.addProperty("price", shop.getPrice()); jsonObject.addProperty("amount", shop.getItem().getAmount()); jsonObject.addProperty("type", shop.isSelling() ? "selling" : "buying"); jsonObject.addProperty("stock", shop.isSelling() ? shop.getRemainingStock() : shop.getRemainingSpace()); diff --git a/src/main/java/net/earthmc/emcapi/util/JSONUtil.java b/src/main/java/net/earthmc/emcapi/util/JSONUtil.java index 38acc3f..918b407 100644 --- a/src/main/java/net/earthmc/emcapi/util/JSONUtil.java +++ b/src/main/java/net/earthmc/emcapi/util/JSONUtil.java @@ -15,16 +15,6 @@ public static JsonObject getJsonObjectFromString(String string) { } } - public static JsonArray getJsonArrayFromStringList(List stringList) { - JsonArray jsonArray = new JsonArray(); - if (stringList == null) return jsonArray; - - for (String item : stringList) { - jsonArray.add(item); - } - return jsonArray; - } - public static String getJsonElementAsStringOrNull(JsonElement element) { if (element == null) return null; diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index c026564..6053376 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -19,3 +19,6 @@ behaviour: # Setting for testing reasons developer_mode: false + + # Should legacy endpoints (if any) be loaded? + load_legacy: true diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 22f083b..441eaef 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -3,9 +3,9 @@ version: "${version}" main: net.earthmc.emcapi.EMCAPI api-version: "1.20" depend: [Towny] -softdepend: [MysteryMaster, Quarters, DiscordSRV, SuperbVote] +softdepend: [MysteryMaster, Quarters, DiscordSRV, SuperbVote, QuickShop] authors: [Fruitloopins] -contributors: [Warriorrr, Yoditi] +contributors: [Warriorrr, Yoditi, Veyronity] description: API for EarthMC using Javalin website: "https://github.com/EarthMC/EMCAPI/commit/${commit}" folia-supported: true @@ -13,6 +13,15 @@ folia-supported: true commands: api: description: Allows you to opt in or out of your information being visible in the API. + permission: emcapi.command + +permissions: + emcapi.command: + default: true + description: Allow access to the /api command + emcapi.key: + default: true + description: Allow players to create API keys libraries: - io.javalin:javalin:${javalin_version} From b2c8f9a270150dd9b0c68fab0aecfbbc520982a4 Mon Sep 17 00:00:00 2001 From: Veyronity Date: Fri, 6 Mar 2026 15:25:31 +0200 Subject: [PATCH 5/7] spotlessApply --- src/main/java/net/earthmc/emcapi/util/JSONUtil.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/net/earthmc/emcapi/util/JSONUtil.java b/src/main/java/net/earthmc/emcapi/util/JSONUtil.java index 918b407..7257c59 100644 --- a/src/main/java/net/earthmc/emcapi/util/JSONUtil.java +++ b/src/main/java/net/earthmc/emcapi/util/JSONUtil.java @@ -3,8 +3,6 @@ import com.google.gson.*; import io.javalin.http.BadRequestResponse; -import java.util.List; - public class JSONUtil { public static JsonObject getJsonObjectFromString(String string) { From c94da03db4723f4c8a6a660a3d4a0fc44182a42a Mon Sep 17 00:00:00 2001 From: Veyronity Date: Fri, 6 Mar 2026 23:28:04 +0200 Subject: [PATCH 6/7] Use consistent Nullable annotation --- src/main/java/net/earthmc/emcapi/endpoint/DiscordEndpoint.java | 2 +- src/main/java/net/earthmc/emcapi/endpoint/LocationEndpoint.java | 2 +- src/main/java/net/earthmc/emcapi/endpoint/NearbyEndpoint.java | 2 +- .../java/net/earthmc/emcapi/endpoint/towny/NationsEndpoint.java | 2 +- .../net/earthmc/emcapi/endpoint/towny/QuartersEndpoint.java | 2 +- .../java/net/earthmc/emcapi/endpoint/towny/TownsEndpoint.java | 2 +- src/main/java/net/earthmc/emcapi/manager/EndpointManager.java | 2 +- .../java/net/earthmc/emcapi/object/endpoint/PostEndpoint.java | 2 +- src/main/java/net/earthmc/emcapi/sse/SSEManager.java | 2 +- src/main/java/net/earthmc/emcapi/util/EndpointUtils.java | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/main/java/net/earthmc/emcapi/endpoint/DiscordEndpoint.java b/src/main/java/net/earthmc/emcapi/endpoint/DiscordEndpoint.java index fcd5730..d0d706a 100644 --- a/src/main/java/net/earthmc/emcapi/endpoint/DiscordEndpoint.java +++ b/src/main/java/net/earthmc/emcapi/endpoint/DiscordEndpoint.java @@ -10,8 +10,8 @@ import net.earthmc.emcapi.util.EndpointUtils; import net.earthmc.emcapi.util.HttpExceptions; import net.earthmc.emcapi.util.JSONUtil; +import org.jetbrains.annotations.Nullable; -import javax.annotation.Nullable; import java.util.UUID; import java.util.regex.Matcher; import java.util.regex.Pattern; diff --git a/src/main/java/net/earthmc/emcapi/endpoint/LocationEndpoint.java b/src/main/java/net/earthmc/emcapi/endpoint/LocationEndpoint.java index 201d547..bfdc286 100644 --- a/src/main/java/net/earthmc/emcapi/endpoint/LocationEndpoint.java +++ b/src/main/java/net/earthmc/emcapi/endpoint/LocationEndpoint.java @@ -12,8 +12,8 @@ import net.earthmc.emcapi.util.JSONUtil; import org.bukkit.Bukkit; import org.bukkit.Location; +import org.jetbrains.annotations.Nullable; -import javax.annotation.Nullable; import java.util.UUID; public class LocationEndpoint extends PostEndpoint> { diff --git a/src/main/java/net/earthmc/emcapi/endpoint/NearbyEndpoint.java b/src/main/java/net/earthmc/emcapi/endpoint/NearbyEndpoint.java index 3b3ef7a..cc14b22 100644 --- a/src/main/java/net/earthmc/emcapi/endpoint/NearbyEndpoint.java +++ b/src/main/java/net/earthmc/emcapi/endpoint/NearbyEndpoint.java @@ -18,8 +18,8 @@ import net.earthmc.emcapi.util.JSONUtil; import org.bukkit.Bukkit; import org.bukkit.Location; +import org.jetbrains.annotations.Nullable; -import javax.annotation.Nullable; import java.util.ArrayList; import java.util.List; import java.util.UUID; diff --git a/src/main/java/net/earthmc/emcapi/endpoint/towny/NationsEndpoint.java b/src/main/java/net/earthmc/emcapi/endpoint/towny/NationsEndpoint.java index b25e655..ff92b63 100644 --- a/src/main/java/net/earthmc/emcapi/endpoint/towny/NationsEndpoint.java +++ b/src/main/java/net/earthmc/emcapi/endpoint/towny/NationsEndpoint.java @@ -12,8 +12,8 @@ import net.earthmc.emcapi.object.endpoint.PostEndpoint; import net.earthmc.emcapi.util.EndpointUtils; import net.earthmc.emcapi.util.JSONUtil; +import org.jetbrains.annotations.Nullable; -import javax.annotation.Nullable; import java.util.List; import java.util.UUID; diff --git a/src/main/java/net/earthmc/emcapi/endpoint/towny/QuartersEndpoint.java b/src/main/java/net/earthmc/emcapi/endpoint/towny/QuartersEndpoint.java index 8b48a4a..8456705 100644 --- a/src/main/java/net/earthmc/emcapi/endpoint/towny/QuartersEndpoint.java +++ b/src/main/java/net/earthmc/emcapi/endpoint/towny/QuartersEndpoint.java @@ -11,8 +11,8 @@ import net.earthmc.emcapi.util.EndpointUtils; import net.earthmc.emcapi.util.JSONUtil; import org.bukkit.Location; +import org.jetbrains.annotations.Nullable; -import javax.annotation.Nullable; import java.awt.*; import java.util.UUID; diff --git a/src/main/java/net/earthmc/emcapi/endpoint/towny/TownsEndpoint.java b/src/main/java/net/earthmc/emcapi/endpoint/towny/TownsEndpoint.java index b943d00..dc2117e 100644 --- a/src/main/java/net/earthmc/emcapi/endpoint/towny/TownsEndpoint.java +++ b/src/main/java/net/earthmc/emcapi/endpoint/towny/TownsEndpoint.java @@ -15,8 +15,8 @@ import net.earthmc.emcapi.object.endpoint.PostEndpoint; import net.earthmc.emcapi.util.EndpointUtils; import net.earthmc.emcapi.util.JSONUtil; +import org.jetbrains.annotations.Nullable; -import javax.annotation.Nullable; import java.util.UUID; public class TownsEndpoint extends PostEndpoint { diff --git a/src/main/java/net/earthmc/emcapi/manager/EndpointManager.java b/src/main/java/net/earthmc/emcapi/manager/EndpointManager.java index 4522f66..d5c6618 100644 --- a/src/main/java/net/earthmc/emcapi/manager/EndpointManager.java +++ b/src/main/java/net/earthmc/emcapi/manager/EndpointManager.java @@ -26,8 +26,8 @@ import net.earthmc.emcapi.integration.QuartersIntegration; import net.earthmc.emcapi.integration.QuickShopIntegration; import net.earthmc.emcapi.util.JSONUtil; +import org.jetbrains.annotations.Nullable; -import javax.annotation.Nullable; import java.util.UUID; public class EndpointManager { diff --git a/src/main/java/net/earthmc/emcapi/object/endpoint/PostEndpoint.java b/src/main/java/net/earthmc/emcapi/object/endpoint/PostEndpoint.java index 1e4a3a6..1e3b0d8 100644 --- a/src/main/java/net/earthmc/emcapi/object/endpoint/PostEndpoint.java +++ b/src/main/java/net/earthmc/emcapi/object/endpoint/PostEndpoint.java @@ -5,8 +5,8 @@ import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; import net.earthmc.emcapi.EMCAPI; +import org.jetbrains.annotations.Nullable; -import javax.annotation.Nullable; import java.util.Map; import java.util.UUID; diff --git a/src/main/java/net/earthmc/emcapi/sse/SSEManager.java b/src/main/java/net/earthmc/emcapi/sse/SSEManager.java index 3166308..3713c63 100644 --- a/src/main/java/net/earthmc/emcapi/sse/SSEManager.java +++ b/src/main/java/net/earthmc/emcapi/sse/SSEManager.java @@ -6,8 +6,8 @@ import io.javalin.http.sse.SseClient; import net.earthmc.emcapi.EMCAPI; import net.earthmc.emcapi.util.EndpointUtils; +import org.jetbrains.annotations.Nullable; -import javax.annotation.Nullable; import java.time.Instant; import java.util.HashSet; import java.util.Map; diff --git a/src/main/java/net/earthmc/emcapi/util/EndpointUtils.java b/src/main/java/net/earthmc/emcapi/util/EndpointUtils.java index ca163b7..6eaa2dd 100644 --- a/src/main/java/net/earthmc/emcapi/util/EndpointUtils.java +++ b/src/main/java/net/earthmc/emcapi/util/EndpointUtils.java @@ -11,9 +11,9 @@ import org.bukkit.Location; import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.maxgamer.quickshop.api.shop.Shop; -import javax.annotation.Nullable; import java.util.ArrayList; import java.util.Collections; import java.util.List; From eefffa3a9c64bcb7048624f1d563a5d9ce79c3b2 Mon Sep 17 00:00:00 2001 From: Veyronity Date: Fri, 6 Mar 2026 23:28:49 +0200 Subject: [PATCH 7/7] Last one --- src/main/java/net/earthmc/emcapi/endpoint/ShopEndpoint.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/net/earthmc/emcapi/endpoint/ShopEndpoint.java b/src/main/java/net/earthmc/emcapi/endpoint/ShopEndpoint.java index 8718b67..2082152 100644 --- a/src/main/java/net/earthmc/emcapi/endpoint/ShopEndpoint.java +++ b/src/main/java/net/earthmc/emcapi/endpoint/ShopEndpoint.java @@ -8,7 +8,7 @@ import net.earthmc.emcapi.object.endpoint.PostEndpoint; import net.earthmc.emcapi.util.EndpointUtils; import net.earthmc.emcapi.util.JSONUtil; -import org.jspecify.annotations.Nullable; +import org.jetbrains.annotations.Nullable; import org.maxgamer.quickshop.api.shop.Shop; import java.util.List;