diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json deleted file mode 100644 index 2380903..0000000 --- a/.devcontainer/devcontainer.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "o7studios/octopus-plugin", - "image": "ubuntu:latest", - "customizations" : { - "jetbrains" : { - "backend" : "IntelliJ", - "plugins": ["com.demonwav.minecraft-dev"] - } - }, - "features": { - "ghcr.io/devcontainers/features/git" : {}, - "ghcr.io/devcontainers/features/java:1": {"version": "23", "installGradle": true} - }, - "runArgs": ["--env-file=${localEnv:HOME}/dev.env"], - "remoteUser": "ubuntu" -} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 35b0d69..2195c1a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,7 +5,7 @@ on: types: [ opened, reopened ] env: - JAVA_VERSION: '23' + JAVA_VERSION: '25' JAVA_DISTRIBUTION: 'temurin' permissions: diff --git a/.gitignore b/.gitignore index e5460ae..fd7084e 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ .vscode .gradle build +/plugin/run diff --git a/README.md b/README.md index 98aed8b..9d9ee78 100644 --- a/README.md +++ b/README.md @@ -16,53 +16,24 @@ dependencies { } ``` -Add _depend_ inside `plugin.yml`: +Add _depend on_ inside `plugin.yml`: ```yaml depend: - Octopus ``` -## Development +### Config -Full development setup available as [Development Container](https://containers.dev/). -Please use it for being able to tell "It works on my machine". +Make sure this is inside of the `/plugins/octopus/config.yml` -**Docker is required to be installed on your machine!** - -### Create ~/dev.env - -The development container is using a local env file on your -host machine for reading e.g. GitHub Tokens, Usernames, Email. -So please make sure it exists with your credentials in `~/dev.env`: - -```text -GITHUB_EMAIL=your-mail@your-domain.com -GITHUB_USERNAME=YOUR_GITHUB_USERNAME -GITHUB_TOKEN=ghp_*** -``` - -The `GITHUB_TOKEN` must've set following permission: - -- `repo` -- `read:packages` -- `read:user` -- `user:email` - -### IntelliJ IDEA - -- Open IntelliJ (Welcome screen) -- Navigate to `Remote Development` - `Dev Containers` -- Press `New Dev Container` -- Select `From VCS Project` -- Select and connect with `Docker` -- Select `IntelliJ IDEA` -- Enter `Git Repository`: `https://github.com/o7studios/octopus-plugin` -- Select `Detection for devcontainer.json file` `Automatic` -- Press `Build Container and Continue` - -### Development Container Issues - -If you encounter an issue with setting up a development container, please -try to rebuild it first before opening a GitHub Issue. It's not uncommon -that some issues may fix themselves after a fresh container rebuild. +```yml +# Configuration of Octopus-Service +octopus: + # Host of Octopus-gRPC Server + host: "127.0.0.1" + # Port of Octopus-gRPC Server + port: 50051 + # Replace to Octopus-API token + token: "development" +``` \ No newline at end of file diff --git a/api/build.gradle.kts b/api/build.gradle.kts index 6d7405b..9cee6d9 100644 --- a/api/build.gradle.kts +++ b/api/build.gradle.kts @@ -3,8 +3,9 @@ repositories { } dependencies { - api("studio.o7:octopus-sdk:0.3.3") - compileOnly("io.papermc.paper:paper-api:1.21.8-R0.1-SNAPSHOT") + api("studio.o7:octopus-sdk:0.5.9") + api("studio.o7:gentle:0.0.2") + compileOnly("io.papermc.paper:paper-api:1.21.11-R0.1-SNAPSHOT") } information { diff --git a/api/src/main/java/studio/o7/octopus/plugin/api/listener/Listener.java b/api/src/main/java/studio/o7/octopus/plugin/api/EventHandler.java similarity index 76% rename from api/src/main/java/studio/o7/octopus/plugin/api/listener/Listener.java rename to api/src/main/java/studio/o7/octopus/plugin/api/EventHandler.java index 6d4866a..4d81cf2 100644 --- a/api/src/main/java/studio/o7/octopus/plugin/api/listener/Listener.java +++ b/api/src/main/java/studio/o7/octopus/plugin/api/EventHandler.java @@ -1,15 +1,15 @@ -package studio.o7.octopus.plugin.api.listener; +package studio.o7.octopus.plugin.api; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NonNull; -import studio.o7.octopus.sdk.gen.api.v1.Object; +import studio.o7.octopus.sdk.v1.Object; import java.util.UUID; @AllArgsConstructor @Getter -public abstract class Listener { +public abstract class EventHandler { /** * ID for identifying this listener. * (Made for internal purposes) @@ -26,11 +26,6 @@ public abstract class Listener { */ protected final String keyPattern; - /** - * Priority of this listener (e.g. determines event order; lower is later) - */ - protected final int priority; - /** * @param obj The affected object. */ diff --git a/api/src/main/java/studio/o7/octopus/plugin/api/Octopus.java b/api/src/main/java/studio/o7/octopus/plugin/api/Octopus.java index 7b39d7f..f83efe1 100644 --- a/api/src/main/java/studio/o7/octopus/plugin/api/Octopus.java +++ b/api/src/main/java/studio/o7/octopus/plugin/api/Octopus.java @@ -1,67 +1,105 @@ package studio.o7.octopus.plugin.api; -import org.jspecify.annotations.NullMarked; +import gentle.Error; +import gentle.Result; import studio.o7.octopus.plugin.Unsafe; -import studio.o7.octopus.plugin.api.listener.Listener; -import studio.o7.octopus.sdk.gen.api.v1.Entry; -import studio.o7.octopus.sdk.gen.api.v1.Object; +import studio.o7.octopus.sdk.v1.Entry; +import studio.o7.octopus.sdk.v1.QueryResponse; -import javax.annotation.Nullable; -import java.time.Instant; -import java.util.Collection; import java.util.UUID; -@NullMarked public interface Octopus { - static Octopus get() { + static Octopus instance() { return Unsafe.getInstance().get(); } /** - * Retrieves existing entries from the database matching a - * key pattern. + * Gets a unique entry of the given key + * + * @param key exact key pattern that will match between entries until one is found + * @return Returns the first {@link studio.o7.octopus.sdk.v1.Entry} that matches the key */ - default Collection get(String keyPattern) { - return get(keyPattern, false); - } - - /** - * Retrieves existing entries from the database matching a - * key pattern. Can optionally include expired objects. - */ - default Collection get(String keyPattern, boolean includeExpired) { - return get(keyPattern, includeExpired, null, null); - } + Result get(String key); /** - * Retrieves existing entries from the database matching a - * key pattern. Can optionally include expired objects and - * filter by revision creation time. + * Query multiple Entries + * + * @param queryParameter Query parameter to build the query request + * @return Returns a collection of matches for this query request */ - Collection get(String keyPattern, boolean includeExpired, @Nullable Instant createdRangeStart, @Nullable Instant createdRangeEnd); - - - void registerListener(Listener listener); - - default void unregisterListener(Listener listener) { - unregisterListener(listener.getListenerUniqueId()); - } - - void unregisterListener(UUID listenerUniqueId); + Result query(QueryParameter queryParameter); /** + *

Call

+ *

* Stores an object on a key with new revision in the database * and returns the stored version, including the new revision * and ID. + * + *

Expired

+ * If an entry is expired. For example a permission, set the expired_at field + * if it's not set and the entry will be flagged as expired + * + *

Deletion

+ * If an entry should be deleted, just set the deleted_at field and it will be + * flagged as deleted + * + * @param obj Object that should be saved inside the database + * @return returns the created {@link studio.o7.octopus.sdk.v1.Entry} */ - Entry call(Object obj); + Result call(studio.o7.octopus.sdk.v1.Object obj); /** + *

Call

+ *

* Stores an object on a key with new revision in the database * and just forgets it. All listeners will be called * as usual without blocking this method. + * + *

Expired

+ * If an entry is expired. For example a permission, set the expired_at field + * if it's not set and the entry will be flagged as expired + * + *

Deletion

+ * If an entry should be deleted, just set the deleted_at field and it will be + * flagged as deleted + * + * @param obj Object that should be saved inside the database */ - void callAndForget(Object obj); + void write(studio.o7.octopus.sdk.v1.Object obj); + /** + *

+ * A registration of a handler will subscribe to its given key pattern + * and receive all updates on the given key pattern. The handlers `onCall` + * method will be invoked on the incoming {@link studio.o7.octopus.sdk.v1.EventCall} + *

+ * + *

+ * When subscribing, be reminded that the key pattern really matches the requested + * EventCalls, using symbols such as `*` and `<` will subscribe on multiple keys + * There's no safeguard to prevent subscribing to the same topic. So please make + * shure you're not handling a topic twice! + *

+ * + * @param eventHandler Handler that will be invoked on matching incoming event + */ + void registerHandler(EventHandler eventHandler); + + /** + * Unregister a handler + * + * @param eventHandler Handler to unregister + */ + default void unregisterHandler(EventHandler eventHandler) { + unregisterHandler(eventHandler.getListenerUniqueId()); + } + + /** + * Unregister a handler + * + * @param listenerUniqueId ID of the handler to unregister + */ + void unregisterHandler(UUID listenerUniqueId); } diff --git a/api/src/main/java/studio/o7/octopus/plugin/api/OctopusError.java b/api/src/main/java/studio/o7/octopus/plugin/api/OctopusError.java new file mode 100644 index 0000000..f0b9356 --- /dev/null +++ b/api/src/main/java/studio/o7/octopus/plugin/api/OctopusError.java @@ -0,0 +1,31 @@ +package studio.o7.octopus.plugin.api; + +import gentle.Error; +import lombok.NonNull; + +public enum OctopusError implements Error { + + GET_REQUEST_FAILED(0, "While trying to get an entry, a gRPC-Error occurred"), + QUERY_REQUEST_FAILED(1, "While trying to query an entry, a gRPC-Error occurred"), + CALL_REQUEST_FAILED(1, "While trying to call an object, a gRPC-Error occurred"), + + ; + + private final int code; + private final String message; + + OctopusError(int code, String message) { + this.code = code; + this.message = message; + } + + @Override + public int code() { + return this.code; + } + + @Override + public @NonNull String message() { + return this.message; + } +} diff --git a/api/src/main/java/studio/o7/octopus/plugin/api/QueryParameter.java b/api/src/main/java/studio/o7/octopus/plugin/api/QueryParameter.java new file mode 100644 index 0000000..fd87df0 --- /dev/null +++ b/api/src/main/java/studio/o7/octopus/plugin/api/QueryParameter.java @@ -0,0 +1,20 @@ +package studio.o7.octopus.plugin.api; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class QueryParameter { + + private String keyPattern; + private String dataFilter; + + private boolean includeExpired; + + private int page; + private int pageSize; + + private com.google.protobuf.Timestamp createdAtStart; + private com.google.protobuf.Timestamp createdAtEnd; + +} diff --git a/api/src/main/java/studio/o7/octopus/plugin/api/adapters/ComponentAdapter.java b/api/src/main/java/studio/o7/octopus/plugin/api/adapters/ComponentAdapter.java deleted file mode 100644 index bd5c6c8..0000000 --- a/api/src/main/java/studio/o7/octopus/plugin/api/adapters/ComponentAdapter.java +++ /dev/null @@ -1,19 +0,0 @@ -package studio.o7.octopus.plugin.api.adapters; - -import com.google.gson.*; -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.serializer.json.JSONComponentSerializer; - -import java.lang.reflect.Type; - -public final class ComponentAdapter implements JsonSerializer, JsonDeserializer { - @Override - public JsonElement serialize(Component component, Type type, JsonSerializationContext jsonSerializationContext) { - return new JsonPrimitive(JSONComponentSerializer.json().serialize(component)); - } - - @Override - public Component deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext) throws JsonParseException { - return JSONComponentSerializer.json().deserialize(jsonElement.getAsString()); - } -} diff --git a/api/src/main/java/studio/o7/octopus/plugin/api/adapters/InetSocketAddressAdapter.java b/api/src/main/java/studio/o7/octopus/plugin/api/adapters/InetSocketAddressAdapter.java deleted file mode 100644 index 680726a..0000000 --- a/api/src/main/java/studio/o7/octopus/plugin/api/adapters/InetSocketAddressAdapter.java +++ /dev/null @@ -1,22 +0,0 @@ -package studio.o7.octopus.plugin.api.adapters; - -import com.google.gson.*; - -import java.lang.reflect.Type; -import java.net.InetSocketAddress; - -public final class InetSocketAddressAdapter implements JsonSerializer, JsonDeserializer { - @Override - public InetSocketAddress deserialize(JsonElement json, Type type, JsonDeserializationContext jsonDeserializationContext) throws JsonParseException { - String[] parts = json.getAsString().split(":", 2); - if (parts.length != 2) throw new JsonParseException("Invalid InetSocketAddress format: " + json.getAsString()); - String host = parts[0]; - int port = Integer.parseInt(parts[1]); - return InetSocketAddress.createUnresolved(host, port); - } - - @Override - public JsonElement serialize(InetSocketAddress add, Type type, JsonSerializationContext jsonSerializationContext) { - return new JsonPrimitive(add.getHostString() + ":" + add.getPort()); - } -} diff --git a/api/src/main/java/studio/o7/octopus/plugin/api/adapters/ItemStackAdapter.java b/api/src/main/java/studio/o7/octopus/plugin/api/adapters/ItemStackAdapter.java deleted file mode 100644 index eaa5cfc..0000000 --- a/api/src/main/java/studio/o7/octopus/plugin/api/adapters/ItemStackAdapter.java +++ /dev/null @@ -1,23 +0,0 @@ -package studio.o7.octopus.plugin.api.adapters; - -import com.google.gson.*; -import org.bukkit.inventory.ItemStack; - -import java.lang.reflect.Type; -import java.util.Map; - -public final class ItemStackAdapter implements JsonSerializer, JsonDeserializer { - @Override - public ItemStack deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - if (json.isJsonNull()) return null; - Map map = context.deserialize(json, Map.class); - return ItemStack.deserialize(map); - } - - @Override - public JsonElement serialize(ItemStack src, Type typeOfSrc, JsonSerializationContext context) { - if (src == null) return JsonNull.INSTANCE; - var map = src.serialize(); - return context.serialize(map); - } -} diff --git a/api/src/main/java/studio/o7/octopus/plugin/api/adapters/LocationAdapter.java b/api/src/main/java/studio/o7/octopus/plugin/api/adapters/LocationAdapter.java deleted file mode 100644 index dc73bcf..0000000 --- a/api/src/main/java/studio/o7/octopus/plugin/api/adapters/LocationAdapter.java +++ /dev/null @@ -1,42 +0,0 @@ -package studio.o7.octopus.plugin.api.adapters; - -import com.google.gson.*; -import org.bukkit.Bukkit; -import org.bukkit.Location; -import org.bukkit.World; - -import java.lang.reflect.Type; -import java.util.Optional; -import java.util.UUID; - -public final class LocationAdapter implements JsonSerializer, JsonDeserializer { - @Override - public Location deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - var object = json.getAsJsonObject(); - - var worldId = object.has("world") ? UUID.fromString(object.get("world").getAsString()) : null; - var world = worldId != null ? Bukkit.getWorld(worldId) : null; - - double x = object.has("x") ? object.get("x").getAsDouble() : 0d; - double y = object.has("y") ? object.get("y").getAsDouble() : 0d; - double z = object.has("z") ? object.get("z").getAsDouble() : 0d; - - float yaw = object.has("yaw") ? object.get("yaw").getAsFloat() : 0f; - float pitch = object.has("pitch") ? object.get("pitch").getAsFloat() : 0f; - - return new Location(world, x, y, z, yaw, pitch); - } - - @Override - public JsonElement serialize(Location src, Type typeOfSrc, JsonSerializationContext context) { - var object = new JsonObject(); - object.addProperty("world", Optional.ofNullable(src.getWorld()).map(World::getUID).map(UUID::toString).orElse(null)); - object.addProperty("x", src.getX()); - object.addProperty("y", src.getY()); - object.addProperty("z", src.getZ()); - object.addProperty("yaw", src.getYaw()); - object.addProperty("pitch", src.getPitch()); - - return object; - } -} diff --git a/api/src/main/java/studio/o7/octopus/plugin/api/adapters/OfflinePlayerAdapter.java b/api/src/main/java/studio/o7/octopus/plugin/api/adapters/OfflinePlayerAdapter.java deleted file mode 100644 index 02735e8..0000000 --- a/api/src/main/java/studio/o7/octopus/plugin/api/adapters/OfflinePlayerAdapter.java +++ /dev/null @@ -1,36 +0,0 @@ -package studio.o7.octopus.plugin.api.adapters; - -import com.google.gson.*; -import org.bukkit.Bukkit; -import org.bukkit.OfflinePlayer; - -import java.lang.reflect.Type; -import java.util.UUID; - -public final class OfflinePlayerAdapter implements JsonSerializer, JsonDeserializer { - - @Override - public OfflinePlayer deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - var object = json.getAsJsonObject(); - - if (object.has("uuid")) { - var uuid = UUID.fromString(object.get("uuid").getAsString()); - return Bukkit.getOfflinePlayer(uuid); - } - - if (object.has("name")) { - var name = object.get("name").getAsString(); - return Bukkit.getOfflinePlayer(name); - } - - return null; - } - - @Override - public JsonElement serialize(OfflinePlayer player, Type typeOfSrc, JsonSerializationContext context) { - var object = new JsonObject(); - object.addProperty("uuid", player.getUniqueId().toString()); - object.addProperty("name", player.getName()); - return object; - } -} diff --git a/api/src/main/java/studio/o7/octopus/plugin/api/adapters/PlayerAdapter.java b/api/src/main/java/studio/o7/octopus/plugin/api/adapters/PlayerAdapter.java deleted file mode 100644 index 8d5ae1e..0000000 --- a/api/src/main/java/studio/o7/octopus/plugin/api/adapters/PlayerAdapter.java +++ /dev/null @@ -1,39 +0,0 @@ -package studio.o7.octopus.plugin.api.adapters; - -import com.google.gson.*; -import org.bukkit.Bukkit; -import org.bukkit.entity.Player; - -import java.lang.reflect.Type; -import java.util.UUID; - -public final class PlayerAdapter implements JsonSerializer, JsonDeserializer { - @Override - public Player deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - var object = json.getAsJsonObject(); - - if (object.has("uuid")) { - var uuid = UUID.fromString(object.get("uuid").getAsString()); - var player = Bukkit.getPlayer(uuid); - if (player != null) return player; - - var offlinePlayer = Bukkit.getOfflinePlayer(uuid); - return offlinePlayer.isOnline() ? offlinePlayer.getPlayer() : null; - } - - if (object.has("name")) { - var name = object.get("name").getAsString(); - return Bukkit.getPlayer(name); - } - - return null; - } - - @Override - public JsonElement serialize(Player player, Type typeOfSrc, JsonSerializationContext context) { - var object = new JsonObject(); - object.addProperty("uuid", player.getUniqueId().toString()); - object.addProperty("name", player.getName()); - return object; - } -} diff --git a/api/src/main/java/studio/o7/octopus/plugin/api/adapters/ResourcePackInfoAdapter.java b/api/src/main/java/studio/o7/octopus/plugin/api/adapters/ResourcePackInfoAdapter.java deleted file mode 100644 index 558e1b1..0000000 --- a/api/src/main/java/studio/o7/octopus/plugin/api/adapters/ResourcePackInfoAdapter.java +++ /dev/null @@ -1,28 +0,0 @@ -package studio.o7.octopus.plugin.api.adapters; - -import com.google.gson.*; -import net.kyori.adventure.resource.ResourcePackInfo; - -import java.lang.reflect.Type; -import java.net.URI; -import java.util.UUID; - -public final class ResourcePackInfoAdapter implements JsonSerializer, JsonDeserializer { - @Override - public ResourcePackInfo deserialize(JsonElement json, Type type, JsonDeserializationContext jsonDeserializationContext) throws JsonParseException { - var object = json.getAsJsonObject(); - var id = UUID.fromString(object.get("id").getAsString()); - var uri = URI.create(object.get("uri").getAsString()); - var hash = object.get("hash").getAsString(); - return ResourcePackInfo.resourcePackInfo(id, uri, hash); - } - - @Override - public JsonElement serialize(ResourcePackInfo info, Type type, JsonSerializationContext jsonSerializationContext) { - var object = new JsonObject(); - object.addProperty("id", info.id().toString()); - object.addProperty("uri", info.uri().toString()); - object.addProperty("hash", info.hash()); - return object; - } -} diff --git a/api/src/main/java/studio/o7/octopus/plugin/api/parser/StructParser.java b/api/src/main/java/studio/o7/octopus/plugin/api/parser/StructParser.java deleted file mode 100644 index 6b31506..0000000 --- a/api/src/main/java/studio/o7/octopus/plugin/api/parser/StructParser.java +++ /dev/null @@ -1,56 +0,0 @@ -package studio.o7.octopus.plugin.api.parser; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.protobuf.Struct; -import com.google.protobuf.util.JsonFormat; -import lombok.NonNull; -import net.kyori.adventure.resource.ResourcePackInfo; -import net.kyori.adventure.text.Component; -import org.bukkit.Location; -import org.bukkit.OfflinePlayer; -import org.bukkit.entity.Player; -import org.bukkit.inventory.ItemStack; -import studio.o7.octopus.plugin.api.adapters.*; - -import java.io.IOException; -import java.net.InetSocketAddress; - -public final class StructParser { - private static final JsonFormat.Parser PARSER = JsonFormat.parser(); - private static final JsonFormat.Printer PRINTER = JsonFormat.printer(); - - private static final ComponentAdapter COMPONENT_ADAPTER = new ComponentAdapter(); - private static final InetSocketAddressAdapter INET_SOCKET_ADDRESS_ADAPTER = new InetSocketAddressAdapter(); - private static final ItemStackAdapter ITEM_STACK_ADAPTER = new ItemStackAdapter(); - private static final LocationAdapter LOCATION_ADAPTER = new LocationAdapter(); - private static final OfflinePlayerAdapter OFFLINE_PLAYER_ADAPTER = new OfflinePlayerAdapter(); - private static final PlayerAdapter PLAYER_ADAPTER = new PlayerAdapter(); - private static final ResourcePackInfoAdapter RESOURCE_PACK_INFO_ADAPTER = new ResourcePackInfoAdapter(); - - private final Gson gson; - - public StructParser(@NonNull GsonBuilder builder) { - builder.disableHtmlEscaping(); - builder.registerTypeAdapter(Component.class, COMPONENT_ADAPTER); - builder.registerTypeAdapter(InetSocketAddress.class, INET_SOCKET_ADDRESS_ADAPTER); - builder.registerTypeAdapter(ItemStack.class, ITEM_STACK_ADAPTER); - builder.registerTypeAdapter(Location.class, LOCATION_ADAPTER); - builder.registerTypeAdapter(OfflinePlayer.class, OFFLINE_PLAYER_ADAPTER); - builder.registerTypeAdapter(Player.class, PLAYER_ADAPTER); - builder.registerTypeAdapter(ResourcePackInfo.class, RESOURCE_PACK_INFO_ADAPTER); - this.gson = builder.create(); - } - - public T toObject(@NonNull Struct struct, @NonNull Class tClass) throws IOException { - var json = PRINTER.print(struct); - return gson.fromJson(json, tClass); - } - - public Struct toStruct(@NonNull T object) throws IOException { - var builder = Struct.newBuilder(); - var json = gson.toJson(object); - PARSER.merge(json, builder); - return builder.build(); - } -} diff --git a/build.gradle.kts b/build.gradle.kts index 004d28c..5822df1 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,7 +1,7 @@ import studio.o7.remora.RemoraPlugin plugins { - id("studio.o7.remora") version "0.3.6" + id("studio.o7.remora") version "0.4.0" } allprojects { diff --git a/plugin/build.gradle.kts b/plugin/build.gradle.kts index 22c723a..d940c53 100644 --- a/plugin/build.gradle.kts +++ b/plugin/build.gradle.kts @@ -1,13 +1,23 @@ +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar import studio.o7.remora.plugin.ApiVersion import studio.o7.remora.plugin.Load +plugins { + id("xyz.jpenilla.run-paper") version "3.0.2" +} + dependencies { implementation(project(":api")) + + implementation("io.grpc:grpc-okhttp:1.78.0") + implementation("io.grpc:grpc-protobuf:1.78.0") + implementation("io.grpc:grpc-stub:1.78.0") } information { artifactId = "octopus" - description = "Octopus paper plugin" + description = "Octopus-API paper plugin implementation" + url = "https://github.com/o7studios/octopus-plugin" } plugin { @@ -16,3 +26,19 @@ plugin { apiVersion = ApiVersion.PAPER_1_21_8 load.set(Load.POST_WORLD) } + +tasks.named("shadowJar") { + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + mergeServiceFiles() + + filesMatching("META-INF/services/**") { + duplicatesStrategy = DuplicatesStrategy.INCLUDE + } +} + +tasks { + runServer { + minecraftVersion("1.21.11") + args("--port", "25565") + } +} diff --git a/plugin/src/main/java/studio/o7/octopus/plugin/OctopusImpl.java b/plugin/src/main/java/studio/o7/octopus/plugin/OctopusImpl.java index e486b66..25de47c 100644 --- a/plugin/src/main/java/studio/o7/octopus/plugin/OctopusImpl.java +++ b/plugin/src/main/java/studio/o7/octopus/plugin/OctopusImpl.java @@ -1,132 +1,125 @@ package studio.o7.octopus.plugin; +import gentle.Error; +import gentle.Result; import io.grpc.stub.StreamObserver; -import it.unimi.dsi.fastutil.Pair; -import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap; -import it.unimi.dsi.fastutil.objects.Object2ObjectMap; -import lombok.NonNull; import lombok.extern.slf4j.Slf4j; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import studio.o7.octopus.plugin.api.Octopus; -import studio.o7.octopus.plugin.api.listener.Listener; -import studio.o7.octopus.plugin.observer.EmptyObserver; -import studio.o7.octopus.plugin.utils.ProtoUtils; -import studio.o7.octopus.sdk.OctopusSDK; -import studio.o7.octopus.sdk.gen.api.v1.*; -import studio.o7.octopus.sdk.gen.api.v1.Object; - -import java.time.Instant; -import java.util.Collection; +import studio.o7.octopus.plugin.api.EventHandler; +import studio.o7.octopus.plugin.api.OctopusError; +import studio.o7.octopus.plugin.api.QueryParameter; +import studio.o7.octopus.plugin.authentication.OctopusCredentials; +import studio.o7.octopus.plugin.channel.OctopusChannelFactory; +import studio.o7.octopus.plugin.observer.OctopusObserver; +import studio.o7.octopus.sdk.v1.*; +import studio.o7.octopus.sdk.v1.Object; + import java.util.UUID; -import java.util.concurrent.atomic.AtomicReference; -@Slf4j (topic = "OctopusPlugin") +@Slf4j(topic = "OctopusPlugin") public final class OctopusImpl implements Octopus { - private static final EmptyObserver EMPTY_OBSERVER = new EmptyObserver(); - private final OctopusGrpc.OctopusStub stub = OctopusSDK.stub(); - private final OctopusGrpc.OctopusBlockingStub blockingStub = OctopusSDK.blockingStub(); - private final Object2ObjectMap>> listeners = new Object2ObjectArrayMap<>(); + private final OctopusObserver streamObserver; + private StreamObserver responseObserver; + + private final OctopusGrpc.OctopusBlockingStub blocking; + private final OctopusGrpc.OctopusStub async; + + public OctopusImpl(String token, String host, int port) { + var channel = OctopusChannelFactory.getOrCreate(host, port); + + var blockingStub = OctopusGrpc.newBlockingStub(channel); + this.blocking = blockingStub.withCallCredentials(new OctopusCredentials(token)); + + var asyncStub = OctopusGrpc.newStub(channel); + async = asyncStub.withCallCredentials(new OctopusCredentials(token)); + + this.streamObserver = new OctopusObserver(); + } @Override - public @NotNull Collection get(@NonNull String keyPattern, boolean includeExpired, @Nullable Instant createdRangeStart, @Nullable Instant createdRangeEnd) { - var builder = GetRequest.newBuilder(); + public Result get(String key) { + var request = GetRequest.newBuilder().setKey(key).build(); + try { + var response = blocking.get(request); + return Result.ok(response.getObject()); + } catch (RuntimeException e) { + log.error("Failed to send a get request: {}", e.getMessage()); + return Result.err(OctopusError.GET_REQUEST_FAILED); + } + } - builder.setKeyPattern(keyPattern); - builder.setIncludeExpired(includeExpired); + @Override + public Result query(QueryParameter queryParameter) { + var request = QueryRequest.newBuilder(); + request.setKeyPattern(queryParameter.getKeyPattern()); + request.setDataFilter(queryParameter.getDataFilter()); + + request.setIncludeExpired(queryParameter.isIncludeExpired()); + + var paginator = Paginator.newBuilder().setPage(queryParameter.getPage()).setPageSize(queryParameter.getPageSize()).build(); + + request.setPaginator(paginator); - if (createdRangeStart != null) - builder.setCreatedAtRangeStart(ProtoUtils.toProto(createdRangeStart)); + if (queryParameter.getCreatedAtStart() != null) { + request.setCreatedAtRangeStart(queryParameter.getCreatedAtStart()); + } - if (createdRangeEnd != null) - builder.setCreatedAtRangeEnd(ProtoUtils.toProto(createdRangeEnd)); + if (queryParameter.getCreatedAtEnd() != null) { + request.setCreatedAtRangeEnd(queryParameter.getCreatedAtEnd()); + } - return this.blockingStub.get(builder.build()).getEntriesList(); + try { + var response = blocking.query(request.build()); + return Result.ok(response); + } catch (RuntimeException e) { + log.error("Failed to send a query request: {}", e.getMessage()); + return Result.err(OctopusError.QUERY_REQUEST_FAILED); + } } @Override - public void registerListener(@NonNull Listener listener) { - var requestRef = new AtomicReference>(); - - var observer = stub.listen(new StreamObserver<>() { - @Override - public void onNext(EventCall value) { - var start = System.currentTimeMillis(); - var request = requestRef.get(); - if (request == null) return; - - listener.onCall(value.getObject()); - - var msg = ListenMessage.newBuilder() - .setCallback(value) - .build(); - - request = requestRef.get(); - if (request == null) return; - - request.onNext(msg); - log.debug("Finished EventCall `{}` in {}ms", value.getCallId(), System.currentTimeMillis() - start); - } - - @Override - public void onError(Throwable t) { - requestRef.set(null); - log.error("Cannot call event on listener {} with key-pattern {}", listener.getListenerUniqueId(), listener.getKeyPattern(), t); - unregisterListener(listener); - } - - @Override - public void onCompleted() { - requestRef.set(null); - unregisterListener(listener); - log.debug("Completed listener `{}`", listener.getListenerUniqueId()); - } - }); - - requestRef.set(observer); - - observer.onNext(ListenMessage.newBuilder() - .setRegister(ListenRegister.newBuilder() - .setKeyPattern(listener.getKeyPattern()) - .setPriority(listener.getPriority()) - .build()).build()); - - this.listeners.put(listener.getListenerUniqueId(), new Pair<>() { - @Override - public Listener left() { - return listener; - } - - @Override - public StreamObserver right() { - return observer; - } - }); + public Result call(studio.o7.octopus.sdk.v1.Object obj) { + try { + var response = blocking.call(obj); + return Result.ok(response); + } catch (RuntimeException e) { + log.error("Failed to send a call request: {}", e.getMessage()); + return Result.err(OctopusError.CALL_REQUEST_FAILED); + } } @Override - public void unregisterListener(@NonNull Listener listener) { - unregisterListener(listener.getListenerUniqueId()); + public void write(Object obj) { + try { + blocking.write(obj); + } catch (RuntimeException e) { + log.error("Failed to send a write request: {}", e.getMessage()); + } } @Override - public void unregisterListener(@NonNull UUID listenerUniqueId) { - var pair = this.listeners.get(listenerUniqueId); - if (pair == null) return; - this.listeners.remove(listenerUniqueId); - var right = pair.right(); - if (right == null) return; - right.onCompleted(); + public void registerHandler(EventHandler eventHandler) { + log.debug("Adding a new handler to the pattern {}", eventHandler.getKeyPattern()); + streamObserver.addHandler(eventHandler); + + if (responseObserver == null) { + log.debug("initializing stream to octopus"); + this.responseObserver = async.listen(streamObserver); + } + + var req = ListenMessage.newBuilder().addAllKeyPattern(streamObserver.getKeys()); + responseObserver.onNext(req.build()); } @Override - public @NotNull Entry call(@NonNull Object obj) { - return blockingStub.call(obj); + public void unregisterHandler(@org.jspecify.annotations.NonNull EventHandler eventHandler) { + this.unregisterHandler(eventHandler.getListenerUniqueId()); } @Override - public void callAndForget(@NonNull Object obj) { - stub.write(obj, EMPTY_OBSERVER); + public void unregisterHandler(UUID listenerUniqueId) { + this.streamObserver.removeHandler(listenerUniqueId); } + } \ No newline at end of file diff --git a/plugin/src/main/java/studio/o7/octopus/plugin/OctopusPlugin.java b/plugin/src/main/java/studio/o7/octopus/plugin/OctopusPlugin.java index ba706d6..d4bd045 100644 --- a/plugin/src/main/java/studio/o7/octopus/plugin/OctopusPlugin.java +++ b/plugin/src/main/java/studio/o7/octopus/plugin/OctopusPlugin.java @@ -4,6 +4,7 @@ import org.bukkit.event.HandlerList; import org.bukkit.plugin.java.JavaPlugin; import studio.o7.octopus.plugin.api.Octopus; +import studio.o7.octopus.plugin.channel.OctopusChannelFactory; @Getter public final class OctopusPlugin extends JavaPlugin implements PluginInstance { @@ -16,11 +17,18 @@ public OctopusPlugin() { @Override public void onEnable() { - octopus = new OctopusImpl(); + saveDefaultConfig(); + + String token = getConfig().getString("octopus.token"); + String host = getConfig().getString("octopus.host"); + int port = getConfig().getInt("octopus.port"); + + octopus = new OctopusImpl(token, host, port); } @Override public void onDisable() { + OctopusChannelFactory.shutdown(); HandlerList.unregisterAll(this); } diff --git a/plugin/src/main/java/studio/o7/octopus/plugin/authentication/OctopusCredentials.java b/plugin/src/main/java/studio/o7/octopus/plugin/authentication/OctopusCredentials.java new file mode 100644 index 0000000..947440e --- /dev/null +++ b/plugin/src/main/java/studio/o7/octopus/plugin/authentication/OctopusCredentials.java @@ -0,0 +1,23 @@ +package studio.o7.octopus.plugin.authentication; + +import io.grpc.CallCredentials; +import io.grpc.Metadata; + +import java.util.concurrent.Executor; + +public class OctopusCredentials extends CallCredentials { + + private final String token; + + public OctopusCredentials(String token) { + this.token = token; + } + + @Override + public void applyRequestMetadata(RequestInfo requestInfo, Executor executor, MetadataApplier metadataApplier) { + Metadata headers = new Metadata(); + Metadata.Key authKey = Metadata.Key.of("authorization", Metadata.ASCII_STRING_MARSHALLER); + headers.put(authKey, this.token); + metadataApplier.apply(headers); + } +} diff --git a/plugin/src/main/java/studio/o7/octopus/plugin/channel/OctopusChannelFactory.java b/plugin/src/main/java/studio/o7/octopus/plugin/channel/OctopusChannelFactory.java new file mode 100644 index 0000000..924146c --- /dev/null +++ b/plugin/src/main/java/studio/o7/octopus/plugin/channel/OctopusChannelFactory.java @@ -0,0 +1,46 @@ +package studio.o7.octopus.plugin.channel; + +import io.grpc.ManagedChannel; +import io.grpc.okhttp.OkHttpChannelBuilder; + +import java.util.concurrent.TimeUnit; + +/** + * Creates and holds the shared Octopus gRPC channel. + * + *

Defaults: + * host = 127.0.0.1 + * port = 50051 + * plaintext

+ */ +public final class OctopusChannelFactory { + + private static volatile ManagedChannel channel; + + private OctopusChannelFactory() {} + + public static ManagedChannel getOrCreate(String host, int port) { + var c = channel; + if (c != null) return c; + + synchronized (OctopusChannelFactory.class) { + if (channel != null) return channel; + + channel = OkHttpChannelBuilder + .forAddress(host, port) + .usePlaintext() + .keepAliveTime(30, TimeUnit.SECONDS) + .keepAliveWithoutCalls(true) + .build(); + + return channel; + } + } + + public static void shutdown() { + var c = channel; + if (c == null) return; + c.shutdown(); + channel = null; + } +} \ No newline at end of file diff --git a/plugin/src/main/java/studio/o7/octopus/plugin/observer/EmptyObserver.java b/plugin/src/main/java/studio/o7/octopus/plugin/observer/EmptyObserver.java deleted file mode 100644 index f4e5500..0000000 --- a/plugin/src/main/java/studio/o7/octopus/plugin/observer/EmptyObserver.java +++ /dev/null @@ -1,21 +0,0 @@ -package studio.o7.octopus.plugin.observer; - -import com.google.protobuf.Empty; -import io.grpc.stub.StreamObserver; - -public class EmptyObserver implements StreamObserver { - @Override - public void onNext(Empty value) { - - } - - @Override - public void onError(Throwable t) { - - } - - @Override - public void onCompleted() { - - } -} diff --git a/plugin/src/main/java/studio/o7/octopus/plugin/observer/OctopusObserver.java b/plugin/src/main/java/studio/o7/octopus/plugin/observer/OctopusObserver.java new file mode 100644 index 0000000..9b25dd7 --- /dev/null +++ b/plugin/src/main/java/studio/o7/octopus/plugin/observer/OctopusObserver.java @@ -0,0 +1,108 @@ +package studio.o7.octopus.plugin.observer; + +import io.grpc.stub.StreamObserver; +import lombok.extern.slf4j.Slf4j; +import studio.o7.octopus.plugin.api.EventHandler; +import studio.o7.octopus.sdk.v1.EventCall; + +import java.util.HashMap; +import java.util.Optional; +import java.util.UUID; + +@Slf4j(topic = "OctopusObserver") +public class OctopusObserver implements StreamObserver { + + private final HashMap handlers; + + public OctopusObserver() { + this.handlers = new HashMap<>(); + } + + public void addHandler(EventHandler handler) { + handlers.put(handler.getListenerUniqueId(), handler); + } + + public void removeHandler(UUID id) { + this.handlers.remove(id); + } + + public Iterable getKeys() { + return handlers.values().stream().map(EventHandler::getKeyPattern).toList(); + } + + public Optional findMatchingHandler(String key) { + String[] keyTokens = key.split("\\."); + + for (EventHandler handler : this.handlers.values()) { + if (matches(handler.getKeyPattern(), keyTokens)) { + return Optional.of(handler); + } + } + return Optional.empty(); + } + + private static boolean matches(String pattern, String[] keyTokens) { + String[] patternTokens = pattern.split("\\."); + + int i = 0; // pattern index + int j = 0; // key index + + while (i < patternTokens.length && j < keyTokens.length) { + String p = patternTokens[i]; + + if (p.equals(">")) { + // '>' matches everything remaining (including nothing) + return true; + } + + if (!p.equals("*") && !p.equals(keyTokens[j])) { + return false; + } + + i++; + j++; + } + + // If pattern has remaining tokens + if (i < patternTokens.length) { + // Only valid if the remaining token is a single '>' + return i == patternTokens.length - 1 && patternTokens[i].equals(">"); + } + + // Match only if key is fully consumed + return j == keyTokens.length; + } + + private String currentCallID; + + @Override + public void onNext(EventCall eventCall) { + this.currentCallID = eventCall.getCallId(); + + var object = eventCall.getObject(); + var optionalHandler = findMatchingHandler(object.getKey()); + + if (optionalHandler.isEmpty()) { + log.error("Failed to find handler for callID: {}", currentCallID); + return; + } + + var handler = optionalHandler.get(); + handler.onCall(object); + log.info("Successfully handled callID: {} with pattern: {}", currentCallID, object.getKey()); + this.currentCallID = ""; + } + + @Override + public void onError(Throwable throwable) { + log.error("Failed listener {}, callID: {}", throwable.getMessage(), currentCallID); + this.currentCallID = ""; + } + + @Override + public void onCompleted() { + log.debug("Completed listener, callID: {}", currentCallID); + this.currentCallID = ""; + + } +} diff --git a/plugin/src/main/java/studio/o7/octopus/plugin/utils/ProtoUtils.java b/plugin/src/main/java/studio/o7/octopus/plugin/utils/ProtoUtils.java deleted file mode 100644 index b9920cc..0000000 --- a/plugin/src/main/java/studio/o7/octopus/plugin/utils/ProtoUtils.java +++ /dev/null @@ -1,20 +0,0 @@ -package studio.o7.octopus.plugin.utils; - -import com.google.protobuf.Timestamp; -import lombok.experimental.UtilityClass; - -import java.time.Instant; - -@UtilityClass -public class ProtoUtils { - public Instant fromProto(Timestamp ts) { - return Instant.ofEpochSecond(ts.getSeconds(), ts.getNanos()); - } - - public Timestamp toProto(Instant instant) { - return Timestamp.newBuilder() - .setSeconds(instant.getEpochSecond()) - .setNanos(instant.getNano()) - .build(); - } -} diff --git a/plugin/src/main/resources/config.yml b/plugin/src/main/resources/config.yml new file mode 100644 index 0000000..894dfab --- /dev/null +++ b/plugin/src/main/resources/config.yml @@ -0,0 +1,8 @@ +# Configuration of Octopus-Service +octopus: + # Host of Octopus-gRPC Server + host: "127.0.0.1" + # Port of Octopus-gRPC Server + port: 50051 + # Replace to Octopus-API token + token: "development" \ No newline at end of file