From 8c6586f008fb567a0b396df2b0fbc3c7093441d5 Mon Sep 17 00:00:00 2001 From: lucko Date: Wed, 6 Mar 2024 21:12:33 +0000 Subject: [PATCH] Add custom payload message to API (#3840) --- .../messaging/CustomMessageReceiveEvent.java | 59 +++++++++++++ .../api/event/messaging/package-info.java | 29 +++++++ .../api/messaging/MessagingService.java | 43 +++++++++- .../messenger/message/type/CustomMessage.java | 56 ++++++++++++ .../implementation/ApiMessagingService.java | 7 ++ .../common/event/EventDispatcher.java | 6 ++ .../messaging/InternalMessagingService.java | 8 ++ .../messaging/LuckPermsMessagingService.java | 24 +++++- .../messaging/message/CustomMessageImpl.java | 85 +++++++++++++++++++ .../standalone/MessagingIntegrationTest.java | 14 ++- 10 files changed, 324 insertions(+), 7 deletions(-) create mode 100644 api/src/main/java/net/luckperms/api/event/messaging/CustomMessageReceiveEvent.java create mode 100644 api/src/main/java/net/luckperms/api/event/messaging/package-info.java create mode 100644 api/src/main/java/net/luckperms/api/messenger/message/type/CustomMessage.java create mode 100644 common/src/main/java/me/lucko/luckperms/common/messaging/message/CustomMessageImpl.java diff --git a/api/src/main/java/net/luckperms/api/event/messaging/CustomMessageReceiveEvent.java b/api/src/main/java/net/luckperms/api/event/messaging/CustomMessageReceiveEvent.java new file mode 100644 index 000000000..2e43b5dea --- /dev/null +++ b/api/src/main/java/net/luckperms/api/event/messaging/CustomMessageReceiveEvent.java @@ -0,0 +1,59 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.luckperms.api.event.messaging; + +import net.luckperms.api.event.LuckPermsEvent; +import net.luckperms.api.event.util.Param; +import net.luckperms.api.messaging.MessagingService; +import org.checkerframework.checker.nullness.qual.NonNull; + +/** + * Called when a custom payload message is received via the {@link MessagingService}. + * + *

This event is effectively the 'other end' of + * {@link MessagingService#sendCustomMessage(String, String)}.

+ * + * @since 5.5 + */ +public interface CustomMessageReceiveEvent extends LuckPermsEvent { + + /** + * Gets the channel id. + * + * @return the channel id + */ + @Param(0) + @NonNull String getChannelId(); + + /** + * Gets the custom payload that was sent. + * + * @return the custom payload + */ + @Param(1) + @NonNull String getPayload(); + +} diff --git a/api/src/main/java/net/luckperms/api/event/messaging/package-info.java b/api/src/main/java/net/luckperms/api/event/messaging/package-info.java new file mode 100644 index 000000000..ed66949e5 --- /dev/null +++ b/api/src/main/java/net/luckperms/api/event/messaging/package-info.java @@ -0,0 +1,29 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +/** + * Events relating to the {@link net.luckperms.api.messaging.MessagingService}. + */ +package net.luckperms.api.event.messaging; \ No newline at end of file diff --git a/api/src/main/java/net/luckperms/api/messaging/MessagingService.java b/api/src/main/java/net/luckperms/api/messaging/MessagingService.java index 3615fd9f2..a1fa20d55 100644 --- a/api/src/main/java/net/luckperms/api/messaging/MessagingService.java +++ b/api/src/main/java/net/luckperms/api/messaging/MessagingService.java @@ -26,11 +26,12 @@ package net.luckperms.api.messaging; import net.luckperms.api.LuckPerms; +import net.luckperms.api.event.messaging.CustomMessageReceiveEvent; import net.luckperms.api.model.user.User; import org.checkerframework.checker.nullness.qual.NonNull; /** - * A means to push changes to other servers using the platforms networking + * A means to send messages to other servers using the platforms networking */ public interface MessagingService { @@ -71,4 +72,44 @@ public interface MessagingService { */ void pushUserUpdate(@NonNull User user); + /** + * Uses the messaging service to send a message with a custom payload. + * + *

The intended use case of this functionality is to allow plugins/mods + * to send lightweight and permissions-related custom messages + * between instances, piggy-backing on top of the messenger abstraction + * already built into LuckPerms.

+ * + *

It is not intended as a full message broker replacement/abstraction. + * Note that some of the messenger implementations in LuckPerms cannot handle + * a high volume of messages being sent (for example the SQL messenger). + * Additionally, some implementations do not give any guarantees that a message + * will be delivered on time or even at all (for example the plugin message + * messengers).

+ * + *

With all of that in mind, please consider that if you are using this + * functionality to send messages that have nothing to do with LuckPerms or + * permissions, or that require guarantees around delivery reliability, you + * are most likely misusing the API and would be better off building your own + * integration with a message broker.

+ * + *

Whilst there is (currently) no strict validation, it is recommended + * that the channel id should use the same format as Minecraft resource locations / + * namespaced keys. For example, a plugin called "SuperRanks" sending rank-up + * notifications using custom payload messages might use the channel id + * {@code "superranks:notifications"} for this purpose.

+ * + *

The payload can be any valid UTF-8 string.

+ * + *

The message will be delivered asynchronously.

+ * + *

Other LuckPerms instances that receive the message will publish it to API + * consumers using the {@link CustomMessageReceiveEvent}.

+ * + * @param channelId the channel id + * @param payload the message payload + * @since 5.5 + */ + void sendCustomMessage(@NonNull String channelId, @NonNull String payload); + } diff --git a/api/src/main/java/net/luckperms/api/messenger/message/type/CustomMessage.java b/api/src/main/java/net/luckperms/api/messenger/message/type/CustomMessage.java new file mode 100644 index 000000000..7fb820ebe --- /dev/null +++ b/api/src/main/java/net/luckperms/api/messenger/message/type/CustomMessage.java @@ -0,0 +1,56 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.luckperms.api.messenger.message.type; + +import net.luckperms.api.messenger.message.Message; +import org.checkerframework.checker.nullness.qual.NonNull; + +/** + * Represents a "custom payload" message. + * + *

Used by API consumers to send custom messages between servers.

+ * + * @see net.luckperms.api.messaging.MessagingService#sendCustomMessage(String, String) + * @see net.luckperms.api.event.messaging.CustomMessageReceiveEvent + * @since 5.5 + */ +public interface CustomMessage extends Message { + + /** + * Gets the channel identifier. + * + * @return the namespace + */ + @NonNull String getChannelId(); + + /** + * Gets the payload. + * + * @return the payload + */ + @NonNull String getPayload(); + +} diff --git a/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiMessagingService.java b/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiMessagingService.java index 064f60505..b86624872 100644 --- a/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiMessagingService.java +++ b/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiMessagingService.java @@ -54,4 +54,11 @@ public class ApiMessagingService implements MessagingService { Objects.requireNonNull(user, "user"); this.handle.pushUserUpdate(ApiUser.cast(user)); } + + @Override + public void sendCustomMessage(@NonNull String channelId, @NonNull String payload) { + Objects.requireNonNull(channelId, "channelId"); + Objects.requireNonNull(payload, "payload"); + this.handle.pushCustomPayload(channelId, payload); + } } diff --git a/common/src/main/java/me/lucko/luckperms/common/event/EventDispatcher.java b/common/src/main/java/me/lucko/luckperms/common/event/EventDispatcher.java index 456ceefb4..5262b9a41 100644 --- a/common/src/main/java/me/lucko/luckperms/common/event/EventDispatcher.java +++ b/common/src/main/java/me/lucko/luckperms/common/event/EventDispatcher.java @@ -58,6 +58,7 @@ import net.luckperms.api.event.log.LogNetworkPublishEvent; import net.luckperms.api.event.log.LogNotifyEvent; import net.luckperms.api.event.log.LogPublishEvent; import net.luckperms.api.event.log.LogReceiveEvent; +import net.luckperms.api.event.messaging.CustomMessageReceiveEvent; import net.luckperms.api.event.node.NodeAddEvent; import net.luckperms.api.event.node.NodeClearEvent; import net.luckperms.api.event.node.NodeMutateEvent; @@ -225,6 +226,10 @@ public final class EventDispatcher { postAsync(LogReceiveEvent.class, id, entry); } + public void dispatchCustomMessageReceive(String channelId, String payload) { + postAsync(CustomMessageReceiveEvent.class, channelId, payload); + } + public void dispatchNodeChanges(PermissionHolder target, DataType dataType, Difference changes) { if (!this.eventBus.shouldPost(NodeAddEvent.class) && !this.eventBus.shouldPost(NodeRemoveEvent.class)) { return; @@ -410,6 +415,7 @@ public final class EventDispatcher { LogNotifyEvent.class, LogPublishEvent.class, LogReceiveEvent.class, + CustomMessageReceiveEvent.class, NodeAddEvent.class, NodeClearEvent.class, NodeRemoveEvent.class, diff --git a/common/src/main/java/me/lucko/luckperms/common/messaging/InternalMessagingService.java b/common/src/main/java/me/lucko/luckperms/common/messaging/InternalMessagingService.java index 2b4ee5521..26b26ec62 100644 --- a/common/src/main/java/me/lucko/luckperms/common/messaging/InternalMessagingService.java +++ b/common/src/main/java/me/lucko/luckperms/common/messaging/InternalMessagingService.java @@ -76,4 +76,12 @@ public interface InternalMessagingService { */ void pushLog(Action logEntry); + /** + * Pushes a custom payload to connected servers. + * + * @param channelId the channel id + * @param payload the payload + */ + void pushCustomPayload(String channelId, String payload); + } diff --git a/common/src/main/java/me/lucko/luckperms/common/messaging/LuckPermsMessagingService.java b/common/src/main/java/me/lucko/luckperms/common/messaging/LuckPermsMessagingService.java index ee9cdb205..e828d032b 100644 --- a/common/src/main/java/me/lucko/luckperms/common/messaging/LuckPermsMessagingService.java +++ b/common/src/main/java/me/lucko/luckperms/common/messaging/LuckPermsMessagingService.java @@ -31,6 +31,7 @@ import me.lucko.luckperms.common.actionlog.LoggedAction; import me.lucko.luckperms.common.cache.BufferedRequest; import me.lucko.luckperms.common.config.ConfigKeys; import me.lucko.luckperms.common.messaging.message.ActionLogMessageImpl; +import me.lucko.luckperms.common.messaging.message.CustomMessageImpl; import me.lucko.luckperms.common.messaging.message.UpdateMessageImpl; import me.lucko.luckperms.common.messaging.message.UserUpdateMessageImpl; import me.lucko.luckperms.common.model.User; @@ -45,6 +46,7 @@ import net.luckperms.api.messenger.Messenger; import net.luckperms.api.messenger.MessengerProvider; import net.luckperms.api.messenger.message.Message; import net.luckperms.api.messenger.message.type.ActionLogMessage; +import net.luckperms.api.messenger.message.type.CustomMessage; import net.luckperms.api.messenger.message.type.UpdateMessage; import net.luckperms.api.messenger.message.type.UserUpdateMessage; import org.checkerframework.checker.nullness.qual.NonNull; @@ -69,7 +71,7 @@ public class LuckPermsMessagingService implements InternalMessagingService, Inco this.messenger = messengerProvider.obtain(this); Objects.requireNonNull(this.messenger, "messenger"); - this.receivedMessages = new ExpiringSet<>(1, TimeUnit.HOURS); + this.receivedMessages = new ExpiringSet<>(5, TimeUnit.MINUTES); this.updateBuffer = new PushUpdateBuffer(plugin); } @@ -136,6 +138,14 @@ public class LuckPermsMessagingService implements InternalMessagingService, Inco }); } + @Override + public void pushCustomPayload(String channelId, String payload) { + this.plugin.getBootstrap().getScheduler().executeAsync(() -> { + UUID requestId = generatePingId(); + this.messenger.sendOutgoingMessage(new CustomMessageImpl(requestId, channelId, payload)); + }); + } + @Override public boolean consumeIncomingMessage(@NonNull Message message) { Objects.requireNonNull(message, "message"); @@ -147,7 +157,8 @@ public class LuckPermsMessagingService implements InternalMessagingService, Inco // determine if the message can be handled by us boolean valid = message instanceof UpdateMessage || message instanceof UserUpdateMessage || - message instanceof ActionLogMessage; + message instanceof ActionLogMessage || + message instanceof CustomMessage; // instead of throwing an exception here, just return false // it means an instance of LP can gracefully handle messages it doesn't @@ -209,6 +220,9 @@ public class LuckPermsMessagingService implements InternalMessagingService, Inco case ActionLogMessageImpl.TYPE: decoded = ActionLogMessageImpl.decode(content, id); break; + case CustomMessageImpl.TYPE: + decoded = CustomMessageImpl.decode(content, id); + break; default: // gracefully return if we just don't recognise the type return false; @@ -270,6 +284,12 @@ public class LuckPermsMessagingService implements InternalMessagingService, Inco this.plugin.getEventDispatcher().dispatchLogReceive(msg.getId(), msg.getAction()); this.plugin.getLogDispatcher().dispatchFromRemote((LoggedAction) msg.getAction()); + + } else if (message instanceof CustomMessage) { + CustomMessage msg = (CustomMessage) message; + + this.plugin.getEventDispatcher().dispatchCustomMessageReceive(msg.getChannelId(), msg.getPayload()); + } else { throw new IllegalArgumentException("Unknown message type: " + message.getClass().getName()); } diff --git a/common/src/main/java/me/lucko/luckperms/common/messaging/message/CustomMessageImpl.java b/common/src/main/java/me/lucko/luckperms/common/messaging/message/CustomMessageImpl.java new file mode 100644 index 000000000..0265eb35a --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/messaging/message/CustomMessageImpl.java @@ -0,0 +1,85 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package me.lucko.luckperms.common.messaging.message; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import me.lucko.luckperms.common.messaging.LuckPermsMessagingService; +import me.lucko.luckperms.common.util.gson.JObject; +import net.luckperms.api.messenger.message.type.CustomMessage; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.util.UUID; + +public class CustomMessageImpl extends AbstractMessage implements CustomMessage { + public static final String TYPE = "custom"; + + public static CustomMessageImpl decode(@Nullable JsonElement content, UUID id) { + if (content == null) { + throw new IllegalStateException("Missing content"); + } + + JsonObject obj = content.getAsJsonObject(); + if (!obj.has("channelId")) { + throw new IllegalStateException("Incoming message has no 'channelId' argument: " + content); + } + if (!obj.has("payload")) { + throw new IllegalStateException("Incoming message has no 'payload' argument: " + content); + } + + String channelId = obj.get("channelId").getAsString(); + String payload = obj.get("payload").getAsString(); + + return new CustomMessageImpl(id, channelId, payload); + } + + private final String channelId; + private final String payload; + + public CustomMessageImpl(UUID id, String channelId, String payload) { + super(id); + this.channelId = channelId; + this.payload = payload; + } + + @Override + public @NonNull String getChannelId() { + return this.channelId; + } + + @Override + public @NonNull String getPayload() { + return this.payload; + } + + @Override + public @NonNull String asEncodedString() { + return LuckPermsMessagingService.encodeMessageAsString( + TYPE, getId(), new JObject().add("channelId", this.channelId).add("payload", this.payload).toJson() + ); + } +} diff --git a/standalone/src/test/java/me/lucko/luckperms/standalone/MessagingIntegrationTest.java b/standalone/src/test/java/me/lucko/luckperms/standalone/MessagingIntegrationTest.java index 7cc8c6cae..beaa604f8 100644 --- a/standalone/src/test/java/me/lucko/luckperms/standalone/MessagingIntegrationTest.java +++ b/standalone/src/test/java/me/lucko/luckperms/standalone/MessagingIntegrationTest.java @@ -26,7 +26,6 @@ package me.lucko.luckperms.standalone; import com.google.common.collect.ImmutableMap; -import com.google.common.collect.ImmutableSet; import me.lucko.luckperms.common.actionlog.LoggedAction; import me.lucko.luckperms.common.messaging.InternalMessagingService; import me.lucko.luckperms.common.model.User; @@ -34,9 +33,9 @@ import me.lucko.luckperms.standalone.utils.TestPluginProvider; import net.luckperms.api.actionlog.Action; import net.luckperms.api.event.EventBus; import net.luckperms.api.event.log.LogReceiveEvent; +import net.luckperms.api.event.messaging.CustomMessageReceiveEvent; import net.luckperms.api.event.sync.PreNetworkSyncEvent; import net.luckperms.api.event.sync.SyncType; -import net.luckperms.api.model.PlayerSaveResult; import net.luckperms.api.platform.Health; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Tag; @@ -53,9 +52,7 @@ import java.util.UUID; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; -import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; @Testcontainers @@ -118,15 +115,24 @@ public class MessagingIntegrationTest { } }); + CountDownLatch latch4 = new CountDownLatch(1); + eventBus.subscribe(CustomMessageReceiveEvent.class, e -> { + if (e.getChannelId().equals("luckperms:test") && e.getPayload().equals("hello")) { + latch4.countDown(); + } + }); + // send some messages from plugin A to plugin B messagingServiceA.pushUpdate(); messagingServiceA.pushUserUpdate(user); messagingServiceA.pushLog(exampleLogEntry); + messagingServiceA.pushCustomPayload("luckperms:test", "hello"); // wait for the messages to be sent/received assertTrue(latch1.await(10, TimeUnit.SECONDS)); assertTrue(latch2.await(10, TimeUnit.SECONDS)); assertTrue(latch3.await(10, TimeUnit.SECONDS)); + assertTrue(latch4.await(10, TimeUnit.SECONDS)); } }