diff --git a/api/src/main/java/net/azisaba/azipluginmessaging/api/AziPluginMessaging.java b/api/src/main/java/net/azisaba/azipluginmessaging/api/AziPluginMessaging.java index 02fc942..d99e100 100644 --- a/api/src/main/java/net/azisaba/azipluginmessaging/api/AziPluginMessaging.java +++ b/api/src/main/java/net/azisaba/azipluginmessaging/api/AziPluginMessaging.java @@ -4,10 +4,17 @@ import net.azisaba.azipluginmessaging.api.entity.PlayerAdapter; import net.azisaba.azipluginmessaging.api.protocol.PacketQueue; import net.azisaba.azipluginmessaging.api.server.PacketSender; +import net.azisaba.azipluginmessaging.api.util.SQLThrowableConsumer; +import net.azisaba.azipluginmessaging.api.yaml.YamlObject; +import org.intellij.lang.annotations.Language; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; +import java.sql.PreparedStatement; import java.util.Optional; import java.util.UUID; +import java.util.concurrent.CompletableFuture; public interface AziPluginMessaging { /** @@ -56,7 +63,28 @@ public interface AziPluginMessaging { @NotNull PacketQueue getPacketQueue(); + /** + * Returns the environment which the plugin is running. + * @return the environment + */ + @NotNull + EnvironmentType getEnvironmentType(); + interface Proxy { + @ApiStatus.Internal + default void loadConfig(@NotNull YamlObject obj) { + throw new UnsupportedOperationException("Unsupported in current environment."); + } + + @Contract + default @NotNull CompletableFuture runPreparedStatement(@Language("SQL") @NotNull String sql, @NotNull SQLThrowableConsumer action) { + throw new UnsupportedOperationException("Unsupported in current environment."); + } + + @Contract + default void checkRankAsync(@NotNull UUID uuid) { + throw new UnsupportedOperationException("Unsupported in current environment."); + } } interface Server { diff --git a/api/src/main/java/net/azisaba/azipluginmessaging/api/AziPluginMessagingConfig.java b/api/src/main/java/net/azisaba/azipluginmessaging/api/AziPluginMessagingConfig.java index 5d4f5e1..9f8179a 100644 --- a/api/src/main/java/net/azisaba/azipluginmessaging/api/AziPluginMessagingConfig.java +++ b/api/src/main/java/net/azisaba/azipluginmessaging/api/AziPluginMessagingConfig.java @@ -23,6 +23,7 @@ public class AziPluginMessagingConfig { public static final Map servers = new ConcurrentHashMap<>(); public static final Map saraShowServers = new ConcurrentHashMap<>(); public static final Map rankableServers = new ConcurrentHashMap<>(); + public static final Map contextualServers = new ConcurrentHashMap<>(); /** * Reloads all configuration from config file. @@ -91,7 +92,39 @@ public static void reload() { "# When the proxy receives a packet from non-enabled server, the proxy will drop the packet.", "rankableServers: # this is meaningless in spigot", " life: life", - " lifepve: life" + " lifepve: life", + "", + "# for ProxyboundSetPrefixPacket and ProxyboundClearPrefixPacket", + "contextualServers:", + " life: life", + " lifepve: life", + "", + "# Database settings (required)", + "# This setting is used for setting the rank temporary.", + "database:", + " # (scheme)://(host):(port)/(database)", + " # Keep the line commented out unless you get an obvious error message indicating that the driver was not found.", + " # Default driver (net.azisaba.taxoffice.libs.org.mariadb.jdbc.Driver) points to the bundled MariaDB driver in the TaxOffice jar.", + " #driver: net.azisaba.taxoffice.libs.org.mariadb.jdbc.Driver", + "", + " # change to jdbc:mysql if you want to use MySQL instead of MariaDB", + " scheme: jdbc:mariadb", + " hostname: localhost", + " port: 3306", + " name: azipm", + " username: azipm", + " password: azipm", + " properties:", + " useSSL: true", + " verifyServerCertificate: true", + " prepStmtCacheSize: 250", + " prepStmtCacheSqlLimit: 2048", + " cachePrepStmts: true", + " useServerPrepStmts: true # use server-side prepared statements for performance boost", + " socketTimeout: 30000 # milliseconds", + " useLocalSessionState: true", + " rewriteBatchedStatements: true", + " maintainTimeStats: false" ), StandardOpenOption.CREATE ); @@ -102,9 +135,13 @@ public static void reload() { try { YamlObject obj = new YamlConfiguration(configPath.toAbsolutePath().toString()).asObject(); debug = obj.getBoolean("debug", false); - readMap(servers, obj, "servers"); - readMap(rankableServers, obj, "rankableServers"); - readMap(saraShowServers, obj, "saraShowServers"); + if (AziPluginMessagingProvider.get().getEnvironmentType() == EnvironmentType.VELOCITY) { + readMap(servers, obj, "servers"); + readMap(rankableServers, obj, "rankableServers"); + readMap(saraShowServers, obj, "saraShowServers"); + readMap(contextualServers, obj, "contextualServers"); + AziPluginMessagingProvider.get().getProxy().loadConfig(obj); + } } catch (IOException ex) { Logger.getCurrentLogger().warn("Failed to read config.yml", ex); } diff --git a/api/src/main/java/net/azisaba/azipluginmessaging/api/AziPluginMessagingProvider.java b/api/src/main/java/net/azisaba/azipluginmessaging/api/AziPluginMessagingProvider.java index 1231cc4..f37a368 100644 --- a/api/src/main/java/net/azisaba/azipluginmessaging/api/AziPluginMessagingProvider.java +++ b/api/src/main/java/net/azisaba/azipluginmessaging/api/AziPluginMessagingProvider.java @@ -14,7 +14,7 @@ public final class AziPluginMessagingProvider { " - the AziPluginMessaging plugin is not installed or threw exception while initializing\n" + " - the plugin is not in the dependency of the plugin in the stacktrace\n" + " - tried to access the API before the plugin is loaded (such as constructor)\n" + - " Call #get() in the plugin's onEnable() method to load the API correctly!"; + " Call #get() in the plugin's onEnable() method (or equivalent one) to load the API correctly!"; private static AziPluginMessaging api; diff --git a/api/src/main/java/net/azisaba/azipluginmessaging/api/EnvironmentType.java b/api/src/main/java/net/azisaba/azipluginmessaging/api/EnvironmentType.java new file mode 100644 index 0000000..5f7eee1 --- /dev/null +++ b/api/src/main/java/net/azisaba/azipluginmessaging/api/EnvironmentType.java @@ -0,0 +1,6 @@ +package net.azisaba.azipluginmessaging.api; + +public enum EnvironmentType { + SPIGOT, + VELOCITY, +} diff --git a/api/src/main/java/net/azisaba/azipluginmessaging/api/Logger.java b/api/src/main/java/net/azisaba/azipluginmessaging/api/Logger.java index 740fdd4..1dd1790 100644 --- a/api/src/main/java/net/azisaba/azipluginmessaging/api/Logger.java +++ b/api/src/main/java/net/azisaba/azipluginmessaging/api/Logger.java @@ -132,7 +132,7 @@ public void error(@NotNull String message, Throwable throwable) { } /** - * Returns the currently active logger. If no logger is active, a default logger is returned. + * Returns the logger on the current environment. Returns a default one if the plugin is not yet enabled. * @return the logger */ static @NotNull Logger getCurrentLogger() { diff --git a/api/src/main/java/net/azisaba/azipluginmessaging/api/protocol/Protocol.java b/api/src/main/java/net/azisaba/azipluginmessaging/api/protocol/Protocol.java index f4a95ed..31e4682 100644 --- a/api/src/main/java/net/azisaba/azipluginmessaging/api/protocol/Protocol.java +++ b/api/src/main/java/net/azisaba/azipluginmessaging/api/protocol/Protocol.java @@ -8,10 +8,12 @@ import net.azisaba.azipluginmessaging.api.protocol.handler.ProxyboundClearPrefixPacket; import net.azisaba.azipluginmessaging.api.protocol.handler.ProxyboundEncryptionPacket; import net.azisaba.azipluginmessaging.api.protocol.handler.ProxyboundGiveGamingSaraPacket; +import net.azisaba.azipluginmessaging.api.protocol.handler.ProxyboundGiveNitroSaraPacket; import net.azisaba.azipluginmessaging.api.protocol.handler.ProxyboundGiveSaraPacket; import net.azisaba.azipluginmessaging.api.protocol.handler.ProxyboundSetPrefixPacket; import net.azisaba.azipluginmessaging.api.protocol.handler.ProxyboundSetRankPacket; import net.azisaba.azipluginmessaging.api.protocol.handler.ProxyboundToggleGamingSaraPacket; +import net.azisaba.azipluginmessaging.api.protocol.handler.ProxyboundToggleNitroSaraPacket; import net.azisaba.azipluginmessaging.api.protocol.handler.ProxyboundToggleSaraHidePacket; import net.azisaba.azipluginmessaging.api.protocol.handler.ProxyboundToggleSaraShowPacket; import net.azisaba.azipluginmessaging.api.protocol.handler.ServerMessageHandler; @@ -22,6 +24,7 @@ import net.azisaba.azipluginmessaging.api.protocol.message.PlayerMessage; import net.azisaba.azipluginmessaging.api.protocol.message.PlayerWithServerMessage; import net.azisaba.azipluginmessaging.api.protocol.message.ProxyboundClearPrefixMessage; +import net.azisaba.azipluginmessaging.api.protocol.message.ProxyboundGiveNitroSaraMessage; import net.azisaba.azipluginmessaging.api.protocol.message.ProxyboundGiveSaraMessage; import net.azisaba.azipluginmessaging.api.protocol.message.ProxyboundSetPrefixMessage; import net.azisaba.azipluginmessaging.api.protocol.message.ProxyboundSetRankMessage; @@ -65,6 +68,8 @@ public final class Protocol, M extends Message> { public static final Protocol P_TOGGLE_SARA_SHOW = new Protocol<>(PacketFlow.TO_PROXY, 0x06, new ProxyboundToggleSaraShowPacket()); // contextual public static final Protocol P_SET_PREFIX = new Protocol<>(PacketFlow.TO_PROXY, 0x07, new ProxyboundSetPrefixPacket()); public static final Protocol P_CLEAR_PREFIX = new Protocol<>(PacketFlow.TO_PROXY, 0x08, new ProxyboundClearPrefixPacket()); + public static final Protocol P_GIVE_NITRO_SARA = new Protocol<>(PacketFlow.TO_PROXY, 0x09, new ProxyboundGiveNitroSaraPacket()); + public static final Protocol P_TOGGLE_NITRO_SARA = new Protocol<>(PacketFlow.TO_PROXY, 0x0A, new ProxyboundToggleNitroSaraPacket()); public static final Protocol S_ENCRYPTION = new Protocol<>(PacketFlow.TO_SERVER, 0x00, new ServerboundEncryptionPacket()); public static final Protocol S_ACTION_RESPONSE = new Protocol<>(PacketFlow.TO_SERVER, 0x01, new ServerboundActionResponsePacket()); diff --git a/api/src/main/java/net/azisaba/azipluginmessaging/api/protocol/handler/ProxyboundClearPrefixPacket.java b/api/src/main/java/net/azisaba/azipluginmessaging/api/protocol/handler/ProxyboundClearPrefixPacket.java index 22cdf50..e359595 100644 --- a/api/src/main/java/net/azisaba/azipluginmessaging/api/protocol/handler/ProxyboundClearPrefixPacket.java +++ b/api/src/main/java/net/azisaba/azipluginmessaging/api/protocol/handler/ProxyboundClearPrefixPacket.java @@ -1,5 +1,6 @@ package net.azisaba.azipluginmessaging.api.protocol.handler; +import net.azisaba.azipluginmessaging.api.AziPluginMessagingConfig; import net.azisaba.azipluginmessaging.api.protocol.message.ProxyboundClearPrefixMessage; import net.azisaba.azipluginmessaging.api.server.PacketSender; import net.azisaba.azipluginmessaging.api.server.ServerConnection; @@ -19,7 +20,9 @@ public class ProxyboundClearPrefixPacket implements ProxyMessageHandler { @Override public @NotNull ProxyboundClearPrefixMessage read(@NotNull ServerConnection server, @NotNull DataInputStream in) throws IOException { - return ProxyboundClearPrefixMessage.read(server.getServerInfo().getName(), in); + String serverName = server.getServerInfo().getName(); + serverName = AziPluginMessagingConfig.contextualServers.getOrDefault(serverName, serverName); + return ProxyboundClearPrefixMessage.read(serverName, in); } @Override @@ -30,7 +33,9 @@ public void handle(@NotNull PacketSender sender, @NotNull ProxyboundClearPrefixM throw new IllegalArgumentException("Could not find an user in LuckPerms database: " + msg.getPlayer().getUniqueId()); } NodeMap map = user.data(); - if (msg.isGlobal()) { + if (msg.isAll()) { + LuckPermsUtil.findAllPrefixNodes(map).forEach(map::remove); + } else if (msg.isGlobal()) { LuckPermsUtil.findPrefixNodes(map, null).forEach(map::remove); } else { LuckPermsUtil.findPrefixNodes(map, msg.getServer()).forEach(map::remove); @@ -38,6 +43,10 @@ public void handle(@NotNull PacketSender sender, @NotNull ProxyboundClearPrefixM String username = user.getUsername(); api.getUserManager().saveUser(user); api.getMessagingService().ifPresent(service -> service.pushUserUpdate(user)); + String descServer = "all servers"; + if (!msg.isAll()) { + descServer = "server=" + msg.getServer(); + } api.getActionLogger().submit( Action.builder() .targetType(Action.Target.Type.USER) @@ -46,7 +55,7 @@ public void handle(@NotNull PacketSender sender, @NotNull ProxyboundClearPrefixM .sourceName("AziPluginMessaging[" + msg.getServer() + "]@" + api.getServerName()) .target(msg.getPlayer().getUniqueId()) .targetName(username) - .description("Clear " + username + "'s prefix in server=" + msg.getServer()) + .description("Clear " + username + "'s prefix in " + descServer) .build()); } } diff --git a/api/src/main/java/net/azisaba/azipluginmessaging/api/protocol/handler/ProxyboundGiveNitroSaraPacket.java b/api/src/main/java/net/azisaba/azipluginmessaging/api/protocol/handler/ProxyboundGiveNitroSaraPacket.java new file mode 100644 index 0000000..4fab4ae --- /dev/null +++ b/api/src/main/java/net/azisaba/azipluginmessaging/api/protocol/handler/ProxyboundGiveNitroSaraPacket.java @@ -0,0 +1,90 @@ +package net.azisaba.azipluginmessaging.api.protocol.handler; + +import net.azisaba.azipluginmessaging.api.AziPluginMessagingProvider; +import net.azisaba.azipluginmessaging.api.Logger; +import net.azisaba.azipluginmessaging.api.entity.Player; +import net.azisaba.azipluginmessaging.api.entity.SimplePlayer; +import net.azisaba.azipluginmessaging.api.protocol.message.ProxyboundGiveNitroSaraMessage; +import net.azisaba.azipluginmessaging.api.server.PacketSender; +import net.azisaba.azipluginmessaging.api.server.ServerConnection; +import net.azisaba.azipluginmessaging.api.util.LuckPermsUtil; +import net.luckperms.api.LuckPerms; +import net.luckperms.api.LuckPermsProvider; +import net.luckperms.api.actionlog.Action; +import net.luckperms.api.model.data.DataType; +import net.luckperms.api.model.data.NodeMap; +import net.luckperms.api.model.user.User; +import net.luckperms.api.node.Node; +import org.jetbrains.annotations.NotNull; + +import java.io.DataInputStream; +import java.io.IOException; +import java.sql.SQLException; +import java.time.Instant; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +public class ProxyboundGiveNitroSaraPacket implements ProxyMessageHandler { + private static final String NITRO_GROUP_NAME = "nitro"; + + @Override + public @NotNull ProxyboundGiveNitroSaraMessage read(@NotNull ServerConnection server, @NotNull DataInputStream in) throws IOException { + Player player = SimplePlayer.read(in); + int time = in.readInt(); + TimeUnit unit = TimeUnit.valueOf(in.readUTF()); + return new ProxyboundGiveNitroSaraMessage(player, time, unit); + } + + @Override + public void handle(@NotNull PacketSender sender, @NotNull ProxyboundGiveNitroSaraMessage msg) throws SQLException { + LuckPerms api = LuckPermsProvider.get(); + User user = api.getUserManager().loadUser(msg.getPlayer().getUniqueId()).join(); + if (user == null || user.getUsername() == null) { + throw new IllegalArgumentException("User " + msg.getPlayer().getUniqueId() + " could not be found in the LuckPerms database."); + } + NodeMap map = user.getData(DataType.NORMAL); + Node node = LuckPermsUtil.findParentNode(map, NITRO_GROUP_NAME, null); + if (node == null) { + LuckPermsUtil.addGroup(map, NITRO_GROUP_NAME, null, -1); + } + Node changeSara = LuckPermsUtil.findParentNode(map, "change" + NITRO_GROUP_NAME, null); + if (changeSara == null) { + LuckPermsUtil.addGroup(map, "change" + NITRO_GROUP_NAME, null, -1); + } + // add time + long time = msg.getUnit().toMillis(msg.getTime()); + long expiresAt = System.currentTimeMillis() + time; + Logger.getCurrentLogger().info("Adding time of rank " + NITRO_GROUP_NAME + " to " + user.getUsername() + " for " + msg.getTime() + " " + msg.getUnit().name().toLowerCase()); + AziPluginMessagingProvider.get().getProxy().runPreparedStatement( + "INSERT INTO `temp_rank` (`player_uuid`, `rank`, `expires_at`, `clear_prefix_on_expire`) VALUES (?, ?, ?, 1) ON DUPLICATE KEY UPDATE `expires_at` = `expires_at` + ?", + (ps) -> { + ps.setString(1, msg.getPlayer().getUniqueId().toString()); + ps.setString(2, NITRO_GROUP_NAME); + ps.setLong(3, expiresAt); + ps.setLong(4, time); + ps.executeUpdate(); + }).join(); + AziPluginMessagingProvider.get().getProxy().runPreparedStatement( + "INSERT INTO `temp_rank` (`player_uuid`, `rank`, `expires_at`, `clear_prefix_on_expire`) VALUES (?, ?, ?, 1) ON DUPLICATE KEY UPDATE `expires_at` = `expires_at` + ?", + (ps) -> { + ps.setString(1, msg.getPlayer().getUniqueId().toString()); + ps.setString(2, "change" + NITRO_GROUP_NAME); + ps.setLong(3, expiresAt); + ps.setLong(4, time); + ps.executeUpdate(); + }).join(); + String username = user.getUsername(); + api.getUserManager().saveUser(user).join(); + api.getMessagingService().ifPresent(service -> service.pushUserUpdate(user)); + api.getActionLogger().submit( + Action.builder() + .targetType(Action.Target.Type.USER) + .timestamp(Instant.now()) + .source(new UUID(0L, 0L)) + .sourceName("AziPluginMessaging@" + api.getServerName()) + .target(msg.getPlayer().getUniqueId()) + .targetName(username) + .description("Added nitro sara to " + username + " (+" + msg.getTime() + " " + msg.getUnit().name().toLowerCase() + ")") + .build()); + } +} diff --git a/api/src/main/java/net/azisaba/azipluginmessaging/api/protocol/handler/ProxyboundSetPrefixPacket.java b/api/src/main/java/net/azisaba/azipluginmessaging/api/protocol/handler/ProxyboundSetPrefixPacket.java index bf5fb84..0a957a4 100644 --- a/api/src/main/java/net/azisaba/azipluginmessaging/api/protocol/handler/ProxyboundSetPrefixPacket.java +++ b/api/src/main/java/net/azisaba/azipluginmessaging/api/protocol/handler/ProxyboundSetPrefixPacket.java @@ -1,5 +1,6 @@ package net.azisaba.azipluginmessaging.api.protocol.handler; +import net.azisaba.azipluginmessaging.api.AziPluginMessagingConfig; import net.azisaba.azipluginmessaging.api.protocol.message.ProxyboundSetPrefixMessage; import net.azisaba.azipluginmessaging.api.server.PacketSender; import net.azisaba.azipluginmessaging.api.server.ServerConnection; @@ -19,7 +20,9 @@ public class ProxyboundSetPrefixPacket implements ProxyMessageHandler { @Override public @NotNull ProxyboundSetPrefixMessage read(@NotNull ServerConnection server, @NotNull DataInputStream in) throws IOException { - return ProxyboundSetPrefixMessage.read(server.getServerInfo().getName(), in); + String serverName = server.getServerInfo().getName(); + serverName = AziPluginMessagingConfig.contextualServers.getOrDefault(serverName, serverName); + return ProxyboundSetPrefixMessage.read(serverName, in); } @Override diff --git a/api/src/main/java/net/azisaba/azipluginmessaging/api/protocol/handler/ProxyboundToggleGamingSaraPacket.java b/api/src/main/java/net/azisaba/azipluginmessaging/api/protocol/handler/ProxyboundToggleGamingSaraPacket.java index ec8c0ce..cecae8f 100644 --- a/api/src/main/java/net/azisaba/azipluginmessaging/api/protocol/handler/ProxyboundToggleGamingSaraPacket.java +++ b/api/src/main/java/net/azisaba/azipluginmessaging/api/protocol/handler/ProxyboundToggleGamingSaraPacket.java @@ -29,6 +29,10 @@ public class ProxyboundToggleGamingSaraPacket implements ProxyMessageHandler { + @Override + public @NotNull PlayerMessage read(@NotNull ServerConnection server, @NotNull DataInputStream in) throws IOException { + return PlayerMessage.read(in); + } + + @Override + public void handle(@NotNull PacketSender sender, @NotNull PlayerMessage msg) { + ProxyboundToggleGamingSaraPacket.toggle(sender, msg, "nitro"); + } +} diff --git a/api/src/main/java/net/azisaba/azipluginmessaging/api/protocol/message/PlayerMessage.java b/api/src/main/java/net/azisaba/azipluginmessaging/api/protocol/message/PlayerMessage.java index 9d1f265..85ad6a4 100644 --- a/api/src/main/java/net/azisaba/azipluginmessaging/api/protocol/message/PlayerMessage.java +++ b/api/src/main/java/net/azisaba/azipluginmessaging/api/protocol/message/PlayerMessage.java @@ -8,7 +8,6 @@ import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; -import java.util.UUID; /** * A message with player argument only. @@ -36,11 +35,6 @@ public void write(@NotNull DataOutputStream out) throws IOException { @Contract("_ -> new") public static @NotNull PlayerMessage read(@NotNull DataInputStream in) throws IOException { - UUID uuid = UUID.fromString(in.readUTF()); - String username = null; - if (in.readBoolean()) { - username = in.readUTF(); - } - return new PlayerMessage(new SimplePlayer(uuid, username)); + return new PlayerMessage(SimplePlayer.read(in)); } } diff --git a/api/src/main/java/net/azisaba/azipluginmessaging/api/protocol/message/ProxyboundClearPrefixMessage.java b/api/src/main/java/net/azisaba/azipluginmessaging/api/protocol/message/ProxyboundClearPrefixMessage.java index 263d5cf..b02e05d 100644 --- a/api/src/main/java/net/azisaba/azipluginmessaging/api/protocol/message/ProxyboundClearPrefixMessage.java +++ b/api/src/main/java/net/azisaba/azipluginmessaging/api/protocol/message/ProxyboundClearPrefixMessage.java @@ -16,25 +16,33 @@ */ public class ProxyboundClearPrefixMessage extends PlayerWithServerMessage { protected final boolean global; + protected final boolean all; - public ProxyboundClearPrefixMessage(@Nullable String server, @NotNull Player player) { + public ProxyboundClearPrefixMessage(@NotNull Player player, @Nullable String server, boolean all) { super(server, player); this.global = "global".equals(server); + this.all = all; } - public ProxyboundClearPrefixMessage(boolean global, @NotNull Player player) { + public ProxyboundClearPrefixMessage(@NotNull Player player, boolean global, boolean all) { super(null, player); this.global = global; + this.all = all; } public boolean isGlobal() { return global; } + public boolean isAll() { + return all; + } + @Override public void write(@NotNull DataOutputStream out) throws IOException { // yes, we don't need to write the server name because it is extracted from the ServerConnection. out.writeBoolean(global); + out.writeBoolean(all); super.write(out); } @@ -45,6 +53,7 @@ public static ProxyboundClearPrefixMessage read(@Nullable String server, @NotNul if (in.readBoolean()) { // global trueServer = "global"; } - return new ProxyboundClearPrefixMessage(trueServer, SimplePlayer.read(in)); // player + boolean all = in.readBoolean(); + return new ProxyboundClearPrefixMessage(SimplePlayer.read(in), trueServer, all); // player } } diff --git a/api/src/main/java/net/azisaba/azipluginmessaging/api/protocol/message/ProxyboundGiveNitroSaraMessage.java b/api/src/main/java/net/azisaba/azipluginmessaging/api/protocol/message/ProxyboundGiveNitroSaraMessage.java new file mode 100644 index 0000000..06190b8 --- /dev/null +++ b/api/src/main/java/net/azisaba/azipluginmessaging/api/protocol/message/ProxyboundGiveNitroSaraMessage.java @@ -0,0 +1,35 @@ +package net.azisaba.azipluginmessaging.api.protocol.message; + +import net.azisaba.azipluginmessaging.api.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +public class ProxyboundGiveNitroSaraMessage extends PlayerMessage { + private final int time; + private final TimeUnit unit; + + public ProxyboundGiveNitroSaraMessage(@NotNull Player player, int time, @NotNull TimeUnit unit) { + super(player); + this.time = time; + this.unit = unit; + } + + public int getTime() { + return time; + } + + @NotNull + public TimeUnit getUnit() { + return unit; + } + + @Override + public void write(@NotNull DataOutputStream out) throws IOException { + super.write(out); + out.writeInt(time); + out.writeUTF(unit.name()); // ordinal might be different with other versions of java + } +} diff --git a/api/src/main/java/net/azisaba/azipluginmessaging/api/protocol/message/ProxyboundSetPrefixMessage.java b/api/src/main/java/net/azisaba/azipluginmessaging/api/protocol/message/ProxyboundSetPrefixMessage.java index c0a7e97..d7355c8 100644 --- a/api/src/main/java/net/azisaba/azipluginmessaging/api/protocol/message/ProxyboundSetPrefixMessage.java +++ b/api/src/main/java/net/azisaba/azipluginmessaging/api/protocol/message/ProxyboundSetPrefixMessage.java @@ -18,18 +18,23 @@ public class ProxyboundSetPrefixMessage extends PlayerWithServerMessage { protected final boolean global; protected final String prefix; - public ProxyboundSetPrefixMessage(@Nullable String server, @NotNull String prefix, @NotNull Player player) { + public ProxyboundSetPrefixMessage(@NotNull Player player, @Nullable String server, @NotNull String prefix) { super(server, player); this.global = "global".equals(server); this.prefix = Objects.requireNonNull(prefix, "prefix cannot be null"); } - public ProxyboundSetPrefixMessage(boolean global, @NotNull String prefix, @NotNull Player player) { + public ProxyboundSetPrefixMessage(@NotNull Player player, boolean global, @NotNull String prefix) { super(null, player); this.global = global; this.prefix = Objects.requireNonNull(prefix, "prefix cannot be null"); } + @Contract("_, _, _ -> new") + public static @NotNull ProxyboundSetPrefixMessage createFromServerside(@NotNull Player player, boolean global, @NotNull String prefix) { + return new ProxyboundSetPrefixMessage(player, global, prefix); + } + @NotNull public String getPrefix() { return prefix; @@ -54,6 +59,7 @@ public static ProxyboundSetPrefixMessage read(@Nullable String server, @NotNull if (in.readBoolean()) { // global trueServer = "global"; } - return new ProxyboundSetPrefixMessage(trueServer, in.readUTF(), SimplePlayer.read(in)); // prefix, player + String prefix = in.readUTF(); + return new ProxyboundSetPrefixMessage(SimplePlayer.read(in), trueServer, prefix); } } diff --git a/api/src/main/java/net/azisaba/azipluginmessaging/api/util/LuckPermsUtil.java b/api/src/main/java/net/azisaba/azipluginmessaging/api/util/LuckPermsUtil.java index 842fb5f..e8a5412 100644 --- a/api/src/main/java/net/azisaba/azipluginmessaging/api/util/LuckPermsUtil.java +++ b/api/src/main/java/net/azisaba/azipluginmessaging/api/util/LuckPermsUtil.java @@ -88,6 +88,18 @@ public static Stream findPrefixNodes(@NotNull NodeMap map, @Nullable Strin node.getContexts().getValues("server").contains(server)); } + /** + * Streams all prefix nodes set by {@link #setPrefix(NodeMap, String, String)}. + * @param map the node map + * @return the stream of prefix nodes + */ + @NotNull + public static Stream findAllPrefixNodes(@NotNull NodeMap map) { + return map.toCollection() + .stream() + .filter(node -> node.getType() == NodeType.PREFIX && node.getKey().startsWith("prefix.666.") && node.getValue()); + } + /** * Find a prefix node set by {@link #setPrefix(NodeMap, String, String)}. * @param map the node map diff --git a/api/src/main/java/net/azisaba/azipluginmessaging/api/util/ReflectionUtil.java b/api/src/main/java/net/azisaba/azipluginmessaging/api/util/ReflectionUtil.java index b42999c..1fd2182 100644 --- a/api/src/main/java/net/azisaba/azipluginmessaging/api/util/ReflectionUtil.java +++ b/api/src/main/java/net/azisaba/azipluginmessaging/api/util/ReflectionUtil.java @@ -4,6 +4,7 @@ import org.jetbrains.annotations.Nullable; import java.lang.reflect.Method; +import java.util.NoSuchElementException; public class ReflectionUtil { // 1. look for method on superclass @@ -31,4 +32,25 @@ public static Method findMethod(@NotNull Class clazz, @NotNull Method m) { } return null; } + + @NotNull + public static Class getCallerClass() { + return getCallerClass(3); // 2 + this method + } + + @NotNull + public static Class getCallerClass(int offset) { + StackTraceElement[] stElements = Thread.currentThread().getStackTrace(); + for (int i = 1 + offset; i < stElements.length; i++) { + StackTraceElement ste = stElements[i]; + if (!ste.getClassName().equals(ReflectionUtil.class.getName()) && !ste.getClassName().contains("java.lang.Thread")) { + try { + return Class.forName(ste.getClassName()); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + } + throw new NoSuchElementException("elements: " + stElements.length + ", offset: " + offset); + } } diff --git a/api/src/main/java/net/azisaba/azipluginmessaging/api/util/SQLThrowableConsumer.java b/api/src/main/java/net/azisaba/azipluginmessaging/api/util/SQLThrowableConsumer.java new file mode 100644 index 0000000..06bddcc --- /dev/null +++ b/api/src/main/java/net/azisaba/azipluginmessaging/api/util/SQLThrowableConsumer.java @@ -0,0 +1,9 @@ +package net.azisaba.azipluginmessaging.api.util; + +import org.jetbrains.annotations.NotNull; + +import java.sql.SQLException; + +public interface SQLThrowableConsumer { + void accept(@NotNull T t) throws SQLException; +} diff --git a/api/src/main/java/net/azisaba/azipluginmessaging/api/util/SQLThrowableFunction.java b/api/src/main/java/net/azisaba/azipluginmessaging/api/util/SQLThrowableFunction.java new file mode 100644 index 0000000..e911dd3 --- /dev/null +++ b/api/src/main/java/net/azisaba/azipluginmessaging/api/util/SQLThrowableFunction.java @@ -0,0 +1,11 @@ +package net.azisaba.azipluginmessaging.api.util; + +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; + +import java.sql.SQLException; + +public interface SQLThrowableFunction { + @Contract(pure = true) + R apply(@NotNull T t) throws SQLException; +} diff --git a/build.gradle.kts b/build.gradle.kts index bf8b86a..956f6ba 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,7 +6,7 @@ plugins { } group = "net.azisaba.azipluginmessaging" -version = "2.2.1-SNAPSHOT" +version = "2.3.0-SNAPSHOT" repositories { mavenCentral() diff --git a/spigot/build.gradle.kts b/spigot/build.gradle.kts index 6fd56a6..e91a0d1 100644 --- a/spigot/build.gradle.kts +++ b/spigot/build.gradle.kts @@ -1,12 +1,13 @@ repositories { mavenLocal() - maven { - url = uri("https://hub.spigotmc.org/nexus/content/groups/public/") - } + maven { url = uri("https://hub.spigotmc.org/nexus/content/groups/public/") } + maven { url = uri("https://repo.acrylicstyle.xyz/repository/maven-public/") } } dependencies { api(project(":api")) + implementation("xyz.acrylicstyle.java-util:common:1.0.0-SNAPSHOT") + compileOnlyApi("xyz.acrylicstyle.java-util:common:1.0.0-SNAPSHOT") // spigot-api compileOnly("org.spigotmc:spigot-api:1.12.2-R0.1-SNAPSHOT") diff --git a/spigot/src/main/java/net/azisaba/azipluginmessaging/spigot/AziPluginMessagingSpigot.java b/spigot/src/main/java/net/azisaba/azipluginmessaging/spigot/AziPluginMessagingSpigot.java index 998ada0..8b45a45 100644 --- a/spigot/src/main/java/net/azisaba/azipluginmessaging/spigot/AziPluginMessagingSpigot.java +++ b/spigot/src/main/java/net/azisaba/azipluginmessaging/spigot/AziPluginMessagingSpigot.java @@ -1,6 +1,7 @@ package net.azisaba.azipluginmessaging.spigot; import net.azisaba.azipluginmessaging.api.AziPluginMessaging; +import net.azisaba.azipluginmessaging.api.EnvironmentType; import net.azisaba.azipluginmessaging.api.Logger; import net.azisaba.azipluginmessaging.api.entity.PlayerAdapter; import net.azisaba.azipluginmessaging.api.protocol.PacketQueue; @@ -61,6 +62,11 @@ public PlayerAdapter getPlayerAdapter(@NotNull Class clazz) { return packetQueue; } + @Override + public @NotNull EnvironmentType getEnvironmentType() { + return EnvironmentType.SPIGOT; + } + public static class ServerImpl implements Server { @Override public @NotNull PacketSender getPacketSender() { diff --git a/spigot/src/main/java/net/azisaba/azipluginmessaging/spigot/command/AziPluginMessagingCommand.java b/spigot/src/main/java/net/azisaba/azipluginmessaging/spigot/command/AziPluginMessagingCommand.java index fe3b6f2..11d5b99 100644 --- a/spigot/src/main/java/net/azisaba/azipluginmessaging/spigot/command/AziPluginMessagingCommand.java +++ b/spigot/src/main/java/net/azisaba/azipluginmessaging/spigot/command/AziPluginMessagingCommand.java @@ -5,6 +5,7 @@ import org.bukkit.command.Command; import org.bukkit.command.CommandSender; import org.bukkit.command.TabExecutor; +import xyz.acrylicstyle.util.InvalidArgumentException; import java.util.Collections; import java.util.List; @@ -35,8 +36,31 @@ public boolean onCommand(CommandSender sender, Command command, String label, St System.arraycopy(args, 1, newArgs, 0, newArgs.length); cmd.execute(sender, newArgs); } catch (Exception e) { - sender.sendMessage(ChatColor.RED + "An internal error occurred while executing command: " + e.getMessage()); - e.printStackTrace(); + if (e instanceof InvalidArgumentException) { + InvalidArgumentException ex = (InvalidArgumentException) e; + String error = ChatColor.RED + "Invalid syntax: " + ex.getMessage(); + if (ex.getContext() == null) { + sender.sendMessage(error); + } + StringBuilder sb = new StringBuilder(error); + sb.append("\n"); + String prev = ex.getContext().peekWithAmount(-Math.min(ex.getContext().index(), 15)); + StringBuilder next = new StringBuilder(ex.getContext().peekWithAmount(Math.min(ex.getContext().readableCharacters(), Math.max(15, ex.getLength())))); + if (next.length() == 0) { + for (int i = 0; i < ex.getLength(); i++) { + next.append(' '); + } + } + sb.append(ChatColor.WHITE).append(prev); + String left = next.substring(0, ex.getLength()); + String right = next.substring(ex.getLength(), next.length()); + sb.append(ChatColor.RED).append(ChatColor.UNDERLINE).append(left); + sb.append(ChatColor.WHITE).append(right); + sender.sendMessage(sb.toString()); + } else { + sender.sendMessage(ChatColor.RED + "An internal error occurred while executing command: " + e.getMessage()); + e.printStackTrace(); + } } return true; } diff --git a/spigot/src/main/java/net/azisaba/azipluginmessaging/spigot/command/CommandManager.java b/spigot/src/main/java/net/azisaba/azipluginmessaging/spigot/command/CommandManager.java index e71a038..dbf5e6a 100644 --- a/spigot/src/main/java/net/azisaba/azipluginmessaging/spigot/command/CommandManager.java +++ b/spigot/src/main/java/net/azisaba/azipluginmessaging/spigot/command/CommandManager.java @@ -1,11 +1,15 @@ package net.azisaba.azipluginmessaging.spigot.command; +import net.azisaba.azipluginmessaging.spigot.commands.ClearPrefixCommand; import net.azisaba.azipluginmessaging.spigot.commands.DumpProtocolCommand; import net.azisaba.azipluginmessaging.spigot.commands.GiveGamingSaraCommand; +import net.azisaba.azipluginmessaging.spigot.commands.GiveNitroSaraCommand; import net.azisaba.azipluginmessaging.spigot.commands.GiveSaraCommand; import net.azisaba.azipluginmessaging.spigot.commands.HelpCommand; +import net.azisaba.azipluginmessaging.spigot.commands.SetPrefixCommand; import net.azisaba.azipluginmessaging.spigot.commands.SetRankCommand; import net.azisaba.azipluginmessaging.spigot.commands.ToggleGamingSaraCommand; +import net.azisaba.azipluginmessaging.spigot.commands.ToggleNitroSaraCommand; import net.azisaba.azipluginmessaging.spigot.commands.ToggleSaraHideCommand; import net.azisaba.azipluginmessaging.spigot.commands.ToggleSaraShowCommand; import org.jetbrains.annotations.Nullable; @@ -22,6 +26,10 @@ public class CommandManager { new ToggleGamingSaraCommand(), new ToggleSaraHideCommand(), new ToggleSaraShowCommand(), + new SetPrefixCommand(), + new ClearPrefixCommand(), + new GiveNitroSaraCommand(), + new ToggleNitroSaraCommand(), new DumpProtocolCommand() ); diff --git a/spigot/src/main/java/net/azisaba/azipluginmessaging/spigot/commands/ClearPrefixCommand.java b/spigot/src/main/java/net/azisaba/azipluginmessaging/spigot/commands/ClearPrefixCommand.java new file mode 100644 index 0000000..87a9ab1 --- /dev/null +++ b/spigot/src/main/java/net/azisaba/azipluginmessaging/spigot/commands/ClearPrefixCommand.java @@ -0,0 +1,61 @@ +package net.azisaba.azipluginmessaging.spigot.commands; + +import net.azisaba.azipluginmessaging.api.entity.Player; +import net.azisaba.azipluginmessaging.api.protocol.Protocol; +import net.azisaba.azipluginmessaging.api.protocol.message.ProxyboundClearPrefixMessage; +import net.azisaba.azipluginmessaging.spigot.SpigotPlugin; +import net.azisaba.azipluginmessaging.spigot.command.Command; +import net.azisaba.azipluginmessaging.spigot.util.PlayerUtil; +import org.bukkit.ChatColor; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.NotNull; +import xyz.acrylicstyle.util.ArgumentParsedResult; +import xyz.acrylicstyle.util.ArgumentParser; +import xyz.acrylicstyle.util.ArgumentParserBuilder; +import xyz.acrylicstyle.util.InvalidArgumentException; + +public class ClearPrefixCommand implements Command { + private static final ArgumentParser PARSER = + ArgumentParserBuilder.builder() + .literalBackslash() + .create(); + + @Override + public void execute(@NotNull CommandSender sender, @NotNull String[] args) throws InvalidArgumentException { + ArgumentParsedResult result = PARSER.parse(String.join(" ", args)); + if (result.unhandledArguments().isEmpty()) { + sender.sendMessage(ChatColor.RED + "Usage: " + getFullUsage()); + return; + } + new Thread(() -> { + Player target = PlayerUtil.getOfflinePlayer(result.unhandledArguments().get(0)); + boolean global = result.containsShortArgument('g'); + boolean all = result.containsShortArgument('a'); + if (global && all) { + sender.sendMessage(ChatColor.RED + "You can't use both -g and -a at the same time."); + return; + } + boolean res = Protocol.P_CLEAR_PREFIX.sendPacket(SpigotPlugin.getAnyPacketSenderOrNull(), new ProxyboundClearPrefixMessage(target, global, all)); + if (res) { + sender.sendMessage(ChatColor.GREEN + "Sent a request to clear the prefix of " + target.getUsername() + ""); + } else { + sender.sendMessage(ChatColor.RED + "Failed to send the packet. Maybe check console for errors?"); + } + }).start(); + } + + @Override + public @NotNull String getName() { + return "clearPrefix"; + } + + @Override + public @NotNull String getDescription() { + return "Clear the prefix of a player."; + } + + @Override + public @NotNull String getUsage() { + return " [-g (global) | -a (all)]"; + } +} diff --git a/spigot/src/main/java/net/azisaba/azipluginmessaging/spigot/commands/GiveNitroSaraCommand.java b/spigot/src/main/java/net/azisaba/azipluginmessaging/spigot/commands/GiveNitroSaraCommand.java new file mode 100644 index 0000000..20800db --- /dev/null +++ b/spigot/src/main/java/net/azisaba/azipluginmessaging/spigot/commands/GiveNitroSaraCommand.java @@ -0,0 +1,47 @@ +package net.azisaba.azipluginmessaging.spigot.commands; + +import net.azisaba.azipluginmessaging.api.entity.Player; +import net.azisaba.azipluginmessaging.api.protocol.Protocol; +import net.azisaba.azipluginmessaging.api.protocol.message.ProxyboundGiveNitroSaraMessage; +import net.azisaba.azipluginmessaging.spigot.SpigotPlugin; +import net.azisaba.azipluginmessaging.spigot.command.Command; +import net.azisaba.azipluginmessaging.spigot.util.PlayerUtil; +import org.bukkit.ChatColor; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.TimeUnit; + +public class GiveNitroSaraCommand implements Command { + @Override + public void execute(@NotNull CommandSender sender, @NotNull String[] args) { + if (args.length < 2) { + sender.sendMessage(ChatColor.RED + "Usage: " + getFullUsage()); + return; + } + new Thread(() -> { + Player target = PlayerUtil.getOfflinePlayer(args[0]); + boolean res = Protocol.P_GIVE_NITRO_SARA.sendPacket(SpigotPlugin.getAnyPacketSenderOrNull(), new ProxyboundGiveNitroSaraMessage(target, Integer.parseInt(args[1]), TimeUnit.MINUTES)); + if (res) { + sender.sendMessage(ChatColor.GREEN + "Sent a request to give " + target.getUsername() + " the nitro sara"); + } else { + sender.sendMessage(ChatColor.RED + "Failed to send the packet (attempted to give " + target.getUsernameOrUniqueId() + " the nitro sara). Maybe check console for errors?"); + } + }).start(); + } + + @Override + public @NotNull String getName() { + return "giveNitroSara"; + } + + @Override + public @NotNull String getDescription() { + return "Give a player the nitro sara."; + } + + @Override + public @NotNull String getUsage() { + return " "; + } +} diff --git a/spigot/src/main/java/net/azisaba/azipluginmessaging/spigot/commands/SetPrefixCommand.java b/spigot/src/main/java/net/azisaba/azipluginmessaging/spigot/commands/SetPrefixCommand.java new file mode 100644 index 0000000..f77d873 --- /dev/null +++ b/spigot/src/main/java/net/azisaba/azipluginmessaging/spigot/commands/SetPrefixCommand.java @@ -0,0 +1,60 @@ +package net.azisaba.azipluginmessaging.spigot.commands; + +import net.azisaba.azipluginmessaging.api.entity.Player; +import net.azisaba.azipluginmessaging.api.protocol.Protocol; +import net.azisaba.azipluginmessaging.api.protocol.message.ProxyboundSetPrefixMessage; +import net.azisaba.azipluginmessaging.spigot.SpigotPlugin; +import net.azisaba.azipluginmessaging.spigot.command.Command; +import net.azisaba.azipluginmessaging.spigot.util.PlayerUtil; +import org.bukkit.ChatColor; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.NotNull; +import xyz.acrylicstyle.util.ArgumentParsedResult; +import xyz.acrylicstyle.util.ArgumentParser; +import xyz.acrylicstyle.util.ArgumentParserBuilder; +import xyz.acrylicstyle.util.InvalidArgumentException; + +public class SetPrefixCommand implements Command { + private static final ArgumentParser PARSER = + ArgumentParserBuilder.builder() + .disallowEscapedLineTerminators() + .disallowEscapedTabCharacter() + .parseOptionsWithoutDash() + .create(); + + @Override + public void execute(@NotNull CommandSender sender, @NotNull String[] args) throws InvalidArgumentException { + ArgumentParsedResult result = PARSER.parse(String.join(" ", args)); + if (result.unhandledArguments().size() < 2) { + sender.sendMessage(ChatColor.RED + "Usage: " + getFullUsage()); + return; + } + new Thread(() -> { + Player target = PlayerUtil.getOfflinePlayer(result.unhandledArguments().get(0)); + boolean global = result.containsShortArgument('g'); + String prefix = result.unhandledArguments().get(1).replace('\u00a7', '&'); + ProxyboundSetPrefixMessage message = ProxyboundSetPrefixMessage.createFromServerside(target, global, prefix); + boolean res = Protocol.P_SET_PREFIX.sendPacket(SpigotPlugin.getAnyPacketSenderOrNull(), message); + if (res) { + sender.sendMessage(ChatColor.GREEN + "Sent a request to set the prefix of " + target.getUsername() + " to " + prefix); + } else { + sender.sendMessage(ChatColor.RED + "Failed to send the packet. Maybe check console for errors?"); + } + }).start(); + } + + @Override + public @NotNull String getName() { + return "setPrefix"; + } + + @Override + public @NotNull String getDescription() { + return "Set the prefix of a player."; + } + + @Override + public @NotNull String getUsage() { + return " [-g]"; + } +} diff --git a/spigot/src/main/java/net/azisaba/azipluginmessaging/spigot/commands/ToggleNitroSaraCommand.java b/spigot/src/main/java/net/azisaba/azipluginmessaging/spigot/commands/ToggleNitroSaraCommand.java new file mode 100644 index 0000000..f42fcfb --- /dev/null +++ b/spigot/src/main/java/net/azisaba/azipluginmessaging/spigot/commands/ToggleNitroSaraCommand.java @@ -0,0 +1,50 @@ +package net.azisaba.azipluginmessaging.spigot.commands; + +import net.azisaba.azipluginmessaging.api.entity.Player; +import net.azisaba.azipluginmessaging.api.protocol.Protocol; +import net.azisaba.azipluginmessaging.api.protocol.message.PlayerMessage; +import net.azisaba.azipluginmessaging.spigot.SpigotPlugin; +import net.azisaba.azipluginmessaging.spigot.command.Command; +import net.azisaba.azipluginmessaging.spigot.entity.PlayerImpl; +import net.azisaba.azipluginmessaging.spigot.util.PlayerUtil; +import org.bukkit.ChatColor; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.NotNull; + +public class ToggleNitroSaraCommand implements Command { + @Override + public void execute(@NotNull CommandSender sender, @NotNull String[] args) { + Player target; + if (args.length >= 1) { + target = PlayerUtil.resolveNameOrUUID(args[0]); + } else { + if (!(sender instanceof org.bukkit.entity.Player)) { + sender.sendMessage(ChatColor.RED + "Usage: " + getFullUsage()); + return; + } else { + target = PlayerImpl.of((org.bukkit.entity.Player) sender); + } + } + boolean res = Protocol.P_TOGGLE_NITRO_SARA.sendPacket(SpigotPlugin.getAnyPacketSender(), new PlayerMessage(target)); + if (res) { + sender.sendMessage(ChatColor.GREEN + "Sent a request to toggle " + target.getUsername() + "'s nitro sara"); + } else { + sender.sendMessage(ChatColor.RED + "Failed to send the packet. Maybe check console for errors?"); + } + } + + @Override + public @NotNull String getName() { + return "toggleNitroSara"; + } + + @Override + public @NotNull String getDescription() { + return "Toggles the nitro sara"; + } + + @Override + public @NotNull String getUsage() { + return "[player]"; + } +} diff --git a/spigot/src/main/java/net/azisaba/azipluginmessaging/spigot/util/PlayerUtil.java b/spigot/src/main/java/net/azisaba/azipluginmessaging/spigot/util/PlayerUtil.java index 67bb64a..f7c2cad 100644 --- a/spigot/src/main/java/net/azisaba/azipluginmessaging/spigot/util/PlayerUtil.java +++ b/spigot/src/main/java/net/azisaba/azipluginmessaging/spigot/util/PlayerUtil.java @@ -38,11 +38,7 @@ public class PlayerUtil { try { return new SimplePlayer(fetchUUIDFromMojangAPI(nameOrUUID), nameOrUUID); } catch (IOException e) { - String message = "Failed to fetch UUID of " + nameOrUUID + " from Mojang API.\n" + - "Possible reasons:\n" + - " - the player does not exist\n" + - " - the Mojang API is down\n" + - " - your internet connection is down"; + String message = "Failed to fetch UUID of " + nameOrUUID + " from Mojang API"; throw new RuntimeException(message, e); } } diff --git a/spigot/src/main/resources/plugin.yml b/spigot/src/main/resources/plugin.yml index 4416e77..2dd380a 100644 --- a/spigot/src/main/resources/plugin.yml +++ b/spigot/src/main/resources/plugin.yml @@ -1,6 +1,6 @@ name: AziPluginMessaging main: net.azisaba.azipluginmessaging.spigot.SpigotPlugin -version: 1.0.0 +version: 2.3.0-SNAPSHOT depend: - LuckPerms commands: diff --git a/velocity/build.gradle.kts b/velocity/build.gradle.kts index 225eeee..430e0b1 100644 --- a/velocity/build.gradle.kts +++ b/velocity/build.gradle.kts @@ -5,6 +5,9 @@ repositories { dependencies { api(project(":api")) + @Suppress("GradlePackageUpdate") // can't upgrade due to java version + implementation("com.zaxxer:HikariCP:4.0.3") + implementation("org.mariadb.jdbc:mariadb-java-client:3.0.6") // velocity-api compileOnly("com.velocitypowered:velocity-api:3.0.1") annotationProcessor("com.velocitypowered:velocity-api:3.0.1") @@ -12,6 +15,8 @@ dependencies { tasks { shadowJar { + relocate("org.mariadb.jdbc", "net.azisaba.azipluginmessaging.libs.org.mariadb.jdbc") + relocate("com.zaxxer.hikari", "net.azisaba.azipluginmessaging.libs.com.zaxxer.hikari") archiveFileName.set("AziPluginMessaging-Velocity-${project.version}.jar") } } diff --git a/velocity/src/main/java/net/azisaba/azipluginmessaging/velocity/AziPluginMessagingVelocity.java b/velocity/src/main/java/net/azisaba/azipluginmessaging/velocity/AziPluginMessagingVelocity.java index 4a0c816..3f1ae3b 100644 --- a/velocity/src/main/java/net/azisaba/azipluginmessaging/velocity/AziPluginMessagingVelocity.java +++ b/velocity/src/main/java/net/azisaba/azipluginmessaging/velocity/AziPluginMessagingVelocity.java @@ -3,19 +3,38 @@ import com.velocitypowered.api.proxy.Player; import com.velocitypowered.api.proxy.ProxyServer; import net.azisaba.azipluginmessaging.api.AziPluginMessaging; +import net.azisaba.azipluginmessaging.api.AziPluginMessagingProvider; +import net.azisaba.azipluginmessaging.api.EnvironmentType; import net.azisaba.azipluginmessaging.api.Logger; import net.azisaba.azipluginmessaging.api.entity.PlayerAdapter; import net.azisaba.azipluginmessaging.api.protocol.PacketQueue; +import net.azisaba.azipluginmessaging.api.util.LuckPermsUtil; +import net.azisaba.azipluginmessaging.api.util.SQLThrowableConsumer; +import net.azisaba.azipluginmessaging.api.yaml.YamlObject; import net.azisaba.azipluginmessaging.velocity.entity.PlayerImpl; +import net.luckperms.api.LuckPerms; +import net.luckperms.api.LuckPermsProvider; +import net.luckperms.api.actionlog.Action; +import net.luckperms.api.model.data.DataType; +import net.luckperms.api.model.data.NodeMap; +import net.luckperms.api.model.user.User; +import net.luckperms.api.node.Node; +import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.time.Instant; +import java.util.Objects; import java.util.Optional; import java.util.UUID; +import java.util.concurrent.CompletableFuture; public class AziPluginMessagingVelocity implements AziPluginMessaging { private final ProxyServer server; private final Logger logger; private final Proxy proxy; + private DatabaseConfig databaseConfig; public AziPluginMessagingVelocity(@NotNull ProxyServer server, @NotNull org.slf4j.Logger slf4jLogger) { this.server = server; @@ -55,6 +74,88 @@ public PlayerAdapter getPlayerAdapter(@NotNull Class clazz) { return PacketQueue.EMPTY; } - public static class ProxyImpl implements Proxy { + @Override + public @NotNull EnvironmentType getEnvironmentType() { + return EnvironmentType.VELOCITY; + } + + @Contract(pure = true) + @NotNull + public static AziPluginMessagingVelocity get() { + return (AziPluginMessagingVelocity) AziPluginMessagingProvider.get(); + } + + @NotNull + public DatabaseConfig getDatabaseConfig() { + return Objects.requireNonNull(databaseConfig, "Did not load database config during initialization"); + } + + public class ProxyImpl implements Proxy { + @Override + public void loadConfig(@NotNull YamlObject obj) { + YamlObject section = obj.getObject("database"); + if (section == null) { + throw new IllegalArgumentException("Missing database section in config"); + } + databaseConfig = new DatabaseConfig(section); + } + + @Contract + @Override + public @NotNull CompletableFuture runPreparedStatement(@NotNull String sql, @NotNull SQLThrowableConsumer action) { + return DBConnector.runPreparedStatement(sql, action); + } + + @Override + public void checkRankAsync(@NotNull UUID uuid) { + DBConnector.runPreparedStatement("SELECT `rank`, `expires_at`, `clear_prefix_on_expire` FROM `temp_rank` WHERE `player_uuid` = ? AND `expires_at` > 0 AND `expires_at` < CURRENT_TIMESTAMP", ps -> { + LuckPerms lp = LuckPermsProvider.get(); + User user = lp.getUserManager().getUser(uuid); + if (user == null) { + logger.warn("Failed to check rank for {} because the user data does not exist", uuid); + return; + } + String username = user.getUsername(); + if (username == null) { + logger.warn("Failed to check rank for {} because the username is null for some reason", uuid); + return; + } + ps.setString(1, uuid.toString()); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + String rank = rs.getString("rank"); + long expiresAt = rs.getLong("expires_at"); + boolean clearPrefixOnExpire = rs.getBoolean("clear_prefix_on_expire"); + // remove rank + logger.info("Removing rank {} for {} (expired at {})", rank, username, expiresAt); + DBConnector.runPreparedStatement("DELETE FROM `temp_rank` WHERE `player_uuid` = ?", ps2 -> { + ps2.setString(1, uuid.toString()); + ps2.executeUpdate(); + }).join(); + NodeMap map = user.getData(DataType.NORMAL); + Node rankNode = LuckPermsUtil.findParentNode(map, rank, null); + if (rankNode == null) { + logger.warn("Failed to remove rank {} for {} (already removed)", rank, uuid); + return; + } + map.remove(rankNode); + if (clearPrefixOnExpire) { + LuckPermsUtil.findAllPrefixNodes(map).forEach(map::remove); + } + lp.getUserManager().saveUser(user); + lp.getMessagingService().ifPresent(service -> service.pushUserUpdate(user)); + lp.getActionLogger().submit( + Action.builder() + .targetType(Action.Target.Type.USER) + .timestamp(Instant.now()) + .source(new UUID(0L, 0L)) + .sourceName("AziPluginMessaging@" + lp.getServerName()) + .target(uuid) + .targetName(username) + .description("Removed " + rank + " from " + username + " (expired at " + expiresAt + ", clear prefix: " + clearPrefixOnExpire + ")") + .build()); + } + }); + } } } diff --git a/velocity/src/main/java/net/azisaba/azipluginmessaging/velocity/DBConnector.java b/velocity/src/main/java/net/azisaba/azipluginmessaging/velocity/DBConnector.java new file mode 100644 index 0000000..49296aa --- /dev/null +++ b/velocity/src/main/java/net/azisaba/azipluginmessaging/velocity/DBConnector.java @@ -0,0 +1,121 @@ +package net.azisaba.azipluginmessaging.velocity; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import net.azisaba.azipluginmessaging.api.util.SQLThrowableConsumer; +import net.azisaba.azipluginmessaging.api.util.SQLThrowableFunction; +import org.intellij.lang.annotations.Language; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.mariadb.jdbc.Driver; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; + +public class DBConnector { + private static @Nullable HikariDataSource dataSource; + + /** + * Initializes the data source and pool. + */ + public static void init() { + new Driver(); + HikariConfig config = new HikariConfig(); + if (AziPluginMessagingVelocity.get().getDatabaseConfig().driver() != null) { + config.setDriverClassName(AziPluginMessagingVelocity.get().getDatabaseConfig().driver()); + } + config.setJdbcUrl(AziPluginMessagingVelocity.get().getDatabaseConfig().toUrl()); + config.setUsername(AziPluginMessagingVelocity.get().getDatabaseConfig().username()); + config.setPassword(AziPluginMessagingVelocity.get().getDatabaseConfig().password()); + config.setDataSourceProperties(AziPluginMessagingVelocity.get().getDatabaseConfig().properties()); + dataSource = new HikariDataSource(config); + createTables(); + } + + public static void createTables() { + runPreparedStatement("CREATE TABLE IF NOT EXISTS `temp_rank` (\n" + + " `player_uuid` VARCHAR(36) NOT NULL,\n" + + " `rank` VARCHAR(128) NOT NULL DEFAULT 0,\n" + + " `expires_at` BIGINT NOT NULL DEFAULT 0,\n" + + " `clear_prefix_on_expire` TINYINT(1) NOT NULL DEFAULT 0,\n" + + " UNIQUE KEY `pair` (`player_uuid`, `rank`)\n" + + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;", PreparedStatement::execute).join(); + } + + /** + * Returns the data source. Throws an exception if the data source is not initialized using {@link #init()}. + * @return the data source + * @throws NullPointerException if the data source is not initialized using {@link #init()} + */ + @Contract(pure = true) + @NotNull + public static HikariDataSource getDataSource() { + return Objects.requireNonNull(dataSource, "#init was not called"); + } + + @NotNull + public static Connection getConnection() throws SQLException { + return getDataSource().getConnection(); + } + + @Contract(pure = true) + public static R use(@NotNull SQLThrowableFunction action) throws SQLException { + try (Connection connection = getConnection()) { + return action.apply(connection); + } + } + + @Contract(pure = true) + public static void use(@NotNull SQLThrowableConsumer action) throws SQLException { + try (Connection connection = getConnection()) { + action.accept(connection); + } + } + + @Contract + public static @NotNull CompletableFuture runPreparedStatement(@Language("SQL") @NotNull String sql, @NotNull SQLThrowableConsumer action) { + return CompletableFuture.runAsync(() -> { + try { + use(connection -> { + try (PreparedStatement statement = connection.prepareStatement(sql)) { + action.accept(statement); + } + }); + } catch (SQLException e) { + throw new RuntimeException(e); + } + }); + } + + @Contract(pure = true) + public static R getPreparedStatement(@Language("SQL") @NotNull String sql, @NotNull SQLThrowableFunction action) throws SQLException { + return use(connection -> { + try (PreparedStatement statement = connection.prepareStatement(sql)) { + return action.apply(statement); + } + }); + } + + @Contract(pure = true) + public static void useStatement(@NotNull SQLThrowableConsumer action) throws SQLException { + use(connection -> { + try (Statement statement = connection.createStatement()) { + action.accept(statement); + } + }); + } + + /** + * Closes the data source if it is initialized. + */ + public static void close() { + if (dataSource != null) { + dataSource.close(); + } + } +} diff --git a/velocity/src/main/java/net/azisaba/azipluginmessaging/velocity/DatabaseConfig.java b/velocity/src/main/java/net/azisaba/azipluginmessaging/velocity/DatabaseConfig.java new file mode 100644 index 0000000..ff68604 --- /dev/null +++ b/velocity/src/main/java/net/azisaba/azipluginmessaging/velocity/DatabaseConfig.java @@ -0,0 +1,96 @@ +package net.azisaba.azipluginmessaging.velocity; + +import net.azisaba.azipluginmessaging.api.yaml.YamlObject; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Objects; +import java.util.Properties; + +public final class DatabaseConfig { + private final String driver; + private final String scheme; + private final String hostname; + private final int port; + private final String name; + private final String username; + private final String password; + private final Properties properties; + + public DatabaseConfig(@NotNull YamlObject section) { + String driver = section.getString("driver"); + String scheme = Objects.requireNonNull(section.getString("scheme"), "database.scheme is missing"); + String hostname = Objects.requireNonNull(section.getString("hostname"), "database.hostname is missing"); + int port = section.getInt("port", 3306); + String name = Objects.requireNonNull(section.getString("name"), "database.name is missing"); + String username = section.getString("username"); + String password = section.getString("password"); + Properties properties = new Properties(); + YamlObject props = section.getObject("properties"); + if (props != null) { + props.getRawData().forEach(((key, value) -> properties.setProperty(key, String.valueOf(value)))); + } + this.driver = driver; + this.scheme = scheme; + this.hostname = hostname; + this.port = port; + this.name = name; + this.username = username; + this.password = password; + this.properties = properties; + } + + @Contract(pure = true) + @Nullable + public String driver() { + return driver; + } + + @Contract(pure = true) + @NotNull + public String scheme() { + return scheme; + } + + @Contract(pure = true) + @NotNull + public String hostname() { + return hostname; + } + + @Contract(pure = true) + public int port() { + return port; + } + + @Contract(pure = true) + @NotNull + public String name() { + return name; + } + + @Contract(pure = true) + @Nullable + public String username() { + return username; + } + + @Contract(pure = true) + @Nullable + public String password() { + return password; + } + + @Contract(pure = true) + @NotNull + public Properties properties() { + return properties; + } + + @Contract(pure = true) + @NotNull + public String toUrl() { + return scheme() + "://" + hostname() + ":" + port() + "/" + name(); + } +} diff --git a/velocity/src/main/java/net/azisaba/azipluginmessaging/velocity/VelocityPlugin.java b/velocity/src/main/java/net/azisaba/azipluginmessaging/velocity/VelocityPlugin.java index 75a1c86..c426c0c 100644 --- a/velocity/src/main/java/net/azisaba/azipluginmessaging/velocity/VelocityPlugin.java +++ b/velocity/src/main/java/net/azisaba/azipluginmessaging/velocity/VelocityPlugin.java @@ -3,6 +3,8 @@ import com.google.inject.Inject; import com.velocitypowered.api.event.Subscribe; import com.velocitypowered.api.event.connection.PluginMessageEvent; +import com.velocitypowered.api.event.player.ServerConnectedEvent; +import com.velocitypowered.api.event.proxy.ProxyShutdownEvent; import com.velocitypowered.api.plugin.Dependency; import com.velocitypowered.api.plugin.Plugin; import com.velocitypowered.api.proxy.ProxyServer; @@ -10,15 +12,22 @@ import com.velocitypowered.api.proxy.messages.LegacyChannelIdentifier; import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier; import net.azisaba.azipluginmessaging.api.AziPluginMessagingConfig; +import net.azisaba.azipluginmessaging.api.AziPluginMessagingProvider; import net.azisaba.azipluginmessaging.api.AziPluginMessagingProviderProvider; import net.azisaba.azipluginmessaging.api.protocol.Protocol; import net.azisaba.azipluginmessaging.velocity.server.ServerConnectionImpl; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; -@Plugin(id = "azi-plugin-messaging", name = "AziPluginMessaging", version = "2.2.1-SNAPSHOT", +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +@Plugin(id = "azi-plugin-messaging", name = "AziPluginMessaging", version = "2.3.0-SNAPSHOT", dependencies = @Dependency(id = "luckperms")) public class VelocityPlugin { + private final Map lastTempRankChecked = new ConcurrentHashMap<>(); + @Inject public VelocityPlugin(@NotNull ProxyServer server, @NotNull Logger logger) { server.getChannelRegistrar().register(new LegacyChannelIdentifier(Protocol.LEGACY_CHANNEL_ID)); @@ -26,10 +35,16 @@ public VelocityPlugin(@NotNull ProxyServer server, @NotNull Logger logger) { AziPluginMessagingVelocity api = new AziPluginMessagingVelocity(server, logger); AziPluginMessagingProviderProvider.register(api); AziPluginMessagingConfig.reload(); + DBConnector.init(); + } + + @Subscribe + public void onProxyShutdown(ProxyShutdownEvent e) { + DBConnector.close(); } @Subscribe - public void onPluginMessage(PluginMessageEvent e) { + public void onPluginMessage(@NotNull PluginMessageEvent e) { //net.azisaba.azipluginmessaging.api.Logger.getCurrentLogger().info("Received plugin message: " + e.getIdentifier().getId() + " (source: " + e.getSource() + ")"); if (!(e.getSource() instanceof ServerConnection) || (!e.getIdentifier().getId().equals(Protocol.CHANNEL_ID) && !e.getIdentifier().getId().equals(Protocol.LEGACY_CHANNEL_ID))) { @@ -39,4 +54,18 @@ public void onPluginMessage(PluginMessageEvent e) { Protocol.handleProxySide(new ServerConnectionImpl((ServerConnection) e.getSource()), e.getData()); e.setResult(PluginMessageEvent.ForwardResult.handled()); } + + // check rank expiration + @Subscribe + public void onServerPostConnect(@NotNull ServerConnectedEvent e) { + // check lastTempRankChecked + if (lastTempRankChecked.containsKey(e.getPlayer().getUniqueId())) { + long lastChecked = lastTempRankChecked.get(e.getPlayer().getUniqueId()); + if (System.currentTimeMillis() - lastChecked < 1000 * 60 * 30) { // skip if the rank was checked within 30 minutes + return; + } + } + lastTempRankChecked.put(e.getPlayer().getUniqueId(), System.currentTimeMillis()); + AziPluginMessagingProvider.get().getProxy().checkRankAsync(e.getPlayer().getUniqueId()); + } }