diff --git a/build.gradle.kts b/build.gradle.kts index 5ccb953..ef4a780 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -32,6 +32,10 @@ repositories { maven("https://nexus.neetgames.com/repository/maven-releases/") { mavenContent { includeGroup("com.gmail.nossr50.mcMMO") } } + + maven("https://repo.warriorrr.dev/releases") { + mavenContent { includeGroupAndSubgroups("dev.warriorrr") } + } } dependencies { @@ -56,6 +60,7 @@ dependencies { compileOnly(libs.lynchpin.pursuits) compileOnly(libs.lynchpin.towny) compileOnly(libs.lynchpin.advancements) + implementation(libs.inventories) } tasks { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 395cf0d..ca72833 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,6 +10,7 @@ quickshop = "6.2.0.11" hikaricp = "7.0.2" mcmmo = "2.2.040" lynchpin-api = "1.0-SNAPSHOT" +inventories = "1.1.0" # plugins conventions = "1.1.0" @@ -30,6 +31,7 @@ mcmmo = { group = "com.gmail.nossr50.mcMMO", name = "mcMMO", version.ref = "mcmm lynchpin-pursuits = { group = "net.earthmc.lynchpin.api", name = "pursuits", version.ref = "lynchpin-api"} lynchpin-towny = { group = "net.earthmc.lynchpin.api", name = "towny", version.ref = "lynchpin-api"} lynchpin-advancements = { group = "net.earthmc.lynchpin.api", name = "advancements", version.ref = "lynchpin-api"} +inventories = { group = "dev.warriorrr.inventories", name = "inventories", version.ref = "inventories" } [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 b0fd198..aad0fbe 100644 --- a/src/main/java/net/earthmc/emcapi/EMCAPI.java +++ b/src/main/java/net/earthmc/emcapi/EMCAPI.java @@ -1,6 +1,7 @@ package net.earthmc.emcapi; import com.zaxxer.hikari.HikariConfig; +import dev.warriorrr.inventories.Inventories; import io.javalin.Javalin; import io.javalin.http.TooManyRequestsResponse; import io.javalin.util.JavalinLogger; @@ -8,7 +9,9 @@ import net.earthmc.emcapi.database.APIDatabase; import net.earthmc.emcapi.database.DatabaseSchema; import net.earthmc.emcapi.integration.Integrations; +import net.earthmc.emcapi.manager.Authorisation; import net.earthmc.emcapi.manager.EndpointManager; +import net.earthmc.emcapi.manager.GUIManager; import net.earthmc.emcapi.manager.KeyManager; import net.earthmc.emcapi.manager.OptOut; import net.earthmc.emcapi.sse.SSEManager; @@ -35,6 +38,8 @@ public final class EMCAPI extends JavaPlugin { private final SSEManager sseManager = new SSEManager(this); private final APIDatabase database = new APIDatabase(); private final OptOut optOut = new OptOut(this); + private final Authorisation auth = new Authorisation(this); + private final GUIManager guiManager = new GUIManager(this); @Override public void onLoad() { @@ -56,6 +61,7 @@ public void onEnable() { getSLF4JLogger().warn("exception while loading API keys: ", e); } optOut.loadOptOut(); + auth.loadAuthSettings(); initialiseJavalin(); @@ -70,8 +76,10 @@ public void onEnable() { if (pm.isPluginEnabled("QuickShop-Hikari")) { pm.registerEvents(new ShopSSEListener(sseManager), this); } + pm.registerEvents(guiManager, this); getServer().getAsyncScheduler().runAtFixedRate(this, t -> CooldownUtil.refresh(), 5, 5, TimeUnit.MINUTES); + Inventories.forPlugin(this).build(); } @Override @@ -167,4 +175,12 @@ public APIDatabase getDatabase() { public OptOut getOptOut() { return optOut; } + + public Authorisation getAuth() { + return auth; + } + + public GUIManager getGUIManager() { + return guiManager; + } } diff --git a/src/main/java/net/earthmc/emcapi/command/ApiCommand.java b/src/main/java/net/earthmc/emcapi/command/ApiCommand.java index e798683..4d7c87d 100644 --- a/src/main/java/net/earthmc/emcapi/command/ApiCommand.java +++ b/src/main/java/net/earthmc/emcapi/command/ApiCommand.java @@ -42,41 +42,33 @@ public static LiteralCommandNode create(final EMCAPI plugin) return Commands.literal("api") .requires(ctx -> ctx.getSender().hasPermission("emcapi.command")) .executes(ctx -> { + if (ctx.getSource().getSender() instanceof Player player) { + plugin.getGUIManager().createRoot(player).openAsRoot(player); + return Command.SINGLE_SUCCESS; + } ctx.getSource().getSender().sendMessage(INFO_MESSAGE); return Command.SINGLE_SUCCESS; }) .then(Commands.literal("opt-out") - .requires(ctx -> ctx.getSender() instanceof Player) + .requires(ctx -> ctx.getSender() instanceof Player player && player.hasPermission("emcapi.opt-out.editor")) .executes(ctx -> { final Player player = (Player) ctx.getSource().getSender(); - - if (plugin.getOptOut().playerOptedOut(player.getUniqueId())) { - player.sendMessage(Component.text("You have already opted out previously!", NamedTextColor.RED)); - } else { - if (isOnCooldown(player, CooldownType.OPT_OUT_CHANGE)) { - return Command.SINGLE_SUCCESS; - } - - player.sendMessage(Component.text("You have opted out of having your information being public on the API.", NamedTextColor.GREEN)); - plugin.getOptOut().setOptedOut(player.getUniqueId(), true); + if (isOnCooldown(player, CooldownType.OPT_OUT_CHANGE)) { + return Command.SINGLE_SUCCESS; } + player.sendMessage(Component.text("Opening Opt out Editor", NamedTextColor.GREEN)); + plugin.getGUIManager().createOptOutMenu(player).openAsRoot(player); return Command.SINGLE_SUCCESS; })) - .then(Commands.literal("opt-in") - .requires(ctx -> ctx.getSender() instanceof Player) + .then(Commands.literal("auth") + .requires(ctx -> ctx.getSender() instanceof Player player && player.hasPermission("emcapi.auth.editor")) .executes(ctx -> { final Player player = (Player) ctx.getSource().getSender(); - - if (plugin.getOptOut().playerOptedOut(player.getUniqueId())) { - if (isOnCooldown(player, CooldownType.OPT_OUT_CHANGE)) { - return Command.SINGLE_SUCCESS; - } - - player.sendMessage(Component.text("You have opted back in to your information being public on the API.", NamedTextColor.GREEN)); - plugin.getOptOut().setOptedOut(player.getUniqueId(), false); - } else { - player.sendMessage(Component.text("You are currently not opted out!", NamedTextColor.RED)); + if (isOnCooldown(player, CooldownType.AUTH_CHANGE)) { + return Command.SINGLE_SUCCESS; } + player.sendMessage(Component.text("Opening Auth Editor", NamedTextColor.GREEN)); + plugin.getGUIManager().createAuthMenu(player).openAsRoot(player); return Command.SINGLE_SUCCESS; })) .then(Commands.literal("key") @@ -138,7 +130,7 @@ private static boolean isOnCooldown(final Player player, final CooldownType type final Instant lastCommandUse = COOLDOWNS.asMap().putIfAbsent(new CommandCooldown(player.getUniqueId(), type), now); if (lastCommandUse != null) { - final long seconds = Duration.between(now, lastCommandUse.plusSeconds(60)).getSeconds(); + final long seconds = Duration.between(now, lastCommandUse.plusSeconds(type.cooldown)).getSeconds(); if (seconds > 0) { player.sendMessage(Component.text("Please wait " + seconds + " more second" + (seconds == 1 ? "" : "s") + " before trying this command again.", NamedTextColor.RED)); @@ -150,8 +142,15 @@ private static boolean isOnCooldown(final Player player, final CooldownType type } private enum CooldownType { - MODIFY_KEY, - OPT_OUT_CHANGE + MODIFY_KEY(60), + OPT_OUT_CHANGE(15), + AUTH_CHANGE(15); + + final int cooldown; + + CooldownType(int cooldown) { + this.cooldown = cooldown; + } } private record CommandCooldown(UUID uuid, CooldownType type) {} diff --git a/src/main/java/net/earthmc/emcapi/database/DatabaseSchema.java b/src/main/java/net/earthmc/emcapi/database/DatabaseSchema.java index d2a0a4a..ec31239 100644 --- a/src/main/java/net/earthmc/emcapi/database/DatabaseSchema.java +++ b/src/main/java/net/earthmc/emcapi/database/DatabaseSchema.java @@ -19,6 +19,18 @@ public static void createTables(Connection connection) throws SQLException { } statement.executeUpdate("CREATE TABLE IF NOT EXISTS opt_out(`uuid` CHAR(36) NOT NULL, PRIMARY KEY (`uuid`))"); + for (String column : getOptOutColumns()) { + try { + statement.executeUpdate("alter table opt_out add column " + column); + } catch (SQLException ignored) {} + } + + statement.executeUpdate("CREATE TABLE IF NOT EXISTS authorised(`uuid` CHAR(36) NOT NULL, PRIMARY KEY (`uuid`))"); + for (String column : getAuthorisedColumns()) { + try { + statement.executeUpdate("alter table authorised add column " + column); + } catch (SQLException ignored) {} + } } } @@ -34,4 +46,21 @@ private static List getApiKeysColumns() { "`api_key` VARCHAR(" + KeyManager.MAX_KEY_LENGTH + ") NOT NULL" ); } + + private static List getOptOutColumns() { + return List.of( + "`override_all` BOOLEAN DEFAULT TRUE", // If true, the player is deemed to have opted out of all the options, without checking each one + "`towny_resident` BOOLEAN DEFAULT TRUE", + "`online_status` BOOLEAN DEFAULT TRUE", + "`quickshops` BOOLEAN DEFAULT TRUE", + "`mcmmo_stats` BOOLEAN DEFAULT TRUE" + ); + } + + private static List getAuthorisedColumns() { + return List.of( + "`shop_sse` TEXT DEFAULT ''", + "`shop_query` TEXT DEFAULT ''" + ); + } } diff --git a/src/main/java/net/earthmc/emcapi/endpoint/McMMOEndpoint.java b/src/main/java/net/earthmc/emcapi/endpoint/McMMOEndpoint.java index 0cc630b..4106948 100644 --- a/src/main/java/net/earthmc/emcapi/endpoint/McMMOEndpoint.java +++ b/src/main/java/net/earthmc/emcapi/endpoint/McMMOEndpoint.java @@ -10,6 +10,7 @@ import net.earthmc.emcapi.integration.McMMOIntegration; import net.earthmc.emcapi.manager.KeyManager; import net.earthmc.emcapi.object.endpoint.PostEndpoint; +import net.earthmc.emcapi.object.optout.OptOutSettings; import net.earthmc.emcapi.util.CooldownUtil; import net.earthmc.emcapi.util.HttpExceptions; import net.earthmc.emcapi.util.JSONUtil; @@ -29,7 +30,7 @@ public McMMOEndpoint(EMCAPI plugin) { @Override public PlayerProfile getObjectOrNull(JsonElement element, @Nullable String key) { String string = JSONUtil.getJsonElementAsStringOrNull(element); - if (string == null) throw HttpExceptions.NOT_A_STRING;; + if (string == null) throw HttpExceptions.NOT_A_STRING; UUID player; try { @@ -42,7 +43,8 @@ public PlayerProfile getObjectOrNull(JsonElement element, @Nullable String key) if (keyOwner == null) { throw HttpExceptions.MISSING_API_KEY; } - if (!player.equals(keyOwner)) { + OptOutSettings settings = plugin.getOptOut().getPlayerSettings(player); + if (!player.equals(keyOwner) && (settings == null || settings.mcmmo())) { throw HttpExceptions.FORBIDDEN; } diff --git a/src/main/java/net/earthmc/emcapi/endpoint/ShopEndpoint.java b/src/main/java/net/earthmc/emcapi/endpoint/ShopEndpoint.java index 03fa544..cd1e977 100644 --- a/src/main/java/net/earthmc/emcapi/endpoint/ShopEndpoint.java +++ b/src/main/java/net/earthmc/emcapi/endpoint/ShopEndpoint.java @@ -8,6 +8,8 @@ import net.earthmc.emcapi.integration.QuickShopIntegration; import net.earthmc.emcapi.manager.KeyManager; import net.earthmc.emcapi.object.endpoint.PostEndpoint; +import net.earthmc.emcapi.object.optout.AuthSettings; +import net.earthmc.emcapi.object.optout.OptOutSettings; import net.earthmc.emcapi.util.CooldownUtil; import net.earthmc.emcapi.util.EndpointUtils; import net.earthmc.emcapi.util.HttpExceptions; @@ -46,7 +48,8 @@ public List getObjectOrNull(JsonElement element, @Nullable String key) { if (keyOwner == null) { throw HttpExceptions.MISSING_API_KEY; } - if (!player.equals(KeyManager.getKeyOwner(key))) { + OptOutSettings settings = plugin.getOptOut().getPlayerSettings(player); + if (!player.equals(keyOwner) && (settings == null || settings.quickShops() && !plugin.getAuth().authorize(player, AuthSettings.Type.SHOP_QUERY, keyOwner))) { throw HttpExceptions.FORBIDDEN; } CooldownUtil.checkAndAddCooldownOrThrow("shop", keyOwner.toString(), COOLDOWN_SECONDS); @@ -57,10 +60,6 @@ public List getObjectOrNull(JsonElement element, @Nullable String key) { public JsonElement getJsonElement(List object, @Nullable String key) { final Map shops = new ConcurrentHashMap<>(); int counter = 0; - UUID keyOwner = KeyManager.getKeyOwner(key); - if (keyOwner == null) { - throw HttpExceptions.MISSING_API_KEY; - } if (object.isEmpty()) { return null; } @@ -68,8 +67,6 @@ public JsonElement getJsonElement(List object, @Nullable String key) { final List> shopFutures = new ArrayList<>(); for (Shop shop : object) { - if (!keyOwner.equals(shop.getOwner().getUniqueId())) continue; - final CompletableFuture shopFuture = new CompletableFuture<>(); shopFutures.add(shopFuture); diff --git a/src/main/java/net/earthmc/emcapi/endpoint/legacy/DiscordEndpoint.java b/src/main/java/net/earthmc/emcapi/endpoint/legacy/DiscordEndpoint.java deleted file mode 100644 index 153790c..0000000 --- a/src/main/java/net/earthmc/emcapi/endpoint/legacy/DiscordEndpoint.java +++ /dev/null @@ -1,108 +0,0 @@ -package net.earthmc.emcapi.endpoint.legacy; - -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import github.scarsz.discordsrv.DiscordSRV; -import io.javalin.http.BadRequestResponse; -import net.earthmc.emcapi.EMCAPI; -import net.earthmc.emcapi.object.endpoint.PostEndpoint; -import net.earthmc.emcapi.object.nearby.DiscordContext; -import net.earthmc.emcapi.object.nearby.DiscordType; -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; -import java.util.regex.Pattern; - -public class DiscordEndpoint extends PostEndpoint { - private static final Pattern ID_PATTERN = Pattern.compile("^\\d{17,19}$"); - private static final BadRequestResponse MISSING_TYPE_TARGET = new BadRequestResponse("Your JSON query is missing a type or target"); - private static final BadRequestResponse INVALID_TYPE_TARGET = new BadRequestResponse("Your JSON query has an invalid type or target"); - - public DiscordEndpoint(final EMCAPI plugin) { - super(plugin); - } - - @Override - public DiscordContext getObjectOrNull(JsonElement element, @Nullable String key) { - JsonObject jsonObject = JSONUtil.getJsonElementAsJsonObjectOrNull(element); - if (jsonObject == null) { - throw HttpExceptions.NOT_A_JSON_OBJECT; - } - - JsonElement typeElement = jsonObject.get("type"); - JsonElement targetElement = jsonObject.get("target"); - if (typeElement == null || targetElement == null) { - throw MISSING_TYPE_TARGET; - } - - String typeString = JSONUtil.getJsonElementAsStringOrNull(typeElement); - String target = JSONUtil.getJsonElementAsStringOrNull(targetElement); - if (typeString == null || target == null) { - throw INVALID_TYPE_TARGET; - } - - UUID uuid = null; - try { - uuid = getUUIDFromStr(target); - } catch (BadRequestResponse ignored) { - try { - uuid = getUUIDFromDiscordId(target); - } catch (BadRequestResponse ignored1) {} - } - - if (uuid != null && plugin.getOptOut().playerOptedOut(uuid)) { - return null; - } - - try { - DiscordType type = DiscordType.valueOf(typeString.toUpperCase()); - return new DiscordContext(type, target); - } catch (IllegalArgumentException e) { - throw new BadRequestResponse("Specified type is not valid"); - } - } - - @Override - public JsonElement getJsonElement(DiscordContext context, @Nullable String key) { - DiscordType type = context.getType(); - String target = context.getTarget(); - - JsonObject discordObject = new JsonObject(); - if (type == DiscordType.DISCORD) { - UUID uuid = getUUIDFromDiscordId(target); - discordObject.addProperty("id", target); - discordObject.addProperty("uuid", uuid == null ? null : uuid.toString()); - } else if (type == DiscordType.MINECRAFT) { - UUID uuid = getUUIDFromStr(target); - if (uuid == null) { - throw new BadRequestResponse(target + " is not a valid Minecraft UUID"); - } - - discordObject.addProperty("id", DiscordSRV.getPlugin().getAccountLinkManager().getDiscordId(uuid)); - discordObject.addProperty("uuid", uuid.toString()); - } - - return discordObject; - } - - private UUID getUUIDFromStr(String uuidStr) { - UUID uuid; - try { - uuid = UUID.fromString(uuidStr); - } catch (IllegalArgumentException e) { - return null; - } - return uuid; - } - - private UUID getUUIDFromDiscordId(String discordId) { - Matcher matcher = ID_PATTERN.matcher(discordId); - - if (!matcher.find()) return null; - - return DiscordSRV.getPlugin().getAccountLinkManager().getUuid(discordId); - } -} diff --git a/src/main/java/net/earthmc/emcapi/endpoint/legacy/MudkipEndpoint.java b/src/main/java/net/earthmc/emcapi/endpoint/legacy/MudkipEndpoint.java deleted file mode 100644 index efcece2..0000000 --- a/src/main/java/net/earthmc/emcapi/endpoint/legacy/MudkipEndpoint.java +++ /dev/null @@ -1,44 +0,0 @@ -package net.earthmc.emcapi.endpoint.legacy; - -public class MudkipEndpoint { - - public String lookup() { - return """ - ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣠⣴⠶⠚⠛⠳⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ - ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⡴⠟⠉⢻⠀⠀⠀⠀⢻⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ - ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⡾⠋⠀⠀⠀⢸⡇⠀⠀⠀⢸⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ - ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣴⠟⡇⠀⠀⠀⠀⢸⡇⠀⠀⠀⢸⣧⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ - ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣾⠃⠀⣿⠀⠀⠀⠀⢸⡇⠀⠀⠀⢸⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ - ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣾⠃⠀⠀⢸⠀⠀⠀⠀⢸⡇⠀⠀⠀⢸⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ - ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣼⠃⠀⠀⠀⢸⠀⠀⠀⠀⢸⡇⠀⠀⠀⢸⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ - ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣿⠀⠀⠀⠀⢸⠀⠀⠀⠀⢸⠀⠀⠀⠀⢸⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ - ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⡟⠀⠀⠀⠀⢸⠀⠀⠀⠀⣸⠀⠀⠀⠀⣾⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ - ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣇⠀⠀⠀⠀⢸⠀⠀⠀⠀⣿⠀⠀⠀⠀⡿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ - ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠸⣿⠀⠀⠀⠀⢸⠀⠀⠀⠀⡇⠀⠀⠀⢰⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ - ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⠀⠀⠀⠀⢸⠀⠀⠀⠀⠀⠀⠀⠀⣸⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ - ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢹⡆⠀⠀⠀⠈⠀⠀⠀⠀⠀⠀⠀⢀⡟⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ - ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⣧⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣼⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ - ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣿⡀⠀⠀⠀⠀⠀⠀⠀⠒⠲⠶⢯⣄⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣤⣤⠶⠶⠶⣤⠀ - ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⡴⠚⠉⠸⣇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠉⠻⢦⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣤⡶⠛⠉⠁⠀⠀⠀⠀⠈⣧ - ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⡾⠋⠀⠀⠀⠀⢹⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠻⣦⡀⠀⠀⠀⠀⠀⠀⠀⢀⣴⠞⠋⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿ - ⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⡾⠋⠀⠀⠀⠀⠀⠀⠈⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠻⣆⠀⠀⠀⢀⣠⠞⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣿ - ⠀⠀⠀⠀⠀⠀⠀⠀⢠⡟⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⣧⢀⡴⠋⠁⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⣤⠶⠞⠋⢩⡇ - ⠀⠀⠀⠀⠀⠀⠀⢠⡟⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⡀⠀⠀⠀⠀⠀⠀⢀⣠⣾⣿⣿⣏⣠⣤⣶⣿⣿⣿⣿⣀⣤⠴⠞⠋⠉⠀⠀⠀⠀⣾⠁ - ⠀⢠⣤⣤⣄⣀⠀⣼⣿⣄⠀⠀⠀⠀⢠⡏⢳⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⡏⢹⡄⠀⠀⠀⠀⣠⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡟⠉⠀⠀⠀⠀⠀⠀⠀⠀⢰⡏⠀ - ⠀⢸⣿⣿⣿⣿⣿⣿⣿⣿⣆⠀⠀⠀⢸⣧⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣷⣾⡇⠀⠀⠀⣰⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡟⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡟⠀⠀ - ⠀⠈⣿⣿⣿⣿⣿⣿⣿⣿⣿⡆⠀⠀⠸⣿⡿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠸⣿⣿⠃⠀⠀⢰⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣤⠀⠀⠀⠀⠀⠀⢀⡾⠁⠀⠀ - ⠀⢀⣼⣿⣿⣿⣿⣿⣿⣿⣿⣿⡶⢤⣤⣌⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣈⣡⣤⣤⣤⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⠀⠀⠀⠀⠀⣼⠃⠀⠀⠀ - ⢠⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⠀⠀⡼⠋⠉⠉⠉⠉⠉⠉⠉⠉⠉⠀⠀⠀⠀⠀⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠿⠛⠁⠀⠀⠀⠀⣼⠃⠀⠀⠀⠀ - ⠻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣇⣸⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣼⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢻⡷⠤⣤⣤⣀⣀⣀⣀⡼⠃⠀⠀⠀⠀⠀ - ⠀⠀⠈⠉⢻⣿⣿⣿⣿⣿⣿⢿⣿⣿⠛⢦⣄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣠⠴⠋⠘⣿⣿⣿⣿⠿⢿⣿⣿⣿⣿⣿⣿⠀⢷⡀⠀⠀⠀⠀⢀⡿⠁⠀⠀⠀⠀⠀⠀ - ⠀⠀⠀⠀⠈⣿⣿⣿⣿⠟⠁⠀⠙⢻⣤⣄⣈⠙⠓⠶⠦⠤⠤⠤⠴⠶⠚⠉⠀⠀⠀⣀⣼⠿⠛⠁⠀⠀⠉⣿⣿⣿⣿⣿⠀⠈⣧⠀⠀⠀⣰⠟⠀⠀⠀⠀⠀⠀⠀⠀ - ⠀⠀⠀⠀⠀⠘⠿⠛⠁⠀⠀⠀⢠⠏⠀⠈⠉⠛⠳⢶⣶⠦⢤⣤⣤⡤⠴⠶⠶⣿⠋⠉⠀⠀⠀⠀⠀⠀⠀⣿⠀⠉⠛⠋⠀⠀⠸⡆⢠⡾⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀ - ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣰⠏⠀⠀⠀⠀⠀⠀⠀⢈⡿⠶⣤⣀⠀⠀⠀⠀⢸⡄⠀⠀⠀⠀⠀⠀⠀⠀⢸⡇⠀⠀⠀⠀⠀⠀⢿⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ - ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⠏⠀⠀⠀⠀⠀⠀⠀⣰⡟⠁⠀⢸⡏⠙⠛⠲⠶⠾⣧⠀⠀⠀⠀⠀⠀⠀⠀⢸⡇⠀⠀⠀⠀⠀⠀⢸⡆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ - ⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⠏⠀⠀⠀⠀⠀⠀⣠⡾⠋⠀⠀⠀⢸⠃⠀⠀⠀⠀⠀⣸⣆⠀⠀⠀⠀⠀⠀⠀⢸⣧⡀⠀⠀⠀⠀⠀⠀⣧⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ - ⠀⠀⠀⠀⠀⠀⠀⢠⡶⣫⠀⠀⠀⠀⠀⣠⡾⠋⠀⠀⠀⠀⠀⡼⣀⢀⡀⠀⠀⣰⠏⠹⣆⠀⠀⠀⠀⠀⠀⢸⠀⠙⠳⣄⡀⠀⣀⠀⡘⣧⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ - ⠀⠀⠀⠀⠀⠀⠀⠹⠾⢧⣧⣤⠤⠶⠛⠁⠀⠀⠀⠀⠀⠀⠀⠛⠿⠿⠤⠶⠞⠁⠀⠀⠹⣆⠀⢠⡀⢀⠀⢸⠀⠀⠀⠀⠙⠶⠾⠖⠋⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ - ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ ⠹⢤⣄⣷⣬⠷⠚⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ - """; - } -} diff --git a/src/main/java/net/earthmc/emcapi/endpoint/legacy/PlayerStatsEndpoint.java b/src/main/java/net/earthmc/emcapi/endpoint/legacy/PlayerStatsEndpoint.java deleted file mode 100644 index e5bcb73..0000000 --- a/src/main/java/net/earthmc/emcapi/endpoint/legacy/PlayerStatsEndpoint.java +++ /dev/null @@ -1,134 +0,0 @@ -package net.earthmc.emcapi.endpoint.legacy; - -import com.google.gson.JsonObject; -import io.papermc.paper.threadedregions.scheduler.ScheduledTask; -import net.earthmc.emcapi.EMCAPI; -import org.bukkit.Bukkit; -import org.bukkit.OfflinePlayer; -import org.bukkit.Registry; -import org.bukkit.Statistic; -import org.jetbrains.annotations.NotNull; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.StandardOpenOption; -import java.util.HashMap; -import java.util.Map; - -public class PlayerStatsEndpoint { - private final EMCAPI plugin; - private final Path statsFilePath; - private ScheduledTask task = null; - private String latestStats = createEmptyStatistics().toString(); // Create empty statistics to return until we can load the actual ones - - public PlayerStatsEndpoint(EMCAPI plugin) { - this.plugin = plugin; - this.statsFilePath = plugin.getDataFolder().toPath().resolve("player-stats.json"); - } - - public void initialize() { - // Load cached stats from disk and start the task to gather up-to-date statistics - loadStatsFromDisk(); - startGatherStatisticsTask(); - } - - // Unused, but may be useful eventually - public void shutdown() { - if (this.task != null) { - this.task.cancel(); - this.task = null; - } - } - - public void startGatherStatisticsTask() { - /*this.task = Bukkit.getServer().getAsyncScheduler().runAtFixedRate(plugin, task -> { - gatherStatistics(); - }, 15L, 30L, TimeUnit.MINUTES);*/ - } - - private void gatherStatistics() { - // a long should be *long* enough to hold all results - Map statistics = new HashMap<>(); - - for (final OfflinePlayer offlinePlayer : Bukkit.getServer().getOfflinePlayers()) { - for (final Statistic statistic : Registry.STATISTIC) { - if (statistic.getType() == Statistic.Type.UNTYPED) - statistics.merge(statistic, (long) offlinePlayer.getStatistic(statistic), Long::sum); - } - } - - JsonObject json = new JsonObject(); - statistics.forEach((statistic, value) -> json.addProperty(fixStatisticKey(statistic), value)); - - this.latestStats = json.toString(); - - // Save stats to disk - try { - Files.writeString(this.statsFilePath, this.latestStats, StandardCharsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); - } catch (IOException e) { - plugin.getSLF4JLogger().warn("Failed to save cached player stats to disk", e); - } - } - - @NotNull - public String latestCachedStatistics() { - return this.latestStats; - } - - private void loadStatsFromDisk() { - plugin.getServer().getAsyncScheduler().runNow(plugin, task -> { - try { - if (!Files.exists(this.statsFilePath)) { - return; - } - - this.latestStats = Files.readString(this.statsFilePath, StandardCharsets.UTF_8); - } catch (IOException e) { - plugin.getSLF4JLogger().warn("Failed to read cached player stats file from disk", e); - } - }); - } - - // time taken: too long - private String fixStatisticKey(final Statistic statistic) { - final String minimal = statistic.getKey().asMinimalString(); - - return switch (minimal) { - case "play_one_minute" -> "play_time"; - case "armor_cleaned" -> "clean_armor"; - case "banner_cleaned" -> "clean_banner"; - case "cake_slices_eaten" -> "eat_cake_slice"; - case "cauldron_used" -> "use_cauldron"; - case "cauldron_filled" -> "fill_cauldron"; - case "chest_opened" -> "open_chest"; - case "dispenser_inspected" -> "inspect_dispenser"; - case "dropper_inspected" -> "inspect_dropper"; - case "hopper_inspected" -> "inspect_hopper"; - case "enderchest_opened" -> "open_enderchest"; - case "furnace_interaction" -> "interact_with_furnace"; - case "crafting_table_interaction" -> "interact_with_crafting_table"; - case "beacon_interaction" -> "interact_with_beacon"; - case "brewingstand_interaction" -> "interact_with_brewingstand"; - case "record_played" -> "play_record"; - case "noteblock_played" -> "play_noteblock"; - case "noteblock_tuned" -> "tune_noteblock"; - case "flower_potted" -> "pot_flower"; - case "shulker_box_opened" -> "open_shulker_box"; - case "item_enchanted" -> "enchant_item"; - case "trapped_chest_triggered" -> "trigger_trapped_chest"; - default -> minimal; - }; - } - - private JsonObject createEmptyStatistics() { - JsonObject stats = new JsonObject(); - - for (final Statistic stat : Registry.STATISTIC) { - stats.addProperty(fixStatisticKey(stat), 0); - } - - return stats; - } -} 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 926e0e0..bf38e86 100644 --- a/src/main/java/net/earthmc/emcapi/endpoint/towny/PlayersEndpoint.java +++ b/src/main/java/net/earthmc/emcapi/endpoint/towny/PlayersEndpoint.java @@ -11,6 +11,7 @@ import net.earthmc.emcapi.integration.Integrations; import net.earthmc.emcapi.manager.KeyManager; import net.earthmc.emcapi.object.endpoint.PostEndpoint; +import net.earthmc.emcapi.object.optout.OptOutType; import net.earthmc.emcapi.util.EndpointUtils; import net.earthmc.emcapi.util.HttpExceptions; import net.earthmc.emcapi.util.JSONUtil; @@ -46,7 +47,7 @@ public Resident getObjectOrNull(JsonElement element, @Nullable String key) { } } - if (resident != null && plugin.getOptOut().playerOptedOut(resident.getUUID()) && !resident.getUUID().equals(KeyManager.getKeyOwner(key))) { + if (resident != null && plugin.getOptOut().playerOptedOut(resident.getUUID(), OptOutType.TOWNY_RESIDENT) && !resident.getUUID().equals(KeyManager.getKeyOwner(key))) { return null; } @@ -74,7 +75,7 @@ public JsonElement getJsonElement(Resident resident, @Nullable String key) { playerObject.add("timestamps", timestampsObject); JsonObject statusObject = new JsonObject(); - statusObject.addProperty("isOnline", resident.isOnline()); + statusObject.addProperty("isOnline", resident.isOnline() && !plugin.getOptOut().playerOptedOut(resident.getUUID(), OptOutType.ONLINE_STATUS)); statusObject.addProperty("isNPC", resident.isNPC()); statusObject.addProperty("isMayor", resident.isMayor()); statusObject.addProperty("isKing", resident.isKing()); diff --git a/src/main/java/net/earthmc/emcapi/manager/Authorisation.java b/src/main/java/net/earthmc/emcapi/manager/Authorisation.java new file mode 100644 index 0000000..e3e9d85 --- /dev/null +++ b/src/main/java/net/earthmc/emcapi/manager/Authorisation.java @@ -0,0 +1,92 @@ +package net.earthmc.emcapi.manager; + +import net.earthmc.emcapi.EMCAPI; +import net.earthmc.emcapi.object.optout.AuthSettings; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +public class Authorisation { + public final Map authMap = new ConcurrentHashMap<>(); + private final EMCAPI plugin; + + public Authorisation(final EMCAPI plugin) { + this.plugin = plugin; + } + + /** + * @param owner The owner of the information, the one who authorises others + * @param type The type to check for + * @param target The target to be authorised + * @return Whether the target is authorised for this action + */ + public boolean authorize(UUID owner, AuthSettings.Type type, UUID target) { + return authMap.containsKey(owner) && authMap.get(owner).authorize(type, target); + } + + public void saveAuthSettings(UUID uuid) { + AuthSettings settings = authMap.get(uuid); + if (settings == null) { + return; + } + + if (!plugin.getDatabase().ready()) { + plugin.getSLF4JLogger().warn("The database has not been properly configured yet, auth changes will not persist across restarts."); + return; + } + + plugin.getServer().getAsyncScheduler().runNow(plugin, t -> { + boolean delete = settings.isRedundant(); + try (Connection connection = plugin.getDatabase().getConnection(); + PreparedStatement ps = connection.prepareStatement(delete ? "DELETE FROM authorised WHERE uuid = ?" + : "INSERT INTO authorised (uuid, shop_sse, shop_query) " + + "VALUES (?, ?, ?) " + + "ON DUPLICATE KEY UPDATE " + + "shop_sse = VALUES(shop_sse), " + + "shop_query = VALUES(shop_query)" + )) { + ps.setString(1, uuid.toString()); + if (!delete) { + ps.setString(2, settings.getStringForType(AuthSettings.Type.SHOP_SSE)); + ps.setString(3, settings.getStringForType(AuthSettings.Type.SHOP_QUERY)); + } + + ps.executeUpdate(); + } catch (SQLException e) { + plugin.getSLF4JLogger().warn("Failed to update authorisation settings for {}", uuid, e); + } + }); + } + + public void loadAuthSettings() { + try (Connection connection = plugin.getDatabase().getConnection(); + PreparedStatement ps = connection.prepareStatement("SELECT * FROM authorised"); + ResultSet rs = ps.executeQuery() + ) { + while (rs.next()) { + try { + UUID uuid = UUID.fromString(rs.getString("uuid")); + Map map = Map.of( + AuthSettings.Type.SHOP_SSE, Objects.requireNonNullElse(rs.getString("shop_sse"), ""), + AuthSettings.Type.SHOP_QUERY, Objects.requireNonNullElse(rs.getString("shop_query"), "") + ); + + AuthSettings settings = AuthSettings.parse(map); + authMap.put(uuid, settings); + } catch (IllegalArgumentException e) { + plugin.getSLF4JLogger().warn("Invalid uuid format '{}' for value in row of table authorised", rs.getString("uuid")); + } catch (SQLException e) { + plugin.getSLF4JLogger().warn("SQLException while loading authorisation data", e); + } + } + } catch (SQLException e) { + plugin.getSLF4JLogger().warn("Failed to load authorisation data", e); + } + } +} diff --git a/src/main/java/net/earthmc/emcapi/manager/GUIManager.java b/src/main/java/net/earthmc/emcapi/manager/GUIManager.java new file mode 100644 index 0000000..e0af0d4 --- /dev/null +++ b/src/main/java/net/earthmc/emcapi/manager/GUIManager.java @@ -0,0 +1,447 @@ +package net.earthmc.emcapi.manager; + +import com.palmergames.bukkit.towny.TownyAPI; +import com.palmergames.bukkit.towny.object.Resident; +import dev.warriorrr.inventories.event.input.StartAwaitingInputEvent; +import dev.warriorrr.inventories.gui.MenuInventory; +import dev.warriorrr.inventories.gui.MenuItem; +import dev.warriorrr.inventories.gui.action.ClickAction; +import dev.warriorrr.inventories.gui.input.response.InputResponse; +import dev.warriorrr.inventories.gui.slot.Slot; +import dev.warriorrr.inventories.gui.slot.anchor.HorizontalAnchor; +import dev.warriorrr.inventories.gui.slot.anchor.SlotAnchor; +import dev.warriorrr.inventories.gui.slot.anchor.VerticalAnchor; +import kotlin.Pair; +import net.earthmc.emcapi.EMCAPI; +import net.earthmc.emcapi.object.optout.AuthSettings; +import net.earthmc.emcapi.object.optout.OptOutSettings; +import net.earthmc.emcapi.object.optout.OptOutType; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextDecoration; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.inventory.InventoryCloseEvent; +import org.bukkit.event.player.PlayerQuitEvent; + +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Supplier; + +public class GUIManager implements Listener { + private final EMCAPI plugin; + private final OptOut optOut; + private final Authorisation auth; + private final Set authPending = ConcurrentHashMap.newKeySet(); + private final Set optOutPending = ConcurrentHashMap.newKeySet(); + private final Set signInputs = ConcurrentHashMap.newKeySet(); + + public GUIManager(EMCAPI plugin) { + this.plugin = plugin; + this.optOut = plugin.getOptOut(); + this.auth = plugin.getAuth(); + } + + public MenuInventory createRoot(Player player) { + signInputs.remove(player.getUniqueId()); // In case of any bugs, re-using the command from the beginning will allow a fresh start + MenuInventory.Builder menu = MenuInventory.builder() + .title(Component.text("API Help Menu", NamedTextColor.AQUA, TextDecoration.BOLD)) + .rows(3); + + MenuItem optOut = MenuItem.builder(Material.BARRIER) + .name(Component.text("Manage your API privacy", NamedTextColor.GREEN, TextDecoration.BOLD)) + .lore(Component.text("Click to manage your API opt out settings", NamedTextColor.GRAY)) + .action(ClickAction.openSilent(() -> createOptOutMenu(player))) + .slot(slot(1, 2)) + .withGlint() + .build(); + + MenuItem authorise = MenuItem.builder(Material.PLAYER_HEAD) + .skullOwner(player.getUniqueId()) + .name(Component.text("Manage your player authorisation settings", NamedTextColor.DARK_AQUA, TextDecoration.BOLD)) + .lore(Component.text("Click to manage which players have what rights to your information", NamedTextColor.GRAY)) + .action(ClickAction.openSilent(() -> createAuthMenu(player))) + .slot(slot(1, 4)) + .build(); + + MenuItem help = MenuItem.builder(Material.COPPER_LANTERN) + .name(Component.text("API Guide", NamedTextColor.YELLOW, TextDecoration.BOLD)) + .lore(Component.text("Click to learn more about the API and your privacy", NamedTextColor.GRAY)) + .action(ClickAction.openSilent(() -> createHelpMenu(player))) + .slot(slot(1, 6)) + .withGlint() + .build(); + + menu.addItem(optOut).addItem(authorise).addItem(help); + return menu.build(); + } + + public MenuInventory createOptOutMenu(Player player) { + OptOutSettings settings = optOut.opted.getOrDefault(player.getUniqueId(), OptOutSettings.DEFAULT); + MenuInventory.Builder menu = MenuInventory.builder() + .title(Component.text("Manage your API privacy", NamedTextColor.DARK_GREEN, TextDecoration.BOLD)) + .rows(3); + + boolean override = settings.override(); + Component name = override ? Component.text("Override: All data disabled.", NamedTextColor.RED, TextDecoration.BOLD) + : Component.text("Override is disabled. Data is shared per-feature.", NamedTextColor.GREEN); + Component lore = override ? Component.text("Click to disable your override settings", NamedTextColor.DARK_GREEN) + : Component.text("Enabling this will override your other preferences", NamedTextColor.DARK_RED); + + menu.addItem(MenuItem.builder(Material.BARRIER) + .slot(slot(0, 4)) + .name(name) + .lore(lore) + .withGlint(override) + .action(ClickAction.openSilent(() -> { + optOut.opted.put(player.getUniqueId(), settings.override(!override)); + optOutPending.add(player.getUniqueId()); + return createOptOutMenu(player); + })) + .build()); + + boolean resident = settings.townyResident(); + name = Component.text("Resident Status", resident ? NamedTextColor.RED : NamedTextColor.GREEN); + lore = resident ? Component.text("Click to show your resident data", NamedTextColor.DARK_GREEN) + : Component.text("Click to hide your resident data", NamedTextColor.DARK_RED); + + menu.addItem(MenuItem.builder(Material.PLAYER_HEAD) + .slot(slot(1, 1)) + .skullOwner(player.getUniqueId()) + .name(name) + .lore(lore) + .action(ClickAction.openSilent(() -> { + optOut.opted.put(player.getUniqueId(), settings.update(OptOutType.TOWNY_RESIDENT, !resident)); + optOutPending.add(player.getUniqueId()); + return createOptOutMenu(player); + })) + .build()); + + boolean online = settings.onlineStatus(); + name = Component.text("Online Status", online ? NamedTextColor.RED : NamedTextColor.GREEN); + lore = online ? Component.text("Click to re-enable your online status", NamedTextColor.DARK_GREEN) + : Component.text("Click to disable your online status", NamedTextColor.DARK_RED); + + menu.addItem(MenuItem.builder(Material.EMERALD) + .slot(slot(1, 3)) + .name(name) + .lore(lore) + .action(ClickAction.openSilent(() -> { + optOut.opted.put(player.getUniqueId(), settings.update(OptOutType.ONLINE_STATUS, !online)); + optOutPending.add(player.getUniqueId()); + return createOptOutMenu(player); + })) + .withGlint(online) + .build()); + + boolean shops = settings.quickShops(); + name = Component.text("QuickShops", shops ? NamedTextColor.RED : NamedTextColor.GREEN); + lore = shops ? Component.text("Click to make your shop data public", NamedTextColor.DARK_GREEN, TextDecoration.BOLD) + : Component.text("Click to make your shop data private, only accessible with your API key", NamedTextColor.DARK_RED, TextDecoration.BOLD); + + menu.addItem(MenuItem.builder(Material.CHEST) + .slot(slot(1, 5)) + .name(name) + .lore(lore) + .action(ClickAction.openSilent(() -> { + optOut.opted.put(player.getUniqueId(), settings.update(OptOutType.QUICKSHOPS, !shops)); + optOutPending.add(player.getUniqueId()); + return createOptOutMenu(player); + })) + .withGlint(shops) + .build()); + + boolean mcmmo = settings.mcmmo(); + name = Component.text("McMMO Stats", mcmmo ? NamedTextColor.RED : NamedTextColor.GREEN); + lore = mcmmo ? Component.text("Click to make your mcMMO stats public", NamedTextColor.DARK_GREEN) + : Component.text("Click to make your mcMMO stats private, only accessible with your API key", NamedTextColor.DARK_RED); + + menu.addItem(MenuItem.builder(Material.DIAMOND_AXE) + .slot(slot(1, 7)) + .name(name) + .lore(lore) + .action(ClickAction.openSilent(() -> { + optOut.opted.put(player.getUniqueId(), settings.update(OptOutType.MCMMO, !mcmmo)); + optOutPending.add(player.getUniqueId()); + return createOptOutMenu(player); + })) + .withGlint(mcmmo) + .build()); + + menu.addItem(createMainMenuButton(player)); + return menu.build(); + } + + public MenuInventory createAuthMenu(Player player) { + AuthSettings settings = auth.authMap.containsKey(player.getUniqueId()) ? auth.authMap.get(player.getUniqueId()) : AuthSettings.getNew(); + MenuInventory.Builder menu = MenuInventory.builder() + .title(Component.text("Manage authorisation", NamedTextColor.DARK_AQUA, TextDecoration.BOLD)) + .rows(5); + + MenuItem main = MenuItem.builder(Material.WRITTEN_BOOK) + .name(Component.text("What's here", NamedTextColor.YELLOW, TextDecoration.BOLD)) + .lore(Component.text("• You can authorise specific players to access your information", NamedTextColor.YELLOW)) + .lore(Component.text("• Currently, you can allow players to listen to your Shop Server-Sent-Events, or to query your shop data.", NamedTextColor.YELLOW)) + .lore(Component.text("• Click the editors below to edit each respectively.", NamedTextColor.GRAY)) + .slot(slot(1, 4)) + .build(); + + UUID playerUUID = player.getUniqueId(); + MenuItem sse = MenuItem.builder(Material.GOAT_HORN) + .name(Component.text("Shop SSE", NamedTextColor.GREEN)) + .lore(Component.text("• Players authorised here will be able to connect to the server's /sse endpoint", NamedTextColor.GREEN)) + .lore(Component.text("• and receive events fired by your QuickShops", NamedTextColor.GREEN)) + .lore(Component.text("• For example, when your shop sells an item or is out of stock", NamedTextColor.GREEN)) + .lore(Component.text("Click to add or remove players", NamedTextColor.WHITE)) + .action(ClickAction.openSilent(() -> editAuthorisedMenu(player, settings, AuthSettings.Type.SHOP_SSE))) + .slot(slot(3, 2)) + .withGlint() + .build(); + + MenuItem query = MenuItem.builder(Material.BARREL) + .name(Component.text("Shop Query", NamedTextColor.DARK_GREEN, TextDecoration.BOLD)) + .lore(Component.text("• Players authorised here will be able to query all your shops in the /shop endpoint", NamedTextColor.DARK_GREEN)) + .lore(Component.text("• This bypasses your shop data not being public in your opt out settings", NamedTextColor.DARK_GREEN)) + .lore(Component.text("Click to add or remove players", NamedTextColor.WHITE)) + .action(ClickAction.openSilent(() -> editAuthorisedMenu(player, settings, AuthSettings.Type.SHOP_QUERY))) + .slot(slot(3, 6)) + .withGlint() + .build(); + + menu.addItem(main).addItem(sse).addItem(query).addItem(createMainMenuButton(player)); + return menu.build(); + } + + public MenuInventory createHelpMenu(Player player) { + MenuInventory.Builder menu = MenuInventory.builder() + .title(Component.text("API Guide", NamedTextColor.GRAY, TextDecoration.BOLD)) + .rows(3); + + MenuItem temp = MenuItem.builder(Material.BEDROCK) + .name(Component.text("Not ready", NamedTextColor.GRAY, TextDecoration.BOLD)) + .lore(Component.text("This feature is still in development.", NamedTextColor.GRAY)) + .slot(slot(1, 4)) + .withGlint() + .build(); + + menu.addItem(temp).addItem(createMainMenuButton(player)); + return menu.build(); + } + + private MenuItem createMainMenuButton(Player player) { + return MenuItem.builder(Material.BARRIER) + .name(Component.text("Main Menu", NamedTextColor.GREEN, TextDecoration.BOLD)) + .lore(Component.text("Click to open", NamedTextColor.GRAY)) + .action(ClickAction.openSilent(() -> createRoot(player))) + .slot(SlotAnchor.bottomRight()) + .build(); + } + + private MenuItem createBackButton(Supplier supplier) { + return MenuItem.builder(Material.BARRIER) + .name(Component.text("Back", NamedTextColor.RED, TextDecoration.BOLD)) + .lore(Component.text("Click to go back", NamedTextColor.GRAY)) + .action(ClickAction.openSilent(supplier)) + .slot(SlotAnchor.bottomRight()) + .build(); + } + + private MenuInventory editAuthorisedMenu(Player player, AuthSettings settings, AuthSettings.Type type) { + MenuInventory.Builder menu = MenuInventory.builder() + .title(Component.text("What would you like to do?", NamedTextColor.AQUA, TextDecoration.BOLD)) + .rows(3); + + UUID playerUUID = player.getUniqueId(); + MenuItem add = MenuItem.builder(Material.GREEN_WOOL) + .name(Component.text("Add a player", NamedTextColor.GREEN)) + .lore(Component.text("Currently authorised: " + settings.size(type), NamedTextColor.GRAY)) + .action(ClickAction.userInput(Component.text("The name or UUID of the player", NamedTextColor.GREEN), input -> { + String text = input.getText(); + InputResponse response = InputResponse.reOpen(() -> { + signInputs.remove(playerUUID); + return editAuthorisedMenu(player, settings, type); + }); + UUID uuid; + String name; + try { + Pair pair = parsePlayer(text); + uuid = pair.getFirst(); + name = pair.getSecond(); + } catch (IllegalStateException e) { + player.sendMessage(Component.text(e.getMessage(), NamedTextColor.RED)); + return response; + } + if (playerUUID.equals(uuid)) { + player.sendMessage(Component.text("You cannot authorise yourself! You already have full access to your own information.", NamedTextColor.RED)); + return response; + } + if (settings.authorize(AuthSettings.Type.SHOP_SSE, uuid)) { + player.sendMessage(Component.text(name + " is already authorised for " + type.name(), NamedTextColor.RED)); + return response; + } + auth.authMap.put(playerUUID, settings.add(type, uuid)); + authPending.add(playerUUID); + player.sendMessage(Component.text("Successfully authorised player " + name + " for " + type.name(), NamedTextColor.GREEN) + .appendNewline() + .append(Component.text("(UUID: " + uuid + ")", NamedTextColor.GRAY)) + ); + return response; + })) + .slot(slot(1, 2)) + .withGlint() + .build(); + + MenuItem remove = MenuItem.builder(Material.RED_WOOL) + .name(Component.text("Remove a player", NamedTextColor.RED)) + .lore(Component.text("Currently authorised: " + settings.size(type), NamedTextColor.GRAY)) + .action(ClickAction.openSilent(() -> removeAuthorised(player, settings, type))) + .slot(slot(1, 4)) + .withGlint() + .build(); + + MenuItem reset = MenuItem.builder(Material.TNT) + .name(Component.text("Reset", NamedTextColor.RED, TextDecoration.BOLD)) + .lore(Component.text("Un-authorise everyone from " + type.name(), NamedTextColor.RED)) + .action(ClickAction.openSilent(() -> { + auth.authMap.put(playerUUID, settings.clear(type)); + authPending.add(playerUUID); + player.sendMessage(Component.text("Successfully cleared your auth list for " + type.name(), NamedTextColor.GREEN)); + return createAuthMenu(player); + })) + .slot(slot(1, 6)) + .withGlint() + .build(); + + menu.addItem(add).addItem(remove).addItem(reset).addItem(createBackButton(() -> createAuthMenu(player))); + return menu.build(); + } + + private MenuInventory removeAuthorised(Player player, AuthSettings settings, AuthSettings.Type type) { + MenuInventory.Builder menu = MenuInventory.builder() + .title(Component.text("Remove authorised players", NamedTextColor.RED, TextDecoration.BOLD)); + + menu.addItem(createBackButton(() -> editAuthorisedMenu(player, settings, type))); + int index = 0; + List authorised = settings.authorised().get(type); + if (authorised == null || authorised.isEmpty()) { + MenuItem empty = MenuItem.builder(Material.WRITTEN_BOOK) + .name(Component.text("No authorised players", NamedTextColor.RED)) + .lore(Component.text("There is noone to remove.", NamedTextColor.GRAY)) + .slot(slot(0, 4)) + .build(); + + return menu.size(1).addItem(empty).build(); + } + menu.size(authorised.size() + 1); + UUID playerUUID = player.getUniqueId(); + for (UUID uuid : authorised) { + String name = Optional.ofNullable(getNameFromUUID(uuid)).orElse(uuid.toString()); + MenuItem head = MenuItem.builder(Material.PLAYER_HEAD) + .skullOwner(uuid) + .name(Component.text(name, NamedTextColor.GREEN)) + .lore(Component.text("Click to remove " + name + "'s authorisation for " + type.name(), NamedTextColor.RED)) + .action(ClickAction.openSilent(() -> { + auth.authMap.put(playerUUID, settings.remove(type, uuid)); + authPending.add(playerUUID); + player.sendMessage(Component.text("Successfully un-authorised player " + name + " for " + type.name(), NamedTextColor.GREEN) + .appendNewline() + .append(Component.text("(UUID: " + uuid + ")", NamedTextColor.GRAY)) + ); + + return removeAuthorised(player, settings, type); // Refresh the inventory + })) + .slot(index) + .build(); + + menu.addItem(head); + if (++index > 50) break; + } + + return menu.build(); + } + + private Pair parsePlayer(String text) throws IllegalStateException { + UUID uuid; + try { + uuid = UUID.fromString(text); + } catch (IllegalArgumentException ignored) { + uuid = getUUIDFromName(text); + } + if (uuid == null) { + throw new IllegalStateException("Could not resolve that username or UUID!"); + } + + String name = getNameFromUUID(uuid); + if (name == null) { + throw new IllegalStateException("No username found"); + } + return new Pair<>(uuid, name); + } + + private String getNameFromUUID(UUID uuid) { + if (uuid == null) { + return null; + } + Resident res = TownyAPI.getInstance().getResident(uuid); + return res != null ? res.getName() : null; + } + + private UUID getUUIDFromName(String name) { + if (name == null) { + return null; + } + Resident res = TownyAPI.getInstance().getResident(name); + return res != null ? res.getUUID() : null; + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onInputStart(StartAwaitingInputEvent event) { + signInputs.add(event.getPlayer().getUniqueId()); + } + + @EventHandler(priority = EventPriority.MONITOR) + public void onQuit(PlayerQuitEvent event) { + UUID uuid = event.getPlayer().getUniqueId(); + + signInputs.remove(uuid); + authPending.remove(uuid); + optOutPending.remove(uuid); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onInventoryClose(InventoryCloseEvent event) { + if (!(event.getView().getTopInventory().getHolder(false) instanceof MenuInventory)) { + return; + } + Player player = (Player) event.getPlayer(); + player.getScheduler().runDelayed(plugin, t -> { + UUID uuid = player.getUniqueId(); + if (signInputs.contains(uuid) || player.getOpenInventory().getTopInventory().getHolder(false) instanceof MenuInventory) { + return; // Player is still editing + } + if (optOutPending.contains(uuid)) { + optOutPending.remove(uuid); + optOut.saveOptOut(uuid); + player.sendMessage(Component.text("Successfully saved your new API opt out settings", NamedTextColor.GREEN)); + } + if (authPending.contains(uuid)) { + authPending.remove(uuid); + auth.saveAuthSettings(uuid); + player.sendMessage(Component.text("Successfully saved your new API authorisation settings", NamedTextColor.GREEN)); + } + }, () -> { + optOutPending.remove(player.getUniqueId()); + authPending.remove(player.getUniqueId()); + }, 50); + } + + private Slot slot(int fromTop, int fromLeft) { + return new SlotAnchor(VerticalAnchor.fromTop(fromTop), HorizontalAnchor.fromLeft(fromLeft)); + } +} diff --git a/src/main/java/net/earthmc/emcapi/manager/OptOut.java b/src/main/java/net/earthmc/emcapi/manager/OptOut.java index a18d0bd..5074248 100644 --- a/src/main/java/net/earthmc/emcapi/manager/OptOut.java +++ b/src/main/java/net/earthmc/emcapi/manager/OptOut.java @@ -1,120 +1,92 @@ package net.earthmc.emcapi.manager; import net.earthmc.emcapi.EMCAPI; +import net.earthmc.emcapi.object.optout.OptOutSettings; +import net.earthmc.emcapi.object.optout.OptOutType; +import org.jetbrains.annotations.Nullable; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; -import java.util.Set; +import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; public class OptOut { - private static final String LEGACY_OPT_OUT_FILE = "opt-out.txt"; - private final Set optedOut = ConcurrentHashMap.newKeySet(); + public final Map opted = new ConcurrentHashMap<>(); private final EMCAPI plugin; public OptOut(final EMCAPI plugin) { this.plugin = plugin; } - public boolean playerOptedOut(UUID uuid) { - return optedOut.contains(uuid); + public boolean playerOptedOut(UUID uuid, OptOutType type) { + return opted.containsKey(uuid) && opted.get(uuid).optedOut(type); } - public void setOptedOut(UUID uuid, boolean optedOut) { - boolean modified; - - if (optedOut) { - modified = this.optedOut.add(uuid); - } else { - modified = this.optedOut.remove(uuid); - } + public @Nullable OptOutSettings getPlayerSettings(UUID uuid) { + return opted.get(uuid); + } - if (!modified) { + public void saveOptOut(UUID uuid) { + OptOutSettings settings = opted.get(uuid); + if (settings == null) { return; } if (!plugin.getDatabase().ready()) { - plugin.getSLF4JLogger().warn("The database has not been properly configured yet, opt out status will not persist across restarts."); + plugin.getSLF4JLogger().warn("The database has not been properly configured yet, opt out changes will not persist across restarts."); return; } plugin.getServer().getAsyncScheduler().runNow(plugin, t -> { - try (final Connection connection = plugin.getDatabase().getConnection(); final PreparedStatement ps = connection.prepareStatement(optedOut - ? "INSERT IGNORE INTO opt_out (uuid) VALUES (?)" - : "DELETE FROM opt_out WHERE uuid = ?" + boolean delete = settings.isRedundant(); + try (Connection connection = plugin.getDatabase().getConnection(); + PreparedStatement ps = connection.prepareStatement(delete ? "DELETE FROM opt_out WHERE uuid = ?" + : "INSERT INTO opt_out (uuid, override_all, towny_resident, online_status, quickshops, mcmmo_stats) " + + "VALUES (?, ?, ?, ?, ?, ?) " + + "ON DUPLICATE KEY UPDATE " + + "override_all = VALUES(override_all), " + + "towny_resident = VALUES(towny_resident), " + + "online_status = VALUES(online_status), " + + "quickshops = VALUES(quickshops), " + + "mcmmo_stats = VALUES(mcmmo_stats)" )) { ps.setString(1, uuid.toString()); + if (!delete) { + ps.setBoolean(2, settings.override()); + ps.setBoolean(3, settings.townyResident()); + ps.setBoolean(4, settings.onlineStatus()); + ps.setBoolean(5, settings.quickShops()); + ps.setBoolean(6, settings.mcmmo()); + } + ps.executeUpdate(); } catch (SQLException e) { - plugin.getSLF4JLogger().warn("Failed to update opt out status to {} for {}", optedOut, uuid, e); + plugin.getSLF4JLogger().warn("Failed to update opt out status for {}", uuid, e); } }); } public void loadOptOut() { - loadLegacyOptOuts(); - - try (final Connection connection = plugin.getDatabase().getConnection(); PreparedStatement ps = connection.prepareStatement("SELECT uuid FROM opt_out"); final ResultSet rs = ps.executeQuery()) { + try (final Connection connection = plugin.getDatabase().getConnection(); + PreparedStatement ps = connection.prepareStatement("SELECT * FROM opt_out"); + final ResultSet rs = ps.executeQuery() + ) { while (rs.next()) { - final UUID uuid; try { - uuid = UUID.fromString(rs.getString("uuid")); + UUID uuid = UUID.fromString(rs.getString("uuid")); + OptOutSettings settings = new OptOutSettings(rs.getBoolean("override_all"), rs.getBoolean("towny_resident"), rs.getBoolean("online_status"), rs.getBoolean("quickshops"), rs.getBoolean("mcmmo_stats")); + opted.put(uuid, settings); } catch (IllegalArgumentException e) { plugin.getSLF4JLogger().warn("Invalid uuid format '{}' for value in row of table opt_out", rs.getString("uuid")); - continue; + } catch (SQLException e) { + plugin.getSLF4JLogger().warn("SQLException while loading opt out", e); } - - optedOut.add(uuid); } } catch (SQLException e) { plugin.getSLF4JLogger().warn("Failed to load opted out players", e); } } - - private void loadLegacyOptOuts() { - final Path file = plugin.getDataPath().resolve(LEGACY_OPT_OUT_FILE); - if (!Files.exists(file)) { - return; - } - - if (!plugin.getDatabase().ready()) { - plugin.getSLF4JLogger().warn("Found an existing {} to migrate, but the database is not configured properly yet.", LEGACY_OPT_OUT_FILE); - return; - } - - try { - Files.readAllLines(file).forEach(playerStr -> { - try { - optedOut.add(UUID.fromString(playerStr)); - } catch (IllegalArgumentException ignored) {} - }); - } catch (IOException e) { - plugin.getSLF4JLogger().error("Failed to migrate legacy {} file", LEGACY_OPT_OUT_FILE, e); - return; - } - - try (final Connection conn = plugin.getDatabase().getConnection(); final PreparedStatement ps = conn.prepareStatement("INSERT IGNORE INTO opt_out (uuid) VALUES(?)")) { - for (final UUID uuid : optedOut) { - ps.setString(1, uuid.toString()); - ps.addBatch(); - } - - ps.executeBatch(); - } catch (SQLException e) { - plugin.getSLF4JLogger().warn("Failed to insert legacy opt outs", e); - return; - } - - try { - Files.delete(file); - } catch (IOException e) { - plugin.getSLF4JLogger().warn("Failed to delete {} after successful migration", LEGACY_OPT_OUT_FILE, e); - } - } } diff --git a/src/main/java/net/earthmc/emcapi/object/optout/AuthSettings.java b/src/main/java/net/earthmc/emcapi/object/optout/AuthSettings.java new file mode 100644 index 0000000..63367b6 --- /dev/null +++ b/src/main/java/net/earthmc/emcapi/object/optout/AuthSettings.java @@ -0,0 +1,77 @@ +package net.earthmc.emcapi.object.optout; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; + +public record AuthSettings(Map> authorised) { + + public static AuthSettings getNew() { + return new AuthSettings(new HashMap<>()); + } + + public static AuthSettings parse(Map map) { + AuthSettings settings = new AuthSettings(new HashMap<>()); + for (Map.Entry entry : map.entrySet()) { + List uuids = parseUUIDs(entry.getValue()); + if (uuids.isEmpty()) continue; + settings.authorised.put(entry.getKey(), uuids); + } + + return settings; + } + + private static List parseUUIDs(String string) { + List list = new ArrayList<>(); + for (String str : string.split(",")) { + try { + list.add(UUID.fromString(str)); + } catch (IllegalArgumentException ignored) {} + } + return list; + } + + public boolean authorize(Type type, UUID uuid) { + return authorised.containsKey(type) && authorised.get(type).contains(uuid); + } + + public AuthSettings add(Type type, UUID uuid) { + authorised.computeIfAbsent(type, k -> new ArrayList<>()).add(uuid); + return this; + } + + public AuthSettings remove(Type type, UUID uuid) { + if (authorised.containsKey(type)) { + authorised.get(type).remove(uuid); + if (authorised.get(type).isEmpty()) { + authorised.remove(type); + } + } + return this; + } + + public AuthSettings clear(Type type) { + authorised.remove(type); + return this; + } + + public int size(Type type) { + return authorised.containsKey(type) ? authorised.get(type).size() : 0; + } + + public boolean isRedundant() { + return authorised.isEmpty(); + } + + public String getStringForType(Type type) { + return authorised.containsKey(type) ? authorised.get(type).stream().map(UUID::toString).collect(Collectors.joining(",")) : ""; + } + + public enum Type { + SHOP_SSE, + SHOP_QUERY + } +} diff --git a/src/main/java/net/earthmc/emcapi/object/optout/OptOutSettings.java b/src/main/java/net/earthmc/emcapi/object/optout/OptOutSettings.java new file mode 100644 index 0000000..971a2c7 --- /dev/null +++ b/src/main/java/net/earthmc/emcapi/object/optout/OptOutSettings.java @@ -0,0 +1,50 @@ +package net.earthmc.emcapi.object.optout; + +/** + * @param override If true, all data sharing is disabled + * @param townyResident If true, a player's Towny resident data is not shared + * @param onlineStatus If true, a player's online status (both in the online endpoint and Towny endpoint) is not shared + * @param quickShops If false, any player with a valid API key may query this player's shops + * @param mcmmo If false, any player with a valid API key may query this player's mcMMO data + */ +public record OptOutSettings(boolean override, boolean townyResident, boolean onlineStatus, boolean quickShops, boolean mcmmo) { + public static final OptOutSettings DEFAULT = new OptOutSettings(false, false, false, true, true); + + public boolean optedOut(OptOutType type) { + if (override) { + return true; + } + return switch (type) { + case TOWNY_RESIDENT -> townyResident; + case ONLINE_STATUS -> onlineStatus; + case QUICKSHOPS -> quickShops; + case MCMMO -> mcmmo; + }; + } + + public OptOutSettings override(boolean value) { + if (override == value) { + return this; + } + return new OptOutSettings(value, townyResident, onlineStatus, quickShops, mcmmo); + } + + public OptOutSettings update(OptOutType type, boolean value) { + return switch (type) { + case TOWNY_RESIDENT -> new OptOutSettings(override, value, onlineStatus, quickShops, mcmmo); + case ONLINE_STATUS -> new OptOutSettings(override, townyResident, value, quickShops, mcmmo); + case QUICKSHOPS -> new OptOutSettings(override, townyResident, onlineStatus, value, mcmmo); + case MCMMO -> new OptOutSettings(override, townyResident, onlineStatus, quickShops, value); + }; + } + + /** + * @return Whether this object is redundant and should not be saved, as it carries the default values + */ + public boolean isRedundant() { + if (override) { + return false; + } + return !townyResident && !onlineStatus && quickShops && mcmmo; + } +} diff --git a/src/main/java/net/earthmc/emcapi/object/optout/OptOutType.java b/src/main/java/net/earthmc/emcapi/object/optout/OptOutType.java new file mode 100644 index 0000000..be9e297 --- /dev/null +++ b/src/main/java/net/earthmc/emcapi/object/optout/OptOutType.java @@ -0,0 +1,8 @@ +package net.earthmc.emcapi.object.optout; + +public enum OptOutType { + TOWNY_RESIDENT, + ONLINE_STATUS, + QUICKSHOPS, + MCMMO +} diff --git a/src/main/java/net/earthmc/emcapi/util/EndpointUtils.java b/src/main/java/net/earthmc/emcapi/util/EndpointUtils.java index 4727cd8..a9adf0e 100644 --- a/src/main/java/net/earthmc/emcapi/util/EndpointUtils.java +++ b/src/main/java/net/earthmc/emcapi/util/EndpointUtils.java @@ -10,6 +10,7 @@ import com.palmergames.bukkit.towny.object.Government; import com.palmergames.bukkit.towny.object.economy.BankTransaction; import net.earthmc.emcapi.EMCAPI; +import net.earthmc.emcapi.object.optout.OptOutType; import net.earthmc.lynchpin.api.towny.pacts.Pact; import net.earthmc.lynchpin.api.towny.warps.Warp; import org.bukkit.Location; @@ -143,7 +144,7 @@ public static JsonObject getOnlinePlayerArray(List players) { JsonArray jsonArray = new JsonArray(); for (Player player : players) { - if (EMCAPI.instance.getOptOut().playerOptedOut(player.getUniqueId())) { + if (EMCAPI.instance.getOptOut().playerOptedOut(player.getUniqueId(), OptOutType.ONLINE_STATUS)) { continue; }