mirror of
https://github.com/kaboomserver/commandspy.git
synced 2025-05-21 07:46:10 +00:00
Merge pull request #11 from amyavi/cleanups
This commit is contained in:
commit
fac2dfe8f2
8 changed files with 282 additions and 75 deletions
8
.github/workflows/main.yml
vendored
8
.github/workflows/main.yml
vendored
|
@ -1,10 +1,5 @@
|
||||||
name: Maven CI
|
name: Maven CI
|
||||||
|
on: [push, pull_request]
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ master ]
|
|
||||||
pull_request:
|
|
||||||
branches: [ master ]
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
@ -31,3 +26,4 @@ jobs:
|
||||||
with:
|
with:
|
||||||
name: CommandSpy
|
name: CommandSpy
|
||||||
path: target/CommandSpy.jar
|
path: target/CommandSpy.jar
|
||||||
|
compression-level: 0
|
||||||
|
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -6,3 +6,5 @@ target/
|
||||||
.classpath
|
.classpath
|
||||||
.project
|
.project
|
||||||
*.iml
|
*.iml
|
||||||
|
.theia/
|
||||||
|
run/
|
|
@ -7,8 +7,8 @@ The plugin is created for the Kaboom server.
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
| Command | Alias | Permission | Description |
|
| Command | Alias | Permission | Description |
|
||||||
| ------- | ----- | ---------- | ----------- |
|
|-------------|----------------|--------------------|----------------------------------------|
|
||||||
|/commandspy | /c, /cs, /cspy | commandspy.command | Allows you to spy on players' commands|
|
| /commandspy | /c, /cs, /cspy | commandspy.command | Allows you to spy on players' commands |
|
||||||
|
|
||||||
## Compiling
|
## Compiling
|
||||||
|
|
||||||
|
|
16
pom.xml
16
pom.xml
|
@ -15,7 +15,7 @@
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.papermc.paper</groupId>
|
<groupId>io.papermc.paper</groupId>
|
||||||
<artifactId>paper-api</artifactId>
|
<artifactId>paper-api</artifactId>
|
||||||
<version>1.18.2-R0.1-SNAPSHOT</version>
|
<version>1.20.4-R0.1-SNAPSHOT</version>
|
||||||
<scope>provided</scope>
|
<scope>provided</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
@ -30,10 +30,22 @@
|
||||||
<build>
|
<build>
|
||||||
<finalName>${project.artifactId}</finalName>
|
<finalName>${project.artifactId}</finalName>
|
||||||
<plugins>
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-jar-plugin</artifactId>
|
||||||
|
<version>3.4.2</version>
|
||||||
|
<configuration>
|
||||||
|
<archive>
|
||||||
|
<manifestEntries>
|
||||||
|
<paperweight-mappings-namespace>mojang</paperweight-mappings-namespace>
|
||||||
|
</manifestEntries>
|
||||||
|
</archive>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
<artifactId>maven-checkstyle-plugin</artifactId>
|
<artifactId>maven-checkstyle-plugin</artifactId>
|
||||||
<version>3.1.2</version>
|
<version>3.6.0</version>
|
||||||
<executions>
|
<executions>
|
||||||
<execution>
|
<execution>
|
||||||
<id>checkstyle</id>
|
<id>checkstyle</id>
|
||||||
|
|
127
src/main/java/pw/kaboom/commandspy/CommandSpyState.java
Normal file
127
src/main/java/pw/kaboom/commandspy/CommandSpyState.java
Normal file
|
@ -0,0 +1,127 @@
|
||||||
|
package pw.kaboom.commandspy;
|
||||||
|
|
||||||
|
import com.google.common.io.Files;
|
||||||
|
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
|
||||||
|
import net.kyori.adventure.text.Component;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.plugin.java.JavaPlugin;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.concurrent.locks.StampedLock;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
public final class CommandSpyState {
|
||||||
|
private static final Logger LOGGER = JavaPlugin.getPlugin(Main.class).getSLF4JLogger();
|
||||||
|
|
||||||
|
private final ObjectOpenHashSet<UUID> users = new ObjectOpenHashSet<>();
|
||||||
|
private final StampedLock usersLock = new StampedLock();
|
||||||
|
private final AtomicBoolean dirty = new AtomicBoolean();
|
||||||
|
private final File file;
|
||||||
|
|
||||||
|
public CommandSpyState(final @NotNull File file) {
|
||||||
|
this.file = file;
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.load();
|
||||||
|
} catch (final FileNotFoundException exception) {
|
||||||
|
try {
|
||||||
|
this.save(); // Create file if it doesn't exist
|
||||||
|
} catch (IOException ignored) {
|
||||||
|
}
|
||||||
|
} catch (final IOException exception) {
|
||||||
|
LOGGER.error("Failed to load state file:", exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void load() throws IOException {
|
||||||
|
final InputStream reader = new BufferedInputStream(new FileInputStream(this.file));
|
||||||
|
|
||||||
|
int read;
|
||||||
|
final ByteBuffer buffer = ByteBuffer.wrap(new byte[16]);
|
||||||
|
|
||||||
|
// Loop until we read less than 16 bytes
|
||||||
|
while ((read = reader.readNBytes(buffer.array(), 0, 16)) == 16) {
|
||||||
|
this.users.add(new UUID(buffer.getLong(0), buffer.getLong(8)));
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.close();
|
||||||
|
if (read != 0) {
|
||||||
|
throw new IOException("Found " + read + " bytes extra whilst reading file");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void save() throws IOException {
|
||||||
|
Files.createParentDirs(this.file);
|
||||||
|
final OutputStream writer = new BufferedOutputStream(new FileOutputStream(this.file));
|
||||||
|
final ByteBuffer buffer = ByteBuffer.wrap(new byte[16]);
|
||||||
|
|
||||||
|
final long stamp = this.usersLock.readLock();
|
||||||
|
for (final UUID uuid : this.users) {
|
||||||
|
buffer.putLong(0, uuid.getMostSignificantBits());
|
||||||
|
buffer.putLong(8, uuid.getLeastSignificantBits());
|
||||||
|
writer.write(buffer.array());
|
||||||
|
}
|
||||||
|
this.usersLock.unlockRead(stamp);
|
||||||
|
|
||||||
|
writer.flush();
|
||||||
|
writer.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void trySave() {
|
||||||
|
// If the state is not dirty, then we don't need to do anything.
|
||||||
|
if (!this.dirty.compareAndExchange(true, false)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.save();
|
||||||
|
} catch (final IOException exception) {
|
||||||
|
LOGGER.error("Failed to save state file:", exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean getCommandSpyState(final @NotNull UUID playerUUID) {
|
||||||
|
final long stamp = this.usersLock.readLock();
|
||||||
|
final boolean result = this.users.contains(playerUUID);
|
||||||
|
this.usersLock.unlockRead(stamp);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCommandSpyState(final @NotNull UUID playerUUID, final boolean state) {
|
||||||
|
final long stamp = this.usersLock.writeLock();
|
||||||
|
|
||||||
|
final boolean dirty;
|
||||||
|
if (state) {
|
||||||
|
dirty = this.users.add(playerUUID);
|
||||||
|
} else {
|
||||||
|
dirty = this.users.remove(playerUUID);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.usersLock.unlockWrite(stamp);
|
||||||
|
if (dirty) {
|
||||||
|
this.dirty.set(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void broadcastSpyMessage(final @NotNull Component message) {
|
||||||
|
// Raw access here, so we can get more performance by not locking/unlocking over and over
|
||||||
|
final long stamp = this.usersLock.readLock();
|
||||||
|
final Collection<Player> players = Bukkit.getOnlinePlayers()
|
||||||
|
.stream()
|
||||||
|
.filter(p -> this.users.contains(p.getUniqueId()))
|
||||||
|
.collect(Collectors.toUnmodifiableSet());
|
||||||
|
this.usersLock.unlockRead(stamp);
|
||||||
|
|
||||||
|
for (final Player recipient : players) {
|
||||||
|
recipient.sendMessage(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,120 +1,188 @@
|
||||||
package pw.kaboom.commandspy;
|
package pw.kaboom.commandspy;
|
||||||
|
|
||||||
import java.util.Set;
|
import net.kyori.adventure.text.Component;
|
||||||
import java.util.UUID;
|
import net.kyori.adventure.text.format.NamedTextColor;
|
||||||
|
|
||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
import org.bukkit.command.Command;
|
import org.bukkit.command.Command;
|
||||||
import org.bukkit.command.CommandExecutor;
|
import org.bukkit.command.CommandExecutor;
|
||||||
import org.bukkit.command.CommandSender;
|
import org.bukkit.command.CommandSender;
|
||||||
import org.bukkit.command.ConsoleCommandSender;
|
|
||||||
import org.bukkit.configuration.file.FileConfiguration;
|
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
import org.bukkit.event.EventHandler;
|
import org.bukkit.event.EventHandler;
|
||||||
|
import org.bukkit.event.EventPriority;
|
||||||
import org.bukkit.event.Listener;
|
import org.bukkit.event.Listener;
|
||||||
import org.bukkit.event.block.SignChangeEvent;
|
import org.bukkit.event.block.SignChangeEvent;
|
||||||
import org.bukkit.event.player.PlayerCommandPreprocessEvent;
|
import org.bukkit.event.player.PlayerCommandPreprocessEvent;
|
||||||
import org.bukkit.plugin.Plugin;
|
|
||||||
import org.bukkit.plugin.java.JavaPlugin;
|
import org.bukkit.plugin.java.JavaPlugin;
|
||||||
import net.kyori.adventure.text.Component;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import net.kyori.adventure.text.format.NamedTextColor;
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
public final class Main extends JavaPlugin implements CommandExecutor, Listener {
|
public final class Main extends JavaPlugin implements CommandExecutor, Listener {
|
||||||
private FileConfiguration config;
|
private CommandSpyState config;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onEnable() {
|
public void onEnable() {
|
||||||
config = getConfig();
|
this.config = new CommandSpyState(new File(this.getDataFolder(), "state.bin"));
|
||||||
|
|
||||||
|
//noinspection DataFlowIssue
|
||||||
this.getCommand("commandspy").setExecutor(this);
|
this.getCommand("commandspy").setExecutor(this);
|
||||||
this.getServer().getPluginManager().registerEvents(this, this);
|
this.getServer().getPluginManager().registerEvents(this, this);
|
||||||
|
|
||||||
|
// Save the state every 30 seconds
|
||||||
|
Bukkit.getScheduler().runTaskTimerAsynchronously(this, this.config::trySave, 600L, 600L);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void enableCommandSpy(final Player player) {
|
@Override
|
||||||
config.set(player.getUniqueId().toString(), true);
|
public void onDisable() {
|
||||||
saveConfig();
|
this.config.trySave();
|
||||||
player.sendMessage(Component.text("Successfully enabled CommandSpy"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void disableCommandSpy(final Player player) {
|
private void updateCommandSpyState(final @NotNull Player target,
|
||||||
config.set(player.getUniqueId().toString(), null);
|
final @NotNull CommandSender source, final boolean state) {
|
||||||
saveConfig();
|
this.config.setCommandSpyState(target.getUniqueId(), state);
|
||||||
player.sendMessage(Component.text("Successfully disabled CommandSpy"));
|
|
||||||
|
final Component stateString = Component.text(state ? "enabled" : "disabled");
|
||||||
|
|
||||||
|
target.sendMessage(Component.empty()
|
||||||
|
.append(Component.text("Successfully "))
|
||||||
|
.append(stateString)
|
||||||
|
.append(Component.text(" CommandSpy")));
|
||||||
|
|
||||||
|
if (source != target) {
|
||||||
|
source.sendMessage(Component.empty()
|
||||||
|
.append(Component.text("Successfully "))
|
||||||
|
.append(stateString)
|
||||||
|
.append(Component.text(" CommandSpy for "))
|
||||||
|
.append(target.name())
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private NamedTextColor getTextColor(final Player player) {
|
private NamedTextColor getTextColor(final Player player) {
|
||||||
if (config.contains(player.getUniqueId().toString())) {
|
if (this.config.getCommandSpyState(player.getUniqueId())) {
|
||||||
return NamedTextColor.YELLOW;
|
return NamedTextColor.YELLOW;
|
||||||
}
|
}
|
||||||
|
|
||||||
return NamedTextColor.AQUA;
|
return NamedTextColor.AQUA;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onCommand(final CommandSender sender, final Command cmd, final String label,
|
public boolean onCommand(final @NotNull CommandSender sender, final @NotNull Command cmd,
|
||||||
final String[] args) {
|
final @NotNull String label, final String[] args) {
|
||||||
if (sender instanceof ConsoleCommandSender) {
|
|
||||||
|
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"));
|
sender.sendMessage(Component.text("Command has to be run by a player"));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
final Player player = (Player) sender;
|
target = player;
|
||||||
|
}
|
||||||
|
|
||||||
if (args.length >= 1 && "on".equalsIgnoreCase(args[0])) {
|
if (state == null) {
|
||||||
enableCommandSpy(player);
|
state = !this.config.getCommandSpyState(target.getUniqueId());
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
if ((args.length >= 1 && "off".equalsIgnoreCase(args[0]))
|
|
||||||
|| config.contains(player.getUniqueId().toString())) {
|
this.updateCommandSpyState(target, sender, state);
|
||||||
disableCommandSpy(player);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
enableCommandSpy(player);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@EventHandler
|
@EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
|
||||||
void onPlayerCommandPreprocess(final PlayerCommandPreprocessEvent event) {
|
void onPlayerCommandPreprocess(final PlayerCommandPreprocessEvent event) {
|
||||||
if (event.isCancelled()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final Player player = event.getPlayer();
|
final Player player = event.getPlayer();
|
||||||
final NamedTextColor color = getTextColor(player);
|
final NamedTextColor color = getTextColor(player);
|
||||||
|
|
||||||
final Component message = Component.text(player.getName(), color)
|
final Component message = Component.text(player.getName(), color)
|
||||||
.append(Component.text(": "))
|
.append(Component.text(": "))
|
||||||
.append(Component.text(event.getMessage()));
|
.append(Component.text(event.getMessage()));
|
||||||
|
|
||||||
for (String uuidString : config.getKeys(false)) {
|
this.config.broadcastSpyMessage(message);
|
||||||
final UUID uuid = UUID.fromString(uuidString);
|
|
||||||
final Player recipient = Bukkit.getPlayer(uuid);
|
|
||||||
|
|
||||||
if (recipient == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
recipient.sendMessage(message);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@EventHandler
|
@EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
|
||||||
void onSignChange(final SignChangeEvent event) {
|
void onSignChange(final SignChangeEvent event) {
|
||||||
final Player player = event.getPlayer();
|
final Player player = event.getPlayer();
|
||||||
final NamedTextColor color = getTextColor(player);
|
final NamedTextColor color = getTextColor(player);
|
||||||
Component message = Component.text(player.getName(), color)
|
Component message = Component.text(player.getName(), color)
|
||||||
.append(Component.text(" created a sign with contents:"));
|
.append(Component.text(" created a sign with contents:"));
|
||||||
|
|
||||||
for (Component line : event.lines()) {
|
for (final Component line : event.lines()) {
|
||||||
message = message
|
message = message
|
||||||
.append(Component.text("\n "))
|
.append(Component.text("\n "))
|
||||||
.append(line);
|
.append(line);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (String uuidString : config.getKeys(false)) {
|
this.config.broadcastSpyMessage(message);
|
||||||
final UUID uuid = UUID.fromString(uuidString);
|
}
|
||||||
final Player recipient = Bukkit.getPlayer(uuid);
|
|
||||||
|
private static Player getPlayer(final String arg) {
|
||||||
if (recipient == null) {
|
final Player player = Bukkit.getPlayer(arg);
|
||||||
continue;
|
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;
|
||||||
}
|
}
|
||||||
recipient.sendMessage(message);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
name: CommandSpy
|
name: CommandSpy
|
||||||
main: pw.kaboom.commandspy.Main
|
main: pw.kaboom.commandspy.Main
|
||||||
description: Plugin that allows you to spy on players' commands.
|
description: Plugin that allows you to spy on players' commands.
|
||||||
api-version: 1.13
|
api-version: '1.20'
|
||||||
version: master
|
version: master
|
||||||
folia-supported: true
|
folia-supported: true
|
||||||
|
|
||||||
commands:
|
commands:
|
||||||
commandspy:
|
commandspy:
|
||||||
aliases: [c,cs,cspy]
|
aliases: [ c, cs, cspy ]
|
||||||
description: Allows you to spy on players' commands
|
description: Allows you to spy on players' commands
|
||||||
usage: /commandspy
|
|
||||||
permission: commandspy.command
|
permission: commandspy.command
|
||||||
|
usage: '/<command> [player] [on|enable|off|disable]'
|
||||||
|
|
|
@ -5,4 +5,6 @@
|
||||||
|
|
||||||
<suppressions>
|
<suppressions>
|
||||||
<suppress checks="Javadoc" files="."/>
|
<suppress checks="Javadoc" files="."/>
|
||||||
|
<suppress checks="MagicNumber" files="."/>
|
||||||
|
<suppress checks="MethodLength" files="."/>
|
||||||
</suppressions>
|
</suppressions>
|
Loading…
Reference in a new issue