Add custom payload message to API (#3840)

This commit is contained in:
lucko 2024-03-06 21:12:33 +00:00 committed by GitHub
parent 79273a8bcc
commit 8c6586f008
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 324 additions and 7 deletions

View File

@ -0,0 +1,59 @@
/*
* This file is part of LuckPerms, licensed under the MIT License.
*
* Copyright (c) lucko (Luck) <luck@lucko.me>
* 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}.
*
* <p>This event is effectively the 'other end' of
* {@link MessagingService#sendCustomMessage(String, String)}.</p>
*
* @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();
}

View File

@ -0,0 +1,29 @@
/*
* This file is part of LuckPerms, licensed under the MIT License.
*
* Copyright (c) lucko (Luck) <luck@lucko.me>
* 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;

View File

@ -26,11 +26,12 @@
package net.luckperms.api.messaging; package net.luckperms.api.messaging;
import net.luckperms.api.LuckPerms; import net.luckperms.api.LuckPerms;
import net.luckperms.api.event.messaging.CustomMessageReceiveEvent;
import net.luckperms.api.model.user.User; import net.luckperms.api.model.user.User;
import org.checkerframework.checker.nullness.qual.NonNull; 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 { public interface MessagingService {
@ -71,4 +72,44 @@ public interface MessagingService {
*/ */
void pushUserUpdate(@NonNull User user); void pushUserUpdate(@NonNull User user);
/**
* Uses the messaging service to send a message with a custom payload.
*
* <p>The intended use case of this functionality is to allow plugins/mods
* to send <b>lightweight</b> and <b>permissions-related</b> custom messages
* between instances, piggy-backing on top of the messenger abstraction
* already built into LuckPerms.</p>
*
* <p>It is <b>not</b> 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).</p>
*
* <p>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.</p>
*
* <p>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.</p>
*
* <p>The payload can be any valid UTF-8 string.</p>
*
* <p>The message will be delivered asynchronously.</p>
*
* <p>Other LuckPerms instances that receive the message will publish it to API
* consumers using the {@link CustomMessageReceiveEvent}.</p>
*
* @param channelId the channel id
* @param payload the message payload
* @since 5.5
*/
void sendCustomMessage(@NonNull String channelId, @NonNull String payload);
} }

View File

@ -0,0 +1,56 @@
/*
* This file is part of LuckPerms, licensed under the MIT License.
*
* Copyright (c) lucko (Luck) <luck@lucko.me>
* 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.
*
* <p>Used by API consumers to send custom messages between servers.</p>
*
* @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();
}

View File

@ -54,4 +54,11 @@ public class ApiMessagingService implements MessagingService {
Objects.requireNonNull(user, "user"); Objects.requireNonNull(user, "user");
this.handle.pushUserUpdate(ApiUser.cast(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);
}
} }

View File

@ -58,6 +58,7 @@ import net.luckperms.api.event.log.LogNetworkPublishEvent;
import net.luckperms.api.event.log.LogNotifyEvent; import net.luckperms.api.event.log.LogNotifyEvent;
import net.luckperms.api.event.log.LogPublishEvent; import net.luckperms.api.event.log.LogPublishEvent;
import net.luckperms.api.event.log.LogReceiveEvent; 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.NodeAddEvent;
import net.luckperms.api.event.node.NodeClearEvent; import net.luckperms.api.event.node.NodeClearEvent;
import net.luckperms.api.event.node.NodeMutateEvent; import net.luckperms.api.event.node.NodeMutateEvent;
@ -225,6 +226,10 @@ public final class EventDispatcher {
postAsync(LogReceiveEvent.class, id, entry); 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<Node> changes) { public void dispatchNodeChanges(PermissionHolder target, DataType dataType, Difference<Node> changes) {
if (!this.eventBus.shouldPost(NodeAddEvent.class) && !this.eventBus.shouldPost(NodeRemoveEvent.class)) { if (!this.eventBus.shouldPost(NodeAddEvent.class) && !this.eventBus.shouldPost(NodeRemoveEvent.class)) {
return; return;
@ -410,6 +415,7 @@ public final class EventDispatcher {
LogNotifyEvent.class, LogNotifyEvent.class,
LogPublishEvent.class, LogPublishEvent.class,
LogReceiveEvent.class, LogReceiveEvent.class,
CustomMessageReceiveEvent.class,
NodeAddEvent.class, NodeAddEvent.class,
NodeClearEvent.class, NodeClearEvent.class,
NodeRemoveEvent.class, NodeRemoveEvent.class,

View File

@ -76,4 +76,12 @@ public interface InternalMessagingService {
*/ */
void pushLog(Action logEntry); 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);
} }

View File

@ -31,6 +31,7 @@ import me.lucko.luckperms.common.actionlog.LoggedAction;
import me.lucko.luckperms.common.cache.BufferedRequest; import me.lucko.luckperms.common.cache.BufferedRequest;
import me.lucko.luckperms.common.config.ConfigKeys; import me.lucko.luckperms.common.config.ConfigKeys;
import me.lucko.luckperms.common.messaging.message.ActionLogMessageImpl; 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.UpdateMessageImpl;
import me.lucko.luckperms.common.messaging.message.UserUpdateMessageImpl; import me.lucko.luckperms.common.messaging.message.UserUpdateMessageImpl;
import me.lucko.luckperms.common.model.User; 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.MessengerProvider;
import net.luckperms.api.messenger.message.Message; import net.luckperms.api.messenger.message.Message;
import net.luckperms.api.messenger.message.type.ActionLogMessage; 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.UpdateMessage;
import net.luckperms.api.messenger.message.type.UserUpdateMessage; import net.luckperms.api.messenger.message.type.UserUpdateMessage;
import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.NonNull;
@ -69,7 +71,7 @@ public class LuckPermsMessagingService implements InternalMessagingService, Inco
this.messenger = messengerProvider.obtain(this); this.messenger = messengerProvider.obtain(this);
Objects.requireNonNull(this.messenger, "messenger"); Objects.requireNonNull(this.messenger, "messenger");
this.receivedMessages = new ExpiringSet<>(1, TimeUnit.HOURS); this.receivedMessages = new ExpiringSet<>(5, TimeUnit.MINUTES);
this.updateBuffer = new PushUpdateBuffer(plugin); 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 @Override
public boolean consumeIncomingMessage(@NonNull Message message) { public boolean consumeIncomingMessage(@NonNull Message message) {
Objects.requireNonNull(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 // determine if the message can be handled by us
boolean valid = message instanceof UpdateMessage || boolean valid = message instanceof UpdateMessage ||
message instanceof UserUpdateMessage || message instanceof UserUpdateMessage ||
message instanceof ActionLogMessage; message instanceof ActionLogMessage ||
message instanceof CustomMessage;
// instead of throwing an exception here, just return false // instead of throwing an exception here, just return false
// it means an instance of LP can gracefully handle messages it doesn't // 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: case ActionLogMessageImpl.TYPE:
decoded = ActionLogMessageImpl.decode(content, id); decoded = ActionLogMessageImpl.decode(content, id);
break; break;
case CustomMessageImpl.TYPE:
decoded = CustomMessageImpl.decode(content, id);
break;
default: default:
// gracefully return if we just don't recognise the type // gracefully return if we just don't recognise the type
return false; return false;
@ -270,6 +284,12 @@ public class LuckPermsMessagingService implements InternalMessagingService, Inco
this.plugin.getEventDispatcher().dispatchLogReceive(msg.getId(), msg.getAction()); this.plugin.getEventDispatcher().dispatchLogReceive(msg.getId(), msg.getAction());
this.plugin.getLogDispatcher().dispatchFromRemote((LoggedAction) 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 { } else {
throw new IllegalArgumentException("Unknown message type: " + message.getClass().getName()); throw new IllegalArgumentException("Unknown message type: " + message.getClass().getName());
} }

View File

@ -0,0 +1,85 @@
/*
* This file is part of LuckPerms, licensed under the MIT License.
*
* Copyright (c) lucko (Luck) <luck@lucko.me>
* 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()
);
}
}

View File

@ -26,7 +26,6 @@
package me.lucko.luckperms.standalone; package me.lucko.luckperms.standalone;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import me.lucko.luckperms.common.actionlog.LoggedAction; import me.lucko.luckperms.common.actionlog.LoggedAction;
import me.lucko.luckperms.common.messaging.InternalMessagingService; import me.lucko.luckperms.common.messaging.InternalMessagingService;
import me.lucko.luckperms.common.model.User; 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.actionlog.Action;
import net.luckperms.api.event.EventBus; import net.luckperms.api.event.EventBus;
import net.luckperms.api.event.log.LogReceiveEvent; 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.PreNetworkSyncEvent;
import net.luckperms.api.event.sync.SyncType; import net.luckperms.api.event.sync.SyncType;
import net.luckperms.api.model.PlayerSaveResult;
import net.luckperms.api.platform.Health; import net.luckperms.api.platform.Health;
import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Tag;
@ -53,9 +52,7 @@ import java.util.UUID;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit; 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.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
@Testcontainers @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 // send some messages from plugin A to plugin B
messagingServiceA.pushUpdate(); messagingServiceA.pushUpdate();
messagingServiceA.pushUserUpdate(user); messagingServiceA.pushUserUpdate(user);
messagingServiceA.pushLog(exampleLogEntry); messagingServiceA.pushLog(exampleLogEntry);
messagingServiceA.pushCustomPayload("luckperms:test", "hello");
// wait for the messages to be sent/received // wait for the messages to be sent/received
assertTrue(latch1.await(10, TimeUnit.SECONDS)); assertTrue(latch1.await(10, TimeUnit.SECONDS));
assertTrue(latch2.await(10, TimeUnit.SECONDS)); assertTrue(latch2.await(10, TimeUnit.SECONDS));
assertTrue(latch3.await(10, TimeUnit.SECONDS)); assertTrue(latch3.await(10, TimeUnit.SECONDS));
assertTrue(latch4.await(10, TimeUnit.SECONDS));
} }
} }