diff --git a/pom.xml b/pom.xml index 38a79f4..e11ffd4 100644 --- a/pom.xml +++ b/pom.xml @@ -59,7 +59,7 @@ io.javalin javalin - 6.7.0 + 7.0.1 provided diff --git a/src/main/java/pro/cloudnode/smp/smpcore/REST.java b/src/main/java/pro/cloudnode/smp/smpcore/REST.java deleted file mode 100644 index 61b023d..0000000 --- a/src/main/java/pro/cloudnode/smp/smpcore/REST.java +++ /dev/null @@ -1,221 +0,0 @@ -package pro.cloudnode.smp.smpcore; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.JsonArray; -import com.google.gson.JsonObject; -import io.javalin.Javalin; -import io.javalin.json.JsonMapper; -import org.bukkit.OfflinePlayer; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.lang.reflect.Type; -import java.util.Optional; -import java.util.Set; -import java.util.UUID; - -public class REST { - final @NotNull Javalin javalin = Javalin.create(config -> config.jsonMapper(new Mapper())); - - private void e404 (final @NotNull io.javalin.http.Context ctx) { - ctx.status(404); - final @NotNull JsonObject obj = new JsonObject(); - obj.addProperty("error", "not found"); - ctx.json(obj); - } - - private @NotNull JsonObject getMemberObject(final @NotNull Member member) { - final @NotNull JsonObject obj = new JsonObject(); - final @NotNull OfflinePlayer player = member.player(); - obj.addProperty("uuid", member.uuid.toString()); - obj.addProperty("name", CachedProfile.getName(player)); - obj.addProperty("nation", member.nationID); - obj.addProperty("staff", member.staff); - obj.addProperty("online", !member.staff && player.isOnline()); - obj.addProperty("whitelisted", player.isWhitelisted()); - obj.addProperty("banned", player.isBanned()); - obj.addProperty("altOwner", member.altOwnerUUID == null ? null : member.altOwnerUUID.toString()); - obj.addProperty("added", member.added.getTime()); - obj.addProperty("lastSeen", member.staff ? 0 : player.getLastSeen()); - obj.addProperty("firstSeen", player.getFirstPlayed()); - obj.addProperty("active", member.isActive()); - return obj; - } - - private @NotNull JsonObject getNationObject(final @NotNull Nation nation) { - final @NotNull JsonObject obj = new JsonObject(); - obj.addProperty("id", nation.id); - obj.addProperty("name", nation.name); - obj.addProperty("shortName", nation.shortName); - obj.addProperty("color", nation.color); - obj.addProperty("leader", nation.leaderUUID.toString()); - obj.addProperty("viceLeader", nation.viceLeaderUUID.toString()); - obj.addProperty("members", nation.citizens().size()); - obj.addProperty("founded", nation.founded.getTime()); - obj.addProperty("foundedGameTicks", nation.foundedTicks); - obj.addProperty("foundedGameDate", SMPCore.gameTime(nation.foundedTicks).getTime()); - obj.addProperty("bank", nation.bank); - return obj; - } - - public REST(final int port) { - javalin.before(ctx -> { - final @Nullable String origin = ctx.header("Origin"); - ctx.header("Access-Control-Allow-Origin", origin == null ? "*" : origin); - ctx.header("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS"); - ctx.header("Access-Control-Allow-Headers", "*"); - ctx.header("Access-Control-Allow-Credentials", "true"); - ctx.header("Access-Control-Max-Age", "3600"); - }); - - javalin.get("/", ctx -> { - final @NotNull JsonObject obj = new JsonObject(); - obj.addProperty("version", SMPCore.getInstance().getPluginMeta().getVersion()); - obj.addProperty("time", SMPCore.gameTime().getTime()); - ctx.json(obj); - }); - - javalin.get("/members", ctx -> { - final @Nullable String filter = ctx.queryParam("filter"); - final @Nullable String limitString = ctx.queryParam("limit"); - final @Nullable String pageString = ctx.queryParam("page"); - final @Nullable String include = ctx.queryParam("include"); - - final @Nullable Integer limit; - if (limitString == null) limit = null; - else { - @Nullable Integer t = null; - try { - t = Integer.parseInt(limitString); - } - catch (final @NotNull NumberFormatException ignored) {} - limit = t; - } - - final int page; - if (pageString == null) page = 1; - else { - int t; - try { - t = Integer.parseInt(pageString); - } - catch (final @NotNull NumberFormatException ignored) { - t = 1; - } - page = t; - } - - final @NotNull Set<@NotNull Member> members = limit == null ? Member.get() : Member.get(limit, page); - final @NotNull JsonArray arr = new JsonArray(); - for (final @NotNull Member member : members) { - if (filter != null) - switch (filter) { - case "online": - if (member.staff || !member.player().isOnline()) - continue; - case "offline": - if (!member.staff && member.player().isOnline()) - continue; - case "banned": - if (!member.player().isBanned()) - continue; - } - final @NotNull JsonObject m = getMemberObject(member); - if (include != null) { - switch (include) { - case "nation" -> { - final @NotNull Optional<@NotNull Nation> optionalNation = member.nation(); - if (optionalNation.isEmpty()) m.add("nation", null); - else m.add("nation", getNationObject(optionalNation.get())); - } - } - } - arr.add(m); - } - ctx.json(arr); - }); - - javalin.get("/members/{uuid}", ctx -> { - final @NotNull UUID uuid; - try { - uuid = UUID.fromString(ctx.pathParam("uuid")); - } - catch (final @NotNull IllegalArgumentException e) { - e404(ctx); - return; - } - final @NotNull OfflinePlayer offlinePlayer = SMPCore.getInstance().getServer() - .getOfflinePlayer(uuid); - final @NotNull Optional<@NotNull Member> member = Member.get(offlinePlayer); - if (member.isEmpty()) { - e404(ctx); - return; - } - final @NotNull Set<@NotNull Member> alts = member.get().getAlts(); - final @NotNull JsonObject obj = getMemberObject(member.get()); - final @NotNull JsonArray altsArray = new JsonArray(); - for (final @NotNull Member alt : alts) { - final @NotNull JsonObject altObj = new JsonObject(); - final @NotNull OfflinePlayer player = alt.player(); - altObj.addProperty("uuid", alt.uuid.toString()); - altObj.addProperty("name", CachedProfile.getName(player)); - altObj.addProperty("nation", alt.nationID); - altObj.addProperty("added", alt.added.getTime()); - altObj.addProperty("lastSeen", alt.staff ? 0 : player.getLastSeen()); - altsArray.add(altObj); - } - obj.add("alts", altsArray); - ctx.json(obj); - }); - - javalin.get("/nations", ctx -> { - final @NotNull Set<@NotNull Nation> nations = Nation.get(); - final @NotNull JsonArray arr = new JsonArray(); - for (final @NotNull Nation nation : nations) - arr.add(getNationObject(nation)); - ctx.json(arr); - }); - - javalin.get("/nations/{id}", ctx -> { - final @Nullable String include = ctx.queryParam("include"); - - final @NotNull Optional<@NotNull Nation> nation = Nation.get(ctx.pathParam("id")); - if (nation.isEmpty()) { - e404(ctx); - return; - } - final @NotNull JsonObject obj = getNationObject(nation.get()); - - if (include != null) { - switch (include) { - case "members" -> { - final @NotNull JsonArray arr = new JsonArray(); - final @NotNull Set<@NotNull Member> members = nation.get().citizens(); - for (final @NotNull Member member : members) - arr.add(getMemberObject(member)); - obj.add("members", arr); - } - } - } - - ctx.json(obj); - }); - - javalin.start(port); - } - - public static final class Mapper implements JsonMapper { - private final @NotNull Gson gson = new GsonBuilder().serializeNulls().create(); - - @Override - public @NotNull String toJsonString(final @NotNull Object obj, final @NotNull Type type) { - return gson.toJson(obj, type); - } - - @Override - public @NotNull T fromJsonString(final @NotNull String json, final @NotNull Type targetType) { - return gson.fromJson(json, targetType); - } - } -} diff --git a/src/main/java/pro/cloudnode/smp/smpcore/SMPCore.java b/src/main/java/pro/cloudnode/smp/smpcore/SMPCore.java index a6fc47c..651a718 100644 --- a/src/main/java/pro/cloudnode/smp/smpcore/SMPCore.java +++ b/src/main/java/pro/cloudnode/smp/smpcore/SMPCore.java @@ -9,6 +9,7 @@ import org.bukkit.plugin.java.JavaPlugin; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import pro.cloudnode.smp.smpcore.api.REST; import pro.cloudnode.smp.smpcore.command.AltsCommand; import pro.cloudnode.smp.smpcore.command.BanCommand; import pro.cloudnode.smp.smpcore.command.CitizensCommand; @@ -112,7 +113,7 @@ public void onDisable() { getLogger().log(Level.SEVERE, "failed to close db connection", e); } db.close(); - if (rest != null) rest.javalin.stop(); + if (rest != null) rest.stop(); } public void reload() { @@ -121,7 +122,7 @@ public void reload() { if (messages != null) messages.reload(); setupDatabase(); Member.createStaffTeam(); - if (rest != null) rest.javalin.stop(); + if (rest != null) rest.stop(); rest = new REST(config.apiPort()); } diff --git a/src/main/java/pro/cloudnode/smp/smpcore/api/REST.java b/src/main/java/pro/cloudnode/smp/smpcore/api/REST.java new file mode 100644 index 0000000..2614d70 --- /dev/null +++ b/src/main/java/pro/cloudnode/smp/smpcore/api/REST.java @@ -0,0 +1,80 @@ +package pro.cloudnode.smp.smpcore.api; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonObject; +import io.javalin.Javalin; +import io.javalin.config.JavalinConfig; +import io.javalin.http.Context; +import io.javalin.json.JsonMapper; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import pro.cloudnode.smp.smpcore.SMPCore; +import pro.cloudnode.smp.smpcore.api.routes.Members; +import pro.cloudnode.smp.smpcore.api.routes.Nations; + +import java.lang.reflect.Type; + +public class REST { + private final @NotNull Javalin javalin; + + public REST(final int port) { + javalin = Javalin.create(config -> { + config.jsonMapper(new Mapper()); + }).start(port); + } + + private static void info(final @NotNull Context ctx) { + final @NotNull JsonObject obj = new JsonObject(); + obj.addProperty("version", SMPCore.getInstance().getPluginMeta().getVersion()); + obj.addProperty("time", SMPCore.gameTime().getTime()); + ctx.json(obj); + } + + public void stop() { + javalin.stop(); + } + + private void configureRoutes(final @NotNull JavalinConfig config) { + config.routes.before(ctx -> { + final @Nullable String origin = ctx.header("Origin"); + ctx.header("Access-Control-Allow-Origin", origin == null ? "*" : origin); + ctx.header("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS"); + ctx.header("Access-Control-Allow-Headers", "*"); + ctx.header("Access-Control-Allow-Credentials", "true"); + ctx.header("Access-Control-Max-Age", "3600"); + }); + + + config.routes.get("/", REST::info); + + final var members = new Members(this); + config.routes.get("/members", members::list); + config.routes.get("/members/{uuid}", members::get); + + final var nations = new Nations(this); + config.routes.get("/nations", nations::list); + config.routes.get("/nations/{id}", nations::get); + } + + public void e404(final @NotNull Context ctx) { + ctx.status(404); + final @NotNull JsonObject obj = new JsonObject(); + obj.addProperty("error", "not found"); + ctx.json(obj); + } + + public static final class Mapper implements JsonMapper { + private final @NotNull Gson gson = new GsonBuilder().serializeNulls().create(); + + @Override + public @NotNull String toJsonString(final @NotNull Object obj, final @NotNull Type type) { + return gson.toJson(obj, type); + } + + @Override + public @NotNull T fromJsonString(final @NotNull String json, final @NotNull Type targetType) { + return gson.fromJson(json, targetType); + } + } +} diff --git a/src/main/java/pro/cloudnode/smp/smpcore/api/routes/Members.java b/src/main/java/pro/cloudnode/smp/smpcore/api/routes/Members.java new file mode 100644 index 0000000..e90261a --- /dev/null +++ b/src/main/java/pro/cloudnode/smp/smpcore/api/routes/Members.java @@ -0,0 +1,134 @@ +package pro.cloudnode.smp.smpcore.api.routes; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import io.javalin.http.Context; +import org.bukkit.OfflinePlayer; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import pro.cloudnode.smp.smpcore.CachedProfile; +import pro.cloudnode.smp.smpcore.Member; +import pro.cloudnode.smp.smpcore.SMPCore; +import pro.cloudnode.smp.smpcore.api.REST; + +import java.util.Optional; +import java.util.OptionalInt; +import java.util.Set; +import java.util.UUID; +import java.util.function.Predicate; + +public final class Members { + private final @NotNull REST rest; + + public Members(final @NotNull REST rest) { + this.rest = rest; + } + + public static @NotNull JsonObject map(final @NotNull Member member) { + final @NotNull JsonObject obj = new JsonObject(); + final @NotNull OfflinePlayer player = member.player(); + obj.addProperty("uuid", member.uuid.toString()); + obj.addProperty("name", CachedProfile.getName(player)); + obj.addProperty("nation", member.nationID); + obj.addProperty("staff", member.staff); + obj.addProperty("online", !member.staff && player.isOnline()); + obj.addProperty("whitelisted", player.isWhitelisted()); + obj.addProperty("banned", player.isBanned()); + obj.addProperty("altOwner", member.altOwnerUUID == null ? null : member.altOwnerUUID.toString()); + obj.addProperty("added", member.added.getTime()); + obj.addProperty("lastSeen", member.staff ? 0 : player.getLastSeen()); + obj.addProperty("firstSeen", player.getFirstPlayed()); + obj.addProperty("active", member.isActive()); + return obj; + } + + private static OptionalInt parseInt(final @Nullable String value) { + if (value == null) + return OptionalInt.empty(); + try { + return OptionalInt.of(Integer.parseInt(value)); + } + catch (final NumberFormatException ignored) { + return OptionalInt.empty(); + } + } + + private static int parseInt(final @Nullable String value, final int defaultValue) { + if (value == null) + return defaultValue; + try { + return Integer.parseInt(value); + } + catch (final NumberFormatException ignored) { + return defaultValue; + } + } + + private static Predicate resolveFilter(final @Nullable String filter) { + return switch (filter) { + case "online" -> member -> !member.staff && member.player().isOnline(); + case "offline" -> member -> member.staff || !member.player().isOnline(); + case "banned" -> member -> member.player().isBanned(); + case null, default -> _ -> true; + }; + } + + private static void applyInclude(@NotNull JsonObject json, @NotNull Member member, @Nullable String include) { + if (include == null) + return; + + if ("nation".equals(include)) { + member.nation() + .map(Nations::map) + .ifPresentOrElse(nation -> json.add("nation", nation), () -> json.add("nation", null)); + } + } + + public void list(final @NotNull Context ctx) { + final var limit = parseInt(ctx.queryParam("limit")); + final var page = parseInt(ctx.queryParam("page"), 1); + + final @NotNull Set<@NotNull Member> members = limit.isEmpty() ? Member.get() + : Member.get(limit.getAsInt(), page); + final @NotNull JsonArray arr = new JsonArray(); + + final var filterPredicate = resolveFilter(ctx.queryParam("filter")); + + final @Nullable String include = ctx.queryParam("include"); + + for (final @NotNull Member member : members) { + if (!filterPredicate.test(member)) + continue; + + final @NotNull JsonObject m = map(member); + applyInclude(m, member, include); + arr.add(m); + } + ctx.json(arr); + } + + public void get(final @NotNull Context ctx) { + final @NotNull UUID uuid; + try { + uuid = UUID.fromString(ctx.pathParam("uuid")); + } + catch (final @NotNull IllegalArgumentException e) { + rest.e404(ctx); + return; + } + final @NotNull OfflinePlayer offlinePlayer = SMPCore.getInstance().getServer().getOfflinePlayer(uuid); + final @NotNull Optional<@NotNull Member> member = Member.get(offlinePlayer); + if (member.isEmpty()) { + rest.e404(ctx); + return; + } + final @NotNull Set<@NotNull Member> alts = member.get().getAlts(); + final @NotNull JsonObject obj = map(member.get()); + final @NotNull JsonArray altsArray = new JsonArray(); + for (final @NotNull Member alt : alts) { + altsArray.add(map(alt)); + } + obj.add("alts", altsArray); + ctx.json(obj); + } +} diff --git a/src/main/java/pro/cloudnode/smp/smpcore/api/routes/Nations.java b/src/main/java/pro/cloudnode/smp/smpcore/api/routes/Nations.java new file mode 100644 index 0000000..f682f37 --- /dev/null +++ b/src/main/java/pro/cloudnode/smp/smpcore/api/routes/Nations.java @@ -0,0 +1,71 @@ +package pro.cloudnode.smp.smpcore.api.routes; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import io.javalin.http.Context; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import pro.cloudnode.smp.smpcore.Member; +import pro.cloudnode.smp.smpcore.Nation; +import pro.cloudnode.smp.smpcore.SMPCore; +import pro.cloudnode.smp.smpcore.api.REST; + +import java.util.Optional; +import java.util.Set; + +public final class Nations { + private final @NotNull REST rest; + + public Nations(final @NotNull REST rest) { + this.rest = rest; + } + + public static @NotNull JsonObject map(final @NotNull Nation nation) { + final @NotNull JsonObject obj = new JsonObject(); + obj.addProperty("id", nation.id); + obj.addProperty("name", nation.name); + obj.addProperty("shortName", nation.shortName); + obj.addProperty("color", nation.color); + obj.addProperty("leader", nation.leaderUUID.toString()); + obj.addProperty("viceLeader", nation.viceLeaderUUID.toString()); + obj.addProperty("members", nation.citizens().size()); + obj.addProperty("founded", nation.founded.getTime()); + obj.addProperty("foundedGameTicks", nation.foundedTicks); + obj.addProperty("foundedGameDate", SMPCore.gameTime(nation.foundedTicks).getTime()); + obj.addProperty("bank", nation.bank); + return obj; + } + + public void list(final @NotNull Context ctx) { + final @NotNull Set<@NotNull Nation> nations = Nation.get(); + final @NotNull JsonArray arr = new JsonArray(); + for (final @NotNull Nation nation : nations) + arr.add(map(nation)); + ctx.json(arr); + } + + public void get(final @NotNull Context ctx) { + final @Nullable String include = ctx.queryParam("include"); + + final @NotNull Optional<@NotNull Nation> nation = Nation.get(ctx.pathParam("id")); + if (nation.isEmpty()) { + rest.e404(ctx); + return; + } + final @NotNull JsonObject obj = map(nation.get()); + + if (include != null) { + switch (include) { + case "members" -> { + final @NotNull JsonArray arr = new JsonArray(); + final @NotNull Set<@NotNull Member> citizens = nation.get().citizens(); + for (final @NotNull Member member : citizens) + arr.add(Members.map(member)); + obj.add("members", arr); + } + } + } + + ctx.json(obj); + } +}