Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
<version>master</version>

<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<maven.test.skip>true</maven.test.skip>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
Expand All @@ -15,7 +15,7 @@
<dependency>
<groupId>io.papermc.paper</groupId>
<artifactId>paper-api</artifactId>
<version>1.20.4-R0.1-SNAPSHOT</version>
<version>1.21.8-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
</dependencies>
Expand Down
170 changes: 68 additions & 102 deletions src/main/java/pw/kaboom/commandspy/Main.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
package pw.kaboom.commandspy;

import com.mojang.brigadier.Command;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.brigadier.exceptions.SimpleCommandExceptionType;
import com.mojang.brigadier.tree.LiteralCommandNode;
import io.papermc.paper.command.brigadier.CommandSourceStack;
import io.papermc.paper.command.brigadier.Commands;
import io.papermc.paper.command.brigadier.MessageComponentSerializer;
import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.Bukkit;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
Expand All @@ -14,19 +21,30 @@
import org.bukkit.event.player.PlayerCommandPreprocessEvent;
import org.bukkit.plugin.java.JavaPlugin;
import org.jetbrains.annotations.NotNull;
import pw.kaboom.commandspy.command.PlayerOrUUIDArgumentType;
import pw.kaboom.commandspy.command.StateArgumentType;

import java.io.File;
import java.util.UUID;
import java.util.List;

import static io.papermc.paper.command.brigadier.Commands.*;
import static pw.kaboom.commandspy.command.PlayerOrUUIDArgumentType.getPlayer;
import static pw.kaboom.commandspy.command.StateArgumentType.getState;

public final class Main extends JavaPlugin implements Listener {
public static final SimpleCommandExceptionType ERROR_NOT_PLAYER =
new SimpleCommandExceptionType(MessageComponentSerializer.message()
.serialize(Component.translatable("permissions.requires.player")));

public final class Main extends JavaPlugin implements CommandExecutor, Listener {
private CommandSpyState config;

@Override
public void onEnable() {
this.config = new CommandSpyState(new File(this.getDataFolder(), "state.bin"));

//noinspection DataFlowIssue
this.getCommand("commandspy").setExecutor(this);

this.getLifecycleManager().registerEventHandler(LifecycleEvents.COMMANDS,
event -> this.registerCommands(event.registrar()));
this.getServer().getPluginManager().registerEvents(this, this);

// Save the state every 30 seconds
Expand All @@ -38,12 +56,50 @@ public void onDisable() {
this.config.trySave();
}

private void updateCommandSpyState(final @NotNull Player target,
final @NotNull CommandSender source, final boolean state) {
private void registerCommands(final Commands registrar) {
final LiteralCommandNode<CommandSourceStack> commandSpyCommand =
literal("commandspy")
.requires(
Commands.restricted(
ctx -> ctx.getSender().hasPermission("commandspy.command")
)
)
.then(
argument("state", new StateArgumentType())
.executes(ctx -> updateState(ctx, null, getState(ctx, "state")))
)
.then(
argument("target", new PlayerOrUUIDArgumentType())
.then(
argument("state", new StateArgumentType())
.executes(
ctx -> updateState(
ctx, getPlayer(ctx, "target"), getState(ctx, "state")
)
)
)
.executes(ctx -> updateState(ctx, getPlayer(ctx, "target"), null))
)
.executes(ctx -> updateState(ctx, null, null))
.build();

registrar.register(commandSpyCommand,
"Allows you to spy on players' commands", List.of("c", "cs", "cspy"));
}

private int updateState(final @NotNull CommandContext<CommandSourceStack> ctx,
Player target, Boolean state) throws CommandSyntaxException {
final CommandSender source = ctx.getSource().getSender();
if (target == null) {
if (!(source instanceof final Player player)) throw ERROR_NOT_PLAYER.create();
target = player;
}

if (state == null) state = !this.config.getCommandSpyState(target.getUniqueId());

this.config.setCommandSpyState(target.getUniqueId(), state);

final Component stateString = Component.text(state ? "enabled" : "disabled");

target.sendMessage(Component.empty()
.append(Component.text("Successfully "))
.append(stateString)
Expand All @@ -54,9 +110,10 @@ private void updateCommandSpyState(final @NotNull Player target,
.append(Component.text("Successfully "))
.append(stateString)
.append(Component.text(" CommandSpy for "))
.append(target.name())
);
.append(target.name()));
}

return Command.SINGLE_SUCCESS;
}

private NamedTextColor getTextColor(final Player player) {
Expand All @@ -67,67 +124,6 @@ private NamedTextColor getTextColor(final Player player) {
return NamedTextColor.AQUA;
}

@Override
public boolean onCommand(final @NotNull CommandSender sender, final @NotNull Command cmd,
final @NotNull String label, final String[] args) {

Player target = null;
Boolean state = null;

switch (args.length) {
case 0 -> {
}
case 1, 2 -> {
// Get the last argument as a state. Fail if we have 2 arguments.
state = getState(args[args.length - 1]);
if (state != null && args.length == 1) {
break;
} else if (state == null && args.length == 2) {
sender.sendMessage(Component.text("Usage: ", NamedTextColor.RED)
.append(Component.text(cmd.getUsage().replace("<command>", label)))
);
return true;
}

// Get the first argument as a player. Fail if it can't be found.
target = getPlayer(args[0]);
if (target != null) {
break;
}

sender.sendMessage(Component.empty()
.append(Component.text("Player \""))
.append(Component.text(args[0]))
.append(Component.text("\" not found"))
);
return true;
}
default -> {
sender.sendMessage(Component.text("Usage: ", NamedTextColor.RED)
.append(Component.text(cmd.getUsage().replace("<command>", label)))
);
return true;
}
}

if (target == null) {
if (!(sender instanceof final Player player)) {
sender.sendMessage(Component.text("Command has to be run by a player"));
return true;
}

target = player;
}

if (state == null) {
state = !this.config.getCommandSpyState(target.getUniqueId());
}

this.updateCommandSpyState(target, sender, state);

return true;
}

@EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
void onPlayerCommandPreprocess(final PlayerCommandPreprocessEvent event) {
final Player player = event.getPlayer();
Expand Down Expand Up @@ -155,34 +151,4 @@ void onSignChange(final SignChangeEvent event) {

this.config.broadcastSpyMessage(message);
}

private static Player getPlayer(final String arg) {
final Player player = Bukkit.getPlayer(arg);
if (player != null) {
return player;
}

final UUID uuid;
try {
uuid = UUID.fromString(arg);
} catch (final IllegalArgumentException ignored) {
return null;
}

return Bukkit.getPlayer(uuid);
}

private static Boolean getState(final String arg) {
switch (arg) {
case "on", "enable" -> {
return true;
}
case "off", "disable" -> {
return false;
}
default -> {
return null;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package pw.kaboom.commandspy.command;

import com.mojang.brigadier.StringReader;
import com.mojang.brigadier.arguments.ArgumentType;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import io.papermc.paper.command.brigadier.CommandSourceStack;
import io.papermc.paper.command.brigadier.argument.ArgumentTypes;
import io.papermc.paper.command.brigadier.argument.CustomArgumentType;
import io.papermc.paper.command.brigadier.argument.resolvers.selector.EntitySelectorArgumentResolver;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;

import java.util.UUID;

public final class PlayerOrUUIDArgumentType implements
CustomArgumentType<@NotNull Player, @NotNull EntitySelectorArgumentResolver> {

// We lie to clients and tell them we're an entity selector so that UUIDs don't show up as red.
@Override
public @NotNull ArgumentType<EntitySelectorArgumentResolver> getNativeType() {
return ArgumentTypes.entity();
}

@Override
public Player parse(final @NotNull StringReader reader) {
throw new IllegalStateException("method should never be called as we implement override");
}

@Override
public <S> Player parse(final @NotNull StringReader reader, final S source)
throws CommandSyntaxException {
if (!(source instanceof final CommandSourceStack stack))
throw new IllegalStateException("source was not a CommandSourceStack");

final int cursor = reader.getCursor();
final String string = reader.readString();

try {
final UUID uuid = UUID.fromString(string);
final Player player = Bukkit.getPlayer(uuid);

if (player != null) return player;
} catch (final IllegalArgumentException ignored) {
}

reader.setCursor(cursor);
return ArgumentTypes.player().parse(reader).resolve(stack).getFirst();
}

public static Player getPlayer(final CommandContext<CommandSourceStack> context,
final String name) {
return context.getArgument(name, Player.class);
}
}
67 changes: 67 additions & 0 deletions src/main/java/pw/kaboom/commandspy/command/StateArgumentType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package pw.kaboom.commandspy.command;

import com.mojang.brigadier.arguments.ArgumentType;
import com.mojang.brigadier.arguments.StringArgumentType;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.brigadier.exceptions.DynamicCommandExceptionType;
import com.mojang.brigadier.suggestion.Suggestions;
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
import io.papermc.paper.command.brigadier.CommandSourceStack;
import io.papermc.paper.command.brigadier.MessageComponentSerializer;
import io.papermc.paper.command.brigadier.argument.CustomArgumentType;
import net.kyori.adventure.text.Component;
import org.jetbrains.annotations.NotNull;

import java.util.Arrays;
import java.util.Collection;
import java.util.concurrent.CompletableFuture;

public final class StateArgumentType implements
CustomArgumentType.Converted<@NotNull Boolean, @NotNull String> {

private static final Collection<String> VALUES = Arrays.asList("on", "enable",
"off", "disable");

private static final DynamicCommandExceptionType ERROR_INVALID_VALUE =
new DynamicCommandExceptionType(o ->
MessageComponentSerializer.message()
.serialize(Component.translatable("argument.enum.invalid")
.arguments(Component.text(o.toString()))));

@Override
public @NotNull ArgumentType<String> getNativeType() {
return StringArgumentType.word();
}

@Override
public Boolean convert(final String s) throws CommandSyntaxException {
switch (s) {
case "on", "enable" -> {
return true;
}
case "off", "disable" -> {
return false;
}

default -> throw ERROR_INVALID_VALUE.create(s);
}
}

@Override
public <S> @NotNull CompletableFuture<Suggestions> listSuggestions(
final @NotNull CommandContext<S> context, final @NotNull SuggestionsBuilder builder) {
for (final String value: VALUES) {
if (!value.startsWith(builder.getRemainingLowerCase())) continue;

builder.suggest(value);
}

return builder.buildFuture();
}

public static boolean getState(final CommandContext<CommandSourceStack> context,
final String name) {
return context.getArgument(name, Boolean.class);
}
}
9 changes: 1 addition & 8 deletions src/main/resources/plugin.yml
Original file line number Diff line number Diff line change
@@ -1,13 +1,6 @@
name: CommandSpy
main: pw.kaboom.commandspy.Main
description: Plugin that allows you to spy on players' commands.
api-version: '1.20'
api-version: '1.21'
version: master
folia-supported: true

commands:
commandspy:
aliases: [ c, cs, cspy ]
description: Allows you to spy on players' commands
permission: commandspy.command
usage: '/<command> [player] [on|enable|off|disable]'