From 965db127a9ce193a0fb9c2a7e198edc885d8cfb8 Mon Sep 17 00:00:00 2001 From: Owen <23108066+Owen1212055@users.noreply.github.com> Date: Wed, 4 Dec 2024 12:36:45 -0500 Subject: [PATCH] InvocationInfo API (#1467) * Invocation Source API Allows proxies to detect if a command is executed from an unsigned/signed/api source. This is useful because it allows commands executed from the player manually or by clicking on a chat message to be controlled. * Update api significantly to improve api coverage * javadoc * javadoc * Update api/src/main/java/com/velocitypowered/api/event/command/CommandExecuteEvent.java Co-authored-by: powercas_gamer * Update api/src/main/java/com/velocitypowered/api/event/command/CommandExecuteEvent.java Co-authored-by: powercas_gamer * Fix rename --------- Co-authored-by: powercas_gamer --- .../event/command/CommandExecuteEvent.java | 92 +++++++++++++++++++ .../proxy/command/VelocityCommandManager.java | 12 ++- .../protocol/packet/chat/CommandHandler.java | 6 +- .../chat/keyed/KeyedCommandHandler.java | 2 +- .../chat/legacy/LegacyCommandHandler.java | 2 +- .../chat/session/SessionCommandHandler.java | 3 +- .../session/SessionPlayerCommandPacket.java | 5 + .../session/UnsignedPlayerCommandPacket.java | 6 ++ 8 files changed, 120 insertions(+), 8 deletions(-) diff --git a/api/src/main/java/com/velocitypowered/api/event/command/CommandExecuteEvent.java b/api/src/main/java/com/velocitypowered/api/event/command/CommandExecuteEvent.java index 65c9b9016a..7ed704fc0a 100644 --- a/api/src/main/java/com/velocitypowered/api/event/command/CommandExecuteEvent.java +++ b/api/src/main/java/com/velocitypowered/api/event/command/CommandExecuteEvent.java @@ -26,6 +26,7 @@ public final class CommandExecuteEvent implements ResultedEvent { private final CommandSource commandSource; private final String command; private CommandResult result; + private InvocationInfo invocationInfo; /** * Constructs a CommandExecuteEvent. @@ -34,9 +35,21 @@ public final class CommandExecuteEvent implements ResultedEvent { * @param command the command being executed without first slash */ public CommandExecuteEvent(CommandSource commandSource, String command) { + this(commandSource, command, new InvocationInfo(SignedState.UNSUPPORTED, Source.API)); + } + + /** + * Constructs a CommandExecuteEvent. + * + * @param commandSource the source executing the command + * @param command the command being executed without first slash + * @param invocationInfo the invocation info of this command + */ + public CommandExecuteEvent(CommandSource commandSource, String command, InvocationInfo invocationInfo) { this.commandSource = Preconditions.checkNotNull(commandSource, "commandSource"); this.command = Preconditions.checkNotNull(command, "command"); this.result = CommandResult.allowed(); + this.invocationInfo = invocationInfo; } /** @@ -61,6 +74,16 @@ public String getCommand() { return command; } + /** + * Returns the info of the command invocation. + * + * @since 3.4.0 + * @return invocation info + */ + public InvocationInfo getInvocationInfo() { + return this.invocationInfo; + } + @Override public CommandResult getResult() { return result; @@ -80,6 +103,75 @@ public String toString() { + '}'; } + /** + * Represents information about a command invocation, including its signed state and source. + * + * @since 3.4.0 + */ + public record InvocationInfo(SignedState signedState, Source source) { + } + + /** + * Represents the signed state of a command invocation. + * + * @since 3.4.0 + */ + public enum SignedState { + /** + * Indicates that the command was executed from a signed source with signed message arguments, + * This is currently only possible by typing a command in chat with signed arguments. + * + *

Note: Cancelling the {@link CommandExecuteEvent} in this state will result in the player being kicked.

+ * + * @since 3.4.0 + */ + SIGNED_WITH_ARGS, + /** + * Indicates that the command was executed from an signed source with no signed message arguments, + * This is currently only possible by typing a command in chat. + * + * @since 3.4.0 + */ + SIGNED_WITHOUT_ARGS, + /** + * Indicates that the command was executed from an unsigned source, + * such as clicking on a component with a {@link net.kyori.adventure.text.event.ClickEvent.Action#RUN_COMMAND}. + * + *

Clients running version 1.20.5 or later will send this state.

+ * + * @since 3.4.0 + */ + UNSIGNED, + /** + * Indicates that the command invocation does not support signing. + * + *

This state is sent by clients running versions prior to 1.19.3.

+ * + * @since 3.4.0 + */ + UNSUPPORTED + } + + /** + * Represents the source of a command invocation. + * + * @since 3.4.0 + */ + public enum Source { + /** + * Indicates that the command was invoked by a player. + * + * @since 3.4.0 + */ + PLAYER, + /** + * Indicates that the command was invoked programmatically through an API call. + * + * @since 3.4.0 + */ + API + } + /** * Represents the result of the {@link CommandExecuteEvent}. */ diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommandManager.java b/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommandManager.java index f8bb39f078..b4b0e85073 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommandManager.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommandManager.java @@ -218,13 +218,14 @@ public void unregister(CommandMeta meta) { * * @param source the source to execute the command for * @param cmdLine the command to execute + * @param invocationInfo the invocation info * @return the {@link CompletableFuture} of the event */ public CompletableFuture callCommandEvent(final CommandSource source, - final String cmdLine) { + final String cmdLine, final CommandExecuteEvent.InvocationInfo invocationInfo) { Preconditions.checkNotNull(source, "source"); Preconditions.checkNotNull(cmdLine, "cmdLine"); - return eventManager.fire(new CommandExecuteEvent(source, cmdLine)); + return eventManager.fire(new CommandExecuteEvent(source, cmdLine, invocationInfo)); } private boolean executeImmediately0(final CommandSource source, final ParseResults parsed) { @@ -266,7 +267,12 @@ public CompletableFuture executeAsync(final CommandSource source, final Preconditions.checkNotNull(source, "source"); Preconditions.checkNotNull(cmdLine, "cmdLine"); - return callCommandEvent(source, cmdLine).thenComposeAsync(event -> { + CommandExecuteEvent.InvocationInfo invocationInfo = new CommandExecuteEvent.InvocationInfo( + CommandExecuteEvent.SignedState.UNSUPPORTED, + CommandExecuteEvent.Source.API + ); + + return callCommandEvent(source, cmdLine, invocationInfo).thenComposeAsync(event -> { CommandExecuteEvent.CommandResult commandResult = event.getResult(); if (commandResult.isForwardToServer() || !commandResult.isAllowed()) { return CompletableFuture.completedFuture(false); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/CommandHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/CommandHandler.java index 9786fe1453..8e39d78a30 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/CommandHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/CommandHandler.java @@ -56,8 +56,10 @@ default CompletableFuture runCommand(VelocityServer server, default void queueCommandResult(VelocityServer server, ConnectedPlayer player, BiFunction> futurePacketCreator, - String message, Instant timestamp, @Nullable LastSeenMessages lastSeenMessages) { - CompletableFuture eventFuture = server.getCommandManager().callCommandEvent(player, message); + String message, Instant timestamp, @Nullable LastSeenMessages lastSeenMessages, + CommandExecuteEvent.InvocationInfo invocationInfo) { + CompletableFuture eventFuture = server.getCommandManager().callCommandEvent(player, message, + invocationInfo); player.getChatQueue().queuePacket( newLastSeenMessages -> eventFuture .thenComposeAsync(event -> futurePacketCreator.apply(event, newLastSeenMessages)) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/keyed/KeyedCommandHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/keyed/KeyedCommandHandler.java index 1d3751e45b..5baedfb41f 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/keyed/KeyedCommandHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/keyed/KeyedCommandHandler.java @@ -111,6 +111,6 @@ public void handlePlayerCommandInternal(KeyedPlayerCommandPacket packet) { } return null; }); - }, packet.getCommand(), packet.getTimestamp(), null); + }, packet.getCommand(), packet.getTimestamp(), null, new CommandExecuteEvent.InvocationInfo(CommandExecuteEvent.SignedState.UNSUPPORTED, CommandExecuteEvent.Source.PLAYER)); } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/legacy/LegacyCommandHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/legacy/LegacyCommandHandler.java index 7c5a2ec3c4..30ad2c99c4 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/legacy/LegacyCommandHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/legacy/LegacyCommandHandler.java @@ -62,6 +62,6 @@ public void handlePlayerCommandInternal(LegacyChatPacket packet) { } return null; }); - }, command, Instant.now(), null); + }, command, Instant.now(), null, new CommandExecuteEvent.InvocationInfo(CommandExecuteEvent.SignedState.UNSUPPORTED, CommandExecuteEvent.Source.PLAYER)); } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionCommandHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionCommandHandler.java index 0e47feedee..8d1dc0f2f3 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionCommandHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionCommandHandler.java @@ -117,6 +117,7 @@ public void handlePlayerCommandInternal(SessionPlayerCommandPacket packet) { } return forwardCommand(fixedPacket, commandToRun); }); - }, packet.command, packet.timeStamp, packet.lastSeenMessages); + }, packet.command, packet.timeStamp, packet.lastSeenMessages, + new CommandExecuteEvent.InvocationInfo(packet.getEventSignedState(), CommandExecuteEvent.Source.PLAYER)); } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionPlayerCommandPacket.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionPlayerCommandPacket.java index 07374c626e..9084f4b593 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionPlayerCommandPacket.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionPlayerCommandPacket.java @@ -18,6 +18,7 @@ package com.velocitypowered.proxy.protocol.packet.chat.session; import com.google.common.collect.Lists; +import com.velocitypowered.api.event.command.CommandExecuteEvent; import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.proxy.connection.MinecraftSessionHandler; import com.velocitypowered.proxy.protocol.MinecraftPacket; @@ -68,6 +69,10 @@ public boolean isSigned() { return !argumentSignatures.isEmpty(); } + public CommandExecuteEvent.SignedState getEventSignedState() { + return !this.argumentSignatures.isEmpty() ? CommandExecuteEvent.SignedState.SIGNED_WITH_ARGS : CommandExecuteEvent.SignedState.SIGNED_WITHOUT_ARGS; + } + @Override public boolean handle(MinecraftSessionHandler handler) { return handler.handle(this); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/UnsignedPlayerCommandPacket.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/UnsignedPlayerCommandPacket.java index b4e26fe075..cb5ac3c400 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/UnsignedPlayerCommandPacket.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/UnsignedPlayerCommandPacket.java @@ -17,6 +17,7 @@ package com.velocitypowered.proxy.protocol.packet.chat.session; +import com.velocitypowered.api.event.command.CommandExecuteEvent; import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.proxy.protocol.ProtocolUtils; import com.velocitypowered.proxy.protocol.packet.chat.LastSeenMessages; @@ -44,6 +45,11 @@ public boolean isSigned() { return false; } + @Override + public CommandExecuteEvent.SignedState getEventSignedState() { + return CommandExecuteEvent.SignedState.UNSIGNED; + } + @Override public String toString() { return "UnsignedPlayerCommandPacket{" +