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.properties b/gradle.properties index 52d728a..8050299 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ group = net.earthmc.emcapi -version = 3.0.0-SNAPSHOT +version = 4.0.0-SNAPSHOT description = EMCAPI org.gradle.configuration-cache=true 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 8ce11a9..4f5a295 100644 --- a/src/main/java/net/earthmc/emcapi/EMCAPI.java +++ b/src/main/java/net/earthmc/emcapi/EMCAPI.java @@ -6,9 +6,14 @@ 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; 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; @@ -25,6 +30,7 @@ public final class EMCAPI extends JavaPlugin { public static EMCAPI instance; private Javalin javalin; private Integrations pluginIntegrations; + private SSEManager sseManager; @Override public void onLoad() { @@ -42,14 +48,16 @@ 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) { getLogger().warning("API command not found."); } else { - OptOutCommand cmd = new OptOutCommand(); + ApiCommand cmd = new ApiCommand(); apiCommand.setExecutor(cmd); apiCommand.setTabCompleter(cmd); } @@ -58,6 +66,21 @@ public void onEnable() { } catch (IOException e) { getLogger().warning("IOException while loading opted-out players: " + e); } + + sseManager = new SSEManager(this); + sseManager.loadSSE(); + 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 @@ -68,6 +91,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() { @@ -120,4 +149,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/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/endpoint/DiscordEndpoint.java b/src/main/java/net/earthmc/emcapi/endpoint/DiscordEndpoint.java index 0dddcff..d0d706a 100644 --- a/src/main/java/net/earthmc/emcapi/endpoint/DiscordEndpoint.java +++ b/src/main/java/net/earthmc/emcapi/endpoint/DiscordEndpoint.java @@ -10,6 +10,7 @@ import net.earthmc.emcapi.util.EndpointUtils; import net.earthmc.emcapi.util.HttpExceptions; import net.earthmc.emcapi.util.JSONUtil; +import org.jetbrains.annotations.Nullable; import java.util.UUID; import java.util.regex.Matcher; @@ -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..bfdc286 100644 --- a/src/main/java/net/earthmc/emcapi/endpoint/LocationEndpoint.java +++ b/src/main/java/net/earthmc/emcapi/endpoint/LocationEndpoint.java @@ -12,11 +12,14 @@ import net.earthmc.emcapi.util.JSONUtil; import org.bukkit.Bukkit; import org.bukkit.Location; +import org.jetbrains.annotations.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..cc14b22 100644 --- a/src/main/java/net/earthmc/emcapi/endpoint/NearbyEndpoint.java +++ b/src/main/java/net/earthmc/emcapi/endpoint/NearbyEndpoint.java @@ -18,14 +18,16 @@ import net.earthmc.emcapi.util.JSONUtil; import org.bukkit.Bukkit; import org.bukkit.Location; +import org.jetbrains.annotations.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..2082152 --- /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.jetbrains.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..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,6 +12,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; @@ -19,7 +20,7 @@ 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..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,6 +11,7 @@ import net.earthmc.emcapi.util.EndpointUtils; import net.earthmc.emcapi.util.JSONUtil; import org.bukkit.Location; +import org.jetbrains.annotations.Nullable; import java.awt.*; import java.util.UUID; @@ -18,7 +19,7 @@ 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..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,6 +15,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.UUID; @@ -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 58e8ac0..d5c6618 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,39 +22,29 @@ 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 org.jetbrains.annotations.Nullable; + +import java.util.UUID; 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() { - DocumentationEndpoint documentationEndpoint = new DocumentationEndpoint(); - javalin.get("/", ctx -> ctx.json(documentationEndpoint.lookup())); - ServerEndpoint serverEndpoint = new ServerEndpoint(plugin); - javalin.get(v3URLPath, ctx -> ctx.json(serverEndpoint.lookup())); - - MysteryMasterEndpoint mysteryMasterEndpoint = new MysteryMasterEndpoint(plugin); - javalin.get(v3URLPath + "/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()); - }); + javalin.get(URLPath, ctx -> ctx.json(serverEndpoint.lookup())); loadPlayersEndpoint(); loadTownsEndpoint(); @@ -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,43 +67,52 @@ 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(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 -> { - Pair parsedBody = parseBody(ctx.body()); - ctx.json(playersEndpoint.lookup(parsedBody.getFirst(), parsedBody.getSecond())); + javalin.post(URLPath + "/players", ctx -> { + QueryBody parsedBody = parseBody(ctx.body()); + ctx.json(playersEndpoint.lookup(parsedBody.query, parsedBody.template, parsedBody.key)); }); } 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 -> { - Pair parsedBody = parseBody(ctx.body()); - ctx.json(townsEndpoint.lookup(parsedBody.getFirst(), parsedBody.getSecond())); + javalin.post(URLPath + "/towns", ctx -> { + QueryBody parsedBody = parseBody(ctx.body()); + ctx.json(townsEndpoint.lookup(parsedBody.query, parsedBody.template, parsedBody.key)); }); } 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 -> { - Pair parsedBody = parseBody(ctx.body()); - ctx.json(nationsEndpoint.lookup(parsedBody.getFirst(), parsedBody.getSecond())); + javalin.post(URLPath + "/nations", ctx -> { + QueryBody parsedBody = parseBody(ctx.body()); + ctx.json(nationsEndpoint.lookup(parsedBody.query, parsedBody.template, parsedBody.key)); }); } @@ -115,32 +120,32 @@ 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())); + QueryBody parsedBody = parseBody(ctx.body()); + ctx.json(quartersEndpoint.lookup(parsedBody.query, parsedBody.template, parsedBody.key)); }); } private void loadLocationEndpoint() { LocationEndpoint locationEndpoint = new LocationEndpoint(); - javalin.post(v3URLPath + "/location", ctx -> { - Pair parsedBody = parseBody(ctx.body()); - ctx.json(locationEndpoint.lookup(parsedBody.getFirst(), parsedBody.getSecond())); + javalin.post(URLPath + "/location", ctx -> { + QueryBody parsedBody = parseBody(ctx.body()); + ctx.json(locationEndpoint.lookup(parsedBody.query, parsedBody.template, parsedBody.key)); }); } private void loadNearbyEndpoint() { NearbyEndpoint nearbyEndpoint = new NearbyEndpoint(); - javalin.post(v3URLPath + "/nearby", ctx -> { - Pair parsedBody = parseBody(ctx.body()); - ctx.json(nearbyEndpoint.lookup(parsedBody.getFirst(), parsedBody.getSecond())); + javalin.post(URLPath + "/nearby", ctx -> { + QueryBody parsedBody = parseBody(ctx.body()); + ctx.json(nearbyEndpoint.lookup(parsedBody.query, parsedBody.template, parsedBody.key)); }); } @@ -148,26 +153,37 @@ 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()); - 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(v3URLPath + "/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(v3URLPath + "/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..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,38 +5,36 @@ import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; import net.earthmc.emcapi.EMCAPI; +import org.jetbrains.annotations.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/sse/SSEManager.java b/src/main/java/net/earthmc/emcapi/sse/SSEManager.java new file mode 100644 index 0000000..3713c63 --- /dev/null +++ b/src/main/java/net/earthmc/emcapi/sse/SSEManager.java @@ -0,0 +1,150 @@ +package net.earthmc.emcapi.sse; + +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 org.jetbrains.annotations.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; + this.javalin = plugin.getJavalin(); + } + + 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.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(); + + 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/TownySSEListener.java b/src/main/java/net/earthmc/emcapi/sse/listeners/TownySSEListener.java new file mode 100644 index 0000000..2f4b885 --- /dev/null +++ b/src/main/java/net/earthmc/emcapi/sse/listeners/TownySSEListener.java @@ -0,0 +1,181 @@ +package net.earthmc.emcapi.sse.listeners; + +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; +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 com.palmergames.bukkit.towny.object.Town; +import net.earthmc.emcapi.sse.SSEManager; +import net.earthmc.emcapi.util.EndpointUtils; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; + +public class TownySSEListener extends AbstractSSEListener { + + public TownySSEListener(SSEManager sse) { + super(sse); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onNewDay(NewDayEvent event) { + sse.sendEvent("NewDay", new JsonObject()); + } + + @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); + } + + @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 4afdb27..6eaa2dd 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.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; +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; @@ -214,4 +222,71 @@ 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) { + if (key == null) return null; + 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("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()); + return jsonObject; + } } diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 0dbb4a2..6053376 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: @@ -16,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}