├── .gitignore ├── src └── main │ ├── java │ └── co │ │ └── melondev │ │ └── Snitch │ │ ├── util │ │ ├── Previewable.java │ │ ├── SnitchDatabaseException.java │ │ ├── AdjustedBlock.java │ │ ├── InvUtil.java │ │ ├── MsgUtil.java │ │ ├── Config.java │ │ ├── TimeUtil.java │ │ ├── BlockUtil.java │ │ ├── EntityUtil.java │ │ ├── JsonUtil.java │ │ └── Reflection.java │ │ ├── enums │ │ ├── EnumSnitchActivity.java │ │ ├── EnumDefaultPlayer.java │ │ ├── EnumActionVariables.java │ │ ├── EnumAction.java │ │ └── EnumParam.java │ │ ├── entities │ │ ├── SnitchCallback.java │ │ ├── SnitchProcessHandler.java │ │ ├── SnitchWorld.java │ │ ├── SnitchPlayer.java │ │ ├── SnitchPosition.java │ │ ├── SnitchRestore.java │ │ ├── SnitchRollback.java │ │ ├── SnitchResult.java │ │ ├── SnitchSession.java │ │ ├── SnitchEntry.java │ │ └── SnitchPreview.java │ │ ├── handlers │ │ ├── NoCapabilityHandler.java │ │ ├── ArmorStandEditHandler.java │ │ ├── EntityDeathHandler.java │ │ ├── CakeHandler.java │ │ ├── BlockCreationHandler.java │ │ ├── SignChangeHandler.java │ │ ├── BlockDestructionHandler.java │ │ ├── BlockSpreadHandler.java │ │ ├── ItemTakeHandler.java │ │ └── ItemInsertHandler.java │ │ ├── storage │ │ └── StorageMethod.java │ │ ├── managers │ │ └── PlayerManager.java │ │ ├── listeners │ │ ├── ChatListener.java │ │ ├── ConnectionListener.java │ │ └── InventoryListener.java │ │ ├── commands │ │ └── SnitchCommand.java │ │ └── SnitchPlugin.java │ └── resources │ ├── plugin.yml │ └── config.yml ├── pom.xml └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea/ 3 | *.iml 4 | target/ 5 | dependency-reduced-pom.xml 6 | -------------------------------------------------------------------------------- /src/main/java/co/melondev/Snitch/util/Previewable.java: -------------------------------------------------------------------------------- 1 | package co.melondev.Snitch.util; 2 | 3 | /** 4 | * Created by Devon on 7/13/18. 5 | */ 6 | public interface Previewable { 7 | 8 | void cancelPreview(); 9 | 10 | void applyPreview(); 11 | 12 | void apply(); 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/main/resources/plugin.yml: -------------------------------------------------------------------------------- 1 | name: Snitch 2 | author: Melon Development, Inc. 3 | version: 1.0-ALPHA 4 | main: co.melondev.Snitch.SnitchPlugin 5 | description: Block logging and rollback done quickly and intuitively. 6 | commands: 7 | snitch: 8 | usage: /snitch 9 | description: Access primary plugin controls. 10 | aliases: [sn, s] -------------------------------------------------------------------------------- /src/main/java/co/melondev/Snitch/enums/EnumSnitchActivity.java: -------------------------------------------------------------------------------- 1 | package co.melondev.Snitch.enums; 2 | 3 | /** 4 | * Created by Devon on 7/14/18. 5 | * 6 | * The different activities that modify the world or the player view within Snitch 7 | */ 8 | public enum EnumSnitchActivity { 9 | 10 | ROLLBACK, 11 | RESTORE, 12 | PREVIEW 13 | 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/co/melondev/Snitch/entities/SnitchCallback.java: -------------------------------------------------------------------------------- 1 | package co.melondev.Snitch.entities; 2 | 3 | import org.bukkit.entity.Player; 4 | 5 | /** 6 | * Created by Devon on 7/14/18. 7 | * 8 | * Used to return data to the user after a completed activity. 9 | */ 10 | public interface SnitchCallback { 11 | 12 | void handle(Player player, SnitchResult result); 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/main/resources/config.yml: -------------------------------------------------------------------------------- 1 | meta: 2 | version: 1.0 3 | data: 4 | method: mysql 5 | mysql: 6 | host: localhost 7 | port: 3306 8 | username: minecraft 9 | password: '' 10 | database: minecraft 11 | prefix: 'snitch_' 12 | autoclean: 13 | enable: true 14 | actions: 15 | - 'before 365d' 16 | - 'actions flow before 7d' 17 | defaults: 18 | area: 20 19 | time: 30m 20 | tools: 21 | wand: GOLD_PICKAXE 22 | wand-block: BEDROCK 23 | disabled-logging: 24 | - 'XP_PICKUP' -------------------------------------------------------------------------------- /src/main/java/co/melondev/Snitch/entities/SnitchProcessHandler.java: -------------------------------------------------------------------------------- 1 | package co.melondev.Snitch.entities; 2 | 3 | import co.melondev.Snitch.enums.EnumSnitchActivity; 4 | 5 | /** 6 | * Created by Devon on 7/14/18. 7 | * 8 | * Implemented by actions to determine how to handle rollback, restores, and previews - as well as what activities they support. 9 | */ 10 | public interface SnitchProcessHandler { 11 | 12 | boolean handleRollback(SnitchSession session, SnitchEntry entry); 13 | 14 | boolean handlePreview(SnitchSession session, SnitchEntry entry); 15 | 16 | boolean handleRestore(SnitchSession session, SnitchEntry entry); 17 | 18 | boolean can(EnumSnitchActivity activity); 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/co/melondev/Snitch/util/SnitchDatabaseException.java: -------------------------------------------------------------------------------- 1 | package co.melondev.Snitch.util; 2 | 3 | /** 4 | * Created by Devon on 8/6/18. 5 | */ 6 | public class SnitchDatabaseException extends Exception { 7 | 8 | public SnitchDatabaseException() { 9 | } 10 | 11 | public SnitchDatabaseException(String message) { 12 | super(message); 13 | } 14 | 15 | public SnitchDatabaseException(String message, Throwable cause) { 16 | super(message, cause); 17 | } 18 | 19 | public SnitchDatabaseException(Throwable cause) { 20 | super(cause); 21 | } 22 | 23 | public SnitchDatabaseException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { 24 | super(message, cause, enableSuppression, writableStackTrace); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/co/melondev/Snitch/util/AdjustedBlock.java: -------------------------------------------------------------------------------- 1 | package co.melondev.Snitch.util; 2 | 3 | import org.bukkit.block.BlockState; 4 | 5 | /** 6 | * Created by Devon on 7/14/18. 7 | * 8 | * Stores blockstates for the old and new versions of a block 9 | */ 10 | public class AdjustedBlock { 11 | 12 | /** 13 | * The old block 14 | */ 15 | private BlockState oldState; 16 | 17 | /** 18 | * The new block 19 | */ 20 | private BlockState newState; 21 | 22 | public AdjustedBlock(BlockState oldState, BlockState newState) { 23 | this.oldState = oldState; 24 | this.newState = newState; 25 | } 26 | 27 | public BlockState getOldState() { 28 | return oldState; 29 | } 30 | 31 | public BlockState getNewState() { 32 | return newState; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/co/melondev/Snitch/entities/SnitchWorld.java: -------------------------------------------------------------------------------- 1 | package co.melondev.Snitch.entities; 2 | 3 | import org.bukkit.Bukkit; 4 | import org.bukkit.World; 5 | 6 | /** 7 | * Represents a world within Snitch 8 | */ 9 | public class SnitchWorld { 10 | 11 | /** 12 | * The ID of the world (decided by database) 13 | */ 14 | private int id; 15 | 16 | /** 17 | * The name of the world 18 | */ 19 | private String worldName; 20 | 21 | public SnitchWorld(int id, String worldName) { 22 | this.id = id; 23 | this.worldName = worldName; 24 | } 25 | 26 | /** 27 | * @return a {@link World} from this SnitchWorld 28 | */ 29 | public World getBukkitWorld(){ 30 | return Bukkit.getWorld(getWorldName()); 31 | } 32 | 33 | public int getId() { 34 | return id; 35 | } 36 | 37 | public String getWorldName() { 38 | return worldName; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/co/melondev/Snitch/handlers/NoCapabilityHandler.java: -------------------------------------------------------------------------------- 1 | package co.melondev.Snitch.handlers; 2 | 3 | import co.melondev.Snitch.entities.SnitchEntry; 4 | import co.melondev.Snitch.entities.SnitchProcessHandler; 5 | import co.melondev.Snitch.entities.SnitchSession; 6 | import co.melondev.Snitch.enums.EnumSnitchActivity; 7 | 8 | /** 9 | * Created by Devon on 7/14/18. 10 | */ 11 | public class NoCapabilityHandler implements SnitchProcessHandler { 12 | @Override 13 | public boolean handleRollback(SnitchSession session, SnitchEntry entry) { 14 | return false; 15 | } 16 | 17 | @Override 18 | public boolean handlePreview(SnitchSession session, SnitchEntry entry) { 19 | return false; 20 | } 21 | 22 | @Override 23 | public boolean handleRestore(SnitchSession session, SnitchEntry entry) { 24 | return false; 25 | } 26 | 27 | @Override 28 | public boolean can(EnumSnitchActivity activity) { 29 | return false; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/co/melondev/Snitch/handlers/ArmorStandEditHandler.java: -------------------------------------------------------------------------------- 1 | package co.melondev.Snitch.handlers; 2 | 3 | import co.melondev.Snitch.entities.SnitchEntry; 4 | import co.melondev.Snitch.entities.SnitchProcessHandler; 5 | import co.melondev.Snitch.entities.SnitchSession; 6 | import co.melondev.Snitch.enums.EnumSnitchActivity; 7 | 8 | /** 9 | * Created by Devon on 7/16/18. 10 | */ 11 | public class ArmorStandEditHandler implements SnitchProcessHandler { 12 | @Override 13 | public boolean handleRollback(SnitchSession session, SnitchEntry entry) { 14 | 15 | return false; 16 | } 17 | 18 | @Override 19 | public boolean handlePreview(SnitchSession session, SnitchEntry entry) { 20 | return false; 21 | } 22 | 23 | @Override 24 | public boolean handleRestore(SnitchSession session, SnitchEntry entry) { 25 | return false; 26 | } 27 | 28 | @Override 29 | public boolean can(EnumSnitchActivity activity) { 30 | return false; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/co/melondev/Snitch/entities/SnitchPlayer.java: -------------------------------------------------------------------------------- 1 | package co.melondev.Snitch.entities; 2 | 3 | import java.sql.ResultSet; 4 | import java.sql.SQLException; 5 | import java.util.UUID; 6 | 7 | /** 8 | * Represents a player or actor within Snitch 9 | */ 10 | public class SnitchPlayer { 11 | 12 | /** 13 | * The internal ID, as used by Snitch 14 | */ 15 | private int id; 16 | 17 | /** 18 | * The player's Mojang UUID 19 | */ 20 | private UUID uuid; 21 | 22 | /** 23 | * The most recent player name we have on file. We'll update this on join. 24 | */ 25 | private String playerName; 26 | 27 | public SnitchPlayer(ResultSet set) throws SQLException { 28 | this.id = set.getInt("id"); 29 | this.uuid = UUID.fromString(set.getString("uuid")); 30 | this.playerName = set.getString("player_name"); 31 | } 32 | 33 | public SnitchPlayer(int id, UUID uuid, String playerName) { 34 | this.id = id; 35 | this.uuid = uuid; 36 | this.playerName = playerName; 37 | } 38 | 39 | public int getId() { 40 | return id; 41 | } 42 | 43 | public UUID getUuid() { 44 | return uuid; 45 | } 46 | 47 | public String getPlayerName() { 48 | return playerName; 49 | } 50 | 51 | public void setPlayerName(String playerName) { 52 | this.playerName = playerName; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/co/melondev/Snitch/entities/SnitchPosition.java: -------------------------------------------------------------------------------- 1 | package co.melondev.Snitch.entities; 2 | 3 | import org.bukkit.Location; 4 | import org.bukkit.World; 5 | import org.bukkit.block.Block; 6 | 7 | /** 8 | * Represents a location within Snitch. We avoid use of {@link Location} as to not load chunks unnecessarily 9 | */ 10 | public class SnitchPosition { 11 | 12 | /** 13 | * The stored X value 14 | */ 15 | private int x; 16 | /** 17 | * The stored Y value 18 | */ 19 | private int y; 20 | /** 21 | * The stored Z value 22 | */ 23 | private int z; 24 | 25 | public SnitchPosition(Location location){ 26 | this(location.getBlockX(), location.getBlockY(), location.getBlockZ()); 27 | } 28 | 29 | public SnitchPosition(Block block){ 30 | this(block.getLocation()); 31 | } 32 | 33 | public SnitchPosition(int x, int y, int z) { 34 | this.x = x; 35 | this.y = y; 36 | this.z = z; 37 | } 38 | 39 | public Location toLocation(SnitchWorld world) { 40 | return toLocation(world.getBukkitWorld()); 41 | } 42 | 43 | 44 | public Location toLocation(World world) { 45 | return new Location(world, this.x, this.y, this.z); 46 | } 47 | 48 | public int getX() { 49 | return x; 50 | } 51 | 52 | public int getY() { 53 | return y; 54 | } 55 | 56 | public int getZ() { 57 | return z; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/co/melondev/Snitch/storage/StorageMethod.java: -------------------------------------------------------------------------------- 1 | package co.melondev.Snitch.storage; 2 | 3 | import co.melondev.Snitch.entities.*; 4 | import co.melondev.Snitch.enums.EnumAction; 5 | import co.melondev.Snitch.util.SnitchDatabaseException; 6 | import com.google.common.collect.ImmutableList; 7 | import com.google.gson.JsonObject; 8 | import org.bukkit.World; 9 | 10 | import java.io.IOException; 11 | import java.util.UUID; 12 | 13 | public interface StorageMethod { 14 | 15 | ImmutableList getWorlds(); 16 | 17 | SnitchWorld register(World world) throws SnitchDatabaseException; 18 | 19 | SnitchPlayer registerPlayer(String playerName, UUID uuid) throws SnitchDatabaseException; 20 | 21 | SnitchPlayer getPlayer(UUID uuid) throws SnitchDatabaseException; 22 | 23 | SnitchPlayer getPlayer(String playerName) throws SnitchDatabaseException; 24 | 25 | SnitchEntry record(EnumAction action, SnitchPlayer player, SnitchWorld world, SnitchPosition position, JsonObject data, long time) throws SnitchDatabaseException; 26 | 27 | int deleteEntries(SnitchQuery query) throws SnitchDatabaseException; 28 | 29 | ImmutableList performLookup(SnitchQuery query) throws SnitchDatabaseException; 30 | 31 | SnitchPlayer getPlayer(int playerID) throws SnitchDatabaseException; 32 | 33 | SnitchWorld getWorld(int worldID); 34 | 35 | void markReverted(SnitchEntry entry, boolean reverted) throws SnitchDatabaseException; 36 | 37 | void closeConnection() throws IOException; 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/co/melondev/Snitch/handlers/EntityDeathHandler.java: -------------------------------------------------------------------------------- 1 | package co.melondev.Snitch.handlers; 2 | 3 | import co.melondev.Snitch.entities.SnitchEntry; 4 | import co.melondev.Snitch.entities.SnitchProcessHandler; 5 | import co.melondev.Snitch.entities.SnitchSession; 6 | import co.melondev.Snitch.enums.EnumSnitchActivity; 7 | import co.melondev.Snitch.util.EntityUtil; 8 | import com.google.gson.JsonObject; 9 | import org.bukkit.entity.Entity; 10 | import org.bukkit.entity.EntityType; 11 | 12 | /** 13 | * Created by Devon on 7/16/18. 14 | */ 15 | public class EntityDeathHandler implements SnitchProcessHandler { 16 | @Override 17 | public boolean handleRollback(SnitchSession session, SnitchEntry entry) { 18 | JsonObject entityData = entry.getData().get("entity").getAsJsonObject(); 19 | 20 | EntityType type = EntityType.valueOf(entityData.get("entityType").getAsString()); 21 | Entity entity = entry.getSnitchWorld().getBukkitWorld().spawnEntity(entry.getSnitchPosition().toLocation(entry.getSnitchWorld()), type); 22 | EntityUtil.rebuildEntity(entity, entityData); 23 | 24 | return true; 25 | } 26 | 27 | @Override 28 | public boolean handlePreview(SnitchSession session, SnitchEntry entry) { 29 | return false; 30 | } 31 | 32 | @Override 33 | public boolean handleRestore(SnitchSession session, SnitchEntry entry) { 34 | return false; 35 | } 36 | 37 | @Override 38 | public boolean can(EnumSnitchActivity activity) { 39 | return activity == EnumSnitchActivity.ROLLBACK; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/co/melondev/Snitch/managers/PlayerManager.java: -------------------------------------------------------------------------------- 1 | package co.melondev.Snitch.managers; 2 | 3 | import co.melondev.Snitch.SnitchPlugin; 4 | import co.melondev.Snitch.entities.SnitchSession; 5 | import co.melondev.Snitch.enums.EnumDefaultPlayer; 6 | import co.melondev.Snitch.util.SnitchDatabaseException; 7 | import com.google.common.cache.Cache; 8 | import com.google.common.cache.CacheBuilder; 9 | import org.bukkit.entity.Player; 10 | 11 | import java.util.UUID; 12 | import java.util.concurrent.TimeUnit; 13 | 14 | public class PlayerManager { 15 | 16 | private SnitchPlugin i; 17 | private Cache sessionCache = CacheBuilder.newBuilder().concurrencyLevel(4).expireAfterAccess(5, TimeUnit.MINUTES).build(); 18 | 19 | public PlayerManager(SnitchPlugin instance) { 20 | this.i = instance; 21 | try { 22 | this.registerDefaultPlayers(); 23 | } catch (SnitchDatabaseException e) { 24 | e.printStackTrace(); 25 | } 26 | } 27 | 28 | public void setSession(Player player, SnitchSession snitchSession) { 29 | sessionCache.put(player.getUniqueId(), snitchSession); 30 | } 31 | 32 | public SnitchSession getSession(Player player) { 33 | return sessionCache.getIfPresent(player.getUniqueId()); 34 | } 35 | 36 | private void registerDefaultPlayers() throws SnitchDatabaseException { 37 | for(EnumDefaultPlayer defaultPlayer : EnumDefaultPlayer.values()){ 38 | i.getStorage().registerPlayer(defaultPlayer.getStorageName(), defaultPlayer.getUuid()); 39 | } 40 | } 41 | 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/co/melondev/Snitch/handlers/CakeHandler.java: -------------------------------------------------------------------------------- 1 | package co.melondev.Snitch.handlers; 2 | 3 | import co.melondev.Snitch.entities.SnitchEntry; 4 | import co.melondev.Snitch.entities.SnitchProcessHandler; 5 | import co.melondev.Snitch.entities.SnitchSession; 6 | import co.melondev.Snitch.enums.EnumSnitchActivity; 7 | import org.bukkit.Location; 8 | import org.bukkit.Material; 9 | import org.bukkit.entity.Player; 10 | 11 | /** 12 | * Created by Devon on 7/17/18. 13 | */ 14 | public class CakeHandler implements SnitchProcessHandler { 15 | @Override 16 | public boolean handleRollback(SnitchSession session, SnitchEntry entry) { 17 | Location location = entry.getSnitchPosition().toLocation(entry.getSnitchWorld()); 18 | location.getBlock().setTypeIdAndData(Material.CAKE_BLOCK.getId(), (byte) 0, false); 19 | return true; 20 | } 21 | 22 | @Override 23 | public boolean handlePreview(SnitchSession session, SnitchEntry entry) { 24 | Player player = session.getPlayer(); 25 | Location location = entry.getSnitchPosition().toLocation(entry.getSnitchWorld()); 26 | player.sendBlockChange(location, Material.CAKE_BLOCK, (byte) 0); 27 | return true; 28 | } 29 | 30 | @Override 31 | public boolean handleRestore(SnitchSession session, SnitchEntry entry) { 32 | Location location = entry.getSnitchPosition().toLocation(entry.getSnitchWorld()); 33 | location.getBlock().setType(Material.AIR); 34 | return true; 35 | } 36 | 37 | @Override 38 | public boolean can(EnumSnitchActivity activity) { 39 | return true; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/co/melondev/Snitch/entities/SnitchRestore.java: -------------------------------------------------------------------------------- 1 | package co.melondev.Snitch.entities; 2 | 3 | import co.melondev.Snitch.enums.EnumSnitchActivity; 4 | import co.melondev.Snitch.util.MsgUtil; 5 | import org.bukkit.entity.Player; 6 | 7 | /** 8 | * Created by Devon on 7/14/18. 9 | * 10 | * A restore will rebuild entries into the world as they were originally logged. 11 | */ 12 | public class SnitchRestore extends SnitchPreview { 13 | 14 | /** 15 | * Provide a session and a callback to begin. 16 | * 17 | * @param session the session (the player, query, etc.) 18 | * @param callback the callback to run upon completion 19 | */ 20 | public SnitchRestore(SnitchSession session, SnitchCallback callback) { 21 | super(session, callback); 22 | activity = EnumSnitchActivity.RESTORE; 23 | } 24 | 25 | /** 26 | * The default callback for restores 27 | */ 28 | public static class DefaultRestoreCallback implements SnitchCallback { 29 | 30 | private long startTime; 31 | 32 | public DefaultRestoreCallback(long startTime) { 33 | this.startTime = startTime; 34 | } 35 | 36 | @Override 37 | public void handle(Player player, SnitchResult result) { 38 | long diff = System.currentTimeMillis() - startTime; 39 | player.sendMessage(MsgUtil.success("Restore successfully completed in " + diff + "ms.")); 40 | player.sendMessage(MsgUtil.record("Total Changes: " + result.getApplied() + "§c§o (" + result.getFailed() + " Failed)")); 41 | if (!result.getMovedEntities().isEmpty()) { 42 | player.sendMessage(MsgUtil.record(result.getMovedEntities().size() + "+ entities were moved to safety")); 43 | } 44 | MsgUtil.staff("§f" + player.getName() + "§b performed a restore: §f" + result.getQuery().getSearchSummary()); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/co/melondev/Snitch/enums/EnumDefaultPlayer.java: -------------------------------------------------------------------------------- 1 | package co.melondev.Snitch.enums; 2 | 3 | import co.melondev.Snitch.SnitchPlugin; 4 | import co.melondev.Snitch.entities.SnitchPlayer; 5 | import co.melondev.Snitch.util.SnitchDatabaseException; 6 | 7 | import java.sql.SQLException; 8 | import java.util.UUID; 9 | 10 | /** 11 | * A list of default actors. These are prefixed by "S-" in storage to prevent collisions with actual players. 12 | */ 13 | public enum EnumDefaultPlayer { 14 | 15 | DRAGON("Dragon", "123e4567-e89b-42d3-a456-556642440001"), 16 | ENDERMAN("Enderman", "123e4567-e89b-42d3-a456-556642440002"), 17 | LAVA("Lava", "123e4567-e89b-42d3-a456-556642440003"), 18 | WATER("Water", "123e4567-e89b-42d3-a456-556642440004"), 19 | FIRE("Fire", "123e4567-e89b-42d3-a456-556642440005"), 20 | BLOCK("Block", "123e4567-e89b-42d3-a456-556642440006"), 21 | TNT("TNT", "123e4567-e89b-42d3-a456-556642440007"), 22 | CREEPER("Creeper", "123e4567-e89b-42d3-a456-556642440008"), 23 | HOPPER("Hopper", "123e4567-e89b-42d3-a456-556642440009"); 24 | 25 | private String name; 26 | private UUID uuid; 27 | 28 | EnumDefaultPlayer(String name, String uuid) { 29 | this.name = name; 30 | this.uuid = UUID.fromString(uuid); 31 | } 32 | 33 | /** 34 | * Get this actor as a Snitch Player. 35 | * 36 | * @return the SnitchPlayer object for this player 37 | * @throws SQLException if there are issues retrieving the data 38 | */ 39 | public SnitchPlayer getSnitchPlayer() throws SnitchDatabaseException { 40 | return SnitchPlugin.getInstance().getStorage().getPlayer(getStorageName()); 41 | } 42 | 43 | public String getStorageName(){ 44 | return "S-" + getName(); 45 | } 46 | 47 | public String getName() { 48 | return name; 49 | } 50 | 51 | public UUID getUuid() { 52 | return uuid; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/co/melondev/Snitch/entities/SnitchRollback.java: -------------------------------------------------------------------------------- 1 | package co.melondev.Snitch.entities; 2 | 3 | import co.melondev.Snitch.enums.EnumSnitchActivity; 4 | import co.melondev.Snitch.util.MsgUtil; 5 | import org.bukkit.entity.Player; 6 | 7 | /** 8 | * Created by Devon on 7/14/18. 9 | * 10 | * Undo changes to a the world by reverting entries. 11 | */ 12 | public class SnitchRollback extends SnitchPreview { 13 | 14 | /** 15 | * Start with a session and a callback. 16 | * 17 | * @param session the session (player performing rollback, query, etc.) 18 | * @param callback the callback to run on completion 19 | */ 20 | public SnitchRollback(SnitchSession session, SnitchCallback callback) { 21 | super(session, callback); 22 | activity = EnumSnitchActivity.ROLLBACK; 23 | } 24 | 25 | /** 26 | * The default callback for rollbacks 27 | */ 28 | public static class DefaultRollbackCallback implements SnitchCallback { 29 | 30 | private long startTime; 31 | 32 | public DefaultRollbackCallback(long startTime) { 33 | this.startTime = startTime; 34 | } 35 | 36 | @Override 37 | public void handle(Player player, SnitchResult result) { 38 | long diff = System.currentTimeMillis() - startTime; 39 | player.sendMessage(MsgUtil.success("Rollback successfully completed in " + diff + "ms.")); 40 | player.sendMessage(MsgUtil.record("Total Changes: " + result.getApplied() + "§c§o (" + result.getFailed() + " Failed)")); 41 | if (!result.getMovedEntities().isEmpty()) { 42 | player.sendMessage(MsgUtil.record(result.getMovedEntities().size() + "+ entities were moved to safety")); 43 | } 44 | player.sendMessage(MsgUtil.record("If you made a mistake, you can §e/snitch undo§7.")); 45 | MsgUtil.staff("§f" + player.getName() + "§b performed a rollback: §f" + result.getQuery().getSearchSummary()); 46 | } 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/co/melondev/Snitch/entities/SnitchResult.java: -------------------------------------------------------------------------------- 1 | package co.melondev.Snitch.entities; 2 | 3 | import co.melondev.Snitch.util.AdjustedBlock; 4 | import org.bukkit.entity.Entity; 5 | 6 | import java.util.List; 7 | import java.util.Map; 8 | 9 | /** 10 | * Created by Devon on 7/14/18. 11 | * 12 | * Contains results for a completed activity 13 | */ 14 | public class SnitchResult { 15 | 16 | /** 17 | * The stats relating to the last activity 18 | */ 19 | private int applied, failed, planned; 20 | 21 | /** 22 | * Whether or not this was a preview 23 | */ 24 | private boolean preview; 25 | 26 | /** 27 | * A map of any moved entities 28 | */ 29 | private Map movedEntities; 30 | 31 | /** 32 | * The query that was used for the last activity 33 | */ 34 | private SnitchQuery query; 35 | 36 | /** 37 | * A list of adjusted blocks 38 | */ 39 | private List changedBlocks; 40 | 41 | public SnitchResult(int applied, int failed, int planned, boolean preview, Map movedEntities, SnitchQuery query, List changedBlocks) { 42 | this.applied = applied; 43 | this.failed = failed; 44 | this.planned = planned; 45 | this.preview = preview; 46 | this.movedEntities = movedEntities; 47 | this.query = query; 48 | this.changedBlocks = changedBlocks; 49 | } 50 | 51 | public int getApplied() { 52 | return applied; 53 | } 54 | 55 | public int getFailed() { 56 | return failed; 57 | } 58 | 59 | public int getPlanned() { 60 | return planned; 61 | } 62 | 63 | public boolean isPreview() { 64 | return preview; 65 | } 66 | 67 | public Map getMovedEntities() { 68 | return movedEntities; 69 | } 70 | 71 | public SnitchQuery getQuery() { 72 | return query; 73 | } 74 | 75 | public List getChangedBlocks() { 76 | return changedBlocks; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/co/melondev/Snitch/handlers/BlockCreationHandler.java: -------------------------------------------------------------------------------- 1 | package co.melondev.Snitch.handlers; 2 | 3 | import co.melondev.Snitch.entities.SnitchEntry; 4 | import co.melondev.Snitch.entities.SnitchProcessHandler; 5 | import co.melondev.Snitch.entities.SnitchSession; 6 | import co.melondev.Snitch.enums.EnumSnitchActivity; 7 | import co.melondev.Snitch.util.BlockUtil; 8 | import com.google.gson.JsonObject; 9 | import net.minecraft.server.v1_12_R1.MojangsonParseException; 10 | import org.bukkit.Location; 11 | import org.bukkit.Material; 12 | import org.bukkit.block.Block; 13 | 14 | /** 15 | * Created by Devon on 7/14/18. 16 | */ 17 | public class BlockCreationHandler implements SnitchProcessHandler { 18 | 19 | @Override 20 | public boolean handleRollback(SnitchSession session, SnitchEntry entry) { 21 | Location loc = entry.getSnitchPosition().toLocation(entry.getSnitchWorld()); 22 | Block block = loc.getBlock(); 23 | block.setTypeIdAndData(Material.AIR.getId(), (byte) 0, false); 24 | return true; 25 | } 26 | 27 | @Override 28 | public boolean handlePreview(SnitchSession session, SnitchEntry entry) { 29 | Location loc = entry.getSnitchPosition().toLocation(entry.getSnitchWorld()); 30 | Block block = loc.getBlock(); 31 | session.getPlayer().sendBlockChange(block.getLocation(), Material.AIR, (byte) 0); 32 | session.recordAdjustedBlock(loc); 33 | return true; 34 | } 35 | 36 | @Override 37 | public boolean handleRestore(SnitchSession session, SnitchEntry entry) { 38 | JsonObject blockData = entry.getData().getAsJsonObject("block"); 39 | Location loc = entry.getSnitchPosition().toLocation(entry.getSnitchWorld()); 40 | Block block = loc.getBlock(); 41 | try { 42 | BlockUtil.rebuildBlock(block, blockData); 43 | } catch (MojangsonParseException e) { 44 | e.printStackTrace(); 45 | } 46 | return true; 47 | } 48 | 49 | @Override 50 | public boolean can(EnumSnitchActivity activity) { 51 | return true; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/co/melondev/Snitch/handlers/SignChangeHandler.java: -------------------------------------------------------------------------------- 1 | package co.melondev.Snitch.handlers; 2 | 3 | import co.melondev.Snitch.entities.SnitchEntry; 4 | import co.melondev.Snitch.entities.SnitchProcessHandler; 5 | import co.melondev.Snitch.entities.SnitchSession; 6 | import co.melondev.Snitch.enums.EnumSnitchActivity; 7 | import com.google.gson.JsonObject; 8 | import org.bukkit.Location; 9 | import org.bukkit.block.Block; 10 | import org.bukkit.block.Sign; 11 | 12 | /** 13 | * Created by Devon on 7/16/18. 14 | */ 15 | public class SignChangeHandler implements SnitchProcessHandler { 16 | @Override 17 | public boolean handleRollback(SnitchSession session, SnitchEntry entry) { 18 | 19 | Location location = entry.getSnitchPosition().toLocation(entry.getSnitchWorld()); 20 | Block block = location.getBlock(); 21 | if ((block.getState() instanceof Sign)) { 22 | Sign sign = (Sign) block.getState(); 23 | JsonObject oldLines = entry.getData().getAsJsonObject("old"); 24 | for (int i = 0; i < 4; i++) { 25 | sign.setLine(i, oldLines.get("line" + i).getAsString()); 26 | } 27 | sign.update(); 28 | } 29 | 30 | return true; 31 | } 32 | 33 | @Override 34 | public boolean handlePreview(SnitchSession session, SnitchEntry entry) { 35 | return false; 36 | } 37 | 38 | @Override 39 | public boolean handleRestore(SnitchSession session, SnitchEntry entry) { 40 | Location location = entry.getSnitchPosition().toLocation(entry.getSnitchWorld()); 41 | Block block = location.getBlock(); 42 | if ((block.getState() instanceof Sign)) { 43 | Sign sign = (Sign) block.getState(); 44 | JsonObject newLines = entry.getData().getAsJsonObject("new"); 45 | for (int i = 0; i < 4; i++) { 46 | sign.setLine(i, newLines.get("line" + i).getAsString()); 47 | } 48 | sign.update(); 49 | } 50 | return true; 51 | } 52 | 53 | @Override 54 | public boolean can(EnumSnitchActivity activity) { 55 | return activity == EnumSnitchActivity.ROLLBACK || activity == EnumSnitchActivity.RESTORE; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/co/melondev/Snitch/handlers/BlockDestructionHandler.java: -------------------------------------------------------------------------------- 1 | package co.melondev.Snitch.handlers; 2 | 3 | import co.melondev.Snitch.entities.SnitchEntry; 4 | import co.melondev.Snitch.entities.SnitchProcessHandler; 5 | import co.melondev.Snitch.entities.SnitchSession; 6 | import co.melondev.Snitch.enums.EnumSnitchActivity; 7 | import co.melondev.Snitch.util.BlockUtil; 8 | import com.google.gson.JsonObject; 9 | import net.minecraft.server.v1_12_R1.MojangsonParseException; 10 | import org.bukkit.Location; 11 | import org.bukkit.Material; 12 | import org.bukkit.block.Block; 13 | 14 | /** 15 | * Created by Devon on 7/14/18. 16 | */ 17 | public class BlockDestructionHandler implements SnitchProcessHandler { 18 | 19 | @Override 20 | public boolean handleRollback(SnitchSession session, SnitchEntry entry) { 21 | JsonObject blockData = entry.getData().getAsJsonObject("block"); 22 | Location loc = entry.getSnitchPosition().toLocation(entry.getSnitchWorld()); 23 | Block block = loc.getBlock(); 24 | try { 25 | BlockUtil.rebuildBlock(block, blockData); 26 | } catch (MojangsonParseException e) { 27 | e.printStackTrace(); 28 | } 29 | return true; 30 | } 31 | 32 | @Override 33 | public boolean handlePreview(SnitchSession session, SnitchEntry entry) { 34 | JsonObject blockData = entry.getData().getAsJsonObject("block"); 35 | Location loc = entry.getSnitchPosition().toLocation(entry.getSnitchWorld()); 36 | Block block = loc.getBlock(); 37 | session.getPlayer().sendBlockChange(block.getLocation(), Material.valueOf(blockData.get("type").getAsString()).getId(), blockData.get("data").getAsByte()); 38 | session.recordAdjustedBlock(loc); 39 | return true; 40 | } 41 | 42 | @Override 43 | public boolean handleRestore(SnitchSession session, SnitchEntry entry) { 44 | Location loc = entry.getSnitchPosition().toLocation(entry.getSnitchWorld()); 45 | Block block = loc.getBlock(); 46 | block.setTypeIdAndData(Material.AIR.getId(), (byte) 0, false); 47 | return true; 48 | } 49 | 50 | @Override 51 | public boolean can(EnumSnitchActivity activity) { 52 | return true; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/co/melondev/Snitch/util/InvUtil.java: -------------------------------------------------------------------------------- 1 | package co.melondev.Snitch.util; 2 | 3 | import co.melondev.Snitch.enums.EnumAction; 4 | import co.melondev.Snitch.listeners.InventoryListener; 5 | import com.google.gson.JsonElement; 6 | import com.google.gson.JsonObject; 7 | import net.minecraft.server.v1_12_R1.MojangsonParseException; 8 | import org.bukkit.Location; 9 | import org.bukkit.entity.Player; 10 | import org.bukkit.inventory.Inventory; 11 | import org.bukkit.inventory.ItemStack; 12 | 13 | /** 14 | * Created by Devon on 7/16/18. 15 | */ 16 | public class InvUtil { 17 | 18 | /** 19 | * Logs all items within a container as a removal 20 | * 21 | * @param player the player who "removed" them 22 | * @param inventory the inventory to loop through 23 | * @param location the location of this inventory 24 | */ 25 | public static void logContentsAsRemoval(Player player, Inventory inventory, Location location) { 26 | if (!EnumAction.ITEM_TAKE.isEnabled()) return; 27 | for (int i = 0; i < inventory.getSize(); i++) { 28 | ItemStack itemStack = inventory.getItem(i); 29 | if (itemStack != null) { 30 | InventoryListener.logAction(player, location, itemStack.clone(), EnumAction.ITEM_TAKE, i, null); 31 | } 32 | } 33 | } 34 | 35 | /** 36 | * Syncs an inventory to match the betadata 37 | * @param inventory the inventory to sync 38 | * @param invData the metadata of the inventory 39 | */ 40 | public static void syncInventory(Inventory inventory, JsonObject invData) { 41 | for (int i = 0; i < inventory.getSize(); i++) { 42 | if (invData.has("slot" + i)) { 43 | JsonElement element = invData.get("slot" + i); 44 | if (element.isJsonNull()) { 45 | inventory.setItem(i, null); 46 | } else { 47 | try { 48 | ItemStack itemStack = ItemUtil.JSONtoItemStack(element.getAsString()); 49 | inventory.setItem(i, itemStack); 50 | } catch (MojangsonParseException e) { 51 | e.printStackTrace(); 52 | } 53 | } 54 | } 55 | } 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/co/melondev/Snitch/handlers/BlockSpreadHandler.java: -------------------------------------------------------------------------------- 1 | package co.melondev.Snitch.handlers; 2 | 3 | import co.melondev.Snitch.entities.SnitchEntry; 4 | import co.melondev.Snitch.entities.SnitchProcessHandler; 5 | import co.melondev.Snitch.entities.SnitchSession; 6 | import co.melondev.Snitch.enums.EnumSnitchActivity; 7 | import co.melondev.Snitch.util.BlockUtil; 8 | import com.google.gson.JsonObject; 9 | import net.minecraft.server.v1_12_R1.MojangsonParseException; 10 | import org.bukkit.Location; 11 | import org.bukkit.Material; 12 | import org.bukkit.block.Block; 13 | 14 | /** 15 | * Created by Devon on 7/14/18. 16 | */ 17 | public class BlockSpreadHandler implements SnitchProcessHandler { 18 | @Override 19 | public boolean handleRollback(SnitchSession session, SnitchEntry entry) { 20 | JsonObject blockData = entry.getData().get("block").getAsJsonObject(); 21 | Location loc = entry.getSnitchPosition().toLocation(entry.getSnitchWorld()); 22 | Block b = loc.getBlock(); 23 | try { 24 | BlockUtil.rebuildBlock(b, blockData); 25 | } catch (MojangsonParseException e) { 26 | e.printStackTrace(); 27 | } 28 | return true; 29 | } 30 | 31 | @Override 32 | public boolean handlePreview(SnitchSession session, SnitchEntry entry) { 33 | JsonObject blockData = entry.getData().get("block").getAsJsonObject(); 34 | Location loc = entry.getSnitchPosition().toLocation(entry.getSnitchWorld()); 35 | Block b = loc.getBlock(); 36 | Material material = Material.valueOf(blockData.get("type").getAsString()); 37 | byte data = blockData.get("data").getAsByte(); 38 | session.getPlayer().sendBlockChange(loc, material, data); 39 | session.recordAdjustedBlock(b.getLocation()); 40 | return true; 41 | } 42 | 43 | @Override 44 | public boolean handleRestore(SnitchSession session, SnitchEntry entry) { 45 | JsonObject sourceData = entry.getData().get("source").getAsJsonObject(); 46 | Location loc = entry.getSnitchPosition().toLocation(entry.getSnitchWorld()); 47 | Block b = loc.getBlock(); 48 | try { 49 | BlockUtil.rebuildBlock(b, sourceData); 50 | } catch (MojangsonParseException e) { 51 | e.printStackTrace(); 52 | } 53 | return true; 54 | } 55 | 56 | @Override 57 | public boolean can(EnumSnitchActivity activity) { 58 | return true; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/co/melondev/Snitch/handlers/ItemTakeHandler.java: -------------------------------------------------------------------------------- 1 | package co.melondev.Snitch.handlers; 2 | 3 | import co.melondev.Snitch.entities.SnitchEntry; 4 | import co.melondev.Snitch.entities.SnitchProcessHandler; 5 | import co.melondev.Snitch.entities.SnitchSession; 6 | import co.melondev.Snitch.enums.EnumSnitchActivity; 7 | import co.melondev.Snitch.util.ItemUtil; 8 | import com.google.gson.JsonObject; 9 | import net.minecraft.server.v1_12_R1.MojangsonParseException; 10 | import org.bukkit.Location; 11 | import org.bukkit.block.Block; 12 | import org.bukkit.inventory.Inventory; 13 | import org.bukkit.inventory.InventoryHolder; 14 | import org.bukkit.inventory.ItemStack; 15 | 16 | /** 17 | * Created by Devon on 7/17/18. 18 | */ 19 | public class ItemTakeHandler implements SnitchProcessHandler { 20 | @Override 21 | public boolean handleRollback(SnitchSession session, SnitchEntry entry) { 22 | 23 | Location loc = entry.getSnitchPosition().toLocation(entry.getSnitchWorld()); 24 | Block block = loc.getBlock(); 25 | if ((block.getState() instanceof InventoryHolder)) { 26 | Inventory inv = ((InventoryHolder) block.getState()).getInventory(); 27 | JsonObject obj = entry.getData(); 28 | int slot = obj.get("slot").getAsInt(); 29 | try { 30 | ItemStack itemStack = ItemUtil.JSONtoItemStack(obj.get("item").getAsJsonObject().get("raw").getAsString()); 31 | inv.setItem(slot, itemStack); 32 | } catch (MojangsonParseException e) { 33 | e.printStackTrace(); 34 | } 35 | } 36 | 37 | return true; 38 | } 39 | 40 | @Override 41 | public boolean handlePreview(SnitchSession session, SnitchEntry entry) { 42 | return false; 43 | } 44 | 45 | @Override 46 | public boolean handleRestore(SnitchSession session, SnitchEntry entry) { 47 | Location loc = entry.getSnitchPosition().toLocation(entry.getSnitchWorld()); 48 | Block block = loc.getBlock(); 49 | if ((block.getState() instanceof InventoryHolder)) { 50 | Inventory inv = ((InventoryHolder) block.getState()).getInventory(); 51 | JsonObject obj = entry.getData(); 52 | int slot = obj.get("slot").getAsInt(); 53 | inv.setItem(slot, null); 54 | } 55 | 56 | return true; 57 | } 58 | 59 | @Override 60 | public boolean can(EnumSnitchActivity activity) { 61 | return activity == EnumSnitchActivity.ROLLBACK || activity == EnumSnitchActivity.RESTORE; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/co/melondev/Snitch/handlers/ItemInsertHandler.java: -------------------------------------------------------------------------------- 1 | package co.melondev.Snitch.handlers; 2 | 3 | import co.melondev.Snitch.entities.SnitchEntry; 4 | import co.melondev.Snitch.entities.SnitchProcessHandler; 5 | import co.melondev.Snitch.entities.SnitchSession; 6 | import co.melondev.Snitch.enums.EnumSnitchActivity; 7 | import co.melondev.Snitch.util.ItemUtil; 8 | import com.google.gson.JsonObject; 9 | import net.minecraft.server.v1_12_R1.MojangsonParseException; 10 | import org.bukkit.Location; 11 | import org.bukkit.block.Block; 12 | import org.bukkit.inventory.Inventory; 13 | import org.bukkit.inventory.InventoryHolder; 14 | import org.bukkit.inventory.ItemStack; 15 | 16 | /** 17 | * Created by Devon on 7/17/18. 18 | */ 19 | public class ItemInsertHandler implements SnitchProcessHandler { 20 | @Override 21 | public boolean handleRollback(SnitchSession session, SnitchEntry entry) { 22 | 23 | Location loc = entry.getSnitchPosition().toLocation(entry.getSnitchWorld()); 24 | Block block = loc.getBlock(); 25 | if ((block.getState() instanceof InventoryHolder)) { 26 | Inventory inv = ((InventoryHolder) block.getState()).getInventory(); 27 | JsonObject obj = entry.getData(); 28 | int slot = obj.get("slot").getAsInt(); 29 | inv.setItem(slot, null); 30 | } 31 | 32 | return true; 33 | } 34 | 35 | @Override 36 | public boolean handlePreview(SnitchSession session, SnitchEntry entry) { 37 | return false; 38 | } 39 | 40 | @Override 41 | public boolean handleRestore(SnitchSession session, SnitchEntry entry) { 42 | Location loc = entry.getSnitchPosition().toLocation(entry.getSnitchWorld()); 43 | Block block = loc.getBlock(); 44 | if ((block.getState() instanceof InventoryHolder)) { 45 | Inventory inv = ((InventoryHolder) block.getState()).getInventory(); 46 | JsonObject obj = entry.getData(); 47 | int slot = obj.get("slot").getAsInt(); 48 | try { 49 | ItemStack itemStack = ItemUtil.JSONtoItemStack(obj.get("item").getAsJsonObject().get("raw").getAsString()); 50 | inv.setItem(slot, itemStack); 51 | } catch (MojangsonParseException e) { 52 | e.printStackTrace(); 53 | } 54 | } 55 | 56 | return true; 57 | } 58 | 59 | @Override 60 | public boolean can(EnumSnitchActivity activity) { 61 | return activity == EnumSnitchActivity.ROLLBACK || activity == EnumSnitchActivity.RESTORE; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/co/melondev/Snitch/listeners/ChatListener.java: -------------------------------------------------------------------------------- 1 | package co.melondev.Snitch.listeners; 2 | 3 | import co.melondev.Snitch.SnitchPlugin; 4 | import co.melondev.Snitch.entities.SnitchPlayer; 5 | import co.melondev.Snitch.entities.SnitchPosition; 6 | import co.melondev.Snitch.entities.SnitchWorld; 7 | import co.melondev.Snitch.enums.EnumAction; 8 | import co.melondev.Snitch.util.SnitchDatabaseException; 9 | import com.google.gson.JsonObject; 10 | import org.bukkit.ChatColor; 11 | import org.bukkit.Location; 12 | import org.bukkit.entity.Player; 13 | import org.bukkit.event.EventHandler; 14 | import org.bukkit.event.EventPriority; 15 | import org.bukkit.event.Listener; 16 | import org.bukkit.event.player.AsyncPlayerChatEvent; 17 | import org.bukkit.event.player.PlayerCommandPreprocessEvent; 18 | 19 | /** 20 | * Created by Devon on 7/16/18. 21 | */ 22 | public class ChatListener implements Listener { 23 | 24 | private SnitchPlugin i; 25 | 26 | public ChatListener(SnitchPlugin i) { 27 | this.i = i; 28 | } 29 | 30 | private void logAction(Player player, String message, Location location, EnumAction action) { 31 | logAction(player, message, location, action, null); 32 | } 33 | 34 | private void logAction(Player player, String message, Location location, EnumAction action, JsonObject data) { 35 | i.async(() -> { 36 | try { 37 | SnitchPlayer snitchPlayer = i.getStorage().getPlayer(player.getUniqueId()); 38 | SnitchWorld world = i.getStorage().register(location.getWorld()); 39 | SnitchPosition position = new SnitchPosition(location); 40 | JsonObject d = data; 41 | if (d == null) { 42 | d = new JsonObject(); 43 | d.addProperty("message", ChatColor.stripColor(message)); 44 | } 45 | i.getStorage().record(action, snitchPlayer, world, position, d, System.currentTimeMillis()); 46 | } catch (SnitchDatabaseException ex) { 47 | ex.printStackTrace(); 48 | } 49 | }); 50 | } 51 | 52 | @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) 53 | public void onChat(PlayerCommandPreprocessEvent event) { 54 | if (!EnumAction.PLAYER_COMMAND.isEnabled()) { 55 | return; 56 | } 57 | final Player player = event.getPlayer(); 58 | final String message = event.getMessage(); 59 | if (message.toLowerCase().startsWith("/snitch")) 60 | return; 61 | logAction(player, message, player.getLocation(), EnumAction.PLAYER_COMMAND); 62 | } 63 | 64 | @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) 65 | public void onChat(AsyncPlayerChatEvent event) { 66 | if (!EnumAction.PLAYER_CHAT.isEnabled()) { 67 | return; 68 | } 69 | final Player player = event.getPlayer(); 70 | final String message = event.getMessage(); 71 | logAction(player, message, player.getLocation(), EnumAction.PLAYER_CHAT); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | snitch 8 | Snitch 9 | 1.0-SNAPSHOT 10 | 11 | ${project.name} 12 | 13 | 14 | true 15 | ${project.basedir}/src/main/resources 16 | 17 | 18 | 19 | 20 | org.apache.maven.plugins 21 | maven-shade-plugin 22 | 2.3 23 | 24 | 25 | 26 | 27 | package 28 | 29 | shade 30 | 31 | 32 | 33 | 34 | 35 | org.apache.maven.plugins 36 | maven-compiler-plugin 37 | 38 | 8 39 | 8 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | spigot-repo 48 | https://hub.spigotmc.org/nexus/content/repositories/snapshots/ 49 | 50 | 51 | 52 | 53 | 54 | org.spigotmc 55 | spigot-api 56 | 1.12.2-R0.1-SNAPSHOT 57 | provided 58 | 59 | 60 | 61 | org.bukkit 62 | bukkit 63 | 1.12.2-R0.1-SNAPSHOT 64 | provided 65 | 66 | 67 | org.spigotmc 68 | spigot 69 | 1.12.2-R0.1-SNAPSHOT 70 | system 71 | ${project.basedir}/src/main/resources/spigot.jar 72 | 73 | 74 | com.zaxxer 75 | HikariCP 76 | 2.7.4 77 | compile 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /src/main/java/co/melondev/Snitch/entities/SnitchSession.java: -------------------------------------------------------------------------------- 1 | package co.melondev.Snitch.entities; 2 | 3 | import co.melondev.Snitch.enums.EnumSnitchActivity; 4 | import co.melondev.Snitch.util.Previewable; 5 | import org.bukkit.Bukkit; 6 | import org.bukkit.Location; 7 | import org.bukkit.entity.Player; 8 | 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | import java.util.UUID; 12 | 13 | /** 14 | * Created by Devon on 7/14/18. 15 | * 16 | * Contains details about an active usage of the Snitch plugin. Used for pagination, preview management, and more. 17 | */ 18 | public class SnitchSession { 19 | 20 | /** 21 | * The UUID of the player associated with this session 22 | */ 23 | private UUID playerUUID; 24 | 25 | /** 26 | * The query that was used for this session. 27 | */ 28 | private SnitchQuery query; 29 | 30 | /** 31 | * Downloaded entries for this search 32 | */ 33 | private List entries; 34 | 35 | /** 36 | * The current page 37 | */ 38 | private int page; 39 | 40 | /** 41 | * The last activity performed against these results. MULL for nothing. 42 | */ 43 | private EnumSnitchActivity lastActivity = null; 44 | 45 | /** 46 | * Any active preview associated with these results. NULL for no preview. 47 | */ 48 | private Previewable activePreview = null; 49 | 50 | /** 51 | * Any adjusted blocks associated with this session. 52 | */ 53 | private List adjustedBlocks = new ArrayList<>(); 54 | 55 | public SnitchSession(Player player, SnitchQuery query, List entries, int page) { 56 | this.playerUUID = player.getUniqueId(); 57 | this.query = query; 58 | this.entries = entries; 59 | this.page = page; 60 | } 61 | 62 | public EnumSnitchActivity getLastActivity() { 63 | return lastActivity; 64 | } 65 | 66 | public void setLastActivity(EnumSnitchActivity lastActivity) { 67 | this.lastActivity = lastActivity; 68 | } 69 | 70 | public List getAdjustedBlocks() { 71 | return adjustedBlocks; 72 | } 73 | 74 | public void setAdjustedBlocks(List adjustedBlocks) { 75 | this.adjustedBlocks = adjustedBlocks; 76 | } 77 | 78 | public void recordAdjustedBlock(Location location) { 79 | this.adjustedBlocks.add(location); 80 | } 81 | 82 | public UUID getPlayerUUID() { 83 | return playerUUID; 84 | } 85 | 86 | public Player getPlayer() { 87 | return Bukkit.getPlayer(this.playerUUID); 88 | } 89 | 90 | public Previewable getActivePreview() { 91 | return activePreview; 92 | } 93 | 94 | public void setActivePreview(Previewable activePreview) { 95 | this.activePreview = activePreview; 96 | } 97 | 98 | public SnitchQuery getQuery() { 99 | return query; 100 | } 101 | 102 | public void setQuery(SnitchQuery query) { 103 | this.query = query; 104 | } 105 | 106 | public List getEntries() { 107 | return entries; 108 | } 109 | 110 | public void setEntries(List entries) { 111 | this.entries = entries; 112 | } 113 | 114 | public int getPage() { 115 | return page; 116 | } 117 | 118 | public void setPage(int page) { 119 | this.page = page; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/main/java/co/melondev/Snitch/listeners/ConnectionListener.java: -------------------------------------------------------------------------------- 1 | package co.melondev.Snitch.listeners; 2 | 3 | import co.melondev.Snitch.SnitchPlugin; 4 | import co.melondev.Snitch.entities.SnitchPlayer; 5 | import co.melondev.Snitch.entities.SnitchPosition; 6 | import co.melondev.Snitch.entities.SnitchWorld; 7 | import co.melondev.Snitch.enums.EnumAction; 8 | import co.melondev.Snitch.util.JsonUtil; 9 | import co.melondev.Snitch.util.SnitchDatabaseException; 10 | import com.google.gson.JsonObject; 11 | import org.bukkit.Location; 12 | import org.bukkit.entity.Player; 13 | import org.bukkit.event.EventHandler; 14 | import org.bukkit.event.EventPriority; 15 | import org.bukkit.event.Listener; 16 | import org.bukkit.event.player.AsyncPlayerPreLoginEvent; 17 | import org.bukkit.event.player.PlayerJoinEvent; 18 | import org.bukkit.event.player.PlayerQuitEvent; 19 | import org.bukkit.event.player.PlayerTeleportEvent; 20 | 21 | import java.util.UUID; 22 | 23 | /** 24 | * Created by Devon on 7/15/18. 25 | */ 26 | public class ConnectionListener implements Listener { 27 | 28 | private SnitchPlugin i; 29 | 30 | public ConnectionListener(SnitchPlugin i) { 31 | this.i = i; 32 | } 33 | 34 | private void logAction(Player player, Location location, EnumAction action, JsonObject data) { 35 | i.async(() -> { 36 | try { 37 | SnitchPlayer snitchPlayer = i.getStorage().getPlayer(player.getUniqueId()); 38 | SnitchWorld world = i.getStorage().register(location.getWorld()); 39 | SnitchPosition position = new SnitchPosition(location); 40 | i.getStorage().record(action, snitchPlayer, world, position, data, System.currentTimeMillis()); 41 | } catch (SnitchDatabaseException ex) { 42 | ex.printStackTrace(); 43 | } 44 | }); 45 | } 46 | 47 | @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) 48 | public void onTeleport(PlayerTeleportEvent event) { 49 | Player player = event.getPlayer(); 50 | PlayerTeleportEvent.TeleportCause cause = event.getCause(); 51 | Location to = event.getTo(); 52 | if (EnumAction.PLAYER_TELEPORT.isEnabled()) { 53 | JsonObject data = new JsonObject(); 54 | data.addProperty("cause", cause.name()); 55 | data.add("location", JsonUtil.jsonify(to)); 56 | logAction(player, player.getLocation(), EnumAction.PLAYER_TELEPORT, data); 57 | } 58 | } 59 | 60 | @EventHandler 61 | public void onJoin(PlayerQuitEvent event) { 62 | Player player = event.getPlayer(); 63 | if (EnumAction.PLAYER_QUIT.isEnabled()) { 64 | logAction(player, player.getLocation(), EnumAction.PLAYER_QUIT, new JsonObject()); 65 | } 66 | } 67 | 68 | @EventHandler 69 | public void onJoin(PlayerJoinEvent event) { 70 | Player player = event.getPlayer(); 71 | if (EnumAction.PLAYER_JOIN.isEnabled()) { 72 | JsonObject obj = new JsonObject(); 73 | obj.addProperty("ip", player.getAddress().getAddress().getHostAddress()); 74 | logAction(player, player.getLocation(), EnumAction.PLAYER_JOIN, obj); 75 | } 76 | } 77 | 78 | @EventHandler 79 | public void onPreLogin(AsyncPlayerPreLoginEvent event) { 80 | String playerName = event.getName(); 81 | UUID uuid = event.getUniqueId(); 82 | try { 83 | i.getStorage().registerPlayer(playerName, uuid); 84 | } catch (SnitchDatabaseException e) { 85 | event.disallow(AsyncPlayerPreLoginEvent.Result.KICK_OTHER, "Error creating your Snitch data!"); 86 | e.printStackTrace(); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/main/java/co/melondev/Snitch/util/MsgUtil.java: -------------------------------------------------------------------------------- 1 | package co.melondev.Snitch.util; 2 | 3 | import co.melondev.Snitch.entities.SnitchEntry; 4 | import co.melondev.Snitch.entities.SnitchPosition; 5 | import co.melondev.Snitch.entities.SnitchQuery; 6 | import net.md_5.bungee.api.chat.BaseComponent; 7 | import net.md_5.bungee.api.chat.ClickEvent; 8 | import net.md_5.bungee.api.chat.ComponentBuilder; 9 | import net.md_5.bungee.api.chat.HoverEvent; 10 | import org.bukkit.Bukkit; 11 | import org.bukkit.command.CommandSender; 12 | import org.bukkit.entity.Player; 13 | 14 | import java.text.SimpleDateFormat; 15 | import java.util.List; 16 | import java.util.concurrent.TimeUnit; 17 | 18 | /** 19 | * Created by Devon on 7/13/18. 20 | */ 21 | public class MsgUtil { 22 | 23 | public static void sendRecords(CommandSender sender, SnitchQuery query, List entries, int page, int perPage) { 24 | 25 | sender.sendMessage(info(query.getSearchSummary())); 26 | if (entries.isEmpty()) { 27 | sender.sendMessage(error("No records match your search criteria. Maybe try being more broad?")); 28 | } else { 29 | int start = perPage * (page - 1); 30 | int end = start + perPage; 31 | boolean nextPage = true; 32 | SimpleDateFormat df = new SimpleDateFormat("MM/dd/yy hh:mm a"); 33 | for (int i = start; i < end; i++) { 34 | if (i < entries.size()) { 35 | SnitchEntry entry = entries.get(i); 36 | String d = entry.getDescriptor(sender); 37 | 38 | String timestamp; 39 | if (System.currentTimeMillis() - entry.getTimestamp() > TimeUnit.DAYS.toMillis(1)) { 40 | timestamp = df.format(entry.getTimestamp()) + ":"; 41 | } else { 42 | timestamp = TimeUtil.formatDateDiff(entry.getTimestamp(), true) + " ago:"; 43 | } 44 | 45 | String rawMsg = record("§e" + timestamp + " §7" + d + "§8 (" + i + ")"); 46 | if ((sender instanceof Player)) { 47 | SnitchPosition p = entry.getSnitchPosition(); 48 | ComponentBuilder builder = new ComponentBuilder("§7Action: §6" + entry.getAction().getName() + "§e (" + entry.getAction().name() + ")\n" + 49 | "§7Actor: §6" + entry.getSnitchPlayer().getPlayerName() + "\n" + 50 | "§7World: §6" + entry.getSnitchWorld().getWorldName() + "\n" + 51 | "§7Location: §6" + p.getX() + "x, " + p.getY() + "y, " + p.getZ() + "z\n" + 52 | "§7Time: §6" + df.format(entry.getTimestamp()) + "§e (" + TimeUtil.formatDateDiff(entry.getTimestamp(), false) + " ago)\n" + 53 | "§f\n§fLeft-Click§7 to teleport to this event."); 54 | BaseComponent[] c = new ComponentBuilder(rawMsg) 55 | .event(new HoverEvent(HoverEvent.Action.SHOW_TEXT, builder.create())) 56 | .event(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/snitch tp " + i)).create(); 57 | ((Player) sender).spigot().sendMessage(c); 58 | } else { 59 | sender.sendMessage(rawMsg); 60 | } 61 | 62 | } else { 63 | nextPage = false; 64 | break; 65 | } 66 | } 67 | if (nextPage) { 68 | sender.sendMessage(info("For more: §6/snitch next§e or §6/snitch prev")); 69 | } 70 | } 71 | 72 | } 73 | 74 | public static void staff(String message) { 75 | for (Player player : Bukkit.getOnlinePlayers()) { 76 | if (player.hasPermission("snitch.notify")) { 77 | player.sendMessage("§f§l[Snitch] §b" + message); 78 | } 79 | } 80 | } 81 | 82 | public static String record(String message) { 83 | return "§f> §7" + message; 84 | } 85 | 86 | public static String info(String message) { 87 | return "§f§l[Snitch] §e" + message; 88 | } 89 | 90 | public static String success(String message) { 91 | return "§f§l[Snitch] §a" + message; 92 | } 93 | 94 | public static String error(String message) { 95 | return "§f§l[Snitch] §c" + message; 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /src/main/java/co/melondev/Snitch/commands/SnitchCommand.java: -------------------------------------------------------------------------------- 1 | package co.melondev.Snitch.commands; 2 | 3 | import co.melondev.Snitch.SnitchPlugin; 4 | import co.melondev.Snitch.enums.EnumSnitchCommand; 5 | import co.melondev.Snitch.util.MsgUtil; 6 | import co.melondev.Snitch.util.SnitchDatabaseException; 7 | import org.bukkit.command.Command; 8 | import org.bukkit.command.CommandExecutor; 9 | import org.bukkit.command.CommandSender; 10 | 11 | import java.util.ArrayList; 12 | import java.util.Arrays; 13 | import java.util.List; 14 | 15 | /** 16 | * Created by Devon on 7/13/18. 17 | * 18 | * The primary command for Snitch. We'll pass these params to {@link EnumSnitchCommand} for processing 19 | */ 20 | public class SnitchCommand implements CommandExecutor { 21 | 22 | 23 | /** 24 | * The primary instance of Snitch. 25 | */ 26 | private SnitchPlugin i; 27 | 28 | public SnitchCommand(SnitchPlugin i) { 29 | this.i = i; 30 | } 31 | 32 | @Override 33 | public boolean onCommand(CommandSender sender, Command command, String s, String[] args) { 34 | 35 | // A majority of our commands operate in another thread. For world-modification specific ones, we can send it back to the main thread when needed. 36 | i.async(() -> { 37 | try { 38 | // #shamelessplug 39 | if (args.length == 0) { 40 | sender.sendMessage(MsgUtil.info(i.getDescription().getName() + " v" + i.getDescription().getVersion())); 41 | sender.sendMessage(MsgUtil.info("Developed by Melon Development, Inc.")); 42 | sender.sendMessage(MsgUtil.info("Help: §6/snitch help")); 43 | return; 44 | } else { 45 | String cmd = args[0]; 46 | // Display a list of the commands for the plugin. These are from EnumSnitchCommand. 47 | if (cmd.equalsIgnoreCase("help") || cmd.equalsIgnoreCase("?")) { 48 | sender.sendMessage(MsgUtil.info("Commands")); 49 | boolean permsForAny = false; 50 | for (EnumSnitchCommand c : EnumSnitchCommand.values()) { 51 | if (c.getPermission() == null || sender.hasPermission(c.getPermission())) { 52 | permsForAny = true; 53 | String usage = c.getArguments().length() > 0 ? " " + c.getArguments() + " " : " "; 54 | sender.sendMessage(MsgUtil.record("/snitch " + String.join("|", c.getCommands())) + usage + "§o" + c.getDescription()); 55 | } 56 | } 57 | // If the user doesn't have permission for any Snitch commands, tell them so. 58 | if (!permsForAny) { 59 | sender.sendMessage(MsgUtil.error("You don't have access to any Snitch commands.")); 60 | } 61 | } else { 62 | 63 | // Match the first argument to a Snitch subcommand. 64 | EnumSnitchCommand c = EnumSnitchCommand.getByCommand(cmd); 65 | if (c != null) { 66 | 67 | // Double check that they have permission, if a permission is specified 68 | if (c.getPermission() == null || sender.hasPermission(c.getPermission())) { 69 | 70 | List a = new ArrayList<>(); 71 | a.addAll(Arrays.asList(args).subList(1, args.length)); 72 | 73 | // onward to processing! 74 | c.run(sender, a); 75 | 76 | } else { 77 | sender.sendMessage(MsgUtil.error("You don't have permission to " + c.getDescription().toLowerCase() + ".")); 78 | } 79 | 80 | } else { 81 | sender.sendMessage(MsgUtil.error("Unknown command: /snitch " + cmd)); 82 | } 83 | } 84 | } 85 | 86 | // Default error handling. Any command errors will through an IAE. 87 | } catch (IllegalArgumentException ex) { 88 | sender.sendMessage(MsgUtil.error(ex.getMessage())); 89 | } catch (SnitchDatabaseException e) { 90 | // Database errors. We don't want to display the problem publicly so we instead log it to console. 91 | sender.sendMessage(MsgUtil.error("Internal database error. Check console for details.")); 92 | e.printStackTrace(); 93 | } 94 | }); 95 | 96 | return true; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/main/java/co/melondev/Snitch/entities/SnitchEntry.java: -------------------------------------------------------------------------------- 1 | package co.melondev.Snitch.entities; 2 | 3 | import co.melondev.Snitch.enums.EnumAction; 4 | import co.melondev.Snitch.enums.EnumActionVariables; 5 | import com.google.gson.JsonElement; 6 | import com.google.gson.JsonObject; 7 | import org.bukkit.command.CommandSender; 8 | 9 | 10 | /** 11 | * Contains all details related to an entry in our logs table. 12 | */ 13 | public class SnitchEntry { 14 | 15 | /** 16 | * The internal record ID, as decided by our database of choice. 17 | */ 18 | private int id; 19 | /** 20 | * The action that was recorded. 21 | */ 22 | private EnumAction action; 23 | /** 24 | * The player or actor that performed this action. 25 | */ 26 | private SnitchPlayer snitchPlayer; 27 | /** 28 | * The world that this action happened within. 29 | */ 30 | private SnitchWorld snitchWorld; 31 | /** 32 | * The position that this happened at. 33 | * By not using {@link org.bukkit.Location}, we avoid loading chunks needlessly 34 | */ 35 | private SnitchPosition snitchPosition; 36 | /** 37 | * The unix timestamp of this event 38 | */ 39 | private long timestamp; 40 | /** 41 | * Any relavent metadata that was recorded. 42 | * This format is parsed and written by {@link co.melondev.Snitch.util.JsonUtil} 43 | */ 44 | private JsonObject data; 45 | /** 46 | * Whether or not this entry has been reverted 47 | */ 48 | private boolean reverted; 49 | 50 | public SnitchEntry(int id, EnumAction action, SnitchPlayer snitchPlayer, SnitchWorld snitchWorld, SnitchPosition snitchPosition, long timestamp, JsonObject data, boolean reverted) { 51 | this.id = id; 52 | this.action = action; 53 | this.snitchPlayer = snitchPlayer; 54 | this.snitchWorld = snitchWorld; 55 | this.snitchPosition = snitchPosition; 56 | this.timestamp = timestamp; 57 | this.data = data; 58 | this.reverted = reverted; 59 | } 60 | 61 | /** 62 | * Returns the time in which this action happened 63 | * @return the unix timestamp of when this activity took place 64 | */ 65 | public long getTimestamp() { 66 | return timestamp; 67 | } 68 | 69 | /** 70 | * Gets a short explanation of what happened, replacing any necessary variables 71 | * @return a formatted, color coded, accurate description of the event that took place 72 | */ 73 | public String getDescriptor(CommandSender sender) { 74 | String base = getAction().getExplained(); 75 | String crossout = isReverted() ? "§m" : ""; 76 | 77 | base = base.replace("%actor", "§6" + crossout + getSnitchPlayer().getPlayerName() + "§7" + crossout); 78 | 79 | for(EnumActionVariables var : EnumActionVariables.values()){ 80 | if (data.has(var.getKey())){ 81 | if (var.shouldRedactDetailsFor(sender)) { 82 | base = base.replace("%" + var.getKey(), "§c" + crossout + "[PRIVATE]§7" + crossout); 83 | } else { 84 | JsonElement e = data.get(var.getKey()); 85 | if (e.isJsonObject()) { 86 | base = base.replace("%" + var.getKey(), "§6" + crossout + var.getReplacement(e.getAsJsonObject()) + "§7" + crossout); 87 | } else { 88 | base = base.replace("%" + var.getKey(), "§6" + crossout + var.getReplacement(data.getAsJsonObject()) + "§7" + crossout); 89 | } 90 | } 91 | } 92 | } 93 | 94 | return base; 95 | } 96 | 97 | /** 98 | * 99 | * @return whether or not this action has been rolled back 100 | */ 101 | public boolean isReverted(){ 102 | return reverted; 103 | } 104 | 105 | /** 106 | * Sets the cached value for whether or not this entry was rolled back 107 | * @param reverted whether or not this entry was reverted 108 | */ 109 | public void setReverted(boolean reverted) { 110 | this.reverted = reverted; 111 | } 112 | 113 | public int getId() { 114 | return id; 115 | } 116 | 117 | public EnumAction getAction() { 118 | return action; 119 | } 120 | 121 | public SnitchPlayer getSnitchPlayer() { 122 | return snitchPlayer; 123 | } 124 | 125 | public SnitchWorld getSnitchWorld() { 126 | return snitchWorld; 127 | } 128 | 129 | public SnitchPosition getSnitchPosition() { 130 | return snitchPosition; 131 | } 132 | 133 | public JsonObject getData() { 134 | return data; 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/main/java/co/melondev/Snitch/SnitchPlugin.java: -------------------------------------------------------------------------------- 1 | package co.melondev.Snitch; 2 | 3 | import co.melondev.Snitch.commands.SnitchCommand; 4 | import co.melondev.Snitch.entities.SnitchQuery; 5 | import co.melondev.Snitch.listeners.*; 6 | import co.melondev.Snitch.managers.PlayerManager; 7 | import co.melondev.Snitch.storage.MySQLStorage; 8 | import co.melondev.Snitch.storage.StorageMethod; 9 | import co.melondev.Snitch.util.Config; 10 | import co.melondev.Snitch.util.SnitchDatabaseException; 11 | import org.bukkit.Bukkit; 12 | import org.bukkit.plugin.java.JavaPlugin; 13 | 14 | import java.io.File; 15 | import java.io.IOException; 16 | import java.sql.SQLException; 17 | import java.util.ArrayList; 18 | import java.util.Arrays; 19 | import java.util.List; 20 | 21 | public class SnitchPlugin extends JavaPlugin { 22 | 23 | private static SnitchPlugin instance; 24 | private StorageMethod storage; 25 | private PlayerManager playerManager; 26 | private Config config; 27 | 28 | public static SnitchPlugin getInstance() { 29 | return instance; 30 | } 31 | 32 | @Override 33 | public void onDisable() { 34 | if (this.storage != null) { 35 | try { 36 | this.storage.closeConnection(); 37 | } catch (IOException e) { 38 | e.printStackTrace(); 39 | } 40 | } 41 | } 42 | 43 | @Override 44 | public void onEnable() { 45 | instance = this; 46 | 47 | File dir = getDataFolder(); 48 | if (!dir.exists()) { 49 | dir.mkdir(); 50 | } 51 | File configFile = new File(dir, "config.yml"); 52 | if (!configFile.exists()) { 53 | saveDefaultConfig(); 54 | } 55 | try { 56 | this.config = new Config(getConfig()); 57 | } catch (Exception e) { 58 | getLogger().severe("Error reading configuration file: " + e.getMessage()); 59 | e.printStackTrace(); 60 | } 61 | 62 | switch (config.getMethod().toLowerCase()) { 63 | case "mysql": 64 | try { 65 | this.storage = new MySQLStorage(config.getMysqlHost(), config.getMysqlPort(), config.getMysqlUsername(), config.getMysqlPassword(), config.getMysqlDatabase(), config.getMysqlPrefix()); 66 | } catch (SQLException e) { 67 | getLogger().severe("Error initializing database: " + e.getMessage()); 68 | e.printStackTrace(); 69 | } 70 | break; 71 | default: 72 | getLogger().severe("Error initializing database: unknown storage method"); 73 | break; 74 | } 75 | 76 | this.playerManager = new PlayerManager(this); 77 | getServer().getPluginManager().registerEvents(new ConnectionListener(this), this); 78 | getServer().getPluginManager().registerEvents(new BlockListener(this), this); 79 | getServer().getPluginManager().registerEvents(new EntityListener(this), this); 80 | getServer().getPluginManager().registerEvents(new InventoryListener(this), this); 81 | getServer().getPluginManager().registerEvents(new ChatListener(this), this); 82 | 83 | getCommand("snitch").setExecutor(new SnitchCommand(this)); 84 | 85 | List cleanup = config.getAutocleanParams(); 86 | if (config.isAutoClean() && !cleanup.isEmpty() && storage != null) { 87 | getLogger().info("Running " + cleanup.size() + " cleanup task(s)..."); 88 | for (String entry : cleanup) { 89 | async(() -> { 90 | try { 91 | SnitchQuery query = new SnitchQuery(); 92 | List args = new ArrayList<>(); 93 | args.addAll(Arrays.asList(entry.split(" "))); 94 | query.parseParams(null, args); 95 | int toDelete = getStorage().deleteEntries(query); 96 | getLogger().info("Executed \"" + entry + "\": Deleted " + toDelete + " record(s)."); 97 | } catch (SnitchDatabaseException | IllegalArgumentException ex) { 98 | getLogger().severe("Error executing cleanup task \"" + entry + "\": " + ex.getMessage()); 99 | } 100 | }); 101 | } 102 | } 103 | } 104 | 105 | public Config getConfiguration() { 106 | return config; 107 | } 108 | 109 | public PlayerManager getPlayerManager() { 110 | return playerManager; 111 | } 112 | 113 | public StorageMethod getStorage() { 114 | return storage; 115 | } 116 | 117 | public void async(Runnable r){ 118 | Bukkit.getServer().getScheduler().runTaskAsynchronously(this, r); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/main/java/co/melondev/Snitch/util/Config.java: -------------------------------------------------------------------------------- 1 | package co.melondev.Snitch.util; 2 | 3 | import co.melondev.Snitch.enums.EnumAction; 4 | import org.bukkit.Material; 5 | import org.bukkit.configuration.file.FileConfiguration; 6 | 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | 10 | /** 11 | * Created by Devon on 7/15/18. 12 | * 13 | * Caches the values from the Snitch configuration 14 | */ 15 | public class Config { 16 | 17 | /** 18 | * The current config version 19 | */ 20 | private String configVersion; 21 | 22 | /** 23 | * The storage method to use 24 | */ 25 | private String method; 26 | 27 | /** 28 | * The host for the MySQL database 29 | */ 30 | private String mysqlHost; 31 | 32 | /** 33 | * The port for the MySQL database 34 | */ 35 | private int mysqlPort; 36 | 37 | /** 38 | * The username for the MySQL database 39 | */ 40 | private String mysqlUsername; 41 | 42 | /** 43 | * The password for the MySQL database 44 | */ 45 | private String mysqlPassword; 46 | 47 | /** 48 | * The name of the mySQL database 49 | */ 50 | private String mysqlDatabase; 51 | 52 | /** 53 | * The prefix to put before all Snitch table names 54 | */ 55 | private String mysqlPrefix; 56 | 57 | /** 58 | * Whether or not auto clean queries should be run 59 | */ 60 | private boolean autoClean; 61 | 62 | /** 63 | * The params to run for the autoclean task 64 | */ 65 | private List autocleanParams; 66 | 67 | /** 68 | * The default range to search, if one isn't specified 69 | */ 70 | private int defaultArea; 71 | 72 | /** 73 | * The default time limit, if one isn't specified 74 | */ 75 | private long defaultTime; 76 | 77 | private Material wand; 78 | private Material wandBlock; 79 | 80 | /** 81 | * Actions not to log 82 | */ 83 | private List disabledLogging; 84 | 85 | /** 86 | * Initializes the config cache using the config file 87 | * 88 | * @param conf the plugin config file 89 | * @throws Exception if there's a parsing error 90 | */ 91 | public Config(FileConfiguration conf) throws Exception { 92 | this.configVersion = conf.getString("meta.version"); 93 | this.method = conf.getString("data.method"); 94 | this.mysqlHost = conf.getString("data.mysql.host"); 95 | this.mysqlUsername = conf.getString("data.mysql.username"); 96 | this.mysqlPassword = conf.getString("data.mysql.password"); 97 | this.mysqlDatabase = conf.getString("data.mysql.database"); 98 | this.mysqlPrefix = conf.getString("data.mysql.prefix"); 99 | this.mysqlPort = conf.getInt("data.mysql.port"); 100 | this.autoClean = conf.getBoolean("autoclean.enable"); 101 | this.autocleanParams = conf.getStringList("autoclean.actions"); 102 | this.defaultArea = conf.getInt("defaults.area"); 103 | this.defaultTime = TimeUtil.parseDateDiff(conf.getString("defaults.time"), true) - System.currentTimeMillis(); 104 | this.wand = Material.valueOf(conf.getString("tools.wand").toUpperCase()); 105 | this.wandBlock = Material.valueOf(conf.getString("tools.wand-block").toUpperCase()); 106 | this.disabledLogging = new ArrayList<>(); 107 | for (String list : conf.getStringList("disabled-logging")) { 108 | EnumAction action = EnumAction.valueOf(list.toUpperCase()); 109 | this.disabledLogging.add(action); 110 | } 111 | } 112 | 113 | public String getConfigVersion() { 114 | return configVersion; 115 | } 116 | 117 | public String getMethod() { 118 | return method; 119 | } 120 | 121 | public String getMysqlHost() { 122 | return mysqlHost; 123 | } 124 | 125 | public int getMysqlPort() { 126 | return mysqlPort; 127 | } 128 | 129 | public String getMysqlUsername() { 130 | return mysqlUsername; 131 | } 132 | 133 | public String getMysqlPassword() { 134 | return mysqlPassword; 135 | } 136 | 137 | public String getMysqlDatabase() { 138 | return mysqlDatabase; 139 | } 140 | 141 | public String getMysqlPrefix() { 142 | return mysqlPrefix; 143 | } 144 | 145 | public boolean isAutoClean() { 146 | return autoClean; 147 | } 148 | 149 | public List getAutocleanParams() { 150 | return autocleanParams; 151 | } 152 | 153 | public int getDefaultArea() { 154 | return defaultArea; 155 | } 156 | 157 | public long getDefaultTime() { 158 | return defaultTime; 159 | } 160 | 161 | public Material getWand() { 162 | return wand; 163 | } 164 | 165 | public Material getWandBlock() { 166 | return wandBlock; 167 | } 168 | 169 | public List getDisabledLogging() { 170 | return disabledLogging; 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/main/java/co/melondev/Snitch/util/TimeUtil.java: -------------------------------------------------------------------------------- 1 | package co.melondev.Snitch.util; 2 | 3 | import java.util.Calendar; 4 | import java.util.GregorianCalendar; 5 | import java.util.regex.Matcher; 6 | import java.util.regex.Pattern; 7 | 8 | public class TimeUtil { 9 | 10 | private static Pattern timePattern = Pattern.compile("(?:([0-9]+)\\s*y[a-z]*[,\\s]*)?" + "(?:([0-9]+)\\s*mo[a-z]*[,\\s]*)?" + "(?:([0-9]+)\\s*w[a-z]*[,\\s]*)?" + "(?:([0-9]+)\\s*d[a-z]*[,\\s]*)?" + "(?:([0-9]+)\\s*h[a-z]*[,\\s]*)?" + "(?:([0-9]+)\\s*m[a-z]*[,\\s]*)?" + "(?:([0-9]+)\\s*(?:s[a-z]*)?)?", Pattern.CASE_INSENSITIVE); 11 | 12 | public static String removeTimePattern(String input) { 13 | return timePattern.matcher(input).replaceFirst("").trim(); 14 | } 15 | 16 | public static long parseDateDiff(String time, boolean future) throws Exception { 17 | Matcher m = timePattern.matcher(time); 18 | int years = 0; 19 | int months = 0; 20 | int weeks = 0; 21 | int days = 0; 22 | int hours = 0; 23 | int minutes = 0; 24 | int seconds = 0; 25 | boolean found = false; 26 | while (m.find()) { 27 | if (m.group() == null || m.group().isEmpty()) { 28 | continue; 29 | } 30 | for (int i = 0; i < m.groupCount(); i++) { 31 | if (m.group(i) != null && !m.group(i).isEmpty()) { 32 | found = true; 33 | break; 34 | } 35 | } 36 | if (found) { 37 | if (m.group(1) != null && !m.group(1).isEmpty()) { 38 | years = Integer.parseInt(m.group(1)); 39 | } 40 | if (m.group(2) != null && !m.group(2).isEmpty()) { 41 | months = Integer.parseInt(m.group(2)); 42 | } 43 | if (m.group(3) != null && !m.group(3).isEmpty()) { 44 | weeks = Integer.parseInt(m.group(3)); 45 | } 46 | if (m.group(4) != null && !m.group(4).isEmpty()) { 47 | days = Integer.parseInt(m.group(4)); 48 | } 49 | if (m.group(5) != null && !m.group(5).isEmpty()) { 50 | hours = Integer.parseInt(m.group(5)); 51 | } 52 | if (m.group(6) != null && !m.group(6).isEmpty()) { 53 | minutes = Integer.parseInt(m.group(6)); 54 | } 55 | if (m.group(7) != null && !m.group(7).isEmpty()) { 56 | seconds = Integer.parseInt(m.group(7)); 57 | } 58 | break; 59 | } 60 | } 61 | if (!found) { 62 | throw new Exception("illegalDate"); 63 | } 64 | Calendar c = new GregorianCalendar(); 65 | if (years > 0) { 66 | c.add(Calendar.YEAR, years * (future ? 1 : -1)); 67 | } 68 | if (months > 0) { 69 | c.add(Calendar.MONTH, months * (future ? 1 : -1)); 70 | } 71 | if (weeks > 0) { 72 | c.add(Calendar.WEEK_OF_YEAR, weeks * (future ? 1 : -1)); 73 | } 74 | if (days > 0) { 75 | c.add(Calendar.DAY_OF_MONTH, days * (future ? 1 : -1)); 76 | } 77 | if (hours > 0) { 78 | c.add(Calendar.HOUR_OF_DAY, hours * (future ? 1 : -1)); 79 | } 80 | if (minutes > 0) { 81 | c.add(Calendar.MINUTE, minutes * (future ? 1 : -1)); 82 | } 83 | if (seconds > 0) { 84 | c.add(Calendar.SECOND, seconds * (future ? 1 : -1)); 85 | } 86 | Calendar max = new GregorianCalendar(); 87 | max.add(Calendar.YEAR, 10); 88 | if (c.after(max)) { 89 | return max.getTimeInMillis(); 90 | } 91 | return c.getTimeInMillis(); 92 | } 93 | 94 | static int dateDiff(int type, Calendar fromDate, Calendar toDate, boolean future) { 95 | int diff = 0; 96 | long savedDate = fromDate.getTimeInMillis(); 97 | while ((future && !fromDate.after(toDate)) || (!future && !fromDate.before(toDate))) { 98 | savedDate = fromDate.getTimeInMillis(); 99 | fromDate.add(type, future ? 1 : -1); 100 | diff++; 101 | } 102 | diff--; 103 | fromDate.setTimeInMillis(savedDate); 104 | return diff; 105 | } 106 | 107 | public static String formatDateDiffWithSeconds(long seconds, boolean shrink) { 108 | long future = System.currentTimeMillis() + (seconds * 1000); 109 | return formatDateDiff(future, shrink); 110 | } 111 | 112 | public static String formatDateDiff(long date, boolean shrink) { 113 | Calendar c = new GregorianCalendar(); 114 | c.setTimeInMillis(date); 115 | Calendar now = new GregorianCalendar(); 116 | return formatDateDiff(now, c, shrink); 117 | } 118 | 119 | public static String formatDateDiff(Calendar fromDate, Calendar toDate, boolean shrink) { 120 | boolean future = false; 121 | if (toDate.equals(fromDate)) { 122 | return "now"; 123 | } 124 | if (toDate.after(fromDate)) { 125 | future = true; 126 | } 127 | StringBuilder sb = new StringBuilder(); 128 | int[] types = new int[]{ 129 | Calendar.YEAR, Calendar.MONTH, Calendar.DAY_OF_MONTH, Calendar.HOUR_OF_DAY, Calendar.MINUTE, Calendar.SECOND 130 | }; 131 | String[] names = getPeriodNames(shrink); 132 | int accuracy = 0; 133 | for (int i = 0; i < types.length; i++) { 134 | if (accuracy > 2) { 135 | break; 136 | } 137 | int diff = dateDiff(types[i], fromDate, toDate, future); 138 | if (diff > 0) { 139 | accuracy++; 140 | if (shrink) { 141 | sb.append(diff).append(names[i * 2 + (diff > 1 ? 1 : 0)]); 142 | } else { 143 | sb.append(" ").append(diff).append(" ").append(names[i * 2 + (diff > 1 ? 1 : 0)]); 144 | } 145 | } 146 | } 147 | if (sb.length() == 0) { 148 | return "now"; 149 | } 150 | return sb.toString().trim(); 151 | } 152 | 153 | private static String[] getPeriodNames(boolean shrink) { 154 | if (shrink) { 155 | return new String[]{ 156 | "yr", "yr", "mo", "mo", "d", "d", "h", "h", "m", "m", "s", "s" 157 | }; 158 | } else { 159 | return new String[]{ 160 | "year", "years", "month", "months", "day", "days", "hour", "hours", "minute", "minutes", "second", "seconds" 161 | }; 162 | } 163 | } 164 | 165 | } -------------------------------------------------------------------------------- /src/main/java/co/melondev/Snitch/util/BlockUtil.java: -------------------------------------------------------------------------------- 1 | package co.melondev.Snitch.util; 2 | 3 | import com.google.gson.JsonArray; 4 | import com.google.gson.JsonElement; 5 | import com.google.gson.JsonObject; 6 | import net.minecraft.server.v1_12_R1.MojangsonParseException; 7 | import org.apache.commons.lang.Validate; 8 | import org.bukkit.*; 9 | import org.bukkit.block.*; 10 | import org.bukkit.block.banner.Pattern; 11 | import org.bukkit.block.banner.PatternType; 12 | import org.bukkit.entity.EntityType; 13 | import org.bukkit.material.Colorable; 14 | import org.bukkit.potion.PotionEffectType; 15 | 16 | import java.util.ArrayList; 17 | import java.util.List; 18 | import java.util.UUID; 19 | 20 | /** 21 | * Created by Devon on 7/16/18. 22 | */ 23 | public class BlockUtil { 24 | 25 | /** 26 | * Updates a physical block to match the provided metadata 27 | * 28 | * @param block the block to update 29 | * @param blockData the metadata 30 | * @throws MojangsonParseException if there's an issue parsing item data 31 | */ 32 | public static void rebuildBlock(Block block, JsonObject blockData) throws MojangsonParseException { 33 | 34 | // Start by updating the physical block 35 | block.setTypeIdAndData(Material.valueOf(blockData.get("type").getAsString()).getId(), 36 | blockData.get("data").getAsByte(), false); 37 | BlockState bs = block.getState(); 38 | bs.update(); 39 | bs = block.getState(); 40 | 41 | // If we have a banner, update the color, and any patterns 42 | if ((bs instanceof Banner)) { 43 | ((Banner) bs).setBaseColor(DyeColor.valueOf(blockData.get("baseColor").getAsString())); 44 | JsonArray patterns = blockData.getAsJsonArray("patterns"); 45 | for (JsonElement element : patterns) { 46 | JsonObject patternData = element.getAsJsonObject(); 47 | DyeColor color = DyeColor.valueOf(patternData.get("color").getAsString()); 48 | PatternType type = PatternType.valueOf(patternData.get("type").getAsString()); 49 | ((Banner) bs).addPattern(new Pattern(color, type)); 50 | } 51 | } 52 | 53 | // If we have a beacon, ensure the effects match what they should 54 | if ((bs instanceof Beacon)) { 55 | JsonElement primary = blockData.get("primaryEffect"); 56 | JsonElement secondary = blockData.get("secondaryEffect"); 57 | if (!primary.isJsonNull()) { 58 | ((Beacon) bs).setPrimaryEffect(PotionEffectType.getByName(primary.getAsString())); 59 | } 60 | if (!secondary.isJsonNull()) { 61 | ((Beacon) bs).setSecondaryEffect(PotionEffectType.getByName(secondary.getAsString())); 62 | } 63 | } 64 | // If the block can be colored, ensure it matches 65 | if ((bs instanceof Colorable)) { 66 | ((Colorable) bs).setColor(DyeColor.valueOf(blockData.get("color").getAsString())); 67 | } 68 | // If we have a brewing stand, set the stand values properly 69 | if ((bs instanceof BrewingStand)) { 70 | ((BrewingStand) bs).setBrewingTime(blockData.get("brewingTime").getAsInt()); 71 | ((BrewingStand) bs).setFuelLevel(blockData.get("fuelLevel").getAsInt()); 72 | } 73 | // If this block is lockable, set it to match 74 | if ((bs instanceof Lockable)) { 75 | Lockable l = (Lockable) bs; 76 | JsonElement lock = blockData.get("locked"); 77 | if (!lock.isJsonNull()) { 78 | l.setLock(lock.getAsString()); 79 | } 80 | } 81 | // If this was a command block, put the name and command back 82 | if ((bs instanceof CommandBlock)) { 83 | CommandBlock cb = (CommandBlock) bs; 84 | cb.setName(blockData.get("name").getAsString()); 85 | cb.setCommand(blockData.get("command").getAsString()); 86 | } 87 | // If this was a spawner, ensure the values match 88 | if ((bs instanceof CreatureSpawner)) { 89 | CreatureSpawner cs = (CreatureSpawner) bs; 90 | cs.setSpawnRange(blockData.get("spawnRange").getAsInt()); 91 | cs.setSpawnedType(EntityType.valueOf(blockData.get("spawnType").getAsString())); 92 | cs.setSpawnCount(blockData.get("spawnCount").getAsInt()); 93 | cs.setRequiredPlayerRange(blockData.get("requiredPlayers").getAsInt()); 94 | cs.setMinSpawnDelay(blockData.get("minDelay").getAsInt()); 95 | cs.setMaxSpawnDelay(blockData.get("maxDelay").getAsInt()); 96 | cs.setMaxNearbyEntities(blockData.get("maxNearby").getAsInt()); 97 | cs.setDelay(blockData.get("delay").getAsInt()); 98 | } 99 | // If this block could be named, put the custom name back 100 | if ((bs instanceof Nameable)) { 101 | Nameable n = (Nameable) bs; 102 | JsonElement name = blockData.get("customName"); 103 | if (!name.isJsonNull()) { 104 | n.setCustomName(name.getAsString()); 105 | } 106 | } 107 | // If this portal goes anywhere specific, set that back 108 | if ((bs instanceof EndGateway)) { 109 | ((EndGateway) bs).setExactTeleport(blockData.get("exactTeleport").getAsBoolean()); 110 | ((EndGateway) bs).setExitLocation(JsonUtil.fromJson(blockData.getAsJsonObject("exitLocation"))); 111 | } 112 | // Flowers are important 113 | if ((bs instanceof FlowerPot)) { 114 | JsonElement content = blockData.get("contents"); 115 | if (!content.isJsonNull()) { 116 | org.bukkit.inventory.ItemStack itemStack = ItemUtil.JSONtoItemStack(content.getAsString()); 117 | ((FlowerPot) bs).setContents(itemStack.getData()); 118 | } 119 | } 120 | // Match the cooking and burn times 121 | if ((bs instanceof Furnace)) { 122 | ((Furnace) bs).setCookTime(blockData.get("cookTime").getAsShort()); 123 | ((Furnace) bs).setBurnTime(blockData.get("burnTime").getAsShort()); 124 | } 125 | // Music are important 126 | if ((bs instanceof NoteBlock)) { 127 | ((NoteBlock) bs).setRawNote(blockData.get("note").getAsByte()); 128 | } 129 | // Restore sign text 130 | if ((bs instanceof Sign)) { 131 | JsonArray signText = blockData.getAsJsonArray("text"); 132 | int line = 0; 133 | for (JsonElement element : signText) { 134 | ((Sign) bs).setLine(line++, element.getAsString()); 135 | } 136 | } 137 | // Restore skull meta 138 | if ((bs instanceof Skull)) { 139 | ((Skull) bs).setSkullType(SkullType.valueOf(blockData.get("skullType").getAsString())); 140 | ((Skull) bs).setRotation(BlockFace.valueOf(blockData.get("rotation").getAsString())); 141 | JsonElement owner = blockData.get("owningPlayer"); 142 | if (!owner.isJsonNull()) { 143 | UUID uuid = UUID.fromString(owner.getAsString()); 144 | ((Skull) bs).setOwningPlayer(Bukkit.getOfflinePlayer(uuid)); 145 | } 146 | } 147 | 148 | // We don't log inventory contents to the block break itself. This would be considered an ITEM REMOVAL, which is rolled back separately. 149 | 150 | bs.update(); 151 | } 152 | 153 | /** 154 | * Remove any of the provided material types and replace them with air 155 | * @param materialList the list of materials to set to air 156 | * @param location the location to remove around 157 | * @param range the range to remove 158 | * @return a list of changed blocks 159 | */ 160 | public static List removeNear(List materialList, Location location, int range) { 161 | List changes = new ArrayList<>(); 162 | Validate.notNull(location, "Location can't be null."); 163 | Validate.isTrue(range > 0, "Range must be bigger than 0."); 164 | Validate.notEmpty(materialList, "Material list can't be empty."); 165 | 166 | int x = location.getBlockX(); 167 | int y = location.getBlockY(); 168 | int z = location.getBlockZ(); 169 | World world = location.getWorld(); 170 | 171 | for (int xx = x - range; xx <= x + range; xx++) { 172 | for (int yy = y - range; yy <= y + range; yy++) { 173 | for (int zz = z - range; zz <= z + range; zz++) { 174 | Location l = new Location(world, xx, yy, zz); 175 | if (l.getBlock().getType() == Material.AIR) 176 | continue; 177 | if (materialList.contains(l.getBlock().getType())) { 178 | final BlockState old = location.getBlock().getState(); 179 | l.getBlock().setType(Material.AIR); 180 | final BlockState newState = location.getBlock().getState(); 181 | changes.add(new AdjustedBlock(old, newState)); 182 | } 183 | } 184 | } 185 | } 186 | return changes; 187 | } 188 | 189 | } 190 | -------------------------------------------------------------------------------- /src/main/java/co/melondev/Snitch/enums/EnumActionVariables.java: -------------------------------------------------------------------------------- 1 | package co.melondev.Snitch.enums; 2 | 3 | import co.melondev.Snitch.util.ItemUtil; 4 | import com.google.gson.JsonElement; 5 | import com.google.gson.JsonObject; 6 | import net.minecraft.server.v1_12_R1.MojangsonParseException; 7 | import org.apache.commons.lang.WordUtils; 8 | import org.bukkit.ChatColor; 9 | import org.bukkit.DyeColor; 10 | import org.bukkit.Material; 11 | import org.bukkit.command.CommandSender; 12 | import org.bukkit.enchantments.Enchantment; 13 | import org.bukkit.inventory.ItemStack; 14 | 15 | import java.text.DecimalFormat; 16 | import java.util.ArrayList; 17 | import java.util.List; 18 | import java.util.Map; 19 | 20 | /** 21 | * Specifies how to internet variables within {@link EnumAction#getExplained()} 22 | */ 23 | public enum EnumActionVariables { 24 | 25 | 26 | BLOCK("block", false) { 27 | @Override 28 | public String getReplacement(JsonObject obj) { 29 | String type = obj.get("type").getAsString(); 30 | byte data = obj.get("data").getAsByte(); 31 | 32 | try { 33 | Material m = Material.valueOf(type.toUpperCase()); 34 | return ItemUtil.getItemName(new ItemStack(m, 1, data)); 35 | }catch (Exception ex){ 36 | return type; 37 | } 38 | } 39 | }, 40 | DYE("dye", false) { 41 | @Override 42 | public String getReplacement(JsonObject obj) { 43 | DyeColor color = DyeColor.valueOf(obj.get("type").getAsString()); 44 | ChatColor chatColor = ChatColor.WHITE; 45 | 46 | switch (color) { 47 | case RED: 48 | chatColor = ChatColor.RED; 49 | break; 50 | case BLUE: 51 | chatColor = ChatColor.BLUE; 52 | break; 53 | case CYAN: 54 | chatColor = ChatColor.DARK_AQUA; 55 | break; 56 | case GRAY: 57 | chatColor = ChatColor.DARK_GRAY; 58 | break; 59 | case LIME: 60 | chatColor = ChatColor.GREEN; 61 | break; 62 | case PINK: 63 | chatColor = ChatColor.LIGHT_PURPLE; 64 | break; 65 | case BLACK: 66 | chatColor = ChatColor.BLACK; 67 | break; 68 | case BROWN: 69 | chatColor = ChatColor.DARK_RED; 70 | break; 71 | case GREEN: 72 | chatColor = ChatColor.DARK_GREEN; 73 | break; 74 | case WHITE: 75 | chatColor = ChatColor.WHITE; 76 | break; 77 | case ORANGE: 78 | chatColor = ChatColor.GOLD; 79 | break; 80 | case PURPLE: 81 | chatColor = ChatColor.DARK_PURPLE; 82 | break; 83 | case SILVER: 84 | chatColor = ChatColor.GRAY; 85 | break; 86 | case YELLOW: 87 | chatColor = ChatColor.YELLOW; 88 | break; 89 | case MAGENTA: 90 | chatColor = ChatColor.LIGHT_PURPLE; 91 | break; 92 | case LIGHT_BLUE: 93 | chatColor = ChatColor.AQUA; 94 | break; 95 | } 96 | 97 | return chatColor + WordUtils.capitalizeFully(color.name().replace("_", " ")) + "§7"; 98 | } 99 | }, 100 | ENCHANTS("enchants", false) { 101 | @Override 102 | public String getReplacement(JsonObject obj) { 103 | List names = new ArrayList<>(); 104 | for(Map.Entry entry : obj.entrySet()){ 105 | String enchantmentName = entry.getKey(); 106 | int level = entry.getValue().getAsInt(); 107 | Enchantment enchantment = Enchantment.getByName(enchantmentName); 108 | names.add(ItemUtil.getEnchantmentName(enchantment) + " " + level); 109 | } 110 | return names.isEmpty() ? "nothing" : String.join(", ", names); 111 | } 112 | }, 113 | 114 | SOURCE_BLOCK("source", false) { 115 | @Override 116 | public String getReplacement(JsonObject obj) { 117 | return BLOCK.getReplacement(obj); 118 | } 119 | }, 120 | BUCKET("bucket", false) { 121 | @Override 122 | public String getReplacement(JsonObject obj) { 123 | return obj.get("type").getAsString().toLowerCase().replace("_", " "); 124 | } 125 | }, 126 | ITEM("item", false) { 127 | @Override 128 | public String getReplacement(JsonObject obj) { 129 | try { 130 | ItemStack itemStack = ItemUtil.JSONtoItemStack(obj.get("raw").getAsString()); 131 | return itemStack.getAmount() + "x " + ItemUtil.getItemName(itemStack); 132 | } catch (MojangsonParseException e) { 133 | e.printStackTrace(); 134 | return "???"; 135 | } 136 | } 137 | }, 138 | ENTITY("entity", false) { 139 | @Override 140 | public String getReplacement(JsonObject obj) { 141 | return obj.get("entityType").getAsString().replace("_", " ").toLowerCase(); 142 | } 143 | }, 144 | MESSAGE("message", false) { 145 | @Override 146 | public String getReplacement(JsonObject obj) { 147 | return obj.get("message").getAsString(); 148 | } 149 | }, 150 | CAUSE("cause", false) { 151 | @Override 152 | public String getReplacement(JsonObject obj) { 153 | return obj.get("cause").getAsString().replace("_", " ").toLowerCase(); 154 | } 155 | }, 156 | LOCATION("location", false) { 157 | @Override 158 | public String getReplacement(JsonObject obj) { 159 | String world = obj.get("world").getAsString(); 160 | double x = obj.get("x").getAsDouble(); 161 | double y = obj.get("y").getAsDouble(); 162 | double z = obj.get("z").getAsDouble(); 163 | DecimalFormat df = new DecimalFormat("#"); 164 | return df.format(x) + "x " + df.format(y) + "y " + df.format(z) + "z in " + world; 165 | } 166 | }, 167 | IP("ip", true) { 168 | @Override 169 | public String getReplacement(JsonObject obj) { 170 | return obj.get("ip").getAsString(); 171 | } 172 | }, 173 | SPAWNEGG("spawnegg", false) { 174 | @Override 175 | public String getReplacement(JsonObject obj) { 176 | return obj.get("type").getAsString(); 177 | } 178 | }, 179 | VEHICLE("vehicle", false) { 180 | @Override 181 | public String getReplacement(JsonObject obj) { 182 | return obj.get("type").getAsString(); 183 | } 184 | }, 185 | XP("xp", false) { 186 | @Override 187 | public String getReplacement(JsonObject obj) { 188 | return obj.get("amount").getAsInt() + ""; 189 | } 190 | }, 191 | SLOT("slot", false) { 192 | @Override 193 | public String getReplacement(JsonObject obj) { 194 | return obj.get("slot").getAsString().toLowerCase(); 195 | } 196 | }, 197 | OLDSIGNTEXT("old", false) { 198 | @Override 199 | public String getReplacement(JsonObject obj) { 200 | List lines = new ArrayList<>(); 201 | JsonObject old = obj.getAsJsonObject("old"); 202 | for (int i = 0; i < 4; i++) { 203 | lines.add(old.get("line" + i).getAsString()); 204 | } 205 | return String.join(", ", lines); 206 | } 207 | }, 208 | NEWSIGNTEXT("new", false) { 209 | @Override 210 | public String getReplacement(JsonObject obj) { 211 | List lines = new ArrayList<>(); 212 | JsonObject old = obj.getAsJsonObject("new"); 213 | for (int i = 0; i < 4; i++) { 214 | lines.add(old.get("line" + i).getAsString()); 215 | } 216 | return String.join(", ", lines); 217 | } 218 | }; 219 | 220 | /** 221 | * The key to replace in {@link EnumAction#getExplained()} 222 | */ 223 | private String key; 224 | private boolean requirePermission; 225 | 226 | EnumActionVariables(String key, boolean requirePermission) { 227 | this.key = key; 228 | this.requirePermission = requirePermission; 229 | } 230 | 231 | public boolean shouldRedactDetailsFor(CommandSender sender) { 232 | return isRequirePermission() && !sender.hasPermission("snitch.viewdata." + getKey().toLowerCase()); 233 | } 234 | 235 | public boolean isRequirePermission() { 236 | return requirePermission; 237 | } 238 | 239 | /** 240 | * Returns the proper replacement for a variable 241 | * 242 | * @param obj the metadata from an event 243 | * @return the replacement 244 | */ 245 | public abstract String getReplacement(JsonObject obj); 246 | 247 | public String getKey() { 248 | return key; 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /src/main/java/co/melondev/Snitch/enums/EnumAction.java: -------------------------------------------------------------------------------- 1 | package co.melondev.Snitch.enums; 2 | 3 | import co.melondev.Snitch.SnitchPlugin; 4 | import co.melondev.Snitch.entities.SnitchProcessHandler; 5 | import co.melondev.Snitch.handlers.*; 6 | import org.apache.commons.lang.WordUtils; 7 | 8 | import java.util.ArrayList; 9 | import java.util.HashMap; 10 | import java.util.List; 11 | import java.util.Map; 12 | 13 | /** 14 | * A list of logged actions within Snitch 15 | */ 16 | public enum EnumAction { 17 | 18 | BLOCK_BURN(0, "burn", "%actor burned %block", new BlockDestructionHandler()), 19 | BLOCK_BREAK(1, "break", "%actor broke %block", new BlockDestructionHandler()), 20 | BLOCK_DISPENSE(2, "dispense", "%block dispensed %item", new NoCapabilityHandler()), 21 | BLOCK_FADE(3, "fade", "%block disappeared", new BlockDestructionHandler()), 22 | BLOCK_FALL(4, "fall", "%block fell", new BlockDestructionHandler()), 23 | BLOCK_FORM(5, "form", "%block formed", new BlockCreationHandler()), 24 | BLOCK_PLACE(6, "place", "%actor placed %block", new BlockCreationHandler()), 25 | BLOCK_SHIFT(7, "shift", "%block shifted", new NoCapabilityHandler()), // TODO 26 | BLOCK_SPREAD(8, "spread", "%source spread to %block", new BlockSpreadHandler()), 27 | BLOCK_USE(9, "use", "%actor used %block", new NoCapabilityHandler()), 28 | BLOCK_EXPLODE(16, "explode", "%actor blew up %block", new BlockDestructionHandler()), 29 | BONEMEAL_USE(10, "bonemeal", "%actor used bonemeal on %block", new NoCapabilityHandler()), // TODO 30 | BUCKET_FILL(11, "fill", "%actor filled a %bucket", new BlockDestructionHandler()), 31 | BUCKET_EMPTY(12, "empty", "%actor emptied a %bucket", new BlockCreationHandler()), 32 | CAKE_EAT(13, "eat", "%actor ate cake", new CakeHandler()), 33 | CONTAINER_ACCESS(14, "access", "%actor accessed %block", new NoCapabilityHandler()), // TODO 34 | CRAFT_ITEM(15, "craft", "%actor crafted %item", new NoCapabilityHandler()), 35 | CROP_TRAMPLE(17, "trample", "%actor trampled %block", new BlockDestructionHandler()), 36 | ENCHANT_ITEM(18, "enchant", "%actor enchanted %item with %enchants", new NoCapabilityHandler()), 37 | ENTITY_BREAK(19, "break", "%actor broke %block", new EntityDeathHandler()), 38 | ENTITY_DYE(20, "dye", "%actor dyed %entity %dye", new NoCapabilityHandler()), 39 | ENTITY_EXPLODE(21, "explode", "%actor exploded", new NoCapabilityHandler()), 40 | ENTITY_FOLLOW(22, "lure", "%actor lured %entity", new NoCapabilityHandler()), 41 | ENTITY_FORMED(23, "form", "%actor formed", new NoCapabilityHandler()), 42 | ENTITY_KILL(24, "kill", "%actor killed %entity", new EntityDeathHandler()), 43 | ENTITY_LEASH(25, "leash", "%actor leashed %entity", new NoCapabilityHandler()), 44 | ENTITY_SHEAR(26, "shear", "%actor sheared %entity", new NoCapabilityHandler()), 45 | ENTITY_SPAWN(27, "spawn", "%entity spawned from %cause", new NoCapabilityHandler()), 46 | ENTITY_UNLEASH(28, "unleash", "%actor unleashed %entity", new NoCapabilityHandler()), 47 | ARMORSTAND_CREATE(64, "place", "%actor placed an armor stand", new NoCapabilityHandler()), 48 | ARMORSTAND_BREAK(63, "break", "%actor broke an armor stand", new EntityDeathHandler()), 49 | ARMORSTAND_EDIT(65, "edit", "%actor changed armor stand's %slot", new NoCapabilityHandler()), 50 | FIRE_SPREAD(30, "fire", "fire spread to %block", new BlockSpreadHandler()), 51 | FIREWORK_LAUNCH(31, "firework", "%actor launched firework", new NoCapabilityHandler()), 52 | HANGING_PLACE(32, "hang", "%actor hung art", new NoCapabilityHandler()), 53 | HANGING_BREAK(33, "unhang", "%actor knocked down art", new EntityDeathHandler()), 54 | ITEM_DROP(34, "drop", "%actor dropped %item", new NoCapabilityHandler()), 55 | ITEM_INSERT(35, "insert", "%actor inserted %item", new ItemInsertHandler()), 56 | ITEM_PICKUP(36, "pickup", "%actor picked up %item", new NoCapabilityHandler()), 57 | ITEM_TAKE(37, "take", "%actor took %item", new ItemTakeHandler()), 58 | ITEM_ROTATE(38, "rotate", "%actor rotated %item", new NoCapabilityHandler()), 59 | LAVA_FLOW(39, "flow", "lava flowed", new BlockCreationHandler()), 60 | BLOCK_IGNITE(40, "ignite", "%actor ignited %block", new BlockCreationHandler()), 61 | LEAF_DECAY(41, "decay", "leaf decayed", new BlockDestructionHandler()), 62 | LIGHTNING(42, "lightning", "lightning struck", new NoCapabilityHandler()), 63 | MUSHROOM_GROW(43, "grow", "%actor grew large mushroom", new BlockCreationHandler()), 64 | PLAYER_CHAT(44, "chat", "%actor said: %message", new NoCapabilityHandler()), 65 | PLAYER_COMMAND(45, "command", "%actor executed: %message", new NoCapabilityHandler()), 66 | PLAYER_DEATH(46, "death", "%actor died", new NoCapabilityHandler()), 67 | PLAYER_JOIN(47, "join", "%actor joined from %ip", new NoCapabilityHandler()), 68 | PLAYER_QUIT(48, "quit", "%actor left", new NoCapabilityHandler()), 69 | PLAYER_TELEPORT(49, "teleport", "%actor teleported to %location via %cause", new NoCapabilityHandler()), 70 | POTION_SPLASH(50, "splash", "%actor threw potion", new NoCapabilityHandler()), 71 | SHEEP_EAT(51, "sheep", "sheep ate %block", new NoCapabilityHandler()), 72 | SIGN_CHANGE(52, "sign", "%actor changed sign: %old > %new", new SignChangeHandler()), 73 | SPAWNEGG_USE(53, "spawnegg", "%actor used %spawnegg egg", new NoCapabilityHandler()), 74 | TNT_PRIME(54, "tnt", "%actor primed TNT", new NoCapabilityHandler()), 75 | TREE_GROW(55, "grow", "%actor grew tree", new BlockCreationHandler()), 76 | VEHICLE_BREAK(56, "break", "%actor broke a %vehicle", new EntityDeathHandler()), 77 | VEHICLE_ENTER(57, "enter", "%actor entered a %vehicle", new NoCapabilityHandler()), 78 | VEHICLE_EXIT(58, "exit", "%actor left a %vehicle", new NoCapabilityHandler()), 79 | VEHICLE_PLACE(59, "place", "%actor placed a %vehicle", new NoCapabilityHandler()), 80 | WATER_FLOW(60, "flow", "water flowed", new BlockCreationHandler()), 81 | WORLD_EDIT(61, "we", "%actor used worldedit", new NoCapabilityHandler()), 82 | XP_PICKUP(62, "xp", "%actor picked up %xp XP", new NoCapabilityHandler()); 83 | 84 | /** 85 | * As actions are looked up, we cache them here for speedy lookups in the future 86 | */ 87 | private static Map actionMap = new HashMap<>(); 88 | 89 | /** 90 | * The ID of this action 91 | */ 92 | private int id; 93 | 94 | /** 95 | * The shortnmae for an action. This doesn't have to be unique, and is used to group similar actions 96 | */ 97 | private String name; 98 | 99 | /** 100 | * The message to show in lookups. Variables from {@link EnumActionVariables} 101 | */ 102 | private String explained; 103 | 104 | /** 105 | * The controller for rollbacks, restores, and previews 106 | */ 107 | private SnitchProcessHandler processHandler; 108 | 109 | EnumAction(int id, String name, String explained, SnitchProcessHandler processHandler) { 110 | this.id = id; 111 | this.name = name; 112 | this.explained = explained; 113 | this.processHandler = processHandler; 114 | } 115 | 116 | /** 117 | * Returns a list of actions matching this name. There can be multiple results, as some actions are related. 118 | * 119 | * @param name the name to search for 120 | * @return a list of matching actions 121 | */ 122 | public static List getByName(String name) { 123 | List results = new ArrayList<>(); 124 | for (EnumAction action : EnumAction.values()) { 125 | if (action.name().equalsIgnoreCase(name) || action.getName().equalsIgnoreCase(name)) { 126 | results.add(action); 127 | } 128 | } 129 | return results; 130 | } 131 | 132 | /** 133 | * Gets an action by the numeric ID. Checks the cached map first 134 | * @param action_id the id to search for 135 | * @return the matching action 136 | */ 137 | public static EnumAction getById(int action_id) { 138 | if (actionMap.containsKey(action_id)) { 139 | return actionMap.get(action_id); 140 | } 141 | for (EnumAction action : EnumAction.values()) { 142 | if (action.getId() == action_id) { 143 | actionMap.put(action.getId(), action); 144 | return action; 145 | } 146 | } 147 | return null; 148 | } 149 | 150 | public SnitchProcessHandler getProcessHandler() { 151 | return processHandler; 152 | } 153 | 154 | /** 155 | * Returns the {@link #name}, properly capitalized and without underscores 156 | * @return the friendly name 157 | */ 158 | public String getFriendlyFullName() { 159 | return WordUtils.capitalizeFully(name().replace("_", " ")); 160 | } 161 | 162 | /** 163 | * Gets the permission node for an action 164 | * @return the node for this action 165 | */ 166 | public String getNode(){ 167 | return "snitch.action." + name().toLowerCase().replace("_", ""); 168 | } 169 | 170 | /** 171 | * Checks if this action is logged against the configuration 172 | * @return whether or not this action is enabled 173 | */ 174 | public boolean isEnabled(){ 175 | return !SnitchPlugin.getInstance().getConfiguration().getDisabledLogging().contains(this); 176 | } 177 | 178 | public String getExplained() { 179 | return explained; 180 | } 181 | 182 | public int getId() { 183 | return id; 184 | } 185 | 186 | public String getName() { 187 | return name; 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/main/java/co/melondev/Snitch/enums/EnumParam.java: -------------------------------------------------------------------------------- 1 | package co.melondev.Snitch.enums; 2 | 3 | import co.melondev.Snitch.SnitchPlugin; 4 | import co.melondev.Snitch.entities.SnitchPlayer; 5 | import co.melondev.Snitch.entities.SnitchPosition; 6 | import co.melondev.Snitch.entities.SnitchQuery; 7 | import co.melondev.Snitch.entities.SnitchWorld; 8 | import co.melondev.Snitch.util.SnitchDatabaseException; 9 | import co.melondev.Snitch.util.TimeUtil; 10 | import org.apache.commons.lang.Validate; 11 | import org.bukkit.Bukkit; 12 | import org.bukkit.World; 13 | import org.bukkit.entity.Player; 14 | 15 | import java.sql.SQLException; 16 | import java.text.ParseException; 17 | import java.text.SimpleDateFormat; 18 | import java.util.Arrays; 19 | import java.util.List; 20 | 21 | /** 22 | * Created by Devon on 7/13/18. 23 | * 24 | * Stores all parameters that can be searched by, as well as the logic for interpreting queries 25 | */ 26 | @SuppressWarnings("Duplicates") 27 | public enum EnumParam { 28 | 29 | /** 30 | * Allows searching for records by a specific {@link SnitchPlayer} 31 | */ 32 | PLAYER(Arrays.asList("player", "players", "p", "actor"), "player Turqmelon") { 33 | @Override 34 | public void parse(Player player, SnitchQuery query, String[] values) throws SnitchDatabaseException { 35 | for (String playerName : values) { 36 | boolean exclude = playerName.startsWith("!"); 37 | if (exclude) { 38 | playerName = playerName.substring(1); 39 | } 40 | SnitchPlayer pl = SnitchPlugin.getInstance().getStorage().getPlayer(playerName); 41 | Validate.notNull(pl, "Unknown player: " + playerName); 42 | if (exclude) { 43 | query.addExcludedPlayer(pl); 44 | } else { 45 | query.addPlayers(pl); 46 | } 47 | } 48 | } 49 | }, 50 | /** 51 | * Allows searching for records by specific {@link EnumAction} 52 | */ 53 | ACTION(Arrays.asList("action", "actions", "a"), "action break") { 54 | @Override 55 | public void parse(Player player, SnitchQuery query, String[] values) throws SnitchDatabaseException { 56 | for (String action : values) { 57 | boolean exclude = action.startsWith("!"); 58 | if (exclude) { 59 | action = action.substring(1); 60 | } 61 | List results = EnumAction.getByName(action); 62 | Validate.notEmpty(results, "Unknown action: " + action); 63 | for (EnumAction a : results) { 64 | if (exclude) { 65 | query.addExcludedAction(a); 66 | } else { 67 | query.addActions(a); 68 | } 69 | } 70 | } 71 | } 72 | }, 73 | /** 74 | * Allows filtering results to only those that happened from a specific time 75 | */ 76 | SINCE(Arrays.asList("since", "from", "s"), "since 1d") { 77 | @Override 78 | public void parse(Player player, SnitchQuery query, String[] values) throws SnitchDatabaseException { 79 | Validate.notEmpty(values, "No time specified for " + name() + "."); 80 | Validate.isTrue(values.length == 1, "Multiple time values provided for " + name() + "."); 81 | query.setSinceTime(EnumParam.getUnixTime(values[0], false)); 82 | } 83 | }, 84 | /** 85 | * Allows filtering results to only those that happened before a specific time 86 | */ 87 | BEFORE(Arrays.asList("before", "prior", "b"), "before 06/12/18") { 88 | @Override 89 | public void parse(Player player, SnitchQuery query, String[] values) throws SnitchDatabaseException { 90 | Validate.notEmpty(values, "No time specified for " + name() + "."); 91 | Validate.isTrue(values.length == 1, "Multiple time values provided for " + name() + "."); 92 | query.setBeforeTime(EnumParam.getUnixTime(values[0], false)); 93 | } 94 | }, 95 | /** 96 | * Allows filtering results to only those within a specific world 97 | * TODO: Add the ability to parse the "world" name correctly. 98 | */ 99 | WORLD(Arrays.asList("world", "w"), "world world_nether") { 100 | @Override 101 | public void parse(Player player, SnitchQuery query, String[] values) throws SnitchDatabaseException { 102 | Validate.notEmpty(values, "No world specified."); 103 | Validate.isTrue(values.length == 1, "Multiple world values provided."); 104 | World world = Bukkit.getWorld(values[0]); 105 | Validate.notNull(world, "Unknown world: " + values[0]); 106 | SnitchWorld w = SnitchPlugin.getInstance().getStorage().register(world); 107 | query.setWorld(w); 108 | } 109 | }, 110 | /** 111 | * Allow specifying another coordinate set to search other areas of a world 112 | * If a world is not specified, we'll default it to the one of the current player 113 | */ 114 | COORDS(Arrays.asList("coords", "relative", "pos", "position"), "relative 100 150 100") { 115 | @Override 116 | public void parse(Player player, SnitchQuery query, String[] values) throws SnitchDatabaseException { 117 | Validate.isTrue(values.length == 3, "Specify an x, y, and z value."); 118 | try { 119 | int x = Integer.parseInt(values[0]); 120 | int y = Integer.parseInt(values[1]); 121 | int z = Integer.parseInt(values[2]); 122 | SnitchPosition pos = new SnitchPosition(x, y, z); 123 | query.setPosition(pos); 124 | if (query.getWorld() == null) 125 | query.setWorld(SnitchPlugin.getInstance().getStorage().register(player.getWorld())); 126 | } catch (NumberFormatException ex) { 127 | throw new IllegalArgumentException("Invalid number provided for " + name() + " param."); 128 | } 129 | } 130 | }, 131 | /** 132 | * Allows limiting results to a certain set. 133 | * For rollbacks, we don't add a limit, for lookups we default this to 1,000 if one is not specified. 134 | */ 135 | LIMIT(Arrays.asList("limit", "lim", "cap"), "limit 50") { 136 | @Override 137 | public void parse(Player player, SnitchQuery query, String[] values) throws SnitchDatabaseException { 138 | Validate.notEmpty(values, "No limit specified."); 139 | Validate.isTrue(values.length == 1, "Multiple limit values specified."); 140 | try { 141 | int limit = Integer.parseInt(values[0]); 142 | if (limit < 1) 143 | throw new NumberFormatException(); 144 | query.limit(limit); 145 | } catch (NumberFormatException ex) { 146 | throw new IllegalArgumentException("Invalid limit: " + values[0]); 147 | } 148 | } 149 | }, 150 | /** 151 | * Filters records to only those that happen within a specific range 152 | */ 153 | RADIUS(Arrays.asList("area", "radius", "range"), "area 20") { 154 | @Override 155 | public void parse(Player player, SnitchQuery query, String[] values) throws SnitchDatabaseException { 156 | Validate.notEmpty(values, "No range specified."); 157 | Validate.isTrue(values.length == 1, "Multiple range values specified."); 158 | try { 159 | int range = Integer.parseInt(values[0]); 160 | SnitchPosition position = query.getPosition(); 161 | if (position == null) { 162 | position = new SnitchPosition(player.getLocation()); 163 | query.setPosition(position); 164 | } 165 | if (query.getWorld() == null) { 166 | World world = player.getWorld(); 167 | SnitchWorld w = SnitchPlugin.getInstance().getStorage().register(world); 168 | query.setWorld(w); 169 | } 170 | query.setRadius(position, range); 171 | } catch (NumberFormatException ex) { 172 | throw new IllegalArgumentException("Invalid range: " + values[0]); 173 | } 174 | } 175 | }; 176 | 177 | private List keywords; 178 | private String example; 179 | 180 | EnumParam(List keywords, String example) { 181 | this.keywords = keywords; 182 | this.example = example; 183 | } 184 | 185 | /** 186 | * Gets the unix time from either an absolute date or a relative time 187 | * 188 | * @param date the date in MM/dd/yy format or in format defined by {@link TimeUtil} 189 | * @param future whether or not this timestamp is in the future (required by {@link TimeUtil}) 190 | * @return the specified time in unix time 191 | */ 192 | private static long getUnixTime(String date, boolean future) { 193 | SimpleDateFormat fullDate = new SimpleDateFormat("MM/dd/yy"); 194 | try { 195 | return fullDate.parse(date).getTime(); 196 | } catch (ParseException e) { 197 | try { 198 | return TimeUtil.parseDateDiff(date, future); 199 | } catch (Exception e1) { 200 | throw new IllegalArgumentException("Unknown time or date: " + date); 201 | } 202 | } 203 | } 204 | 205 | /** 206 | * Matches the specified string to a param 207 | * @param keyword the keyword to search for 208 | * @return the matching param, null if no result 209 | */ 210 | public static EnumParam getByKeyword(String keyword) { 211 | for (EnumParam param : values()) { 212 | if (param.getKeywords().contains(keyword.toLowerCase())) { 213 | return param; 214 | } 215 | } 216 | return null; 217 | } 218 | 219 | public String getExample() { 220 | return example; 221 | } 222 | 223 | public List getKeywords() { 224 | return keywords; 225 | } 226 | 227 | /** 228 | * Parses the specified values and updates your search query 229 | * @param player the player performing the lookup or rollback 230 | * @param query the query object associated with their actions 231 | * @param values the values provided following the param keyword 232 | * @throws SQLException if there's any database errors 233 | */ 234 | public abstract void parse(Player player, SnitchQuery query, String[] values) throws SnitchDatabaseException; 235 | 236 | } 237 | -------------------------------------------------------------------------------- /src/main/java/co/melondev/Snitch/entities/SnitchPreview.java: -------------------------------------------------------------------------------- 1 | package co.melondev.Snitch.entities; 2 | 3 | import co.melondev.Snitch.SnitchPlugin; 4 | import co.melondev.Snitch.enums.EnumSnitchActivity; 5 | import co.melondev.Snitch.util.*; 6 | import org.bukkit.Location; 7 | import org.bukkit.Material; 8 | import org.bukkit.block.Block; 9 | import org.bukkit.entity.Entity; 10 | import org.bukkit.entity.Player; 11 | import org.bukkit.scheduler.BukkitRunnable; 12 | 13 | import java.util.*; 14 | 15 | /** 16 | * Created by Devon on 7/14/18. 17 | */ 18 | 19 | /** 20 | * Contains primary logic for rollbacks and restores. 21 | */ 22 | public class SnitchPreview implements Previewable { 23 | 24 | /** 25 | * The active session associated with this activity. 26 | */ 27 | protected SnitchSession session; 28 | 29 | /** 30 | * The type of activity. 31 | */ 32 | protected EnumSnitchActivity activity; 33 | 34 | /** 35 | * A map of the entities that were moved by this activity 36 | */ 37 | protected Map movedEntities = new HashMap<>(); 38 | 39 | /** 40 | * A summary of activity statistics 41 | */ 42 | protected int failed, applied, planned, changesIndex = 0; 43 | 44 | /** 45 | * A list of entries that still need to be processed 46 | */ 47 | private List pendingEntries; 48 | 49 | /** 50 | * Code to call at the end of this activity 51 | */ 52 | private SnitchCallback callback; 53 | 54 | /** 55 | * We initialize a preview with a session and a callback 56 | * 57 | * @param session the session associated with this activity. This contains the player, query, last action etc. 58 | * @param callback the callback to run upon completion of this activity 59 | */ 60 | public SnitchPreview(SnitchSession session, SnitchCallback callback) { 61 | this.session = session; 62 | this.activity = EnumSnitchActivity.PREVIEW; 63 | this.pendingEntries = new ArrayList<>(); 64 | this.pendingEntries.addAll(session.getEntries()); 65 | this.callback = callback; 66 | } 67 | 68 | /** 69 | * Cancels the visualization and reverts it to those as decided by the server. 70 | */ 71 | @Override 72 | public void cancelPreview() { 73 | for (Location loc : session.getAdjustedBlocks()) { 74 | Block block = loc.getBlock(); 75 | session.getPlayer().sendBlockChange(block.getLocation(), block.getType(), block.getData()); 76 | } 77 | MsgUtil.success("Preview cancelled."); 78 | } 79 | 80 | /** 81 | * Converts this preview to an actual rollback 82 | * Resets the statistics and redefines the callback to be {@link co.melondev.Snitch.entities.SnitchRollback.DefaultRollbackCallback} 83 | */ 84 | @Override 85 | public void applyPreview() { 86 | session.getPlayer().sendMessage(MsgUtil.info("Applying rollback: " + session.getQuery().getSearchSummary().toLowerCase() + "...")); 87 | this.activity = EnumSnitchActivity.ROLLBACK; 88 | this.applied = 0; 89 | this.failed = 0; 90 | this.planned = 0; 91 | this.callback = new SnitchRollback.DefaultRollbackCallback(System.currentTimeMillis()); 92 | apply(); 93 | } 94 | 95 | /** 96 | * @return whether or not this is a preview 97 | */ 98 | public boolean isPreview() { 99 | return this.activity == EnumSnitchActivity.PREVIEW; 100 | } 101 | 102 | /** 103 | * Applies this query to this world. 104 | * If doing this from a preview, call {@link #applyPreview()} first 105 | */ 106 | @Override 107 | public void apply() { 108 | 109 | // Do we have entries to process? 110 | if (!pendingEntries.isEmpty()) { 111 | session.getPlayer().sendMessage(MsgUtil.record("Planned rollback entries: §l" + pendingEntries.size())); 112 | changesIndex = 0; 113 | 114 | // We process these entries in batches per tick. By doing this we don't overload 115 | // the server with a mass amount of adjustments. 116 | new BukkitRunnable() { 117 | @Override 118 | public void run() { 119 | // There are no pending entries to rollback. Cancel. 120 | if (pendingEntries.isEmpty()) { 121 | session.getPlayer().sendMessage(MsgUtil.error("No changes found matching your search: " + session.getQuery().getSearchSummary().toLowerCase())); 122 | this.cancel(); 123 | return; 124 | } 125 | 126 | // Process our current batch of changes 127 | int iteration = 0; 128 | final int offset = changesIndex; 129 | if (offset < pendingEntries.size()) { 130 | primaryloop: 131 | 132 | // Loop through the entries associated with this patch 133 | for (final Iterator iterator = pendingEntries.listIterator(offset); iterator.hasNext(); ) { 134 | SnitchEntry entry = iterator.next(); 135 | if (isPreview()) // If this is a preview, we're just going to increment the changesIndex. We're not actually removing entries from the queue. 136 | changesIndex++; 137 | iteration++; // If we've processed 1,000 changes, break and leave the rest to be continued on. 138 | if (iteration >= 1000) { 139 | break; 140 | } 141 | 142 | // Retrieve the process handler for the action of this entry. 143 | SnitchProcessHandler handler = entry.getAction().getProcessHandler(); 144 | if (!handler.can(activity)) { // can this action be altered by this activity? if not we remove it from our queue 145 | iterator.remove(); 146 | continue; 147 | } 148 | try { 149 | 150 | boolean result; 151 | 152 | // Apply the necessary actions to this activity, depending on what we're doing 153 | switch (activity) { 154 | case ROLLBACK: 155 | if (entry.isReverted()) { 156 | iterator.remove(); 157 | continue primaryloop; 158 | } 159 | result = handler.handleRollback(session, entry); 160 | break; 161 | case RESTORE: 162 | if (!entry.isReverted()) { 163 | iterator.remove(); 164 | continue; 165 | } 166 | result = handler.handleRestore(session, entry); 167 | break; 168 | case PREVIEW: 169 | if (entry.isReverted()) { 170 | iterator.remove(); 171 | continue; 172 | } 173 | result = handler.handlePreview(session, entry); 174 | break; 175 | default: 176 | throw new IllegalArgumentException("Unsupported activity type."); 177 | } 178 | 179 | // If our adjustment was successful, we'll mark it accordimgly 180 | if (result) { 181 | if (activity == EnumSnitchActivity.ROLLBACK) { // Mark a rolled back action as reverted 182 | SnitchPlugin.getInstance().async(() -> { 183 | try { 184 | SnitchPlugin.getInstance().getStorage().markReverted(entry, true); 185 | } catch (SnitchDatabaseException e) { 186 | e.printStackTrace(); 187 | } 188 | }); 189 | } else if (activity == EnumSnitchActivity.RESTORE) { // Mark a rolled back action as restored 190 | SnitchPlugin.getInstance().async(() -> { 191 | try { 192 | SnitchPlugin.getInstance().getStorage().markReverted(entry, false); 193 | } catch (SnitchDatabaseException e) { 194 | e.printStackTrace(); 195 | } 196 | }); 197 | } 198 | applied++; 199 | } else { 200 | failed++; 201 | } 202 | 203 | // If this wasn't a preview, remove it from the queue for real 204 | if (!isPreview()) { 205 | iterator.remove(); 206 | } 207 | 208 | } catch (Exception ex) { // We have to catch all exception as to not interrupt the activity. We'll log to cancel and mark it as a failure. 209 | ex.printStackTrace(); 210 | failed++; 211 | iterator.remove(); 212 | } 213 | } 214 | } 215 | // When we've completed the queue, cancel this task and run post-processing code 216 | if (pendingEntries.isEmpty() || changesIndex >= pendingEntries.size()) { 217 | this.cancel(); 218 | postProcess(); 219 | } 220 | } 221 | }.runTaskTimer(SnitchPlugin.getInstance(), 1L, 1L); 222 | } else { 223 | session.getPlayer().sendMessage(MsgUtil.error("No changes found matching your search: " + session.getQuery().getSearchSummary().toLowerCase())); 224 | } 225 | } 226 | 227 | private void postProcess() { 228 | if (isPreview()) { // If this is a preview, update the session so we can interact with it with the apply and cancel commands. 229 | session.setActivePreview(this); 230 | } else { 231 | // If this was a rollback or restore with an area defined, we'll remove any nearby fires 232 | if (session.getQuery().isAreaSelection()) { 233 | List changed = BlockUtil.removeNear(Arrays.asList(Material.FIRE), session.getQuery().getPosition().toLocation(session.getQuery().getWorld()), (int) session.getQuery().getRange()); 234 | if (!changed.isEmpty()) { 235 | session.getPlayer().sendMessage(MsgUtil.info("Extinguished " + changed.size() + " fires.")); 236 | } 237 | } 238 | } 239 | // To support the undo command, we log what the last activity was so we can revert it 240 | session.setLastActivity(this.activity); 241 | 242 | // run the callback 243 | this.callback.handle(session.getPlayer(), new SnitchResult(applied, failed, planned, isPreview(), movedEntities, session.getQuery(), new ArrayList<>())); 244 | } 245 | 246 | /** 247 | * Contains the default logic for the PreviewCallback 248 | */ 249 | public static class DefaultPreviewCallback implements SnitchCallback { 250 | 251 | private SnitchQuery query; 252 | 253 | public DefaultPreviewCallback(SnitchQuery query) { 254 | this.query = query; 255 | } 256 | 257 | @Override 258 | public void handle(Player player, SnitchResult result) { 259 | player.sendMessage(MsgUtil.success("Previewing rollback for " + query.getSearchSummary().toLowerCase())); 260 | player.sendMessage(MsgUtil.record("Showing " + result.getApplied() + " planned changes")); 261 | if (!result.getMovedEntities().isEmpty()) { 262 | player.sendMessage(MsgUtil.record(result.getMovedEntities().size() + "+ entities will be moved to safety")); 263 | } 264 | player.sendMessage(MsgUtil.record("Type §a/snitch pv apply§7 or §c/snitch pv cancel§7 to continue.")); 265 | } 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /src/main/java/co/melondev/Snitch/listeners/InventoryListener.java: -------------------------------------------------------------------------------- 1 | package co.melondev.Snitch.listeners; 2 | 3 | import co.melondev.Snitch.SnitchPlugin; 4 | import co.melondev.Snitch.entities.SnitchPlayer; 5 | import co.melondev.Snitch.entities.SnitchPosition; 6 | import co.melondev.Snitch.entities.SnitchWorld; 7 | import co.melondev.Snitch.enums.EnumAction; 8 | import co.melondev.Snitch.enums.EnumDefaultPlayer; 9 | import co.melondev.Snitch.util.JsonUtil; 10 | import co.melondev.Snitch.util.SnitchDatabaseException; 11 | import com.google.gson.JsonObject; 12 | import org.bukkit.Location; 13 | import org.bukkit.Material; 14 | import org.bukkit.block.Block; 15 | import org.bukkit.block.BlockState; 16 | import org.bukkit.block.DoubleChest; 17 | import org.bukkit.block.Hopper; 18 | import org.bukkit.entity.Entity; 19 | import org.bukkit.entity.HumanEntity; 20 | import org.bukkit.entity.Player; 21 | import org.bukkit.event.EventHandler; 22 | import org.bukkit.event.EventPriority; 23 | import org.bukkit.event.Listener; 24 | import org.bukkit.event.enchantment.EnchantItemEvent; 25 | import org.bukkit.event.entity.EntityPickupItemEvent; 26 | import org.bukkit.event.inventory.*; 27 | import org.bukkit.event.player.PlayerDropItemEvent; 28 | import org.bukkit.inventory.InventoryHolder; 29 | import org.bukkit.inventory.ItemStack; 30 | 31 | import java.util.Map; 32 | import java.util.Set; 33 | 34 | public class InventoryListener implements Listener { 35 | 36 | private SnitchPlugin i; 37 | 38 | public InventoryListener(SnitchPlugin i) { 39 | this.i = i; 40 | } 41 | 42 | public static void logAction(Player player, Location location, ItemStack itemStack, EnumAction action, int slot, InventoryClickEvent event) { 43 | if (action.isEnabled()) { 44 | int finalQty = itemStack.getAmount(); 45 | if (event != null) { 46 | if (event.isRightClick()) { 47 | switch (action) { 48 | case ITEM_TAKE: 49 | finalQty = (finalQty - (int) Math.floor(finalQty / 2)); 50 | break; 51 | case ITEM_INSERT: 52 | finalQty = 1; 53 | break; 54 | } 55 | } 56 | } 57 | JsonObject data = new JsonObject(); 58 | data.add("item", JsonUtil.jsonify(itemStack)); 59 | data.addProperty("changedCount", finalQty); 60 | data.addProperty("slot", slot); 61 | logAction(player, itemStack, location, action, data); 62 | } 63 | } 64 | 65 | public static void logAction(Player player, ItemStack itemStack, Location location, EnumAction action, JsonObject data) { 66 | SnitchPlugin i = SnitchPlugin.getInstance(); 67 | i.async(() -> { 68 | try { 69 | SnitchPlayer snitchPlayer = i.getStorage().getPlayer(player.getUniqueId()); 70 | SnitchWorld world = i.getStorage().register(location.getWorld()); 71 | SnitchPosition position = new SnitchPosition(location); 72 | JsonObject d = data; 73 | if (d == null) { 74 | d = new JsonObject(); 75 | d.add("item", JsonUtil.jsonify(itemStack)); 76 | } 77 | i.getStorage().record(action, snitchPlayer, world, position, d, System.currentTimeMillis()); 78 | } catch (SnitchDatabaseException ex) { 79 | ex.printStackTrace(); 80 | } 81 | }); 82 | } 83 | 84 | private void logAction(EnumDefaultPlayer defaultPlayer, ItemStack itemStack, Location location, EnumAction action) { 85 | logAction(defaultPlayer, itemStack, location, action, null); 86 | } 87 | 88 | private void logAction(Player player, ItemStack itemStack, Location location, EnumAction action) { 89 | logAction(player, itemStack, location, action, null); 90 | } 91 | 92 | private void logAction(EnumDefaultPlayer defaultPlayer, ItemStack itemStack, Location location, EnumAction action, JsonObject data) { 93 | i.async(() -> { 94 | try { 95 | SnitchPlayer snitchPlayer = defaultPlayer.getSnitchPlayer(); 96 | SnitchWorld world = i.getStorage().register(location.getWorld()); 97 | SnitchPosition position = new SnitchPosition(location); 98 | JsonObject d = data; 99 | if (d == null) { 100 | d = new JsonObject(); 101 | d.add("item", JsonUtil.jsonify(itemStack)); 102 | } 103 | i.getStorage().record(action, snitchPlayer, world, position, d, System.currentTimeMillis()); 104 | } catch (SnitchDatabaseException ex) { 105 | ex.printStackTrace(); 106 | } 107 | }); 108 | } 109 | 110 | @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) 111 | public void onPickup(EntityPickupItemEvent event) { 112 | Entity entity = event.getEntity(); 113 | if (!EnumAction.ITEM_DROP.isEnabled()) 114 | return; 115 | if ((entity instanceof Player)) { 116 | Player player = (Player) entity; 117 | ItemStack itemStack = event.getItem().getItemStack(); 118 | logAction(player, itemStack, player.getLocation(), EnumAction.ITEM_PICKUP); 119 | } 120 | } 121 | 122 | @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) 123 | public void onClick(InventoryClickEvent event) { 124 | if (EnumAction.ITEM_INSERT.isEnabled() || EnumAction.ITEM_TAKE.isEnabled()) { 125 | HumanEntity clicker = event.getWhoClicked(); 126 | if ((clicker instanceof Player)) { 127 | Player player = (Player) clicker; 128 | ItemStack current = event.getCurrentItem(); 129 | ItemStack cursor = event.getCursor(); 130 | 131 | InventoryHolder holder = event.getInventory().getHolder(); 132 | 133 | Location location = null; 134 | if ((holder instanceof BlockState)) { 135 | location = ((BlockState) holder).getLocation(); 136 | } else if ((holder instanceof Entity)) { 137 | location = ((Entity) holder).getLocation(); 138 | } else if ((holder instanceof DoubleChest)) { 139 | location = ((DoubleChest) holder).getLocation(); 140 | } 141 | 142 | int size = ((holder instanceof DoubleChest)) ? event.getView().getType().getDefaultSize() * 2 : event.getView().getType().getDefaultSize(); 143 | int slot = event.getSlot(); 144 | int rawSlot = event.getRawSlot(); 145 | 146 | if (slot == rawSlot && rawSlot <= size) { 147 | ItemStack added = null; 148 | ItemStack taken = null; 149 | 150 | if (current != null && current.getType() != Material.AIR && cursor != null && cursor.getType() != Material.AIR) { 151 | if (current.isSimilar(cursor)) { 152 | int count = event.isRightClick() ? 1 : current.getAmount(); 153 | int left = current.getMaxStackSize() - current.getAmount(); 154 | int inserted = count <= left ? count : left; 155 | if (inserted > 0) { 156 | added = cursor.clone(); 157 | added.setAmount(inserted); 158 | } 159 | } else { 160 | added = cursor.clone(); 161 | taken = current.clone(); 162 | } 163 | } else if (current != null && current.getType() != Material.AIR) { 164 | taken = current.clone(); 165 | } else if (cursor != null && cursor.getType() != Material.AIR) { 166 | added = cursor.clone(); 167 | } 168 | 169 | if (added != null) { 170 | logAction(player, location, added, EnumAction.ITEM_INSERT, rawSlot, event); 171 | } 172 | if (taken != null) { 173 | logAction(player, location, taken, EnumAction.ITEM_TAKE, rawSlot, event); 174 | } 175 | return; 176 | } 177 | 178 | if (event.isShiftClick() && current != null && current.getType() != Material.AIR) { 179 | logAction(player, location, current, EnumAction.ITEM_INSERT, -1, event); 180 | } 181 | 182 | } 183 | 184 | } 185 | } 186 | 187 | @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) 188 | public void onDrag(InventoryDragEvent event) { 189 | if (EnumAction.ITEM_TAKE.isEnabled() || EnumAction.ITEM_INSERT.isEnabled()) { 190 | InventoryHolder holder = event.getInventory().getHolder(); 191 | if ((holder instanceof BlockState)) { 192 | Location loc = ((BlockState) holder).getLocation(); 193 | HumanEntity clicker = event.getWhoClicked(); 194 | if ((clicker instanceof Player)) { 195 | Player player = (Player) clicker; 196 | for (Map.Entry newItems : event.getNewItems().entrySet()) { 197 | logAction(player, loc, newItems.getValue(), EnumAction.ITEM_INSERT, newItems.getKey(), null); 198 | } 199 | } 200 | } 201 | } 202 | } 203 | 204 | @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) 205 | public void onInventoryPickup(InventoryPickupItemEvent event) { 206 | if (!EnumAction.ITEM_PICKUP.isEnabled()) { 207 | return; 208 | } 209 | if ((event.getInventory() instanceof Hopper)) { 210 | logAction(EnumDefaultPlayer.HOPPER, event.getItem().getItemStack().clone(), event.getItem().getLocation(), EnumAction.ITEM_PICKUP); 211 | } 212 | } 213 | 214 | @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) 215 | public void onMoveItem(InventoryMoveItemEvent event) { 216 | if (EnumAction.ITEM_INSERT.isEnabled() && event.getDestination() != null) { 217 | InventoryHolder dest = event.getSource().getHolder(); 218 | Location location = null; 219 | if ((dest instanceof BlockState)) { 220 | location = ((BlockState) dest).getLocation(); 221 | } 222 | if (location != null && (event.getSource() instanceof Hopper)) { 223 | logAction(EnumDefaultPlayer.HOPPER, event.getItem().clone(), location, EnumAction.ITEM_INSERT); 224 | } 225 | } 226 | if (EnumAction.ITEM_TAKE.isEnabled() && event.getSource() != null) { 227 | InventoryHolder source = event.getSource().getHolder(); 228 | Location location = null; 229 | if ((source instanceof BlockState)) { 230 | location = ((BlockState) source).getLocation(); 231 | } 232 | if (location != null) { 233 | if ((event.getDestination() instanceof Hopper)) { 234 | logAction(EnumDefaultPlayer.HOPPER, event.getItem().clone(), location, EnumAction.ITEM_TAKE); 235 | } 236 | } 237 | } 238 | } 239 | 240 | @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) 241 | public void onDrop(PlayerDropItemEvent event) { 242 | Player player = event.getPlayer(); 243 | if (!EnumAction.ITEM_DROP.isEnabled()) 244 | return; 245 | ItemStack itemStack = event.getItemDrop().getItemStack(); 246 | logAction(player, itemStack, player.getLocation(), EnumAction.ITEM_DROP); 247 | } 248 | 249 | @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) 250 | public void onEnchant(EnchantItemEvent event){ 251 | 252 | if (!EnumAction.ENCHANT_ITEM.isEnabled()){ 253 | return; 254 | } 255 | 256 | Player player = event.getEnchanter(); 257 | ItemStack itemStack = event.getItem(); 258 | Block block = event.getEnchantBlock(); 259 | 260 | JsonObject obj = new JsonObject(); 261 | obj.add("item", JsonUtil.jsonify(itemStack)); 262 | obj.add("block", JsonUtil.jsonify(block.getState())); 263 | obj.add("enchants", JsonUtil.jsonify(event.getEnchantsToAdd())); 264 | 265 | logAction(player, itemStack, block.getLocation(), EnumAction.ENCHANT_ITEM, obj); 266 | } 267 | 268 | @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) 269 | public void onCraft(CraftItemEvent event){ 270 | if (!EnumAction.CRAFT_ITEM.isEnabled()){ 271 | return; 272 | } 273 | HumanEntity e = event.getWhoClicked(); 274 | if ((e instanceof Player)){ 275 | Player p = (Player) e; 276 | ItemStack itemStack = event.getRecipe().getResult(); 277 | JsonObject data = new JsonObject(); 278 | data.add("item", JsonUtil.jsonify(itemStack)); 279 | 280 | Location l; 281 | Block target = p.getTargetBlock((Set)null, 10); 282 | if (target != null && target.getType() == Material.WORKBENCH){ 283 | l = target.getLocation(); 284 | } 285 | else{ 286 | l = p.getLocation(); 287 | } 288 | 289 | logAction(p, itemStack, l, EnumAction.CRAFT_ITEM, data); 290 | } 291 | } 292 | } 293 | -------------------------------------------------------------------------------- /src/main/java/co/melondev/Snitch/util/EntityUtil.java: -------------------------------------------------------------------------------- 1 | package co.melondev.Snitch.util; 2 | 3 | import com.google.gson.JsonElement; 4 | import com.google.gson.JsonObject; 5 | import net.minecraft.server.v1_12_R1.MojangsonParseException; 6 | import org.bukkit.Art; 7 | import org.bukkit.DyeColor; 8 | import org.bukkit.Rotation; 9 | import org.bukkit.TreeSpecies; 10 | import org.bukkit.entity.*; 11 | import org.bukkit.entity.minecart.CommandMinecart; 12 | import org.bukkit.material.Colorable; 13 | import org.bukkit.material.MaterialData; 14 | import org.bukkit.util.EulerAngle; 15 | import org.bukkit.util.Vector; 16 | 17 | import java.util.UUID; 18 | 19 | /** 20 | * Created by Devon on 7/16/18. 21 | */ 22 | public class EntityUtil { 23 | 24 | /** 25 | * Rebuilds an entity to match the metadata 26 | * 27 | * @param entity the entity to rebuild 28 | * @param entityData the metadata to apply 29 | */ 30 | public static void rebuildEntity(Entity entity, JsonObject entityData) { 31 | entity.setFireTicks(entityData.get("fire").getAsInt()); 32 | entity.setCustomNameVisible(entityData.get("nameVisible").getAsBoolean()); 33 | if (!entityData.get("customName").isJsonNull()) { 34 | entity.setCustomName(entityData.get("customName").getAsString()); 35 | } 36 | entity.setFallDistance(entityData.get("fallDistance").getAsFloat()); 37 | entity.setGlowing(entityData.get("glowing").getAsBoolean()); 38 | entity.setGravity(entityData.get("gravity").getAsBoolean()); 39 | entity.setInvulnerable(entityData.get("invulnerable").getAsBoolean()); 40 | entity.setPortalCooldown(entityData.get("portalCooldown").getAsInt()); 41 | entity.setSilent(entityData.get("silent").getAsBoolean()); 42 | entity.setTicksLived(entityData.get("ticksLived").getAsInt()); 43 | 44 | String[] velocityRaw = entityData.get("velocity").getAsString().split(","); 45 | entity.setVelocity(new Vector(Double.parseDouble(velocityRaw[0]), Double.parseDouble(velocityRaw[1]), Double.parseDouble(velocityRaw[2]))); 46 | 47 | if ((entity instanceof LivingEntity)) { 48 | LivingEntity le = (LivingEntity) entity; 49 | le.setAI(entityData.get("ai").getAsBoolean()); 50 | le.setCanPickupItems(entityData.get("pickupItems").getAsBoolean()); 51 | le.setCollidable(entityData.get("collidable").getAsBoolean()); 52 | le.setGliding(entityData.get("gliding").getAsBoolean()); 53 | le.setLastDamage(entityData.get("lastDamage").getAsDouble()); 54 | le.setMaximumAir(entityData.get("maxAir").getAsInt()); 55 | le.setMaximumNoDamageTicks(entityData.get("maxNoDamageTicks").getAsInt()); 56 | le.setRemainingAir(entityData.get("air").getAsInt()); 57 | le.setRemoveWhenFarAway(entityData.get("removeWhenFar").getAsBoolean()); 58 | if ((le instanceof Bat)) { 59 | ((Bat) le).setAwake(entityData.get("awake").getAsBoolean()); 60 | } 61 | if ((le instanceof Enderman)) { 62 | JsonElement carrying = entityData.get("carrying"); 63 | if (!carrying.isJsonNull()) { 64 | try { 65 | org.bukkit.inventory.ItemStack itemStack = ItemUtil.JSONtoItemStack(carrying.getAsString()); 66 | ((Enderman) le).setCarriedMaterial(new MaterialData(itemStack.getType(), (byte) itemStack.getDurability())); 67 | } catch (MojangsonParseException e) { 68 | e.printStackTrace(); 69 | } 70 | } 71 | } 72 | if ((le instanceof AbstractHorse)) { 73 | ((AbstractHorse) le).setDomestication(entityData.get("domestication").getAsInt()); 74 | ((AbstractHorse) le).setJumpStrength(entityData.get("jumpStrength").getAsDouble()); 75 | ((AbstractHorse) le).setMaxDomestication(entityData.get("maxDomestication").getAsInt()); 76 | } 77 | if ((le instanceof ChestedHorse)) { 78 | ((ChestedHorse) le).setCarryingChest(entityData.get("carryingChest").getAsBoolean()); 79 | } 80 | if ((le instanceof Creeper)) { 81 | ((Creeper) le).setExplosionRadius(entityData.get("explosionRadius").getAsInt()); 82 | ((Creeper) le).setMaxFuseTicks(entityData.get("fuseTicks").getAsInt()); 83 | ((Creeper) le).setPowered(entityData.get("powered").getAsBoolean()); 84 | } 85 | if ((le instanceof Horse)) { 86 | ((Horse) le).setColor(Horse.Color.valueOf(entityData.get("color").getAsString())); 87 | ((Horse) le).setStyle(Horse.Style.valueOf(entityData.get("style").getAsString())); 88 | } 89 | if ((le instanceof IronGolem)) { 90 | ((IronGolem) le).setPlayerCreated(entityData.get("playerCreated").getAsBoolean()); 91 | } 92 | if ((le instanceof Llama)) { 93 | ((Llama) le).setColor(Llama.Color.valueOf(entityData.get("color").getAsString())); 94 | } 95 | if ((le instanceof Ocelot)) { 96 | ((Ocelot) le).setCatType(Ocelot.Type.valueOf(entityData.get("type").getAsString())); 97 | } 98 | if ((le instanceof Parrot)) { 99 | ((Parrot) le).setVariant(Parrot.Variant.valueOf(entityData.get("variant").getAsString())); 100 | } 101 | if ((le instanceof Pig)) { 102 | ((Pig) le).setSaddle(entityData.get("saddle").getAsBoolean()); 103 | } 104 | if ((le instanceof PigZombie)) { 105 | ((PigZombie) le).setAnger(entityData.get("anger").getAsInt()); 106 | ((PigZombie) le).setAngry(entityData.get("angry").getAsBoolean()); 107 | } 108 | if ((le instanceof Rabbit)) { 109 | ((Rabbit) le).setRabbitType(Rabbit.Type.valueOf(entityData.get("type").getAsString())); 110 | } 111 | if ((le instanceof Colorable)) { 112 | ((Colorable) le).setColor(DyeColor.valueOf(entityData.get("dyeColor").getAsString())); 113 | } 114 | if ((le instanceof Slime)) { 115 | ((Slime) le).setSize(entityData.get("size").getAsInt()); 116 | } 117 | if ((le instanceof Snowman)) { 118 | ((Snowman) le).setDerp(entityData.get("derp").getAsBoolean()); 119 | } 120 | if ((le instanceof Spellcaster)) { 121 | ((Spellcaster) le).setSpell(Spellcaster.Spell.valueOf(entityData.get("spell").getAsString())); 122 | } 123 | if ((le instanceof Wolf)) { 124 | ((Wolf) le).setCollarColor(DyeColor.valueOf(entityData.get("collar").getAsString())); 125 | ((Wolf) le).setAngry(entityData.get("angry").getAsBoolean()); 126 | } 127 | if ((le instanceof Villager)) { 128 | ((Villager) le).setCareer(Villager.Career.valueOf(entityData.get("career").getAsString())); 129 | ((Villager) le).setProfession(Villager.Profession.valueOf(entityData.get("profession").getAsString())); 130 | ((Villager) le).setRiches(entityData.get("riches").getAsInt()); 131 | } 132 | if ((le instanceof Ageable)) { 133 | Ageable a = (Ageable) le; 134 | a.setBreed(entityData.get("canBreed").getAsBoolean()); 135 | a.setAge(entityData.get("age").getAsInt()); 136 | a.setAgeLock(entityData.get("ageLock").getAsBoolean()); 137 | if (entityData.get("adult").getAsBoolean()) { 138 | a.setAdult(); 139 | } else { 140 | a.setBaby(); 141 | } 142 | } 143 | if ((le instanceof Sittable)) { 144 | ((Sittable) le).setSitting(entityData.get("sitting").getAsBoolean()); 145 | } 146 | if ((le instanceof Tameable)) { 147 | Tameable t = (Tameable) le; 148 | t.setTamed(entityData.get("tamed").getAsBoolean()); 149 | JsonElement owner = entityData.get("owner"); 150 | if (!owner.isJsonNull()) { 151 | t.setOwner(new AnimalTamer() { 152 | @Override 153 | public String getName() { 154 | return owner.getAsJsonObject().get("name").getAsString(); 155 | } 156 | 157 | @Override 158 | public UUID getUniqueId() { 159 | return UUID.fromString(owner.getAsJsonObject().get("uuid").getAsString()); 160 | } 161 | }); 162 | } 163 | } 164 | if ((le instanceof Zombie)) { 165 | Zombie z = (Zombie) le; 166 | z.setBaby(entityData.get("baby").getAsBoolean()); 167 | } 168 | if ((le instanceof ZombieVillager)) { 169 | ZombieVillager zv = (ZombieVillager) le; 170 | zv.setVillagerProfession(Villager.Profession.valueOf(entityData.get("zombieProfession").getAsString())); 171 | } 172 | } 173 | 174 | if ((entity instanceof Painting)) { 175 | ((Painting) entity).setArt(Art.valueOf(entityData.get("art").getAsString())); 176 | } 177 | if ((entity instanceof CommandMinecart)) { 178 | ((CommandMinecart) entity).setName(entityData.get("name").getAsString()); 179 | ((CommandMinecart) entity).setCommand(entityData.get("command").getAsString()); 180 | } 181 | if ((entity instanceof Item)) { 182 | try { 183 | ((Item) entity).setItemStack(ItemUtil.JSONtoItemStack(entityData.get("item").getAsString())); 184 | } catch (MojangsonParseException e) { 185 | e.printStackTrace(); 186 | } 187 | ((Item) entity).setPickupDelay(entityData.get("pickupDelay").getAsInt()); 188 | } 189 | if ((entity instanceof ItemFrame)) { 190 | JsonElement item = entityData.get("item"); 191 | if (!item.isJsonNull()) { 192 | try { 193 | ((ItemFrame) entity).setItem(ItemUtil.JSONtoItemStack(item.getAsString())); 194 | } catch (MojangsonParseException e) { 195 | e.printStackTrace(); 196 | } 197 | } 198 | ((ItemFrame) entity).setRotation(Rotation.valueOf(entityData.get("rotation").getAsString())); 199 | } 200 | if ((entity instanceof Boat)) { 201 | ((Boat) entity).setWoodType(TreeSpecies.valueOf(entityData.get("treeSpecies").getAsString())); 202 | } 203 | if ((entity instanceof ArmorStand)) { 204 | ArmorStand as = (ArmorStand) entity; 205 | as.setBodyPose(rebuildAngle(entityData.getAsJsonObject("bodyPose"))); 206 | as.setHeadPose(rebuildAngle(entityData.getAsJsonObject("headPose"))); 207 | as.setLeftArmPose(rebuildAngle(entityData.getAsJsonObject("leftArmPose"))); 208 | as.setLeftLegPose(rebuildAngle(entityData.getAsJsonObject("leftLegPose"))); 209 | as.setRightArmPose(rebuildAngle(entityData.getAsJsonObject("rightArmPose"))); 210 | as.setRightLegPose(rebuildAngle(entityData.getAsJsonObject("rightLegPose"))); 211 | as.setArms(entityData.get("arms").getAsBoolean()); 212 | as.setBasePlate(entityData.get("basePlate").getAsBoolean()); 213 | as.setMarker(entityData.get("marker").getAsBoolean()); 214 | as.setSmall(entityData.get("small").getAsBoolean()); 215 | as.setVisible(entityData.get("visible").getAsBoolean()); 216 | JsonElement boots = entityData.get("boots"); 217 | JsonElement legs = entityData.get("leggings"); 218 | JsonElement chest = entityData.get("chestplate"); 219 | JsonElement helm = entityData.get("helmet"); 220 | JsonElement hand = entityData.get("hand"); 221 | if (!boots.isJsonNull()) { 222 | try { 223 | as.setBoots(ItemUtil.JSONtoItemStack(boots.getAsString())); 224 | } catch (MojangsonParseException e) { 225 | e.printStackTrace(); 226 | } 227 | } 228 | if (!legs.isJsonNull()) { 229 | try { 230 | as.setLeggings(ItemUtil.JSONtoItemStack(legs.getAsString())); 231 | } catch (MojangsonParseException e) { 232 | e.printStackTrace(); 233 | } 234 | } 235 | if (!chest.isJsonNull()) { 236 | try { 237 | as.setChestplate(ItemUtil.JSONtoItemStack(chest.getAsString())); 238 | } catch (MojangsonParseException e) { 239 | e.printStackTrace(); 240 | } 241 | } 242 | if (!helm.isJsonNull()) { 243 | try { 244 | as.setHelmet(ItemUtil.JSONtoItemStack(helm.getAsString())); 245 | } catch (MojangsonParseException e) { 246 | e.printStackTrace(); 247 | } 248 | } 249 | if (!hand.isJsonNull()) { 250 | try { 251 | as.setItemInHand(ItemUtil.JSONtoItemStack(hand.getAsString())); 252 | } catch (MojangsonParseException e) { 253 | e.printStackTrace(); 254 | } 255 | } 256 | } 257 | } 258 | 259 | /** 260 | * Re-creates EulerAngle to match the metadata 261 | * @param obj the metadata 262 | * @return the rebuilt angle 263 | */ 264 | private static EulerAngle rebuildAngle(JsonObject obj) { 265 | double x = obj.get("x").getAsDouble(); 266 | double y = obj.get("y").getAsDouble(); 267 | double z = obj.get("z").getAsDouble(); 268 | return new EulerAngle(x, y, z); 269 | } 270 | 271 | } 272 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Logo](https://d3vv6lp55qjaqc.cloudfront.net/items/402j1g2B023f0d412u3K/GraphicText.png?X-CloudApp-Visitor-Id=1484866&v=247e27a7) 2 | 3 | Snitch block logging and rollback. 4 | 5 | **WARNING:** Snitch is still very much in development. We do not recommend using it on production servers at this time. 6 | 7 | ![Welcome](https://d3vv6lp55qjaqc.cloudfront.net/items/39292t0P011q1j2y1v3W/Screen%20Shot%202018-07-17%20at%209.30.58%20PM.png?X-CloudApp-Visitor-Id=1484866&v=749fdf45) 8 | 9 | ## Why Snitch? 10 | Snitch is the only block logging and rollback plugin that was built on the APIs of 1.12. This ensures that events that are not caught by other plugins, such as armor stand interaction, item frames, and many other specific details are logged and rolled back properly. 11 | 12 | ## How does it compare? 13 | We try to take the best features of many other popular plugins you've grown to love: 14 | 15 | * Rollback previews from HawkEye 16 | * A simple command structure from LogBlock 17 | * Powerful and intuitive features from Prism 18 | 19 | ## How do I know that Snitch will be kept up to date? 20 | Snitch is always going to be open source and community-first. We'll always be looking for available PRs to extend functionality and ensure that our users have the best experience possible. 21 | 22 | ## And 1.13? 23 | We want to support 1.13 as soon as we can, but also realize that there are a lot of users who will continue to use 1.12. We hope to have 1.13 support as soon as possible. 24 | 25 | ## And 1.8? 26 | There are no plans for 1.8 support at this time. 27 | 28 | --- 29 | # Commands 30 | 31 | Commands in Snitch are super easy to get the hang of. We'll cover the basics, first: 32 | 33 | * `/snitch actions|a` provides an in-game reference for the available, searchable actions 34 | * `/snitch params|p` provides an in-game reference of the different parameters available to you, with examples 35 | * `/snitch rollback|rb ` performs a rollback using the provided parameters 36 | * `/snitch restore|rs ` re-applies actions from a rollback. Think of it like an undo command 37 | * `/snitch preview|pv ` provides a player-only preview of a rollback, making it simple to preview your changes before they happen 38 | * `/snitch lookup|l ` performs a lookup using the specified parameters 39 | * `/snitch near [radius]` is a shortcut for typing `/snitch l area 5`. Enter a different radius to search nearby within that radius 40 | * `/snitch delete|del ` deletes records from the Snitch database 41 | * `/snitch teleport|tp <#>` teleport to a record from a lookup. You can also just click on the record in your chat. 42 | * `/snitch inspector|i` toggle the inspector. 43 | * `/snitch next|prev` change pages in a lookup 44 | * `/snitch page <#>` go to a specific page from a lookup 45 | * `/snitch drain|dr [radius]` drain all liquids in the provided radius. Defaults to 10 if not specified. 46 | * `/snitch extinguish|ex [radius]` extinguishes all fires in the provided radius. Defaults to 10 if not specified. 47 | 48 | # Permissions 49 | 50 | Snitch has very configurable permissions so you can give exactly what you want to each player or staff member. 51 | 52 | * `snitch.actions` - Grants access to the **/snitch actions** command 53 | * `snitch.params` - Grants access to the **/snitch params** command 54 | * `snitch.rollback` - Grants access to the **/snitch rollback** command. _(This permission is not required to apply rollback previews.)_ 55 | * `snitch.restore` - Grants access to the **/snitch restore** command. _(This permission is not required to undo a messed up rollback.)_ 56 | * `snitch.delete` - Grants access to the **/snitch delete** command. _(DANGEROUS!)_ 57 | * `snitch.preview` - Grants access to the **/snitch preview** command. _(Also grants the user access to perform rollbacks via /snitch pv apply.)_ 58 | * `snitch.lookup` - Grants access to the **/snitch lookup** command. This permission also grants access to **/snitch next**, **/snitch prev** and **/snitch page**. 59 | * `snitch.near` - Grants access to the **/snitch near** command. _(This will not grant them /snitch lookup. Near is a much more restricted lookup view.)_ 60 | * `snitch.teleport` - Grants access to the **/snitch teleport** command, and the ability to click on entries. 61 | * `snitch.inspector` - Grants access to the **/snitch inspector** command. 62 | * `snitch.drain` - Grants access to the **/snitch drain** command. 63 | * `snitch.extinguish` - Grants access to the **/snitch ex** command. 64 | * `snitch.undo` - Grants access to the **/snitch undo** command, to quickly reverse a restore or rollback 65 | * `snitch.actions.all` - Grants access to ALL actions to use within commands the user has access to 66 | * `snitch.action.` - Grants access to a specific action to use within commands the user has access to. _(View the list of actions in /snitch actions)_ 67 | * `snitch.range.global` - Allows the user to not specify a range for their activites. If they don't have permission, then a range MUST be provided. 68 | * `snitch.viewdata.ip` - Allows users to see IP addresses of players if you log player joins 69 | * `snitch.tool` - Permits the user to use Snitch inspector tools 70 | * `snitch.notify` - Allows the user to receive notifications for rollbacks and restores 71 | 72 | --- 73 | # Using Snitch 74 | If you've ever used a logging or rollback plugin before, learning Snitch will be a snap. There are no complicated syntax or symbol formats to remember, just the data you want to search by. 75 | 76 | ## Hello, Inspector. 77 | 78 | ![Inspector](https://d3vv6lp55qjaqc.cloudfront.net/items/3X1m0s0B0I213e421L2v/Screen%20Shot%202018-07-17%20at%209.23.36%20PM.png?X-CloudApp-Visitor-Id=1484866&v=da8ec191) 79 | 80 | The **Snitch Inspector** is used for single-block investigations, and requires no tools like other plugins may. Toggle the inspector quickly by using `/s i`. 81 | 82 | To look into a block that was broken, right click where it once was. 83 | To look into a block that was placed, just punch it. 84 | 85 | You'll get quick records in chat informing you of the activity for that space. Turn off the inspector with the same command to continue on your business. 86 | 87 | ## Nearby Investigation 88 | 89 | ![Near](https://d3vv6lp55qjaqc.cloudfront.net/items/0B0R1d2i3R31042B1V2v/Screen%20Shot%202018-07-17%20at%209.25.06%20PM.png?X-CloudApp-Visitor-Id=1484866&v=b4be1fe2) 90 | Sometimes one block isn't enough to tell you the whole story. Use `/s near` to give you a history of all activity for within 5 blocks of you. You can also optionally use `/s near #` to change this radius to something larger, like 20 blocks. 91 | 92 | Like the inspector, you'll get a chat popup for everything that's happened. 93 | 94 | ## Specifics Specifics 95 | Sometimes you want to get really specific with your criteria. The near command may have provided you the name of the troublemaker, but how do we find out what else they may have caused? 96 | 97 | ![Specific 1](https://d3vv6lp55qjaqc.cloudfront.net/items/2U3R1q0B3o2V0E0W3431/Screen%20Shot%202018-07-17%20at%209.26.00%20PM.png?X-CloudApp-Visitor-Id=1484866&v=4a194de8) 98 | Let's look up all their activity for the past day: 99 | `/s l player SomeGriefer from 1d` 100 | 101 | There are a few ways I could type that as well. Instead of `player`, I could say `p` or instead of `from` I could say `since` or just `s`. Syntax isn't a major concern when performing lookups. 102 | 103 | ![Specific 2](https://d3vv6lp55qjaqc.cloudfront.net/items/0m3u2B1c273u2p064206/Screen%20Shot%202018-07-17%20at%209.26.54%20PM.png?X-CloudApp-Visitor-Id=1484866&v=b203455b) 104 | _Crossed out entries indicate an action that's been rolled back.)_ 105 | 106 | Maybe he had an accomplice that we want to include: 107 | `/s l p SomeGriefer SomeBaddie s 1d` 108 | 109 | You can include multiple bits of data just by typing them out. We also used the one-letter versions in that command to save time. 110 | 111 | ## Teleporting to the Damage 112 | 113 | ![Teleport](https://d3vv6lp55qjaqc.cloudfront.net/items/0031260U2B2X3A0u3H2s/Screen%20Shot%202018-07-17%20at%209.28.07%20PM.png?X-CloudApp-Visitor-Id=1484866&v=27ffbc59) 114 | We found what we're looking for, but it may be a bit far. Luckily, Snitch makes teleporting to specific events a breeze. 115 | 116 | * When viewing records, a dark gray ID will be shown to the far right of the entry. This is the **Record Index**. To teleport to it, simply type **/snitch tp <#>**. 117 | * Alternatively, you can also just click on the record in chat. 118 | 119 | ## It's like it never happened. 120 | Snitch performs near-perfect rollbacks of the damaged area. We do this by logging almost all attrbitures about blocks and entities that are destroyed, removed, or otherwise broken. 121 | 122 | ### Rollback Previews 123 | You can preview the effect of a rollback before applying it to the server by using `/snitch preview`. When you preview a rollback, only the block changes are shown to you. Other adjustments, like killed monsters or displaced entities may not be reflected by a rollback preview. Say we wanted to preview the rollback for our criteria above: 124 | 125 | ![Preview](https://d3vv6lp55qjaqc.cloudfront.net/items/1h3s1e3t061F1r471x2g/Screen%20Shot%202018-07-17%20at%209.29.04%20PM.png?X-CloudApp-Visitor-Id=1484866&v=118376a9) 126 | `/snitch pv player SomeGriefer SomeBaddie since 1d` 127 | 128 | Snitch will give you a glance at what it'll look like. From there, you can type either `/snitch pv apply` to convert those changes to an actual rollback, or `/snitch pv cancel` to cancel the visualization. You can also perform another preview request with altered criteria to alter what you see. 129 | 130 | ### Making the Rollback 131 | From a preview, you can simply use `/snitch pv apply` to apply the rollback, but if you're confident in your abilities, you can make the rollback directly. Using the same criteria above, we'll use the following command: 132 | 133 | ![Rollback](https://d3vv6lp55qjaqc.cloudfront.net/items/273s2H0D0c1W191f1P1A/Screen%20Shot%202018-07-17%20at%209.29.31%20PM.png?X-CloudApp-Visitor-Id=1484866&v=204ef764) 134 | `/snitch rb player SomeGriefer SomeBaddie since 1d` 135 | 136 | Without any further confirmation, the rollback will commence and the damage will be reverted. 137 | 138 | ## Re-Applying World Changes 139 | Uh oh, you accidentally reverted something you shouldn't have! Luckily, Snitch maintains records _even after you've rolled them back_, just marked off specifically. Let's say **SomeBaddie** built a nice house near our spawn area we want to keep. We cna run the following command: 140 | 141 | ![Restore](https://d3vv6lp55qjaqc.cloudfront.net/items/2s1f3h0C062i3K1B353H/Screen%20Shot%202018-07-17%20at%209.30.00%20PM.png?X-CloudApp-Visitor-Id=1484866&v=fea33fe1) 142 | `/snitch restore player SomeBaddie area 20` 143 | 144 | Snitch will re-apply the world changes to the specific area and mark the logs as non-reverted for future use. Lovely! 145 | 146 | We can also do this if we don't want to travel directly to the area, if we know the coordinates. Modifying the original command slightly... 147 | 148 | `/snitch restore player SomeBaddie area 20 relative 100 150 100` 149 | 150 | It's like the rollback never happened there! 151 | 152 | ## Deleting Unnecessary Records 153 | 154 | ### Auto-Clean 155 | In the Snitch configuration, you can turn on **Auto-Clean** to automatically purge your server of old records each time your server starts up. It's important to have proper autoclean rules defined to keep Snitch running as fast and efficiently as possible. 156 | 157 | By default, Snitch will delete all records older then 1 year, and water/lava flow data older than 7 days. 158 | 159 | The configuration looks something like this: 160 | 161 | ```yml 162 | autoclean: 163 | enable: true 164 | actions: 165 | - 'before 365d' 166 | - 'actions flow before 7d' 167 | ``` 168 | 169 | Auto-Clean actions use the same parameters that you use for lookups and rollbacks. Simply specify the filters for the data you'd like to delete. You can even limit the amount of data deleted per task by specifying `limit x` next to any of your clean tasks. 170 | 171 | ### Manually 172 | 173 | You can also delete data yourself in-game using the `/snitch delete` command. 174 | 175 | Say I wanted to delete ALL history of liquid flowing COMPLETELY. I can do that with the simple command: `/snitch delete a flow`. 176 | 177 | Snitch will do a quick check on how many records this will affect and make sure I know what I'm doing: 178 | ![Manual Delete](https://d3vv6lp55qjaqc.cloudfront.net/items/391n2I3Q0q1A1W3r1v01/Screen%20Shot%202018-08-06%20at%207.30.06%20PM.png?X-CloudApp-Visitor-Id=1484866&v=b7f67583) 179 | 180 | You can see that even with the dangerous command I typed, Snitch auto-completed to only delete flow records for the past 3 days. (This is the default time search as specified in the `config.yml`). I can override this by specifying `since` myself, but I'll leave it as the 3 day default. 181 | 182 | If I'm sure, I just type `/snitch delete confirm` and the matching records will be deleted. 183 | 184 | ![Delete Success](https://d3vv6lp55qjaqc.cloudfront.net/items/0o0i0C0t3t3N2H1X0c2l/Screen%20Shot%202018-08-06%20at%207.31.40%20PM.png?X-CloudApp-Visitor-Id=1484866&v=c4ed3b94) 185 | 186 | **IMPORTANT:** The `/snitch delete` command performs a TRUE delete. There is no way to recover the data that was removed using it, so it's important to only give this command to users you trust to not do your server harm. 187 | 188 | 189 | --- 190 | # Actions 191 | Actions are a list of what's logged by Snitch. By default, all actions are enabled but can be specifically disabled by listing them in the `config.yml` file. 192 | 193 | ![Actions](https://d3vv6lp55qjaqc.cloudfront.net/items/0E3L1n3X3G3F0y1s3F2s/Screen%20Shot%202018-07-17%20at%209.30.26%20PM.png?X-CloudApp-Visitor-Id=1484866&v=58f7984d) 194 | You can view a list of logged actions in-game using `/snitch actions`. 195 | 196 | --- 197 | # Parameters 198 | Parameters (or params) are the different criteria you can specify in rollback, restore, and lookup commands. Mastering what you can specify is key to ensuring accurate and correct rollbacks. 199 | 200 | ## Actor 201 | The **Actor** parameter allows you to specify changes done by a specific player, entity, or block. 202 | 203 | * Aliases: `player`, `players`, `p`, or `actor` 204 | * Accepts: One or multiple entries 205 | * Examples: `Turqmelon`, `S-TNT`, `S-Enderman` 206 | 207 | * `/snitch l player Turqmelon` 208 | * `/snitch l actor S-TNT` 209 | * `/snitch l players Turqmelon JustPants Rhonim` 210 | 211 | ### Snitch Actors 212 | Snitch maintains an internal player list used for frequent logs. We prefix these actor names with `S-` as to not collide with any real MC names. 213 | 214 | Say you wanted to rollback enderman griefing, it'd be as easy as `/snitch rb area 20 player S-Enderman`. 215 | 216 | ## Action 217 | The **Action** parameters allows you to narrow down your activities by specific things players can do. 218 | 219 | * Aliases: `action`, `actions`, `a` 220 | * Accepts: One or multiple entries 221 | * Examples: `break`, `explode`, `chat`, `block_explode` 222 | 223 | * `/snitch l action burn` 224 | * `/snitch l action chat` 225 | * `/snitch l player Turqmelon action break` 226 | 227 | ## Since 228 | The **Since** parameter allows you to narrow down your results to happening _after_ a specific time. 229 | 230 | * Aliases: `since`, `from`, `s` 231 | * Accepts: A specific date or a relative time 232 | * Examples: `07/01/18`, `1d`, `30m` 233 | 234 | * `/snitch l player Turqmelon since 10m` 235 | * `/snitch l action chat since 1d` 236 | 237 | ## Before 238 | The **Before** parameter allows you to narrow down your results to happening _before_ a specific time. 239 | 240 | * Aliases: `before`, `prior`, `b` 241 | * Accepts: A specific date or relative time 242 | * Examples: `07/01/18`, `1d`, `30m` 243 | 244 | * `/snitch l player Turqmelon since 7d before 6d` 245 | * `/snitch l action break before 07/02/18 since 06/30/18` 246 | 247 | ## World 248 | The **World** parameter allows you to reference a specific world. If you leave this out, it'll default to searching globally. (If you specify the Range paramater, this will be automatically set as your current world.) 249 | 250 | * Aliases: `world`, `w` 251 | * Accepts: A single world 252 | * Examples: `world_nether` 253 | 254 | * `/snitch l player Turqmelon world world_nether` 255 | * `/snitch l action explode world skyworld` 256 | 257 | ## Radius 258 | The **Radius** parameter filters down your search to a specific area. If you don't specify a location with the **Coords** param, your own location will be used, and if you don't specify a **World**, your own world will be used. Don't specify this parameter for a global lookup. 259 | 260 | * Aliases: `radius`, `range`, `area` 261 | * Accepts: A single number 262 | * Examples: `5`, `20` 263 | 264 | * `/snitch l area 20` 265 | * `/snitch l player Turqmelon area 50` 266 | * `/snitch l action burn range 5` 267 | 268 | ## Coords 269 | The **Coords** parameter allows you to use a location other then your own as the lookup point. This is primarily used with the **Range** param to search places you're not. 270 | 271 | * Aliases: `coords`, `relative`, `pos`, `position` 272 | * Accepts: A set of coordinates 273 | * Examples: `100 150 100` 274 | 275 | * `/snitch l area 20 relative 100 150 100 world skyworld` 276 | 277 | -------------------------------------------------------------------------------- /src/main/java/co/melondev/Snitch/util/JsonUtil.java: -------------------------------------------------------------------------------- 1 | package co.melondev.Snitch.util; 2 | 3 | import com.google.gson.JsonArray; 4 | import com.google.gson.JsonObject; 5 | import org.bukkit.*; 6 | import org.bukkit.block.*; 7 | import org.bukkit.block.banner.Pattern; 8 | import org.bukkit.enchantments.Enchantment; 9 | import org.bukkit.entity.*; 10 | import org.bukkit.entity.minecart.CommandMinecart; 11 | import org.bukkit.inventory.ItemStack; 12 | import org.bukkit.material.Colorable; 13 | import org.bukkit.material.MaterialData; 14 | import org.bukkit.util.EulerAngle; 15 | 16 | import java.util.Map; 17 | 18 | public class JsonUtil { 19 | 20 | /** 21 | * Transforms a dye color into a usable dye object 22 | * 23 | * @param dye the dye color 24 | * @return the dye as json 25 | */ 26 | public static JsonObject jsonify(DyeColor dye){ 27 | JsonObject obj = new JsonObject(); 28 | obj.addProperty("type", dye.name()); 29 | return obj; 30 | } 31 | 32 | /** 33 | * Transforms entity properties into JSON 34 | * @param entity the entity to save 35 | * @return the properties of the entity 36 | */ 37 | public static JsonObject jsonify(Entity entity){ 38 | 39 | JsonObject obj = new JsonObject(); 40 | obj.addProperty("entityType", entity.getType().name()); 41 | obj.addProperty("fire", entity.getFireTicks()); 42 | obj.addProperty("nameVisible", entity.isCustomNameVisible()); 43 | obj.addProperty("customName", entity.getCustomName()); 44 | obj.addProperty("fallDistance", entity.getFallDistance()); 45 | obj.addProperty("glowing", entity.isGlowing()); 46 | obj.addProperty("gravity", entity.hasGravity()); 47 | obj.addProperty("invulnerable", entity.isInvulnerable()); 48 | obj.addProperty("portalCooldown", entity.getPortalCooldown()); 49 | obj.addProperty("silent", entity.isSilent()); 50 | obj.addProperty("ticksLived", entity.getTicksLived()); 51 | obj.addProperty("velocity", entity.getVelocity().toString()); 52 | 53 | if ((entity instanceof LivingEntity)){ 54 | obj.addProperty("living", true); 55 | LivingEntity le = (LivingEntity) entity; 56 | obj.addProperty("ai", le.hasAI()); 57 | obj.addProperty("pickupItems", le.getCanPickupItems()); 58 | obj.addProperty("collidable", le.isCollidable()); 59 | obj.addProperty("gliding", le.isGliding()); 60 | obj.addProperty("lastDamage", le.getLastDamage()); 61 | obj.addProperty("maxAir", le.getMaximumAir()); 62 | obj.addProperty("maxNoDamageTicks", le.getMaximumNoDamageTicks()); 63 | obj.addProperty("noDamageTicks", le.getNoDamageTicks()); 64 | obj.addProperty("air", le.getRemainingAir()); 65 | obj.addProperty("removeWhenFar", le.getRemoveWhenFarAway()); 66 | if ((le instanceof Bat)){ 67 | obj.addProperty("awake", ((Bat) le).isAwake()); 68 | } 69 | if ((le instanceof Enderman)){ 70 | Enderman e = (Enderman) le; 71 | MaterialData carrying = e.getCarriedMaterial(); 72 | obj.add("carrying", carrying!=null?JsonUtil.jsonify(new ItemStack(carrying.getItemType(), 1, carrying.getData())):null); 73 | } 74 | if ((le instanceof AbstractHorse)){ 75 | AbstractHorse horse = (AbstractHorse) le; 76 | obj.addProperty("domestication", horse.getDomestication()); 77 | obj.addProperty("jumpStrength", horse.getJumpStrength()); 78 | obj.addProperty("maxDomestication", horse.getMaxDomestication()); 79 | } 80 | if ((le instanceof ChestedHorse)){ 81 | obj.addProperty("carryingChest", ((ChestedHorse) le).isCarryingChest()); 82 | } 83 | if ((le instanceof Creeper)){ 84 | Creeper c = (Creeper) le; 85 | obj.addProperty("explosionRadius", c.getExplosionRadius()); 86 | obj.addProperty("fuseTicks", c.getMaxFuseTicks()); 87 | obj.addProperty("powered", c.isPowered()); 88 | } 89 | 90 | if ((le instanceof Horse)){ 91 | obj.addProperty("color", ((Horse) le).getColor().name()); 92 | obj.addProperty("style", ((Horse) le).getStyle().name()); 93 | } 94 | 95 | if ((le instanceof IronGolem)){ 96 | obj.addProperty("playerCreated", ((IronGolem) le).isPlayerCreated()); 97 | } 98 | 99 | if ((le instanceof Llama)){ 100 | obj.addProperty("color", ((Llama) le).getColor().name()); 101 | } 102 | 103 | if ((le instanceof Ocelot)){ 104 | obj.addProperty("type", ((Ocelot)le).getCatType().name()); 105 | } 106 | 107 | if ((le instanceof Parrot)){ 108 | obj.addProperty("variant", ((Parrot)le).getVariant().name()); 109 | } 110 | 111 | if ((le instanceof Pig)){ 112 | obj.addProperty("saddle", ((Pig)le).hasSaddle()); 113 | } 114 | 115 | if ((le instanceof PigZombie)){ 116 | PigZombie pz = (PigZombie) le; 117 | obj.addProperty("anger", pz.getAnger()); 118 | obj.addProperty("angry", pz.isAngry()); 119 | } 120 | 121 | if ((le instanceof Rabbit)){ 122 | obj.addProperty("type", ((Rabbit)le).getRabbitType().name()); 123 | } 124 | 125 | if ((le instanceof Sheep)){ 126 | obj.addProperty("sheared", ((Sheep)le).isSheared()); 127 | } 128 | 129 | if ((le instanceof Colorable)){ 130 | obj.addProperty("dyeColor", ((Colorable)le).getColor().name()); 131 | } 132 | 133 | if ((le instanceof Slime)){ 134 | obj.addProperty("size", ((Slime)le).getSize()); 135 | } 136 | 137 | if ((le instanceof Snowman)){ 138 | obj.addProperty("derp", ((Snowman)le).isDerp()); 139 | } 140 | 141 | if ((le instanceof Spellcaster)){ 142 | Evoker e = (Evoker) le; 143 | obj.addProperty("spell", e.getSpell().name()); 144 | } 145 | 146 | if ((le instanceof Wolf)){ 147 | Wolf w = (Wolf) le; 148 | obj.addProperty("collar", w.getCollarColor().name()); 149 | obj.addProperty("angry", w.isAngry()); 150 | } 151 | 152 | if ((le instanceof Villager)){ 153 | Villager v = (Villager) le; 154 | obj.addProperty("career", v.getCareer().name()); 155 | obj.addProperty("profession", v.getProfession().name()); 156 | obj.addProperty("riches", v.getRiches()); 157 | } 158 | 159 | if ((le instanceof Ageable)){ 160 | Ageable a = (Ageable) le; 161 | obj.addProperty("canBreed", a.canBreed()); 162 | obj.addProperty("age", a.getAge()); 163 | obj.addProperty("ageLock", a.getAgeLock()); 164 | obj.addProperty("adult", a.isAdult()); 165 | } 166 | 167 | if ((le instanceof Sittable)){ 168 | obj.addProperty("sitting", ((Sittable)le).isSitting()); 169 | } 170 | 171 | if ((le instanceof Tameable)){ 172 | obj.addProperty("tamed", ((Tameable)le).isTamed()); 173 | Tameable t= (Tameable) le; 174 | if (t.getOwner() != null){ 175 | obj.add("owner", jsonify(t.getOwner())); 176 | } 177 | else{ 178 | obj.add("owner", null); 179 | } 180 | 181 | } 182 | 183 | if ((le instanceof Zombie)){ 184 | Zombie z = (Zombie) le; 185 | obj.addProperty("baby", z.isBaby()); 186 | } 187 | 188 | if ((le instanceof ZombieVillager)){ 189 | obj.addProperty("zombieProfession", ((ZombieVillager)le).getVillagerProfession().name()); 190 | } 191 | 192 | } 193 | else{ 194 | obj.addProperty("living", false); 195 | } 196 | 197 | if ((entity instanceof Painting)){ 198 | Painting painting = (Painting) entity; 199 | obj.addProperty("art", painting.getArt().name()); 200 | } 201 | 202 | if ((entity instanceof CommandMinecart)){ 203 | CommandMinecart cmd = (CommandMinecart) entity; 204 | obj.addProperty("name", cmd.getCustomName()); 205 | obj.addProperty("command", cmd.getCommand()); 206 | } 207 | 208 | if ((entity instanceof Item)){ 209 | Item item = (Item) entity; 210 | obj.add("item", jsonify(item.getItemStack())); 211 | obj.addProperty("pickupDelay", item.getPickupDelay()); 212 | } 213 | 214 | if ((entity instanceof ItemFrame)){ 215 | ItemFrame itemFrame = (ItemFrame) entity; 216 | obj.add("item", itemFrame.getItem() != null ? jsonify(itemFrame.getItem()) : null); 217 | obj.addProperty("rotation", itemFrame.getRotation().name()); 218 | } 219 | 220 | if ((entity instanceof Boat)){ 221 | obj.addProperty("treeSpecies", ((Boat)entity).getWoodType().name()); 222 | } 223 | 224 | if ((entity instanceof ArmorStand)){ 225 | ArmorStand armorStand = (ArmorStand) entity; 226 | obj.add("bodyPose", jsonify(armorStand.getBodyPose())); 227 | obj.add("headPose", jsonify(armorStand.getHeadPose())); 228 | obj.add("leftArmPose", jsonify(armorStand.getLeftArmPose())); 229 | obj.add("leftLegPose", jsonify(armorStand.getLeftLegPose())); 230 | obj.add("rightArmPose", jsonify(armorStand.getRightArmPose())); 231 | obj.add("rightLegPose", jsonify(armorStand.getRightLegPose())); 232 | obj.addProperty("arms", armorStand.hasArms()); 233 | obj.addProperty("basePlate", armorStand.hasBasePlate()); 234 | obj.addProperty("marker", armorStand.isMarker()); 235 | obj.addProperty("small", armorStand.isSmall()); 236 | obj.addProperty("visible", armorStand.isVisible()); 237 | obj.add("boots", armorStand.getBoots() != null ? jsonify(armorStand.getBoots()) : null); 238 | obj.add("leggings", armorStand.getLeggings() != null ? jsonify(armorStand.getLeggings()) : null); 239 | obj.add("chestplate", armorStand.getChestplate() != null ? jsonify(armorStand.getChestplate()) : null); 240 | obj.add("helmet", armorStand.getHelmet() != null ? jsonify(armorStand.getHelmet()) : null); 241 | obj.add("hand", armorStand.getItemInHand() != null ? jsonify(armorStand.getItemInHand()) : null); 242 | } 243 | 244 | return obj; 245 | 246 | } 247 | 248 | /** 249 | * Transforms an angle into JSON 250 | * @param angle the angle to save 251 | * @return the json 252 | */ 253 | public static JsonObject jsonify(EulerAngle angle){ 254 | JsonObject obj = new JsonObject(); 255 | obj.addProperty("x", angle.getX()); 256 | obj.addProperty("y", angle.getY()); 257 | obj.addProperty("z", angle.getZ()); 258 | return obj; 259 | } 260 | 261 | /** 262 | * Transforms a tamer into json 263 | * @param tamer the tamer to save 264 | * @return the json 265 | */ 266 | public static JsonObject jsonify(AnimalTamer tamer){ 267 | JsonObject obj = new JsonObject(); 268 | obj.addProperty("name", tamer.getName()); 269 | obj.addProperty("uuid", tamer.getUniqueId().toString()); 270 | return obj; 271 | } 272 | public static JsonObject jsonify(Map enchants){ 273 | 274 | JsonObject obj = new JsonObject(); 275 | for(Map.Entry entry : enchants.entrySet()){ 276 | Enchantment ench = entry.getKey(); 277 | int level = entry.getValue(); 278 | obj.addProperty(ench.getName(), level); 279 | } 280 | 281 | return obj; 282 | } 283 | 284 | public static JsonObject jsonify(ItemStack itemStack){ 285 | JsonObject obj = new JsonObject(); 286 | obj.addProperty("type", itemStack.getType().name()); 287 | obj.addProperty("amount", itemStack.getAmount()); 288 | obj.addProperty("data", itemStack.getDurability()); 289 | obj.addProperty("raw", ItemUtil.itemToJSON(itemStack)); 290 | return obj; 291 | 292 | } 293 | 294 | public static JsonObject jsonify(Pattern pattern) { 295 | JsonObject obj = new JsonObject(); 296 | obj.addProperty("color", pattern.getColor().name()); 297 | obj.addProperty("type", pattern.getPattern().name()); 298 | return obj; 299 | } 300 | 301 | public static JsonObject jsonify(BlockState block) { 302 | JsonObject obj = new JsonObject(); 303 | obj.addProperty("type", block.getType().name()); 304 | obj.addProperty("data", block.getRawData()); 305 | if ((block instanceof Banner)) { 306 | obj.addProperty("baseColor", ((Banner) block).getBaseColor().name()); 307 | JsonArray patterns = new JsonArray(); 308 | for (Pattern pattern : ((Banner) block).getPatterns()) { 309 | patterns.add(jsonify(pattern)); 310 | } 311 | obj.add("patterns", patterns); 312 | } 313 | if ((block instanceof Beacon)) { 314 | obj.addProperty("primaryEffect", ((Beacon) block).getPrimaryEffect().getType().getName()); 315 | obj.addProperty("secondaryEffect", ((Beacon) block).getSecondaryEffect().getType().getName()); 316 | } 317 | if ((block instanceof Colorable)) { 318 | obj.addProperty("color", ((Colorable) block).getColor().name()); 319 | } 320 | if ((block instanceof BrewingStand)) { 321 | obj.addProperty("brewingTime", ((BrewingStand) block).getBrewingTime()); 322 | obj.addProperty("fuelLevel", ((BrewingStand) block).getFuelLevel()); 323 | } 324 | if ((block instanceof Lockable)) { 325 | Lockable lock = (Lockable) block; 326 | obj.addProperty("locked", lock.getLock()); 327 | } 328 | if ((block instanceof CommandBlock)) { 329 | obj.addProperty("name", ((CommandBlock) block).getName()); 330 | obj.addProperty("command", ((CommandBlock) block).getCommand()); 331 | } 332 | if ((block instanceof CreatureSpawner)) { 333 | CreatureSpawner spawner = (CreatureSpawner) block; 334 | obj.addProperty("spawnRange", spawner.getSpawnRange()); 335 | obj.addProperty("spawnType", spawner.getSpawnedType().name()); 336 | obj.addProperty("spawnCount", spawner.getSpawnCount()); 337 | obj.addProperty("requiredPlayers", spawner.getRequiredPlayerRange()); 338 | obj.addProperty("minDelay", spawner.getMinSpawnDelay()); 339 | obj.addProperty("maxDelay", spawner.getMaxSpawnDelay()); 340 | obj.addProperty("maxNearby", spawner.getMaxNearbyEntities()); 341 | obj.addProperty("delay", spawner.getDelay()); 342 | } 343 | if ((block instanceof Nameable)) { 344 | obj.addProperty("customName", ((Nameable) block).getCustomName()); 345 | } 346 | if ((block instanceof EndGateway)) { 347 | obj.addProperty("exactTeleport", ((EndGateway) block).isExactTeleport()); 348 | obj.add("exitLocation", jsonify(((EndGateway) block).getExitLocation())); 349 | } 350 | if ((block instanceof FlowerPot)) { 351 | obj.addProperty("contents", ItemUtil.itemToJSON(new ItemStack(((FlowerPot) block).getContents().getItemType(), 1, ((FlowerPot) block).getContents().getData()))); 352 | } 353 | if ((block instanceof Furnace)) { 354 | Furnace furnace = (Furnace) block; 355 | obj.addProperty("cookTime", furnace.getCookTime()); 356 | obj.addProperty("burnTime", furnace.getBurnTime()); 357 | } 358 | if ((block instanceof NoteBlock)) { 359 | NoteBlock note = (NoteBlock) block; 360 | obj.addProperty("note", note.getRawNote()); 361 | } 362 | if ((block instanceof Sign)) { 363 | JsonArray signText = new JsonArray(); 364 | for (int i = 0; i < 4; i++) { 365 | signText.add(((Sign) block).getLine(i)); 366 | } 367 | obj.add("text", signText); 368 | } 369 | if ((block instanceof Skull)) { 370 | obj.addProperty("skullType", ((Skull) block).getSkullType().name()); 371 | obj.addProperty("rotation", ((Skull) block).getRotation().name()); 372 | obj.addProperty("owningPlayer", ((Skull) block).hasOwner() ? ((Skull) block).getOwningPlayer().getUniqueId().toString() : null); 373 | } 374 | return obj; 375 | 376 | } 377 | 378 | public static Location fromJson(JsonObject obj) { 379 | World world = Bukkit.getWorld(obj.get("world").getAsString()); 380 | double x = obj.get("x").getAsDouble(); 381 | double y = obj.get("y").getAsDouble(); 382 | double z = obj.get("z").getAsDouble(); 383 | return new Location(world, x, y, z); 384 | } 385 | 386 | public static JsonObject jsonify(Location exitLocation) { 387 | JsonObject obj = new JsonObject(); 388 | obj.addProperty("world", exitLocation.getWorld().getName()); 389 | obj.addProperty("x", exitLocation.getX()); 390 | obj.addProperty("y", exitLocation.getY()); 391 | obj.addProperty("z", exitLocation.getZ()); 392 | return obj; 393 | } 394 | 395 | } 396 | -------------------------------------------------------------------------------- /src/main/java/co/melondev/Snitch/util/Reflection.java: -------------------------------------------------------------------------------- 1 | package co.melondev.Snitch.util; 2 | 3 | import org.bukkit.Bukkit; 4 | 5 | import java.lang.reflect.Constructor; 6 | import java.lang.reflect.Field; 7 | import java.lang.reflect.InvocationTargetException; 8 | import java.lang.reflect.Method; 9 | 10 | @SuppressWarnings({ "WeakerAccess", "unchecked", "unused" }) 11 | public final class Reflection { 12 | 13 | /** 14 | * The version string that makes up part of CraftBukkit or MinecraftServer imports. 15 | */ 16 | public static final String VERSION_STRING = Bukkit.getServer().getClass().getPackage().getName().split("\\.")[3]; 17 | 18 | /** 19 | * The version number. 170 for 1_7_R0, 181 for 1_8_R1, etc. 20 | */ 21 | public static final int VERSION_NUMBER = Integer.parseInt(VERSION_STRING.replaceAll("[v_R]", "")); 22 | 23 | /** 24 | * The prefix for all NMS packages (e.g. net.minecraft.server.version.). 25 | */ 26 | public static final String NMS_PREFIX = "net.minecraft.server." + VERSION_STRING + '.'; 27 | 28 | /** 29 | * The prefix for all Craftbukkit packages (e.g. org.bukkit.craftbukkit.version.). 30 | */ 31 | public static final String CRAFT_PREFIX = "org.bukkit.craftbukkit." + VERSION_STRING + '.'; 32 | 33 | /** 34 | * Get an instance of the specified class object optionally 35 | * getting the object even if the access is denied. 36 | *
37 | * If the object is not accessible and access is set to false 38 | * or an exception is thrown this will return null. 39 | * 40 | * @param clazz The class to get an instance of. 41 | * @param params The parameters to pass into the constructor method. 42 | * @param The object type to get the instance for. 43 | * @return A new instance of the given class or null if it is not possible. 44 | */ 45 | public static T getInstance(final Class clazz, final Object... params) { 46 | final Constructor con = Reflection.getConstructor(clazz, Reflection.getClassesForObjects(params)); 47 | return con == null ? null : Reflection.getInstance(con, params); 48 | } 49 | 50 | /** 51 | * Get an instance of the class with the given constructor 52 | * using the given parameters. Optionally getting the instance 53 | * whether the constructor is accessible or not. 54 | * 55 | * @param con The constructor of the object class. 56 | * @param params The object parameters to pass to the constructor. 57 | * @param The type of object to retrieve. 58 | * @return The object type of the given constructor. 59 | */ 60 | public static T getInstance(final Constructor con, final Object... params) { 61 | 62 | if (!con.isAccessible()) { 63 | con.setAccessible(true); 64 | } 65 | 66 | try { 67 | return con.newInstance(params); 68 | } catch (final InstantiationException | IllegalAccessException | InvocationTargetException e) { 69 | e.printStackTrace(); 70 | return null; 71 | } 72 | } 73 | 74 | /** 75 | * Set the value of the field with the given name inside of 76 | * the specified class and optionally in the instance of the 77 | * given object. If there is no field with the given name this 78 | * method will do nothing. 79 | * 80 | * @param clazz The class type that the field belongs to. 81 | * @param instance The instance of the class to set the field for. 82 | * @param name The name of the field to set the value of. 83 | * @param value The value to give the field. 84 | */ 85 | public static void setValue(final Class clazz, final Object instance, final String name, final Object value) { 86 | Reflection.setValue(Reflection.getField(clazz, name), instance, value); 87 | } 88 | 89 | /** 90 | * Set a value to the given field providing the object instance 91 | * and the value to set. 92 | * 93 | * @param field The field to set the value to. 94 | * @param instance The instance of the class to set the field for. 95 | * @param value The value to give the field. 96 | */ 97 | public static void setValue(final Field field, final Object instance, final Object value) { 98 | 99 | if (field != null) { 100 | 101 | if (!field.isAccessible()) { 102 | field.setAccessible(true); 103 | } 104 | 105 | try { 106 | field.set(instance, value); 107 | } catch (IllegalAccessException e) { 108 | e.printStackTrace(); 109 | } 110 | } 111 | } 112 | 113 | /** 114 | * Get the value of a field inside of the specified object's 115 | * class with the given name and casting it to the given type. 116 | * If the type of the field is different then the return type T 117 | * then a {@link ClassCastException} will be thrown. 118 | * 119 | * @param clazz The class type that the field belongs to. 120 | * @param instance The instance of the class to get the field for. 121 | * @param name The name of the field to get. 122 | * @param The declaration type of the field. 123 | * @return The value of the given field or null if none exists. 124 | */ 125 | public static T getValue(final Class clazz, final Object instance, final String name) { 126 | return Reflection.getValue(Reflection.getField(clazz, name), instance); 127 | } 128 | 129 | /** 130 | * Get the value of the given field using the given object 131 | * as an instance to access it. 132 | * 133 | * @param field The field to get the value of. 134 | * @param instance The instance of the class to get the field for. 135 | * @param The declaration type of the field. 136 | * @return The value of the given field or null if none exists. 137 | */ 138 | // May need to validate the generic return type T by taking a Class as parameter 139 | public static T getValue(final Field field, final Object instance) { 140 | 141 | if (field == null) { 142 | return null; 143 | } 144 | 145 | if (!field.isAccessible()) { 146 | field.setAccessible(true); 147 | } 148 | 149 | T value = null; 150 | try { 151 | 152 | value = (T) field.get(instance); 153 | 154 | // For now catch the ClassCastException 155 | } catch (final ClassCastException | IllegalAccessException e) { 156 | e.printStackTrace(); 157 | } 158 | 159 | return value; 160 | } 161 | 162 | /** 163 | * Get the field with the specified name whether it is 164 | * accessible or not. If there is no field with the specified 165 | * name or if the field is in a parent class in the hierarchy 166 | * and is inaccessible then this method will return null. 167 | * 168 | * @param clazz The class the field belongs to. 169 | * @param name The name of the field. 170 | * @return The field or null if no field exists. 171 | */ 172 | public static Field getField(final Class clazz, final String name) { 173 | 174 | try { 175 | return clazz.getDeclaredField(name); 176 | } catch (final NoSuchFieldException e) { 177 | 178 | try { 179 | final Class superClazz = clazz.getSuperclass(); 180 | return superClazz == null ? null : superClazz.getField(name); 181 | } catch (final NoSuchFieldException e1) { 182 | // Do nothing just continue 183 | } 184 | } 185 | 186 | for (final Field field : clazz.getDeclaredFields()) { 187 | 188 | if (field.getName().equals(name)) { 189 | return field; 190 | } 191 | } 192 | 193 | return null; 194 | } 195 | 196 | /** 197 | * Invoke a method from the given class with the given name 198 | * and matching the types of the parameters given returning 199 | * the type given. If the type given is not the type of the 200 | * return value of the method found then a {@link ClassCastException} 201 | * will be thrown. 202 | * 203 | * @param clazz The class that the method belong to. 204 | * @param instance The instance to invoke the method on. 205 | * @param name The name of the method to invoke. 206 | * @param params The parameters to pass to the method. 207 | * @param The method return type (if different an exception will be thrown). 208 | * @return The value that the method returned. 209 | */ 210 | public static T invokeMethod(final Class clazz, final Object instance, final String name, final Object... params) { 211 | return Reflection.invokeMethod(Reflection.getMethod(clazz, name, Reflection.getClassesForObjects(params)), instance, params); 212 | } 213 | 214 | /** 215 | * Invoke the given method on the given object instance and with 216 | * the given parameters. 217 | * 218 | * @param method The method to invoke. 219 | * @param instance The instance to invoke the method on. 220 | * @param params The parameters to pass to the method. 221 | * @param The method return type (if different an exception will be thrown). 222 | * @return The value that the method returned. 223 | */ 224 | // May need to validate the generic return type T by taking a Class as parameter 225 | public static T invokeMethod(final Method method, final Object instance, final Object... params) { 226 | 227 | if (method == null) { 228 | return null; 229 | } 230 | 231 | if (!method.isAccessible()) { 232 | method.setAccessible(true); 233 | } 234 | 235 | T value = null; 236 | try { 237 | 238 | value = (T) method.invoke(instance, params); 239 | 240 | // For now catch the ClassCastException 241 | } catch (final ClassCastException | IllegalAccessException | InvocationTargetException e) { 242 | e.printStackTrace(); 243 | } 244 | 245 | return value; 246 | } 247 | 248 | /** 249 | * Get the method with the specified name whether it is 250 | * accessible or not. If there is no method with the specified 251 | * name or if the method is in a parent class in the hierarchy 252 | * and is inaccessible then this method will return null. 253 | * 254 | * @param clazz The class the method belongs to. 255 | * @param name The name of the method. 256 | * @param paramTypes The parameter types of the method. 257 | * @return The method or null if no method exists. 258 | */ 259 | public static Method getMethod(final Class clazz, final String name, final Class... paramTypes) { 260 | 261 | try { 262 | return clazz.getDeclaredMethod(name, paramTypes); 263 | } catch (final NoSuchMethodException e) { 264 | 265 | try { 266 | // No class only methods can be found so search public super class 267 | return clazz.getSuperclass().getMethod(name, paramTypes); 268 | } catch (final NoSuchMethodException e1) { 269 | // Do nothing just continue 270 | } 271 | } 272 | 273 | for (final Method method : clazz.getDeclaredMethods()) { 274 | 275 | if (method.getName().equals(name) && method.getParameterCount() == paramTypes.length 276 | && Reflection.matchParams(method.getParameterTypes(), paramTypes)) { 277 | return method; 278 | } 279 | } 280 | 281 | final Class superClazz = clazz.getSuperclass(); 282 | if (superClazz == null) { 283 | return null; 284 | } 285 | 286 | for (final Method method : superClazz.getMethods()) { 287 | 288 | if (method.getName().equals(name) && method.getParameterCount() == paramTypes.length 289 | && Reflection.matchParams(method.getParameterTypes(), paramTypes)) { 290 | return method; 291 | } 292 | } 293 | 294 | return null; 295 | } 296 | 297 | /** 298 | * Get the constructor in the given class, who's parameter 299 | * types match the parameter types given. 300 | * 301 | * @param clazz The class to get the constructor from. 302 | * @param paramTypes The parameters types to match to. 303 | * @param The type of the class to retrieve the constructor for. 304 | * @return The constructor that matches the requested or null if none is found. 305 | */ 306 | public static Constructor getConstructor(final Class clazz, final Class... paramTypes) { 307 | 308 | try { 309 | return clazz.getDeclaredConstructor(paramTypes); 310 | } catch (final NoSuchMethodException e) { 311 | // Do nothing just continue 312 | } 313 | 314 | // Sometimes we fail to find the constructor due to a type 315 | // that is assignable from the true type, but not the exact type. 316 | // Reflection can be stupid in that way sometimes. 317 | 318 | // Therefore, below, we will get each constructor and check it 319 | // for compatibility ourselves, but widen the restrictions a bit. 320 | 321 | // All constructors in class regardless of accessibility 322 | for (final Constructor con : clazz.getDeclaredConstructors()) { 323 | 324 | // Must have the same amount of parameters 325 | if (con.getParameterCount() == paramTypes.length && Reflection.matchParams(con.getParameterTypes(), paramTypes)) { 326 | // Class is the class we're searching so it's a safe cast 327 | return (Constructor) con; 328 | } 329 | } 330 | 331 | return null; 332 | } 333 | 334 | /** 335 | * Get a class in any NMS package omitting the 336 | * beginning of the canonical name and enter anything 337 | * following the version package. 338 | *

339 | * For example, to get net.minecraft.server.version.PacketPlayOutChat 340 | * simply input PacketPlayOutChat omitting the 341 | * net.minecraft.server.version. 342 | * 343 | * @param name The name of the class to retrieve. 344 | * @return The Minecraft class for the given name or null if class was not found. 345 | */ 346 | public static Class getMcClass(final String name) { 347 | return Reflection.getClass(NMS_PREFIX + name); 348 | } 349 | 350 | /** 351 | * Get a class in any Craftbukkit package omitting the 352 | * beginning of the canonical name and enter anything 353 | * following the version package. 354 | *

355 | * For example, to get org.bukkit.craftbukkit.version.CraftServer 356 | * simply input CraftServer omitting the 357 | * org.bukkit.craftbukkit.version. In addition, in order 358 | * get org.bukkit.craftbukkit.version.entity.CraftPlayer 359 | * simply input entity.CraftPlayer. 360 | * 361 | * @param name The name of the class to retrieve. 362 | * @return The Craftbukkit class for the given name or null if class was not found. 363 | */ 364 | public static Class getCraftClass(final String name) { 365 | return Reflection.getClass(CRAFT_PREFIX + name); 366 | } 367 | 368 | /** 369 | * Tell whether the parameter types of a method or constructor 370 | * match the second given parameter types either exactly or somewhere 371 | * in the hierarchy. 372 | * 373 | * @param params The parameters of the method or constructor. 374 | * @param paramTypes The parameter types to test against. 375 | * @return Whether the parameters match or not. 376 | */ 377 | private static boolean matchParams(final Class[] params, final Class... paramTypes) { 378 | 379 | for (int i = 0; i < params.length; ++i) { 380 | 381 | final Class param = params[i]; 382 | final Class paramType = paramTypes[i]; 383 | 384 | // If there is anything that does not match then return false 385 | if (!param.isAssignableFrom(paramType)) { 386 | 387 | // Primitives can have mismatch problems sometimes 388 | if (param.isPrimitive() || paramType.isPrimitive()) { 389 | final Class type1 = param.isPrimitive() ? param : Reflection.getValue(param, null, "TYPE"); 390 | final Class type2 = 391 | paramType.isPrimitive() ? paramType : Reflection.getValue(paramType, null, "TYPE"); 392 | return type1 == type2; 393 | } 394 | 395 | return false; 396 | } 397 | } 398 | 399 | return true; 400 | } 401 | 402 | /** 403 | * Get all the classes of each object given. 404 | * If no objects are given this will return an empty array. 405 | * 406 | * @param params the parameter objects to get the classes for. 407 | * @return The class types of each parameter object. 408 | */ 409 | private static Class[] getClassesForObjects(final Object... params) { 410 | 411 | final Class[] paramClasses = new Class[params.length]; 412 | for (int i = 0; i < params.length; ++i) { 413 | final Object param = params[i]; // Null check 414 | paramClasses[i] = param == null ? Void.class : param.getClass(); 415 | } 416 | 417 | return paramClasses; 418 | } 419 | 420 | /** 421 | * Get the class for it's exact canonical name. 422 | * 423 | * @param name The canonical name of the class to retrieve. 424 | * @return The class with the name or null if the class is not found. 425 | */ 426 | private static Class getClass(final String name) { 427 | 428 | try { 429 | return Class.forName(name); 430 | } catch (final ClassNotFoundException e) { 431 | return null; 432 | } 433 | } 434 | 435 | // No instance accessibility 436 | private Reflection() { 437 | } 438 | } 439 | --------------------------------------------------------------------------------