diff --git a/build.gradle.kts b/build.gradle.kts index 5ab14fcf..b8ee7ac4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -28,6 +28,8 @@ dependencies { implementation("org.tomlj:tomlj:1.1.1") // Config lang implementation("com.rabbitmq:amqp-client:5.21.0") // Message broker implementation("dev.hollowcube:polar:1.9.4") // Polar + implementation("com.google.guava:guava:33.2.0-jre") // a lot of things, but mostly caching + implementation("redis.clients:jedis:3.7.0") // redis client } tasks.withType { diff --git a/src/main/java/net/cytonic/cytosis/CytonicNetwork.java b/src/main/java/net/cytonic/cytosis/CytonicNetwork.java new file mode 100644 index 00000000..b424ef52 --- /dev/null +++ b/src/main/java/net/cytonic/cytosis/CytonicNetwork.java @@ -0,0 +1,34 @@ +package net.cytonic.cytosis; + +import lombok.Getter; +import net.cytonic.cytosis.data.RedisDatabase; + +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +/** + * A class that holds data about the status of the Cytonic network + */ +@Getter +public class CytonicNetwork { + private final Set networkPlayers = new HashSet<>(); + private final Set networkPlayerUUIDs = new HashSet<>(); + + public void importDataFromRedis(RedisDatabase redisDatabase) { + networkPlayers.clear(); + networkPlayerUUIDs.clear(); + networkPlayers.addAll(redisDatabase.getOnlinePlayers()); + networkPlayerUUIDs.addAll(redisDatabase.getOnlineUUIDs()); + } + + public void addPlayer(String name, UUID uuid) { + networkPlayers.add(name); + networkPlayerUUIDs.add(uuid); + } + + public void removePlayer(String name, UUID uuid) { + networkPlayers.remove(name); + networkPlayerUUIDs.remove(uuid); + } +} diff --git a/src/main/java/net/cytonic/cytosis/Cytosis.java b/src/main/java/net/cytonic/cytosis/Cytosis.java index e22386e9..1823828d 100644 --- a/src/main/java/net/cytonic/cytosis/Cytosis.java +++ b/src/main/java/net/cytonic/cytosis/Cytosis.java @@ -1,10 +1,5 @@ package net.cytonic.cytosis; -import java.util.HashSet; -import java.util.List; -import java.util.Optional; -import java.util.Set; -import java.util.UUID; import lombok.Getter; import net.cytonic.cytosis.commands.CommandHandler; import net.cytonic.cytosis.config.CytosisSettings; @@ -29,10 +24,13 @@ import net.minestom.server.instance.block.Block; import net.minestom.server.network.ConnectionManager; import net.minestom.server.permission.Permission; +import org.jetbrains.annotations.Nullable; + +import java.util.*; @Getter public class Cytosis { - + public static final String SERVER_ID = generateID(); // manager stuff @Getter private static MinecraftServer minecraftServer; @@ -60,6 +58,9 @@ public class Cytosis { private static RankManager rankManager; @Getter private static ChatManager chatManager; + @Nullable + @Getter + private static CytonicNetwork cytonicNetwork; private static List FLAGS; @@ -129,6 +130,12 @@ public static Optional getPlayer(String username) { return Optional.ofNullable(target); } + /** + * Gets the player if they are on THIS instance, by UUID + * + * @param uuid The uuid to fetch the player by + * @return The optional holding the player if they exist + */ public static Optional getPlayer(UUID uuid) { Player target = null; for (Player onlinePlayer : getOnlinePlayers()) { @@ -159,7 +166,7 @@ public static void loadWorld() { return; } Logger.info(STR."Loading world '\{CytosisSettings.SERVER_WORLD_NAME}'"); - databaseManager.getDatabase().getWorld(CytosisSettings.SERVER_WORLD_NAME).whenComplete((polarWorld, throwable) -> { + databaseManager.getMysqlDatabase().getWorld(CytosisSettings.SERVER_WORLD_NAME).whenComplete((polarWorld, throwable) -> { if (throwable != null) { Logger.error("An error occurred whilst initializing the world!", throwable); } else { @@ -173,7 +180,7 @@ public static void loadWorld() { public static void completeNonEssentialTasks(long start) { Logger.info("Initializing database"); databaseManager = new DatabaseManager(); - databaseManager.setupDatabase(); + databaseManager.setupDatabases(); Logger.info("Database initialized!"); Logger.info("Setting up event handlers"); eventHandler = new EventHandler(MinecraftServer.getGlobalEventHandler()); @@ -184,7 +191,7 @@ public static void completeNonEssentialTasks(long start) { MinecraftServer.getSchedulerManager().buildShutdownTask(() -> { databaseManager.shutdown(); - Logger.info("Good night!"); + messagingManager.shutdown(); }); Logger.info("Initializing server commands"); @@ -205,6 +212,13 @@ public static void completeNonEssentialTasks(long start) { rankManager = new RankManager(); rankManager.init(); + if (CytosisSettings.SERVER_PROXY_MODE) { + Logger.info("Loading network setup!"); + cytonicNetwork = new CytonicNetwork(); + cytonicNetwork.importDataFromRedis(databaseManager.getRedisDatabase()); + } + + // Start the server Logger.info(STR."Server started on port \{CytosisSettings.SERVER_PORT}"); minecraftServer.start("0.0.0.0", CytosisSettings.SERVER_PORT); @@ -217,4 +231,16 @@ public static void completeNonEssentialTasks(long start) { MinecraftServer.stopCleanly(); } } + + private static String generateID() { + //todo: make a check for existing server ids + StringBuilder id = new StringBuilder("Cytosis-"); + Random random = new Random(); + id.append((char) (random.nextInt(26) + 'a')); + for (int i = 0; i < 4; i++) { + id.append(random.nextInt(10)); + } + id.append((char) (random.nextInt(26) + 'a')); + return id.toString(); + } } \ No newline at end of file diff --git a/src/main/java/net/cytonic/cytosis/auditlog/Category.java b/src/main/java/net/cytonic/cytosis/auditlog/Category.java new file mode 100644 index 00000000..ddf18463 --- /dev/null +++ b/src/main/java/net/cytonic/cytosis/auditlog/Category.java @@ -0,0 +1,10 @@ +package net.cytonic.cytosis.auditlog; + +public enum Category { + BAN, + UNBAN, + MUTE, + UNMUTE, + IPBAN, + IPUNBAN +} diff --git a/src/main/java/net/cytonic/cytosis/auditlog/Entry.java b/src/main/java/net/cytonic/cytosis/auditlog/Entry.java new file mode 100644 index 00000000..7fa84615 --- /dev/null +++ b/src/main/java/net/cytonic/cytosis/auditlog/Entry.java @@ -0,0 +1,6 @@ +package net.cytonic.cytosis.auditlog; + +import java.util.UUID; + +public record Entry(UUID uuid, UUID actor, Category category, String reason) { +} diff --git a/src/main/java/net/cytonic/cytosis/commands/CommandHandler.java b/src/main/java/net/cytonic/cytosis/commands/CommandHandler.java index 0fef946a..5b1b0df8 100644 --- a/src/main/java/net/cytonic/cytosis/commands/CommandHandler.java +++ b/src/main/java/net/cytonic/cytosis/commands/CommandHandler.java @@ -1,6 +1,7 @@ package net.cytonic.cytosis.commands; import net.cytonic.cytosis.Cytosis; +import net.cytonic.cytosis.commands.moderation.BanCommand; import net.minestom.server.command.CommandManager; import net.minestom.server.entity.Player; import java.util.Scanner; @@ -22,9 +23,11 @@ public void registerCytosisCommands() { CommandManager cm = Cytosis.getCommandManager(); cm.register(new GamemodeCommand()); cm.register(new RankCommand()); + cm.register(new BanCommand()); cm.register(new ChatChannelCommand()); } + @SuppressWarnings("UnstableApiUsage") public void recalculateCommands(Player player) { player.sendPacket(Cytosis.getCommandManager().createDeclareCommandsPacket(player)); } diff --git a/src/main/java/net/cytonic/cytosis/commands/RankCommand.java b/src/main/java/net/cytonic/cytosis/commands/RankCommand.java index 47cf9ca3..53e8e2ea 100644 --- a/src/main/java/net/cytonic/cytosis/commands/RankCommand.java +++ b/src/main/java/net/cytonic/cytosis/commands/RankCommand.java @@ -40,15 +40,17 @@ public RankCommand() { sender.sendMessage(MM."The player \{context.get(group).getRaw("player")} doesn't exist!"); return; } + if (player == sender) { sender.sendMessage(MM."You cannot change your own rank!"); return; } - Cytosis.getDatabaseManager().getDatabase().getPlayerRank(player.getUuid()).whenComplete((rank, throwable) -> { + Cytosis.getDatabaseManager().getMysqlDatabase().getPlayerRank(player.getUuid()).whenComplete((rank, throwable) -> { if (throwable != null) { sender.sendMessage("An error occurred whilst fetching the old rank!"); return; } + // if it's a console we don't care (There isn't a console impl) if (sender instanceof Player s) { PlayerRank senderRank = Cytosis.getRankManager().getPlayerRank(s.getUuid()).orElseThrow(); @@ -57,13 +59,14 @@ public RankCommand() { return; } } + setRank(player, newRank, sender); }); }, group); } private void setRank(Player player, PlayerRank rank, CommandSender sender) { - Cytosis.getDatabaseManager().getDatabase().setPlayerRank(player.getUuid(), rank).whenComplete((_, t) -> { + Cytosis.getDatabaseManager().getMysqlDatabase().setPlayerRank(player.getUuid(), rank).whenComplete((_, t) -> { if (t != null) { sender.sendMessage(MM."An error occurred whilst setting \{player.getUsername()}'s rank! Check the console for more details."); Logger.error(STR."An error occurred whilst setting \{player.getUsername()}'s rank! Check the console for more details.", t); @@ -73,4 +76,4 @@ private void setRank(Player player, PlayerRank rank, CommandSender sender) { sender.sendMessage(MM."Successfully updated \{player.getUsername()}'s rank!"); }); } -} \ No newline at end of file +} diff --git a/src/main/java/net/cytonic/cytosis/commands/moderation/BanCommand.java b/src/main/java/net/cytonic/cytosis/commands/moderation/BanCommand.java new file mode 100644 index 00000000..d5a0de85 --- /dev/null +++ b/src/main/java/net/cytonic/cytosis/commands/moderation/BanCommand.java @@ -0,0 +1,101 @@ +package net.cytonic.cytosis.commands.moderation; + +import net.cytonic.cytosis.Cytosis; +import net.cytonic.cytosis.auditlog.Category; +import net.cytonic.cytosis.auditlog.Entry; +import net.cytonic.cytosis.enums.BanReason; +import net.cytonic.cytosis.logging.Logger; +import net.cytonic.cytosis.messaging.KickReason; +import net.cytonic.cytosis.utils.BanData; +import net.cytonic.cytosis.utils.DurationParser; +import net.cytonic.cytosis.utils.MessageUtils; +import net.cytonic.cytosis.utils.OfflinePlayer; +import net.minestom.server.command.builder.Command; +import net.minestom.server.command.builder.arguments.ArgumentEnum; +import net.minestom.server.command.builder.arguments.ArgumentType; +import net.minestom.server.command.builder.suggestion.SuggestionEntry; +import net.minestom.server.entity.Player; + +import java.time.Instant; + +import static net.cytonic.cytosis.utils.MiniMessageTemplate.MM; + +public class BanCommand extends Command { + public BanCommand() { + super("ban"); + setCondition((sender, _) -> sender.hasPermission("cytosis.commands.moderation.ban")); + + var playerArg = ArgumentType.Word("target"); + System.out.println(playerArg.getSuggestionCallback()); + playerArg.setSuggestionCallback((sender, context, suggestion) -> { + if (sender instanceof Player player) { + player.sendActionBar(MM."Fetching online players..."); + } + Cytosis.getDatabaseManager().getRedisDatabase().getOnlinePlayers().forEach(player -> + suggestion.addEntry(new SuggestionEntry(player))); + }); + var durationArg = ArgumentType.Word("duration"); + var reasonArg = ArgumentType.Enum("reason", BanReason.class).setFormat(ArgumentEnum.Format.LOWER_CASED); + + var group = ArgumentType.Group("ban-group", playerArg, durationArg, reasonArg); + + addSyntax((sender, context) -> { + if (sender instanceof Player actor) { + + if (!actor.hasPermission("cytosis.commands.moderation.ban")) { + actor.sendMessage(MM."You don't have permission to use this command!"); + } + + final String player = context/*.get(group)*/.get(playerArg); + final String reason = context/*.get(group)*/.get(reasonArg).getReason(); + final String rawDur = context/*.get(group)*/.get(durationArg); + final Instant dur = DurationParser.parse(rawDur); + + if (!Cytosis.getDatabaseManager().getRedisDatabase().getOnlinePlayers().contains(player)) { + sender.sendMessage(MM."The player \{context.get(group).getRaw("player")} doesn't exist!"); + return; + } + Cytosis.getDatabaseManager().getMysqlDatabase().findUUIDByName(player).whenComplete((uuid, throwable) -> { + if (throwable != null) { + sender.sendMessage(MM."An error occured whilst finding \{player}!"); + Logger.error("error; ", throwable); + return; + } + Cytosis.getDatabaseManager().getMysqlDatabase().isBanned(uuid).whenComplete((banned, throwable1) -> { + if (throwable1 != null) { + sender.sendMessage(MM."An error occured whilst finding if \{player} is banned!"); + Logger.error("error; ", throwable1); + return; + } + if (banned.isBanned()) { + sender.sendMessage(MM."\{player} is already banned!"); + return; + } + Cytosis.getDatabaseManager().getMysqlDatabase().getPlayerRank(uuid).whenComplete((playerRank, throwable2) -> { + if (throwable2 != null) { + sender.sendMessage(MM."An error occured whilst finding \{player}'s rank!"); + Logger.error("error; ", throwable2); + return; + } + OfflinePlayer op = new OfflinePlayer(player, uuid, playerRank); + if (op.hasPermission("cytosis.moderation.ban_immune")) { + sender.sendMessage(MM."\{player} cannot be banned!"); + return; + } + + Cytosis.getDatabaseManager().getMysqlDatabase().banPlayer(uuid, reason, dur).whenComplete((_, throwable3) -> { + if (throwable3 != null) { + actor.sendMessage(MM."An error occured whilst banning \{player}!"); + return; + } + Cytosis.getMessagingManager().getRabbitMQ().kickPlayer(op, KickReason.BANNED, MessageUtils.formatBanMessage(new BanData(reason, dur, true))); + actor.sendMessage(MM."\{player} was successfully banned for \{DurationParser.unparseFull(dur)}."); + Cytosis.getDatabaseManager().getMysqlDatabase().addAuditLogEntry(new Entry(uuid, actor.getUuid(), Category.BAN, reason)); + }); + }); + }); + }); + } + }, playerArg, durationArg, reasonArg); + } +} diff --git a/src/main/java/net/cytonic/cytosis/config/CytosisSettings.java b/src/main/java/net/cytonic/cytosis/config/CytosisSettings.java index 1ffc596e..72c75f1c 100644 --- a/src/main/java/net/cytonic/cytosis/config/CytosisSettings.java +++ b/src/main/java/net/cytonic/cytosis/config/CytosisSettings.java @@ -1,6 +1,7 @@ package net.cytonic.cytosis.config; import net.cytonic.cytosis.logging.Logger; + import net.cytonic.cytosis.utils.PosSerializer; import net.minestom.server.coordinate.Pos; import java.util.Map; @@ -14,6 +15,7 @@ public class CytosisSettings { public static boolean LOG_PLAYER_QUITS = true; public static boolean LOG_PLAYER_COMMANDS = true; public static boolean LOG_PLAYER_CHAT = true; + // Database public static boolean DATABASE_ENABLED = true; public static String DATABASE_USER = ""; @@ -22,10 +24,12 @@ public class CytosisSettings { public static int DATABASE_PORT = 3306; public static String DATABASE_NAME = ""; public static boolean DATABASE_USE_SSL = false; + // server public static boolean SERVER_PROXY_MODE = false; public static String SERVER_SECRET = ""; public static int SERVER_PORT = 25565; + public static String SERVER_WORLD_NAME = ""; public static Pos SERVER_SPAWN_POS = new Pos(0, 1, 0); public static String SERVER_HOSTNAME = "UNKNOWN"; @@ -36,6 +40,11 @@ public class CytosisSettings { public static String RABBITMQ_USERNAME = ""; public static int RABBITMQ_PORT = 5672; + //Redis + public static int REDIS_PORT = 6379; + public static String REDIS_HOST = ""; + public static String REDIS_PASSWORD = ""; + public static void inportConfig(Map config) { Logger.info("Importing config!"); config.forEach((key, value) -> { @@ -68,6 +77,11 @@ public static void inportConfig(Map config) { case "rabbitmq.port" -> RABBITMQ_PORT = toInt(value); case "rabbitmq.enabled" -> RABBITMQ_ENABLED = (boolean) value; + // Redis + case "redis.port" -> REDIS_PORT = toInt(value); + case "redis.host" -> REDIS_HOST = (String) value; + case "redis.password" -> REDIS_PASSWORD = (String) value; + default -> { /*Do nothing*/ } } } catch (ClassCastException e) { @@ -88,14 +102,25 @@ public static void loadEnvironmentVariables() { if (!(System.getenv("LOG_PLAYER_QUITS") == null)) CytosisSettings.LOG_PLAYER_QUITS = Boolean.parseBoolean(System.getenv("LOG_PLAYER_QUITS")); if (!(System.getenv("LOG_PLAYER_COMMANDS") == null)) CytosisSettings.LOG_PLAYER_COMMANDS = Boolean.parseBoolean(System.getenv("LOG_PLAYER_COMMANDS")); if (!(System.getenv("LOG_PLAYER_CHAT") == null)) CytosisSettings.LOG_PLAYER_CHAT = Boolean.parseBoolean(System.getenv("LOG_PLAYER_CHAT")); + if (!(System.getenv("LOG_PLAYER_JOINS") == null)) + LOG_PLAYER_JOINS = Boolean.parseBoolean(System.getenv("LOG_PLAYER_JOINS")); + if (!(System.getenv("LOG_PLAYER_QUITS") == null)) + LOG_PLAYER_QUITS = Boolean.parseBoolean(System.getenv("LOG_PLAYER_QUITS")); + if (!(System.getenv("LOG_PLAYER_COMMANDS") == null)) + LOG_PLAYER_COMMANDS = Boolean.parseBoolean(System.getenv("LOG_PLAYER_COMMANDS")); + if (!(System.getenv("LOG_PLAYER_CHAT") == null)) + LOG_PLAYER_CHAT = Boolean.parseBoolean(System.getenv("LOG_PLAYER_CHAT")); // database - if (!(System.getenv("DATABASE_ENABLED") == null)) CytosisSettings.DATABASE_ENABLED = Boolean.parseBoolean(System.getenv("DATABASE_ENABLED")); - if (!(System.getenv("DATABASE_USER") == null)) CytosisSettings.DATABASE_USER = System.getenv("DATABASE_USER"); - if (!(System.getenv("DATABASE_PASSWORD") == null)) CytosisSettings.DATABASE_PASSWORD = System.getenv("DATABASE_PASSWORD"); - if (!(System.getenv("DATABASE_HOST") == null)) CytosisSettings.DATABASE_HOST = System.getenv("DATABASE_HOST"); - if (!(System.getenv("DATABASE_PORT") == null)) CytosisSettings.DATABASE_PORT = Integer.parseInt((System.getenv("DATABASE_PORT"))); - if (!(System.getenv("DATABASE_NAME") == null)) CytosisSettings.DATABASE_NAME = System.getenv("DATABASE_NAME"); - if (!(System.getenv("DATABASE_USE_SSL") == null)) CytosisSettings.DATABASE_USE_SSL = Boolean.parseBoolean(System.getenv("DATABASE_USE_SSL")); + if (!(System.getenv("DATABASE_ENABLED") == null)) + DATABASE_ENABLED = Boolean.parseBoolean(System.getenv("DATABASE_ENABLED")); + if (!(System.getenv("DATABASE_USER") == null)) DATABASE_USER = System.getenv("DATABASE_USER"); + if (!(System.getenv("DATABASE_PASSWORD") == null)) DATABASE_PASSWORD = System.getenv("DATABASE_PASSWORD"); + if (!(System.getenv("DATABASE_HOST") == null)) DATABASE_HOST = System.getenv("DATABASE_HOST"); + if (!(System.getenv("DATABASE_PORT") == null)) + DATABASE_PORT = Integer.parseInt((System.getenv("DATABASE_PORT"))); + if (!(System.getenv("DATABASE_NAME") == null)) DATABASE_NAME = System.getenv("DATABASE_NAME"); + if (!(System.getenv("DATABASE_USE_SSL") == null)) + DATABASE_USE_SSL = Boolean.parseBoolean(System.getenv("DATABASE_USE_SSL")); //server if (!(System.getenv("SERVER_PROXY_MODE") == null)) CytosisSettings.SERVER_PROXY_MODE = Boolean.parseBoolean(System.getenv("SERVER_PROXY_MODE")); if (!(System.getenv("SERVER_SECRET") == null)) CytosisSettings.SERVER_SECRET = System.getenv("SERVER_SECRET"); @@ -103,12 +128,22 @@ public static void loadEnvironmentVariables() { if (!(System.getenv("SERVER_WORLD_NAME") == null)) CytosisSettings.SERVER_WORLD_NAME = System.getenv("SERVER_WORLD_NAME"); if (!(System.getenv("SERVER_SPAWN_POINT") == null)) CytosisSettings.SERVER_SPAWN_POS = PosSerializer.deserialize(System.getenv("SERVER_SPAWN_POINT")); if (!(System.getenv("SERVER_HOSTNAME") == null)) CytosisSettings.SERVER_HOSTNAME = System.getenv("SERVER_HOSTNAME"); + if (!(System.getenv("SERVER_PROXY_MODE") == null)) + SERVER_PROXY_MODE = Boolean.parseBoolean(System.getenv("SERVER_PROXY_MODE")); + if (!(System.getenv("SERVER_SECRET") == null)) SERVER_SECRET = System.getenv("SERVER_SECRET"); + if (!(System.getenv("SERVER_PORT") == null)) SERVER_PORT = Integer.parseInt(System.getenv("SERVER_PORT")); // RabbitMQ - if (!(System.getenv("RABBITMQ_ENABLED") == null)) CytosisSettings.RABBITMQ_ENABLED = Boolean.parseBoolean(System.getenv("RABBITMQ_ENABLED")); - if (!(System.getenv("RABBITMQ_HOST") == null)) CytosisSettings.RABBITMQ_HOST = System.getenv("RABBITMQ_HOST"); - if (!(System.getenv("RABBITMQ_PASSWORD") == null)) CytosisSettings.RABBITMQ_PASSWORD = System.getenv("RABBITMQ_PASSWORD"); - if (!(System.getenv("RABBITMQ_USERNAME") == null)) CytosisSettings.RABBITMQ_USERNAME = System.getenv("RABBITMQ_USERNAME"); - if (!(System.getenv("RABBITMQ_PORT") == null)) CytosisSettings.RABBITMQ_PORT = Integer.parseInt(System.getenv("RABBITMQ_PORT")); + if (!(System.getenv("RABBITMQ_ENABLED") == null)) + RABBITMQ_ENABLED = Boolean.parseBoolean(System.getenv("RABBITMQ_ENABLED")); + if (!(System.getenv("RABBITMQ_HOST") == null)) RABBITMQ_HOST = System.getenv("RABBITMQ_HOST"); + if (!(System.getenv("RABBITMQ_PASSWORD") == null)) RABBITMQ_PASSWORD = System.getenv("RABBITMQ_PASSWORD"); + if (!(System.getenv("RABBITMQ_USERNAME") == null)) RABBITMQ_USERNAME = System.getenv("RABBITMQ_USERNAME"); + if (!(System.getenv("RABBITMQ_PORT") == null)) RABBITMQ_PORT = Integer.parseInt(System.getenv("RABBITMQ_PORT")); + + // redis + if (!(System.getenv("REDIS_HOST") == null)) REDIS_HOST = System.getenv("REDIS_HOST"); + if (!(System.getenv("REDIS_PORT") == null)) REDIS_PORT = Integer.parseInt(System.getenv("REDIS_PORT")); + if (!(System.getenv("REDIS_PASSWORD") == null)) REDIS_PASSWORD = System.getenv("REDIS_PASSWORD"); } public static void loadCommandArgs() { @@ -118,13 +153,24 @@ public static void loadCommandArgs() { if (!(System.getProperty("LOG_PLAYER_QUITS") == null)) CytosisSettings.LOG_PLAYER_QUITS = Boolean.parseBoolean(System.getProperty("LOG_PLAYER_QUITS")); if (!(System.getProperty("LOG_PLAYER_COMMANDS") == null)) CytosisSettings.LOG_PLAYER_COMMANDS = Boolean.parseBoolean(System.getProperty("LOG_PLAYER_COMMANDS")); if (!(System.getProperty("LOG_PLAYER_CHAT") == null)) CytosisSettings.LOG_PLAYER_CHAT = Boolean.parseBoolean(System.getProperty("LOG_PLAYER_CHAT")); + if (!(System.getProperty("LOG_PLAYER_JOINS") == null)) + LOG_PLAYER_JOINS = Boolean.parseBoolean(System.getProperty("LOG_PLAYER_JOINS")); + if (!(System.getProperty("LOG_PLAYER_QUITS") == null)) + LOG_PLAYER_QUITS = Boolean.parseBoolean(System.getProperty("LOG_PLAYER_QUITS")); + if (!(System.getProperty("LOG_PLAYER_COMMANDS") == null)) + LOG_PLAYER_COMMANDS = Boolean.parseBoolean(System.getProperty("LOG_PLAYER_COMMANDS")); + if (!(System.getProperty("LOG_PLAYER_CHAT") == null)) + LOG_PLAYER_CHAT = Boolean.parseBoolean(System.getProperty("LOG_PLAYER_CHAT")); // database - if (!(System.getProperty("DATABASE_USER") == null)) CytosisSettings.DATABASE_USER = System.getProperty("DATABASE_USER"); - if (!(System.getProperty("DATABASE_PASSWORD") == null)) CytosisSettings.DATABASE_PASSWORD = System.getProperty("DATABASE_PASSWORD"); - if (!(System.getProperty("DATABASE_HOST") == null)) CytosisSettings.DATABASE_HOST = System.getProperty("DATABASE_HOST"); - if (!(System.getProperty("DATABASE_PORT") == null)) CytosisSettings.DATABASE_PORT = Integer.parseInt((System.getProperty("DATABASE_PORT"))); - if (!(System.getProperty("DATABASE_NAME") == null)) CytosisSettings.DATABASE_NAME = System.getProperty("DATABASE_NAME"); - if (!(System.getProperty("DATABASE_USE_SSL") == null)) CytosisSettings.DATABASE_USE_SSL = Boolean.parseBoolean(System.getProperty("DATABASE_USE_SSL")); + if (!(System.getProperty("DATABASE_USER") == null)) DATABASE_USER = System.getProperty("DATABASE_USER"); + if (!(System.getProperty("DATABASE_PASSWORD") == null)) + DATABASE_PASSWORD = System.getProperty("DATABASE_PASSWORD"); + if (!(System.getProperty("DATABASE_HOST") == null)) DATABASE_HOST = System.getProperty("DATABASE_HOST"); + if (!(System.getProperty("DATABASE_PORT") == null)) + DATABASE_PORT = Integer.parseInt((System.getProperty("DATABASE_PORT"))); + if (!(System.getProperty("DATABASE_NAME") == null)) DATABASE_NAME = System.getProperty("DATABASE_NAME"); + if (!(System.getProperty("DATABASE_USE_SSL") == null)) + DATABASE_USE_SSL = Boolean.parseBoolean(System.getProperty("DATABASE_USE_SSL")); //server if (!(System.getProperty("SERVER_PROXY_MODE") == null)) CytosisSettings.SERVER_PROXY_MODE = Boolean.parseBoolean(System.getProperty("SERVER_PROXY_MODE")); if (!(System.getProperty("SERVER_SECRET") == null)) CytosisSettings.SERVER_SECRET = System.getProperty("SERVER_SECRET"); @@ -132,11 +178,26 @@ public static void loadCommandArgs() { if (!(System.getProperty("SERVER_WORLD_NAME") == null)) CytosisSettings.SERVER_WORLD_NAME = System.getProperty("SERVER_WORLD_NAME"); if (!(System.getProperty("SERVER_SPAWN_POINT") == null)) CytosisSettings.SERVER_SPAWN_POS = PosSerializer.deserialize(System.getProperty("SERVER_SPAWN_POINT")); if (!(System.getProperty("SERVER_HOSTNAME") == null)) CytosisSettings.SERVER_HOSTNAME = System.getProperty("SERVER_HOSTNAME"); + if (!(System.getProperty("SERVER_PROXY_MODE") == null)) + SERVER_PROXY_MODE = Boolean.parseBoolean(System.getProperty("SERVER_PROXY_MODE")); + if (!(System.getProperty("SERVER_SECRET") == null)) SERVER_SECRET = System.getProperty("SERVER_SECRET"); + if (!(System.getProperty("SERVER_PORT") == null)) + SERVER_PORT = Integer.parseInt(System.getProperty("SERVER_PORT")); // RabbitMQ - if (!(System.getProperty("RABBITMQ_ENABLED") == null)) CytosisSettings.RABBITMQ_ENABLED = Boolean.parseBoolean(System.getProperty("RABBITMQ_ENABLED")); - if (!(System.getProperty("RABBITMQ_HOST") == null)) CytosisSettings.RABBITMQ_HOST = System.getProperty("RABBITMQ_HOST"); - if (!(System.getProperty("RABBITMQ_PASSWORD") == null)) CytosisSettings.RABBITMQ_PASSWORD = System.getProperty("RABBITMQ_PASSWORD"); - if (!(System.getProperty("RABBITMQ_USERNAME") == null)) CytosisSettings.RABBITMQ_USERNAME = System.getProperty("RABBITMQ_USERNAME"); - if (!(System.getProperty("RABBITMQ_PORT") == null)) CytosisSettings.RABBITMQ_PORT = Integer.parseInt(System.getProperty("RABBITMQ_PORT")); + if (!(System.getProperty("RABBITMQ_ENABLED") == null)) + RABBITMQ_ENABLED = Boolean.parseBoolean(System.getProperty("RABBITMQ_ENABLED")); + if (!(System.getProperty("RABBITMQ_HOST") == null)) RABBITMQ_HOST = System.getProperty("RABBITMQ_HOST"); + if (!(System.getProperty("RABBITMQ_PASSWORD") == null)) + RABBITMQ_PASSWORD = System.getProperty("RABBITMQ_PASSWORD"); + if (!(System.getProperty("RABBITMQ_USERNAME") == null)) + RABBITMQ_USERNAME = System.getProperty("RABBITMQ_USERNAME"); + if (!(System.getProperty("RABBITMQ_PORT") == null)) + RABBITMQ_PORT = Integer.parseInt(System.getProperty("RABBITMQ_PORT")); + + // redis + if (!(System.getProperty("REDIS_HOST") == null)) REDIS_HOST = System.getProperty("REDIS_HOST"); + if (!(System.getProperty("REDIS_PORT") == null)) + REDIS_PORT = Integer.parseInt(System.getProperty("REDIS_PORT")); + if (!(System.getProperty("REDIS_PASSWORD") == null)) REDIS_PASSWORD = System.getProperty("REDIS_PASSWORD"); } } \ No newline at end of file diff --git a/src/main/java/net/cytonic/cytosis/data/Database.java b/src/main/java/net/cytonic/cytosis/data/Database.java index 8b74c9a4..e69de29b 100644 --- a/src/main/java/net/cytonic/cytosis/data/Database.java +++ b/src/main/java/net/cytonic/cytosis/data/Database.java @@ -1,385 +0,0 @@ -package net.cytonic.cytosis.data; - -import java.net.SocketAddress; -import java.sql.Connection; -import java.sql.DriverManager; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.util.UUID; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import net.cytonic.cytosis.data.enums.ChatChannel; -import org.jetbrains.annotations.NotNull; -import net.cytonic.cytosis.Cytosis; -import net.cytonic.cytosis.config.CytosisSettings; -import net.cytonic.cytosis.logging.Logger; -import net.cytonic.cytosis.ranks.PlayerRank; -import net.cytonic.cytosis.utils.PosSerializer; -import net.hollowcube.polar.PolarReader; -import net.hollowcube.polar.PolarWorld; -import net.hollowcube.polar.PolarWriter; -import net.minestom.server.MinecraftServer; -import net.minestom.server.coordinate.Pos; - -public class Database { - - private final ExecutorService worker; - private final String host; - private final int port; - private final String database; - private final String username; - private final String password; - private final boolean ssl; - private Connection connection; - - public Database() { - this.worker = Executors.newSingleThreadExecutor(Thread.ofVirtual().name("CytosisDatabaseWorker").uncaughtExceptionHandler((t, e) -> Logger.error(STR."An uncaught exception occoured on the thread: \{t.getName()}", e)).factory()); - this.host = CytosisSettings.DATABASE_HOST; - this.port = CytosisSettings.DATABASE_PORT; - this.database = CytosisSettings.DATABASE_NAME; - this.username = CytosisSettings.DATABASE_USER; - this.password = CytosisSettings.DATABASE_PASSWORD; - this.ssl = CytosisSettings.DATABASE_USE_SSL; - try { - Class.forName("com.mysql.cj.jdbc.Driver"); - } catch (ClassNotFoundException e) { - Logger.error("Failed to load database driver", e); - } - } - - public boolean isConnected() { - return (connection != null); - } - - public void connect() { - worker.submit(() -> { - if (!isConnected()) { - try { - connection = DriverManager.getConnection(STR."jdbc:mysql://\{host}:\{port}/\{database}?useSSL=\{ssl}&autoReconnect=true&allowPublicKeyRetrieval=true", username, password); - Logger.info("Successfully connected to the MySQL Database!"); - Cytosis.loadWorld(); - } catch (SQLException e) { - Logger.error("Invalid Database Credentials!", e); - MinecraftServer.stopCleanly(); - } - } - }); - } - - public void disconnect() { - worker.submit(() -> { - if (isConnected()) { - try { - connection.close(); - Logger.info("Database connection closed!"); - } catch (SQLException e) { - Logger.error("An error occurred whilst disconnecting from the database.", e); - } - } - }); - } - - public void createTables() { - createRanksTable(); - createChatTable(); - createWorldTable(); - createPlayerJoinsTable(); - createChatChannelsTable(); - } - - private Connection getConnection() { - return connection; - } - - /** - * Creates the 'cytonic_chat' table in the database if it doesn't exist. - * The table contains information about player chat messages. - * - * @throws IllegalStateException if the database connection is not open. - */ - private void createChatTable() { - worker.submit(() -> { - if (isConnected()) { - PreparedStatement ps; - try { - ps = getConnection().prepareStatement("CREATE TABLE IF NOT EXISTS cytonic_chat (id INT NOT NULL AUTO_INCREMENT, timestamp TIMESTAMP, uuid VARCHAR(36), message TEXT, PRIMARY KEY(id))"); - ps.executeUpdate(); - } catch (SQLException e) { - Logger.error("An error occurred whilst creating the `cytonic_chat` table.", e); - } - } - }); - } - - /** - * Creates the 'cytonic_ranks' table in the database if it doesn't exist. - * The table contains information about player ranks. - * - * @throws IllegalStateException if the database connection is not open. - */ - private void createRanksTable() { - worker.submit(() -> { - if (isConnected()) { - PreparedStatement ps; - try { - ps = getConnection().prepareStatement("CREATE TABLE IF NOT EXISTS cytonic_ranks (uuid VARCHAR(36), rank_id VARCHAR(16), PRIMARY KEY(uuid))"); - ps.executeUpdate(); - } catch (SQLException e) { - Logger.error("An error occurred whilst creating the `cytonic_ranks` table.", e); - } - } - }); - } - - /** - * Creates the 'cytonic_worlds' table in the database if it doesn't exist. - * The table contains information about the worlds stored in the database. - * - * @throws IllegalStateException if the database connection is not open. - */ - public void createWorldTable() { - worker.submit(() -> { - if (isConnected()) { - PreparedStatement ps; - try { - ps = getConnection().prepareStatement("CREATE TABLE IF NOT EXISTS cytonic_worlds (world_name TEXT, world_type TEXT, last_modified TIMESTAMP, world_data MEDIUMBLOB, spawn_point TEXT, extra_data varchar(100))"); - ps.executeUpdate(); - } catch (SQLException e) { - Logger.error("An error occurred whilst creating the `cytonic_worlds` table.", e); - } - } - }); - } - - /** - * Creates the 'cytonic_chat_channels' table in the database if it doesn't exist. - * The table contains information about player's chat channels. - * - * @throws IllegalStateException if the database connection is not open. - */ -private void createChatChannelsTable() { - worker.submit(() -> { - if (isConnected()) { - PreparedStatement ps; - try { - ps = getConnection().prepareStatement("CREATE TABLE IF NOT EXISTS cytonic_chat_channels (uuid VARCHAR(36), chat_channel VARCHAR(16), PRIMARY KEY(uuid))"); - ps.executeUpdate(); - } catch (SQLException e) { - Logger.error("An error occurred whilst creating the `cytonic_chat_channels` table.", e); - } - } - }); -} - - private void createPlayerJoinsTable() { - worker.submit(() -> { - if (isConnected()) { - try (PreparedStatement ps = getConnection().prepareStatement("CREATE TABLE IF NOT EXISTS cytonic_player_joins (joined TIMESTAMP, uuid VARCHAR(36), ip TEXT)")) { - ps.executeUpdate(); - } catch (SQLException e) { - Logger.error("An error occurred whilst creating the `cytonic_player_joins` table.", e); - } - } - }); - } - - /** - * Gets the player's rank. This returns {@link PlayerRank#DEFAULT} even if the player doesn't exist. - * - * @param uuid the player to fetch the id from - * @return The player's {@link PlayerRank} - * @throws IllegalStateException if the database isn't connected - */ - @NotNull - public CompletableFuture getPlayerRank(@NotNull final UUID uuid) { - CompletableFuture future = new CompletableFuture<>(); - if (!isConnected()) - throw new IllegalStateException("The database must have an open connection to fetch a player's rank!"); - worker.submit(() -> { - String id = "DEFAULT"; - try { - PreparedStatement ps = connection.prepareStatement("SELECT rank_id FROM cytonic_ranks WHERE uuid = ?"); - ps.setString(1, uuid.toString()); - ResultSet rs = ps.executeQuery(); - while (rs.next()) { - id = rs.getString("rank_id"); - } - future.complete(PlayerRank.valueOf(id)); - } catch (SQLException e) { - Logger.error(STR."An error occurred whilst fetching the rank of '\{uuid}'"); - } - }); - return future; - } - - /** - * Sets the given player's rank to the specified rank. - * - * @param uuid The player's UUID - * @param rank The player's rank constant - * @throws IllegalStateException if the database isn't connected - */ - public CompletableFuture setPlayerRank(UUID uuid, PlayerRank rank) { - if (!isConnected()) - throw new IllegalStateException("The database must have an open connection to set a player's rank!"); - CompletableFuture future = new CompletableFuture<>(); - worker.submit(() -> { - try { - PreparedStatement ps = connection.prepareStatement("INSERT INTO cytonic_ranks (uuid, rank_id) VALUES (?, ?) ON DUPLICATE KEY UPDATE rank_id = VALUES(rank_id)"); - ps.setString(1, uuid.toString()); - ps.setString(2, rank.name()); - ps.executeUpdate(); - future.complete(null); - } catch (SQLException e) { - Logger.error(STR."An error occurred whilst setting the rank of '\{uuid}'"); - future.completeExceptionally(e); - } - }); - return future; - } - - /** - * Adds a players chat message to the database. - * - * @param uuid The player's UUID. - * @param message The player's message. - * @throws IllegalStateException if the database isn't connected. - */ - public void addChat(UUID uuid, String message) { - worker.submit(() -> { - if (!isConnected()) - throw new IllegalStateException("The database must have an open connection to add a player's chat!"); - PreparedStatement ps; - try { - ps = connection.prepareStatement("INSERT INTO cytonic_chat (timestamp, uuid, message) VALUES (CURRENT_TIMESTAMP,?,?)"); - ps.setString(1, uuid.toString()); - ps.setString(2, message); - ps.executeUpdate(); - } catch (SQLException e) { - Logger.error("An error occurred whilst adding a chat message.", e); - } - }); - } - - public void setChatChannel(UUID uuid, ChatChannel chatChannel) { - worker.submit(() -> { - if (!isConnected()) - throw new IllegalStateException("The database must have an open connection to add a player's chat!"); - PreparedStatement ps; - try { - ps = connection.prepareStatement("INSERT INTO cytonic_chat_channels (uuid, chat_channel) VALUES (?, ?) ON DUPLICATE KEY UPDATE chat_channel = VALUES(chat_channel)"); - ps.setString(1, uuid.toString()); - ps.setString(2, chatChannel.name()); - ps.executeUpdate(); - } catch (SQLException e) { - Logger.error("An error occurred whilst setting a players chat channel.", e); - } - }); - } - - public CompletableFuture getChatChannel(@NotNull final UUID uuid) { - CompletableFuture future = new CompletableFuture<>(); - if (!isConnected()) - throw new IllegalStateException("The database must have an open connection to fetch a player's chat channel!"); - worker.submit(() -> { - String channel = "ALL"; - try { - PreparedStatement ps = connection.prepareStatement("SELECT chat_channel FROM cytonic_chat_channels WHERE uuid = ?"); - ps.setString(1, uuid.toString()); - ResultSet rs = ps.executeQuery(); - while (rs.next()) { - channel = rs.getString("chat_channel"); - } - future.complete(ChatChannel.valueOf(channel)); - } catch (SQLException e) { - Logger.error(STR."An error occurred whilst fetching the chat channel of '\{uuid}'", e); - } - }); - return future; - } - - /** - * Adds a new world to the database. - * - * @param worldName The name of the world to be added. - * @param worldType The type of the world. - * @param world The PolarWorld object representing the world. - * @param spawnPoint The spawn point of the world. - * @throws IllegalStateException If the database connection is not open. - */ - public void addWorld(String worldName, String worldType, PolarWorld world, Pos spawnPoint) { - if (!isConnected()) - throw new IllegalStateException("The database must have an open connection to add a world!"); - worker.submit(() -> { - try { - PreparedStatement ps = connection.prepareStatement("INSERT INTO cytonic_worlds (world_name, world_type, last_modified, world_data, spawn_point) VALUES (?,?, CURRENT_TIMESTAMP,?,?)"); - ps.setString(1, worldName); - ps.setString(2, worldType); - ps.setBytes(3, PolarWriter.write(world)); - ps.setString(4, PosSerializer.serialize(spawnPoint)); - ps.executeUpdate(); - } catch (SQLException e) { - Logger.error("An error occurred whilst adding a world!", e); - } - }); - } - - /** - * Retrieves a world from the database. - * - * @param worldName The name of the world to fetch. - * @return A {@link CompletableFuture} that completes with the fetched {@link PolarWorld}. - * If the world does not exist in the database, the future will complete exceptionally with a {@link RuntimeException}. - * @throws IllegalStateException If the database connection is not open. - */ - public CompletableFuture getWorld(String worldName) { - CompletableFuture future = new CompletableFuture<>(); - if (!isConnected()) - throw new IllegalStateException("The database must have an open connection to fetch a world!"); - worker.submit(() -> { - try (PreparedStatement ps = connection.prepareStatement("SELECT * FROM cytonic_worlds WHERE world_name = ?")) { - ps.setString(1, worldName); - ResultSet rs = ps.executeQuery(); - if (rs.next()) { - PolarWorld world = PolarReader.read(rs.getBytes("world_data")); - CytosisSettings.SERVER_SPAWN_POS = PosSerializer.deserialize(rs.getString("spawn_point")); - future.complete(world); - } else { - Logger.error("The result set is empty!"); - throw new RuntimeException(STR."World not found: \{worldName}"); - } - } catch (Exception e) { - Logger.error("An error occurred whilst fetching a world!", e); - future.completeExceptionally(e); - throw new RuntimeException(e); - } - }); - return future; - } - - /** - * Logs a player's join event to the database. - * - * @param uuid The unique identifier of the player. - * @param ip The IP address of the player. - *

- * This method uses a worker thread to execute the database operation. - * It prepares a SQL statement to insert a new record into the 'cytonic_player_joins' table. - * The 'joined' column is set to the current timestamp, the 'uuid' column is set to the provided UUID, - * and the 'ip' column is set to the provided IP address. - * If an error occurs during the database operation, it is logged using the Logger. - */ - public void logPlayerJoin(UUID uuid, SocketAddress ip) { - worker.submit(() -> { - try (PreparedStatement ps = connection.prepareStatement("INSERT INTO cytonic_player_joins (joined, uuid, ip) VALUES (CURRENT_TIMESTAMP,?,?)")) { - ps.setString(1, uuid.toString()); - ps.setString(2, ip.toString()); - ps.executeUpdate(); - } catch (SQLException e) { - Logger.error("Failed to add a player to the database!", e); - } - }); - } -} \ No newline at end of file diff --git a/src/main/java/net/cytonic/cytosis/data/DatabaseManager.java b/src/main/java/net/cytonic/cytosis/data/DatabaseManager.java index 60fbfaa8..6e66c1e6 100644 --- a/src/main/java/net/cytonic/cytosis/data/DatabaseManager.java +++ b/src/main/java/net/cytonic/cytosis/data/DatabaseManager.java @@ -1,22 +1,35 @@ package net.cytonic.cytosis.data; import lombok.Getter; +import net.cytonic.cytosis.logging.Logger; @Getter public class DatabaseManager { - private Database database; + private MysqlDatabase mysqlDatabase; + private RedisDatabase redisDatabase; public DatabaseManager() { } public void shutdown() { - database.disconnect(); + mysqlDatabase.disconnect(); + Logger.info("Good night!"); } - public void setupDatabase() { - database = new Database(); - database.connect(); - database.createTables(); + public void setupDatabases() { + Logger.info("Connecting to MySQL Database."); + mysqlDatabase = new MysqlDatabase(); + mysqlDatabase.connect(); + mysqlDatabase.createTables(); + + Logger.info("Connecting to the Redis Database."); + try { + redisDatabase = new RedisDatabase(); // it handles itnitialization in the constructor + } catch (Exception ex) { + Logger.error("An error occured!", ex); + } + Logger.info("All mysqlDatabases connected."); } + } \ No newline at end of file diff --git a/src/main/java/net/cytonic/cytosis/data/MysqlDatabase.java b/src/main/java/net/cytonic/cytosis/data/MysqlDatabase.java new file mode 100644 index 00000000..8fbe5533 --- /dev/null +++ b/src/main/java/net/cytonic/cytosis/data/MysqlDatabase.java @@ -0,0 +1,548 @@ +package net.cytonic.cytosis.data; + +import net.cytonic.cytosis.auditlog.Category; +import net.cytonic.cytosis.auditlog.Entry; +import net.cytonic.cytosis.config.CytosisSettings; +import net.cytonic.cytosis.data.enums.ChatChannel; +import net.cytonic.cytosis.logging.Logger; +import net.cytonic.cytosis.ranks.PlayerRank; +import net.cytonic.cytosis.utils.BanData; +import net.cytonic.cytosis.utils.PosSerializer; +import net.hollowcube.polar.PolarReader; +import net.hollowcube.polar.PolarWorld; +import net.hollowcube.polar.PolarWriter; +import net.minestom.server.MinecraftServer; +import net.minestom.server.coordinate.Pos; +import net.minestom.server.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.net.SocketAddress; +import java.sql.*; +import java.time.Instant; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class MysqlDatabase { + + private final ExecutorService worker; + private final String host; + private final int port; + private final String database; + private final String username; + private final String password; + private final boolean ssl; + private Connection connection; + + public MysqlDatabase() { + this.worker = Executors.newSingleThreadExecutor(Thread.ofVirtual().name("CytosisDatabaseWorker").uncaughtExceptionHandler((t, e) -> Logger.error(STR."An uncaught exception occoured on the thread: \{t.getName()}", e)).factory()); + this.host = CytosisSettings.DATABASE_HOST; + this.port = CytosisSettings.DATABASE_PORT; + this.database = CytosisSettings.DATABASE_NAME; + this.username = CytosisSettings.DATABASE_USER; + this.password = CytosisSettings.DATABASE_PASSWORD; + this.ssl = CytosisSettings.DATABASE_USE_SSL; + try { + Class.forName("com.mysql.cj.jdbc.Driver"); + } catch (ClassNotFoundException e) { + Logger.error("Failed to load database driver", e); + } + } + + public boolean isConnected() { + return (connection != null); + } + + public void connect() { + worker.submit(() -> { + if (!isConnected()) { + try { + connection = DriverManager.getConnection(STR."jdbc:mysql://\{host}:\{port}/\{database}?useSSL=\{ssl}&autoReconnect=true&allowPublicKeyRetrieval=true", username, password); + Logger.info("Successfully connected to the MySQL Database!"); + } catch (SQLException e) { + Logger.error("Invalid Database Credentials!", e); + MinecraftServer.stopCleanly(); + } + } + }); + + } + + public void disconnect() { + worker.submit(() -> { + if (isConnected()) { + try { + connection.close(); + Logger.info("Database connection closed!"); + } catch (SQLException e) { + Logger.error("An error occurred whilst disconnecting from the database. Please report the following stacktrace to Foxikle: ", e); + } + } + }); + } + + public void createTables() { + createRanksTable(); + createChatTable(); + createBansTable(); + createAuditLogTable(); + createPlayersTable(); + createWorldTable(); + createPlayerJoinsTable(); + createChatChannelsTable(); + } + + private Connection getConnection() { + return connection; + } + + private void createChatTable() { + worker.submit(() -> { + if (isConnected()) { + PreparedStatement ps; + try { + ps = getConnection().prepareStatement("CREATE TABLE IF NOT EXISTS cytonicchat (id INT NOT NULL AUTO_INCREMENT, timestamp TIMESTAMP, uuid VARCHAR(36), message TEXT, PRIMARY KEY(id))"); + ps.executeUpdate(); + } catch (SQLException e) { + Logger.error("An error occoured whilst fetching data from the database. Please report the following stacktrace to Foxikle:", e); + } + } + }); + } + + private void createRanksTable() { + worker.submit(() -> { + if (isConnected()) { + PreparedStatement ps; + try { + ps = getConnection().prepareStatement("CREATE TABLE IF NOT EXISTS cytonic_ranks (uuid VARCHAR(36), rank_id VARCHAR(16), PRIMARY KEY(uuid))"); + ps.executeUpdate(); + } catch (SQLException e) { + Logger.error("An error occoured whilst creating the `cytonic_ranks` table.", e); + } + } + }); + } + + private void createBansTable() { + worker.submit(() -> { + if (isConnected()) { + PreparedStatement ps; + try { + ps = getConnection().prepareStatement("CREATE TABLE IF NOT EXISTS cytonic_bans (uuid VARCHAR(36), to_expire VARCHAR(100), reason TINYTEXT, PRIMARY KEY(uuid))"); + ps.executeUpdate(); + } catch (SQLException e) { + Logger.error("An error occoured whilst creating the `cytonic_bans` table.", e); + } + } + }); + } + + private void createPlayersTable() { + worker.submit(() -> { + if (isConnected()) { + PreparedStatement ps; + try { + ps = getConnection().prepareStatement("CREATE TABLE IF NOT EXISTS cytonic_players (uuid VARCHAR(36), name VARCHAR(16), PRIMARY KEY(uuid))"); + ps.executeUpdate(); + } catch (SQLException e) { + Logger.error("An error occoured whilst creating the `cytonic_players` table.", e); + } + } + }); + } + + /** + * actor is staff

+ * category would be BAN, see {@link Category}

+ * uuid is the player

+ * id and timestamp are handled by mysql

+ */ + private void createAuditLogTable() { + worker.submit(() -> { + if (isConnected()) { + PreparedStatement ps; + try { + ps = getConnection().prepareStatement("CREATE TABLE IF NOT EXISTS cytonic_audit_log (id INT NOT NULL AUTO_INCREMENT, timestamp TIMESTAMP, uuid VARCHAR(36), reason TINYTEXT, category VARCHAR(50), actor VARCHAR(36), PRIMARY KEY(id))"); + ps.executeUpdate(); + } catch (SQLException e) { + Logger.error("An error occoured whilst fetching data from the database. Please report the following stacktrace to Foxikle:", e); + } + } + }); + } + + /** + * Gets the player's rank. This returns {@link PlayerRank#DEFAULT} even if the player doesn't exist. + * + * @param uuid the player to fetch the id from + * @return The player's {@link PlayerRank} + * @throws IllegalStateException if the database isn't connected + */ + @NotNull + public CompletableFuture getPlayerRank(@NotNull final UUID uuid) { + CompletableFuture future = new CompletableFuture<>(); + if (!isConnected()) + throw new IllegalStateException("The database must have an open connection to fetch a player's rank!"); + worker.submit(() -> { + try { + PreparedStatement ps = connection.prepareStatement("SELECT rank_id FROM cytonic_ranks WHERE uuid = ?"); + ps.setString(1, uuid.toString()); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + future.complete(PlayerRank.valueOf(rs.getString("rank_id"))); + } else { + future.complete(PlayerRank.DEFAULT); + setPlayerRank(uuid, PlayerRank.DEFAULT); + } + } catch (SQLException e) { + Logger.error(STR."An error occurred whilst fetching the rank of '\{uuid}'"); + } + }); + return future; + } + + /** + * Sets the given player's rank to the specified rank. + * + * @param uuid The player's UUID + * @param rank The player's rank constant + * @throws IllegalStateException if the database isn't connected + */ + public CompletableFuture setPlayerRank(UUID uuid, PlayerRank rank) { + if (!isConnected()) + throw new IllegalStateException("The database must have an open connection to set a player's rank!"); + CompletableFuture future = new CompletableFuture<>(); + worker.submit(() -> { + try { + PreparedStatement ps = connection.prepareStatement("INSERT INTO cytonic_ranks (uuid, rank_id) VALUES (?, ?) ON DUPLICATE KEY UPDATE rank_id = VALUES(rank_id)"); + ps.setString(1, uuid.toString()); + ps.setString(2, rank.name()); + ps.executeUpdate(); + future.complete(null); + } catch (SQLException e) { + Logger.error(STR."An error occurred whilst setting the rank of '\{uuid}'"); + } + }); + return future; + } + + public void addChat(UUID uuid, String message) { + worker.submit(() -> { + PreparedStatement ps; + try { + ps = connection.prepareStatement("INSERT INTO cytonicchat (timestamp, uuid, message) VALUES (CURRENT_TIMESTAMP,?,?)"); + ps.setString(1, uuid.toString()); + ps.setString(2, message); + ps.executeUpdate(); + } catch (SQLException e) { + throw new RuntimeException(e); + } + }); + } + + public CompletableFuture addAuditLogEntry(Entry entry) { + if (!isConnected()) throw new IllegalStateException("The database must be connected to add an auditlog entry."); + CompletableFuture future = new CompletableFuture<>(); + worker.submit(() -> { + PreparedStatement ps; + try { + ps = connection.prepareStatement("INSERT INTO cytonic_audit_log (timestamp, uuid, reason, category, actor) VALUES (CURRENT_TIMESTAMP,?,?,?,?)"); + ps.setString(1, entry.uuid().toString()); + ps.setString(2, entry.reason()); + ps.setString(3, entry.category().name()); + ps.setString(4, entry.actor().toString()); + ps.executeUpdate(); + future.complete(null); + } catch (SQLException e) { + Logger.error("An error occurred whilst adding an auditlog entry!", e); + future.completeExceptionally(e); + } + }); + return future; + } + + public CompletableFuture banPlayer(UUID uuid, String reason, Instant toExpire) { + CompletableFuture future = new CompletableFuture<>(); + + worker.submit(() -> { + if (!isConnected()) throw new IllegalStateException("The database must be connected to ban players."); + try { + PreparedStatement ps = getConnection().prepareStatement("INSERT IGNORE INTO cytonic_bans (uuid, to_expire, reason) VALUES (?,?,?)"); + ps.setString(1, uuid.toString()); + ps.setString(2, toExpire.toString()); + ps.setString(3, reason); + ps.executeUpdate(); + future.complete(null); + } catch (SQLException e) { + Logger.error(STR."An error occurred whilst banning the player \{uuid}.", e); + future.completeExceptionally(e); + } + }); + + return future; + } + + /** + * The concurrent friendly way to fetch a player's ban status + * + * @param uuid THe player to check + * @return The CompletableFuture that holds the player's ban status + */ + public CompletableFuture isBanned(UUID uuid) { + if (!isConnected()) throw new IllegalStateException("The database must be connected."); + CompletableFuture future = new CompletableFuture<>(); + worker.submit(() -> { + try { + PreparedStatement ps = getConnection().prepareStatement("SELECT * FROM cytonic_bans WHERE uuid = ?"); + ps.setString(1, uuid.toString()); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + Instant expiry = Instant.parse(rs.getString("to_expire")); + if (expiry.isBefore(Instant.now())) { + future.complete(new BanData(null, null, false)); + unbanPlayer(uuid); + } else { + try { + BanData banData = new BanData(rs.getString("reason"), expiry, true); + future.complete(banData); + } catch (Exception e) { + Logger.error(STR."An error occurred whilst determining if the player \{uuid} is banned.", e); + future.complete(new BanData(null, null, true)); + } + } + } else { + future.complete(new BanData(null, null, false)); + } + } catch (SQLException e) { + Logger.error(STR."An error occurred whilst determining if the player \{uuid} is banned.", e); + future.completeExceptionally(e); + } + }); + return future; + } + + public CompletableFuture findUUIDByName(String name) { + if (!isConnected()) throw new IllegalStateException("The database must be connected."); + CompletableFuture future = new CompletableFuture<>(); + worker.submit(() -> { + try { + PreparedStatement ps = getConnection().prepareStatement("SELECT * FROM cytonic_players WHERE name = ?"); + ps.setString(1, name); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + future.complete(UUID.fromString(rs.getString("uuid"))); + } else { + future.completeExceptionally(new IllegalArgumentException(STR."The player '\{name}' doesn't exist!")); + } + } catch (SQLException e) { + Logger.error(STR."An error occurred whilst determining \{name}'s UUID.", e); + future.completeExceptionally(e); + } + }); + return future; + } + + public CompletableFuture addPlayer(Player player) { + if (!isConnected()) throw new IllegalStateException("The database must be connected."); + CompletableFuture future = new CompletableFuture<>(); + worker.submit(() -> { + try { + PreparedStatement ps = getConnection().prepareStatement("INSERT IGNORE INTO cytonic_players (name, uuid) VALUES (?,?) ON DUPLICATE KEY UPDATE name = ?"); + ps.setString(1, player.getUsername()); + ps.setString(2, player.getUuid().toString()); + ps.setString(3, player.getUsername()); + ps.executeUpdate(); + future.complete(null); + } catch (SQLException e) { + Logger.error(STR."An error occurred whilst setting the name of \{player.getUuid().toString()}.", e); + future.completeExceptionally(e); + } + }); + return future; + } + + public CompletableFuture unbanPlayer(UUID uuid) { + if (!isConnected()) throw new IllegalStateException("The database must be connected."); + CompletableFuture future = new CompletableFuture<>(); + worker.submit(() -> { + try { + PreparedStatement ps = getConnection().prepareStatement("DELETE FROM cytonic_bans WHERE uuid = ?"); + ps.setString(1, uuid.toString()); + ps.executeUpdate(); + future.complete(null); + } catch (SQLException e) { + Logger.error(STR."An error occurred whilst unbanning the player \{uuid}.", e); + future.completeExceptionally(e); + } + }); + return future; + } + + public void createWorldTable() { + worker.submit(() -> { + if (isConnected()) { + PreparedStatement ps; + try { + ps = getConnection().prepareStatement("CREATE TABLE IF NOT EXISTS cytonic_worlds (world_name TEXT, world_type TEXT, last_modified TIMESTAMP, world_data MEDIUMBLOB, spawn_point TEXT, extra_data varchar(100))"); + ps.executeUpdate(); + } catch (SQLException e) { + Logger.error("An error occurred whilst creating the `cytonic_worlds` table.", e); + } + } + }); + } + + /** + * Creates the 'cytonic_chat_channels' table in the database if it doesn't exist. + * The table contains information about player's chat channels. + * + * @throws IllegalStateException if the database connection is not open. + */ + private void createChatChannelsTable() { + worker.submit(() -> { + if (isConnected()) { + PreparedStatement ps; + try { + ps = getConnection().prepareStatement("CREATE TABLE IF NOT EXISTS cytonic_chat_channels (uuid VARCHAR(36), chat_channel VARCHAR(16), PRIMARY KEY(uuid))"); + ps.executeUpdate(); + } catch (SQLException e) { + Logger.error("An error occurred whilst creating the `cytonic_chat_channels` table.", e); + } + } + }); + } + + private void createPlayerJoinsTable() { + worker.submit(() -> { + if (isConnected()) { + try (PreparedStatement ps = getConnection().prepareStatement("CREATE TABLE IF NOT EXISTS cytonic_player_joins (joined TIMESTAMP, uuid VARCHAR(36), ip TEXT)")) { + ps.executeUpdate(); + } catch (SQLException e) { + Logger.error("An error occurred whilst creating the `cytonic_player_joins` table.", e); + } + } + }); + } + + public void setChatChannel(UUID uuid, ChatChannel chatChannel) { + worker.submit(() -> { + if (!isConnected()) + throw new IllegalStateException("The database must have an open connection to add a player's chat!"); + PreparedStatement ps; + try { + ps = connection.prepareStatement("INSERT INTO cytonic_chat_channels (uuid, chat_channel) VALUES (?, ?) ON DUPLICATE KEY UPDATE chat_channel = VALUES(chat_channel)"); + ps.setString(1, uuid.toString()); + ps.setString(2, chatChannel.name()); + ps.executeUpdate(); + } catch (SQLException e) { + Logger.error("An error occurred whilst setting a players chat channel.", e); + } + }); + } + + public CompletableFuture getChatChannel(@NotNull final UUID uuid) { + CompletableFuture future = new CompletableFuture<>(); + if (!isConnected()) + throw new IllegalStateException("The database must have an open connection to fetch a player's chat channel!"); + worker.submit(() -> { + String channel = "ALL"; + try { + PreparedStatement ps = connection.prepareStatement("SELECT chat_channel FROM cytonic_chat_channels WHERE uuid = ?"); + ps.setString(1, uuid.toString()); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + channel = rs.getString("chat_channel"); + } + future.complete(ChatChannel.valueOf(channel)); + } catch (SQLException e) { + Logger.error(STR."An error occurred whilst fetching the chat channel of '\{uuid}'", e); + } + }); + return future; + } + + /** + * Adds a new world to the database. + * + * @param worldName The name of the world to be added. + * @param worldType The type of the world. + * @param world The PolarWorld object representing the world. + * @param spawnPoint The spawn point of the world. + * @throws IllegalStateException If the database connection is not open. + */ + public void addWorld(String worldName, String worldType, PolarWorld world, Pos spawnPoint) { + if (!isConnected()) + throw new IllegalStateException("The database must have an open connection to add a world!"); + worker.submit(() -> { + try { + PreparedStatement ps = connection.prepareStatement("INSERT INTO cytonic_worlds (world_name, world_type, last_modified, world_data, spawn_point) VALUES (?,?, CURRENT_TIMESTAMP,?,?)"); + ps.setString(1, worldName); + ps.setString(2, worldType); + ps.setBytes(3, PolarWriter.write(world)); + ps.setString(4, PosSerializer.serialize(spawnPoint)); + ps.executeUpdate(); + } catch (SQLException e) { + Logger.error("An error occurred whilst adding a world!", e); + } + }); + } + + /** + * Retrieves a world from the database. + * + * @param worldName The name of the world to fetch. + * @return A {@link CompletableFuture} that completes with the fetched {@link PolarWorld}. + * If the world does not exist in the database, the future will complete exceptionally with a {@link RuntimeException}. + * @throws IllegalStateException If the database connection is not open. + */ + public CompletableFuture getWorld(String worldName) { + CompletableFuture future = new CompletableFuture<>(); + if (!isConnected()) + throw new IllegalStateException("The database must have an open connection to fetch a world!"); + worker.submit(() -> { + try (PreparedStatement ps = connection.prepareStatement("SELECT * FROM cytonic_worlds WHERE world_name = ?")) { + ps.setString(1, worldName); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + PolarWorld world = PolarReader.read(rs.getBytes("world_data")); + CytosisSettings.SERVER_SPAWN_POS = PosSerializer.deserialize(rs.getString("spawn_point")); + future.complete(world); + } else { + Logger.error("The result set is empty!"); + throw new RuntimeException(STR."World not found: \{worldName}"); + } + } catch (Exception e) { + Logger.error("An error occurred whilst fetching a world!", e); + future.completeExceptionally(e); + throw new RuntimeException(e); + } + }); + return future; + } + + /** + * Logs a player's join event to the database. + * + * @param uuid The unique identifier of the player. + * @param ip The IP address of the player. + *

+ * This method uses a worker thread to execute the database operation. + * It prepares a SQL statement to insert a new record into the 'cytonic_player_joins' table. + * The 'joined' column is set to the current timestamp, the 'uuid' column is set to the provided UUID, + * and the 'ip' column is set to the provided IP address. + * If an error occurs during the database operation, it is logged using the Logger. + */ + public void logPlayerJoin(UUID uuid, SocketAddress ip) { + worker.submit(() -> { + try (PreparedStatement ps = connection.prepareStatement("INSERT INTO cytonic_player_joins (joined, uuid, ip) VALUES (CURRENT_TIMESTAMP,?,?)")) { + ps.setString(1, uuid.toString()); + ps.setString(2, ip.toString()); + ps.executeUpdate(); + } catch (SQLException e) { + Logger.error("Failed to add a player to the database!", e); + } + }); + } +} \ No newline at end of file diff --git a/src/main/java/net/cytonic/cytosis/data/RedisDatabase.java b/src/main/java/net/cytonic/cytosis/data/RedisDatabase.java new file mode 100644 index 00000000..43b55772 --- /dev/null +++ b/src/main/java/net/cytonic/cytosis/data/RedisDatabase.java @@ -0,0 +1,75 @@ +package net.cytonic.cytosis.data; + +import lombok.Getter; +import net.cytonic.cytosis.config.CytosisSettings; +import net.cytonic.cytosis.logging.Logger; +import redis.clients.jedis.*; + +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * A class that holds the connection to the redis cache + */ +public class RedisDatabase extends JedisPubSub { + public static final String ONLINE_PLAYER_NAME_KEY = "online_player_names"; + public static final String ONLINE_PLAYER_UUID_KEY = "online_player_uuids"; + public static final String PLAYER_STATUS_CHANNEL = "player_status"; + public static final String SERVER_SHUTDOWN_KEY = "server_shutdown"; + private final Jedis jedis; + private final ExecutorService worker = Executors.newSingleThreadExecutor(Thread.ofVirtual().name("CytosisRedisWorker").factory()); + @Getter + private final Set onlinePlayers; + @Getter + private final Set onlineUUIDs; + + /** + * Initializes the connection to redis using the loaded settings and the Jedis client + */ + public RedisDatabase() { + HostAndPort hostAndPort = new HostAndPort(CytosisSettings.REDIS_HOST, CytosisSettings.REDIS_PORT); + JedisClientConfig config = DefaultJedisClientConfig.builder().password(CytosisSettings.REDIS_PASSWORD).build(); + this.jedis = new Jedis(hostAndPort, config); + this.jedis.auth(CytosisSettings.REDIS_PASSWORD); + + + onlinePlayers = jedis.smembers(ONLINE_PLAYER_NAME_KEY); + Set uuids = new HashSet<>(); + jedis.smembers(ONLINE_PLAYER_UUID_KEY).forEach(s -> uuids.add(UUID.fromString(s))); + this.onlineUUIDs = uuids; + Logger.info(STR."Loaded \{this.onlineUUIDs.size()} players."); + worker.submit(() -> jedis.subscribe(this, PLAYER_STATUS_CHANNEL)); + } + + public void sendShutdownMessage() { + jedis.set(SERVER_SHUTDOWN_KEY, ""); + } + + + /** + * Consumes messages on the redis pub/sub interface to determine the online players + * + * @param channel The channel that was messaged + * @param message The connent of the message + */ + @Override + public void onMessage(String channel, String message) { + if (!channel.equals(PLAYER_STATUS_CHANNEL)) return; + // |:||:| + String[] parts = message.split("\\|:\\|"); + if (parts[2].equalsIgnoreCase("JOIN")) { + onlinePlayers.add(parts[0]); + onlineUUIDs.add(UUID.fromString(parts[1])); + } else { + onlinePlayers.remove(parts[0]); + onlineUUIDs.remove(UUID.fromString(parts[1])); + } + } + + public void disconnect() { + jedis.disconnect(); + } +} diff --git a/src/main/java/net/cytonic/cytosis/enums/BanReason.java b/src/main/java/net/cytonic/cytosis/enums/BanReason.java new file mode 100644 index 00000000..15554c40 --- /dev/null +++ b/src/main/java/net/cytonic/cytosis/enums/BanReason.java @@ -0,0 +1,21 @@ +package net.cytonic.cytosis.enums; + +import lombok.Getter; + +@Getter +public enum BanReason { + CHEATING("Cheating/Hacking"), + INAPROPRIATE_COSMETICS("Using a skin, cape, or other inappropriate cosmetics"), + INAPROPRIATE_BUILDING("Building a structure that violates our terms of service"), + SECURITY("This account is suspended due to security concerns"), + SEVERE_CHAT_INFRACTION("Severe chat infractions"), + EXPLOITING("Exploiting bugs or defects to your advantage"), + SCAMMING("Violating our terms of service by attempting to scam another player"), + ; + + private final String reason; + + BanReason(String reason) { + this.reason = reason; + } +} diff --git a/src/main/java/net/cytonic/cytosis/events/ServerEventListeners.java b/src/main/java/net/cytonic/cytosis/events/ServerEventListeners.java index 6c9fa00a..c48bdd5e 100644 --- a/src/main/java/net/cytonic/cytosis/events/ServerEventListeners.java +++ b/src/main/java/net/cytonic/cytosis/events/ServerEventListeners.java @@ -4,12 +4,15 @@ import net.cytonic.cytosis.config.CytosisSettings; import net.cytonic.cytosis.data.enums.ChatChannel; import net.cytonic.cytosis.logging.Logger; +import net.cytonic.cytosis.messaging.KickReason; +import net.cytonic.cytosis.utils.MessageUtils; import net.kyori.adventure.text.Component; import net.minestom.server.entity.Player; import net.minestom.server.event.player.AsyncPlayerConfigurationEvent; import net.minestom.server.event.player.PlayerChatEvent; import net.minestom.server.event.player.PlayerDisconnectEvent; import net.minestom.server.event.player.PlayerSpawnEvent; + import static net.cytonic.cytosis.utils.MiniMessageTemplate.MM; public class ServerEventListeners { @@ -24,11 +27,28 @@ public static void initServerEvents() { Logger.info("Registering player spawn event."); Cytosis.getEventHandler().registerListener(new EventListener<>("core:player-spawn", false, 1, PlayerSpawnEvent.class, (event -> { + Cytosis.getDatabaseManager().getMysqlDatabase().isBanned(event.getPlayer().getUuid()).whenComplete((data, throwable) -> { + final Player player = event.getPlayer(); + if (throwable != null) { + Logger.error("An error occoured whilst checking if the player is banned!", throwable); + player.kick(MM."An error occured whilst initiating the login sequence!"); + return; + } + + if (data.isBanned()) { + Cytosis.getMessagingManager().getRabbitMQ().kickPlayer(player, KickReason.BANNED, MessageUtils.formatBanMessage(data)); + return; + } + + Logger.info(STR."\{event.getPlayer().getUsername()} (\{event.getPlayer().getUuid()}) joined with the ip: \{player.getPlayerConnection().getServerAddress()}"); + Cytosis.getDatabaseManager().getMysqlDatabase().addPlayer(player); + Cytosis.getRankManager().addPlayer(player); + }); final Player player = event.getPlayer(); Logger.info(STR."\{player.getUsername()} (\{player.getUuid()}) joined with the ip: \{player.getPlayerConnection().getServerAddress()}"); - Cytosis.getDatabaseManager().getDatabase().logPlayerJoin(player.getUuid(), player.getPlayerConnection().getRemoteAddress()); + Cytosis.getDatabaseManager().getMysqlDatabase().logPlayerJoin(player.getUuid(), player.getPlayerConnection().getRemoteAddress()); Cytosis.getRankManager().addPlayer(player); - Cytosis.getDatabaseManager().getDatabase().getChatChannel(player.getUuid()).whenComplete(((chatChannel, throwable) -> { + Cytosis.getDatabaseManager().getMysqlDatabase().getChatChannel(player.getUuid()).whenComplete(((chatChannel, throwable) -> { if (throwable != null) { Logger.error("An error occurred whilst getting a player's chat channel!", throwable); } else Cytosis.getChatManager().setChannel(player.getUuid(), chatChannel); @@ -39,7 +59,7 @@ public static void initServerEvents() { Cytosis.getEventHandler().registerListener(new EventListener<>("core:player-chat", false, 1, PlayerChatEvent.class, event -> { final Player player = event.getPlayer(); if (CytosisSettings.LOG_PLAYER_CHAT) - Cytosis.getDatabaseManager().getDatabase().addChat(player.getUuid(), event.getMessage()); + Cytosis.getDatabaseManager().getMysqlDatabase().addChat(player.getUuid(), event.getMessage()); event.setCancelled(true); String originalMessage = event.getMessage(); if (!originalMessage.contains("|")) { @@ -65,6 +85,7 @@ public static void initServerEvents() { Cytosis.getOnlinePlayers().forEach((p) -> p.sendMessage(message)); } } else player.sendMessage(MM."Hey you cannot do that!"); + Cytosis.getDatabaseManager().getMysqlDatabase().addChat(player.getUuid(), event.getMessage()); })); Logger.info("Registering player disconnect event."); diff --git a/src/main/java/net/cytonic/cytosis/logging/Logger.java b/src/main/java/net/cytonic/cytosis/logging/Logger.java index ff8d8841..b7508765 100644 --- a/src/main/java/net/cytonic/cytosis/logging/Logger.java +++ b/src/main/java/net/cytonic/cytosis/logging/Logger.java @@ -13,7 +13,7 @@ static Logger logger() { /** * Debug log entries contain common debug information. */ - static Logger debug() { + private static Logger debug() { return logger().level(Level.DEBUG); } @@ -45,7 +45,7 @@ static Logger setup(Throwable throwable, Object... args) { /** * Info log entries contain important relevant information. */ - static Logger info() { + private static Logger info() { return logger().level(Level.INFO); } @@ -61,7 +61,7 @@ static Logger info(Throwable throwable, Object... args) { /** * Warn log entries contain technical warnings. Typically, warnings do not prevent the application from continuing. */ - static Logger warn() { + private static Logger warn() { return logger().level(Level.WARN); } @@ -77,7 +77,7 @@ static Logger warn(Throwable throwable, Object... args) { /** * Error log entries contain technical errors. Errors WILL stop the application from continuing. */ - static Logger error() { + private static Logger error() { return logger().level(Level.ERROR); } diff --git a/src/main/java/net/cytonic/cytosis/managers/ChatManager.java b/src/main/java/net/cytonic/cytosis/managers/ChatManager.java index efa707e5..86a60e1d 100644 --- a/src/main/java/net/cytonic/cytosis/managers/ChatManager.java +++ b/src/main/java/net/cytonic/cytosis/managers/ChatManager.java @@ -17,7 +17,7 @@ public void removeChannel(UUID uuid) { public void setChannel(UUID uuid, ChatChannel channel) { channels.put(uuid, channel); - Cytosis.getDatabaseManager().getDatabase().setChatChannel(uuid,channel); + Cytosis.getDatabaseManager().getMysqlDatabase().setChatChannel(uuid, channel); } public ChatChannel getChannel(UUID uuid) { diff --git a/src/main/java/net/cytonic/cytosis/messaging/KickReason.java b/src/main/java/net/cytonic/cytosis/messaging/KickReason.java new file mode 100644 index 00000000..8efe1cbd --- /dev/null +++ b/src/main/java/net/cytonic/cytosis/messaging/KickReason.java @@ -0,0 +1,18 @@ +package net.cytonic.cytosis.messaging; + +import lombok.Getter; + +@Getter +public enum KickReason { + BANNED(false), + INTERNAL_ERROR(true), + INVALID_WORLD(true), + ; + + private final boolean rescuable; + + KickReason(boolean rescuable) { + this.rescuable = rescuable; + } + +} \ No newline at end of file diff --git a/src/main/java/net/cytonic/cytosis/messaging/MessagingManager.java b/src/main/java/net/cytonic/cytosis/messaging/MessagingManager.java index 44fe832d..97200fd1 100644 --- a/src/main/java/net/cytonic/cytosis/messaging/MessagingManager.java +++ b/src/main/java/net/cytonic/cytosis/messaging/MessagingManager.java @@ -1,5 +1,8 @@ package net.cytonic.cytosis.messaging; +import lombok.Getter; +import net.cytonic.cytosis.config.CytosisSettings; + import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -31,4 +34,12 @@ public CompletableFuture initialize() { }); return future; } + + public void shutdown() { + if (rabbitMQ != null) { + rabbitMQ.sendServerShutdownMessage(); + rabbitMQ.shutdown(); + } + worker.shutdown(); + } } \ No newline at end of file diff --git a/src/main/java/net/cytonic/cytosis/messaging/RabbitMQ.java b/src/main/java/net/cytonic/cytosis/messaging/RabbitMQ.java index 1c944ccd..0781580b 100644 --- a/src/main/java/net/cytonic/cytosis/messaging/RabbitMQ.java +++ b/src/main/java/net/cytonic/cytosis/messaging/RabbitMQ.java @@ -1,22 +1,27 @@ package net.cytonic.cytosis.messaging; -import java.io.IOException; -import java.net.*; -import java.nio.charset.StandardCharsets; -import java.util.concurrent.TimeoutException; import com.rabbitmq.client.*; import net.cytonic.cytosis.Cytosis; import net.cytonic.cytosis.config.CytosisSettings; import net.cytonic.cytosis.data.enums.ChatChannel; import net.cytonic.cytosis.logging.Logger; +import net.cytonic.cytosis.utils.OfflinePlayer; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.serializer.json.JSONComponentSerializer; +import net.minestom.server.entity.Player; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.TimeoutException; public class RabbitMQ { public static final String SERVER_DECLARE_QUEUE = "server-declaration"; public static final String SHUTDOWN_QUEUE = "server-shutdown"; - public static final String CHAT_CHANNEL_QUEUE = STR."chat-channel-\{CytosisSettings.SERVER_HOSTNAME}"; + public static final String CHAT_CHANNEL_QUEUE = STR."chat-channel-\{Cytosis.SERVER_ID}"; + public static final String PLAYER_KICK_QUEUE = "player-kick"; public static final String CHAT_CHANNEL_EXCHANGE = "chat-exchange"; private Connection connection; private Channel channel; @@ -59,6 +64,11 @@ public void initializeQueues() { } catch (IOException e) { Logger.error("An error occurred whilst initializing the 'CHAT_CHANNEL_QUEUE'.", e); } + try { + channel.queueDeclare(PLAYER_KICK_QUEUE, false, false, false, null); + } catch (IOException e) { + Logger.error("An error occoured whilst initializing the 'PLAYER_KICK_QUEUE'.", e); + } } public void sendServerDeclarationMessage() { @@ -70,7 +80,7 @@ public void sendServerDeclarationMessage() { Logger.error("An error occurred whilst fetching this server's IP address! Bailing out!", e); return; } - String message = STR."\{CytosisSettings.SERVER_HOSTNAME}|:|\{serverIP}|:|\{CytosisSettings.SERVER_PORT}"; + String message = STR."\{Cytosis.SERVER_ID}|:|\{serverIP}|:|\{CytosisSettings.SERVER_PORT}"; try { channel.basicPublish("", SERVER_DECLARE_QUEUE, null, message.getBytes()); } catch (IOException e) { @@ -79,17 +89,57 @@ public void sendServerDeclarationMessage() { Logger.info(STR."Server Declaration message sent! '\{message}'."); } - /** - * Sends a chat message to all the servers. - * - * @param chatMessage The chat message to be sent. This should be a {@link Component}. - * @param chatChannel The channel to which the chat message should be sent. - */ + public void sendServerShutdownMessage() { + //formatting: {server-name}|:|{server-ip}|:|{server-port} + String serverIP; + try { + serverIP = InetAddress.getLocalHost().getHostAddress(); + } catch (UnknownHostException e) { + Logger.error("An error occoured whilst fetching this server's IP address! Bailing out!", e); + return; + } + String message = STR."\{Cytosis.SERVER_ID}|:|\{serverIP}|:|\{CytosisSettings.SERVER_PORT}"; + try { + channel.basicPublish("", SHUTDOWN_QUEUE, null, message.getBytes()); + } catch (IOException e) { + Logger.error("An error occoured whilst attempting to send the server declaration message!", e); + } + Logger.info(STR."Server Declaration message sent! '\{message}'."); + } + + public void shutdown() { + try { + connection.close(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public void kickPlayer(Player player, KickReason reason, Component message) { + // FORMAT: {uuid}|:|{reason}|:|{name}|:|{message}|:|{rescuable} + String rawMessage = STR."\{player.getUuid().toString()}|:|\{reason}|:|\{player.getUsername()}|:|\{JSONComponentSerializer.json().serialize(message)}|:|\{reason.isRescuable()}"; + try { + channel.basicPublish("", PLAYER_KICK_QUEUE, null, rawMessage.getBytes()); + } catch (IOException e) { + Logger.error(STR."An error occoured whilst attempting to kick the player \{player.getName()}.", e); + } + } + + public void kickPlayer(OfflinePlayer player, KickReason reason, Component message) { + // FORMAT: {uuid}|:|{reason}|:|{name}|:|{message}|:|{rescuable} + String rawMessage = STR."\{player.uuid().toString()}|:|\{reason}|:|\{player.name()}|:|\{JSONComponentSerializer.json().serialize(message)}|:|\{reason.isRescuable()}"; + try { + channel.basicPublish("", PLAYER_KICK_QUEUE, null, rawMessage.getBytes()); + } catch (IOException e) { + Logger.error(STR."An error occoured whilst attempting to kick the player \{player.name()}.", e); + } + } + public void sendChatMessage(Component chatMessage, ChatChannel chatChannel) { - // Formatting: {chat-message}|:|{chat-channel} + //formatting: {chat-message}|{chat-channel} String message = STR."\{JSONComponentSerializer.json().serialize(chatMessage)}|\{chatChannel.name()}"; try { - channel.basicPublish(CHAT_CHANNEL_EXCHANGE, "", null, message.getBytes()); + channel.basicPublish("chat-messages", CHAT_CHANNEL_QUEUE, null, message.getBytes()); } catch (IOException e) { Logger.error("An error occurred whilst attempting to send a chat message!", e); } @@ -123,12 +173,12 @@ public void receiveChatMessages() { }); case LEAGUE -> {// leagues.. } - case PARTY -> {// parties.. } } }; - channel.basicConsume(CHAT_CHANNEL_QUEUE, true, deliverCallback, _ -> {}); + channel.basicConsume(CHAT_CHANNEL_QUEUE, true, deliverCallback, _ -> { + }); } catch (IOException e) { Logger.error("error", e); } diff --git a/src/main/java/net/cytonic/cytosis/ranks/RankManager.java b/src/main/java/net/cytonic/cytosis/ranks/RankManager.java index 9a63d0c7..97c137e0 100644 --- a/src/main/java/net/cytonic/cytosis/ranks/RankManager.java +++ b/src/main/java/net/cytonic/cytosis/ranks/RankManager.java @@ -33,7 +33,7 @@ public void init() { public void addPlayer(Player player) { // cache the rank - Cytosis.getDatabaseManager().getDatabase().getPlayerRank(player.getUuid()).whenComplete((playerRank, throwable) -> { + Cytosis.getDatabaseManager().getMysqlDatabase().getPlayerRank(player.getUuid()).whenComplete((playerRank, throwable) -> { if (throwable != null) { Logger.error(STR."An error occured whilst fetching \{player.getUsername()}'s rank!", throwable); return; diff --git a/src/main/java/net/cytonic/cytosis/utils/BanData.java b/src/main/java/net/cytonic/cytosis/utils/BanData.java new file mode 100644 index 00000000..8d738ae4 --- /dev/null +++ b/src/main/java/net/cytonic/cytosis/utils/BanData.java @@ -0,0 +1,13 @@ +package net.cytonic.cytosis.utils; + +import org.jetbrains.annotations.Nullable; + +import java.time.Instant; + +public record BanData(@Nullable String reason, @Nullable Instant expiry, boolean isBanned) { + public BanData(@Nullable String reason, @Nullable Instant expiry, boolean isBanned) { + this.reason = reason; + this.expiry = expiry; + this.isBanned = isBanned; + } +} diff --git a/src/main/java/net/cytonic/cytosis/utils/DurationParser.java b/src/main/java/net/cytonic/cytosis/utils/DurationParser.java new file mode 100644 index 00000000..7d649022 --- /dev/null +++ b/src/main/java/net/cytonic/cytosis/utils/DurationParser.java @@ -0,0 +1,129 @@ +package net.cytonic.cytosis.utils; + +import org.jetbrains.annotations.Nullable; + +import java.time.Duration; +import java.time.Instant; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class DurationParser { + private static final Pattern PATTERN = Pattern.compile("(\\d+)([ydhms])"); + + /** + * Parses a duration from a string akin to "1y5d6h23m12s" + * `-1` provides a null, representing a permanant value + * + * @param input The string to parse the value from + * @return null if the duration is permanant, returns {@link Instant#now()} if the input doesn't contain any parsable data, or an {@link Instant} with the specified duration from now. + */ + @Nullable + public static Instant parse(String input) { + if (input.equalsIgnoreCase("-1")) return null; + + Matcher matcher = PATTERN.matcher(input); + Duration duration = Duration.ZERO; + + while (matcher.find()) { + long amount = Long.parseLong(matcher.group(1)); + String unit = matcher.group(2); + + duration = switch (unit) { + case "y" -> duration.plusDays(amount * 365); + case "d" -> duration.plusDays(amount); + case "h" -> duration.plusHours(amount); + case "m" -> duration.plusMinutes(amount); + case "s" -> duration.plusSeconds(amount); + default -> duration; + }; + } + + return Instant.now().plus(duration); + } + + /** + * Converts an Instant to a duration string + * + * @param instant The instant to convert to duration string + * @param spacing The spacing between tokens; a spacing of " " would result a string similar to "1y 29d 4h 10m 3s" + * @return The duration string representing the duration from now to the given instant + */ + public static String unparse(@Nullable Instant instant, String spacing) { + if (instant == null) return null; + Duration duration = Duration.between(Instant.now(), instant); + + long years = duration.toDays() / 365; + long days = duration.toDays() % 365; + long hours = duration.toHours() % 24; + long minutes = duration.toMinutes() % 60; + long seconds = duration.getSeconds() % 60; + + StringBuilder builder = new StringBuilder(); + + if (years > 0) { + builder.append(years).append("y").append(spacing); + } + if (days > 0) { + builder.append(days).append("d").append(spacing); + } + if (hours > 0) { + builder.append(hours).append("h").append(spacing); + } + if (minutes > 0) { + builder.append(minutes).append("m").append(spacing); + } + if (seconds > 0) { + builder.append(seconds).append("s").append(spacing); + } + + return builder.toString(); + } + + /** + * Converts an Instant to a duration string with full timeunits written out + * + * @param instant The instant to convert to duration string + * @return The duration string representing the duration from now to the given instant + */ + public static String unparseFull(@Nullable Instant instant) { + if (instant == null) return null; + Duration duration = Duration.between(Instant.now(), instant); + + long years = duration.toDays() / 365; + long days = duration.toDays() % 365; + long hours = duration.toHours() % 24; + long minutes = duration.toMinutes() % 60; + long seconds = duration.getSeconds() % 60; + + StringBuilder builder = new StringBuilder(); + + if (years > 1) { + builder.append(years).append(" Years "); + } else if (years == 1) { + builder.append(years).append(" Year "); + } + if (days > 1) { + builder.append(days).append(" Days "); + } else if (days == 1) { + builder.append(days).append(" Day "); + } + if (hours > 1) { + builder.append(hours).append(" Hours "); + } else if (hours == 1) { + builder.append(hours).append(" Hour "); + } + if (minutes > 1) { + builder.append(minutes).append(" Minutes "); + } else if (minutes == 1) { + builder.append(minutes).append(" Minute "); + } + if (seconds > 1) { + builder.append(seconds).append(" Seconds"); + } else if (seconds == 1) { + builder.append(seconds).append(" Second"); + } + + + return builder.toString(); + } +} diff --git a/src/main/java/net/cytonic/cytosis/utils/MessageUtils.java b/src/main/java/net/cytonic/cytosis/utils/MessageUtils.java new file mode 100644 index 00000000..e9b1fcc7 --- /dev/null +++ b/src/main/java/net/cytonic/cytosis/utils/MessageUtils.java @@ -0,0 +1,17 @@ +package net.cytonic.cytosis.utils; + +import net.kyori.adventure.text.Component; + +public class MessageUtils { + + public static Component formatBanMessage(BanData banData) { + if (!banData.isBanned()) return Component.empty(); + Component finalComp = Component.empty() + .append(MiniMessageTemplate.MM."You are currently banned from the Cytonic Network!".appendNewline().appendNewline()) + .append(MiniMessageTemplate.MM."Reason: \{banData.reason()}".appendNewline()) + .append(MiniMessageTemplate.MM."Expires: \{DurationParser.unparse(banData.expiry(), " ")}".appendNewline().appendNewline()) + .append(MiniMessageTemplate.MM."Appeal at: https://cytonic.net".appendNewline()); + + return finalComp; + } +} diff --git a/src/main/java/net/cytonic/cytosis/utils/OfflinePlayer.java b/src/main/java/net/cytonic/cytosis/utils/OfflinePlayer.java new file mode 100644 index 00000000..ce4c24a5 --- /dev/null +++ b/src/main/java/net/cytonic/cytosis/utils/OfflinePlayer.java @@ -0,0 +1,21 @@ +package net.cytonic.cytosis.utils; + +import net.cytonic.cytosis.ranks.PlayerRank; +import net.minestom.server.permission.Permission; +import net.minestom.server.permission.PermissionHandler; +import org.jetbrains.annotations.NotNull; + +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +public record OfflinePlayer(String name, UUID uuid, PlayerRank rank) implements PermissionHandler { + + @Override + public @NotNull Set getAllPermissions() { + var strings = Set.of(rank.getPermissions()); + Set perms = new HashSet<>(); + strings.forEach(s -> perms.add(new Permission(s))); + return perms; + } +} diff --git a/src/main/resources/config.toml b/src/main/resources/config.toml index c885f7c0..9d0743eb 100644 --- a/src/main/resources/config.toml +++ b/src/main/resources/config.toml @@ -8,7 +8,12 @@ user = "" # Username to connect to the database password = "" # Password to connect to the database use_ssl = false # Whether to use SSL -# server stuff +[redis] +port = 6379 +host = "" +password = "" #No username :) + +# Networking stuff [server] proxy_mode = true secret = "hi i am the secret" # this can NOT be empty