From 18100d42410bfea495b913b1b791e59defa02e08 Mon Sep 17 00:00:00 2001 From: ArikSquad <75741608+ArikSquad@users.noreply.github.com> Date: Fri, 16 Jan 2026 23:28:37 +0200 Subject: [PATCH 01/27] feat: punishments --- commons/build.gradle.kts | 3 + .../java/net/swofty/commons/ServiceType.java | 3 +- .../net/swofty/commons/StringUtility.java | 17 ++ .../swofty/commons/config/ConfigProvider.java | 20 +- .../net/swofty/commons/config/Settings.java | 3 + .../PunishPlayerProtocolObject.java | 116 +++++++++++ .../swofty/commons/proxy/ToProxyChannels.java | 1 + .../to/KickPlayerRequirements.java | 20 ++ .../to/PlayerHandlerRequirements.java | 3 +- .../commons/punishment/PunishmentId.java | 14 ++ .../commons/punishment/PunishmentReason.java | 48 +++++ .../commons/punishment/PunishmentRedis.java | 182 ++++++++++++++++++ .../commons/punishment/PunishmentTag.java | 30 +++ .../commons/punishment/PunishmentType.java | 7 + .../commons/punishment/template/BanType.java | 84 ++++++++ .../commons/punishment/template/MuteType.java | 39 ++++ .../template/PunishmentCategory.java | 9 + .../punishment/template/UnpunishReason.java | 27 +++ .../java/net/swofty/proxyapi/ProxyPlayer.java | 14 +- .../service/generic/SkyBlockService.java | 3 +- service.punishment/build.gradle.kts | 49 +++++ .../swofty/service/punishment/ProxyRedis.java | 65 +++++++ .../punishment/PunishmentDatabase.java | 75 ++++++++ .../service/punishment/PunishmentService.java | 31 +++ .../endpoints/PunishPlayerEndpoint.java | 78 ++++++++ settings.gradle.kts | 1 + type.generic/build.gradle.kts | 4 +- .../type/generic/HypixelGenericLoader.java | 13 ++ .../generic/command/commands/BanCommand.java | 123 ++++++++++++ .../generic/command/commands/MuteCommand.java | 123 ++++++++++++ .../command/commands/UnBanCommand.java | 49 +++++ .../event/actions/ActionPlayerMute.java | 29 +++ .../generic/terminal/MinestomCompleter.java | 47 +++++ .../generic/terminal/MinestomTerminal.java | 86 +++++++++ .../net/swofty/velocity/SkyBlockVelocity.java | 35 +++- .../gamemanager/BalanceConfigurations.java | 1 + .../listeners/ListenerPlayerHandler.java | 3 + .../listeners/ListenerPlayerPunished.java | 72 +++++++ .../handler/PacketDecodeHandler.java | 2 +- 39 files changed, 1515 insertions(+), 14 deletions(-) create mode 100644 commons/src/main/java/net/swofty/commons/protocol/objects/punishment/PunishPlayerProtocolObject.java create mode 100644 commons/src/main/java/net/swofty/commons/proxy/requirements/to/KickPlayerRequirements.java create mode 100644 commons/src/main/java/net/swofty/commons/punishment/PunishmentId.java create mode 100644 commons/src/main/java/net/swofty/commons/punishment/PunishmentReason.java create mode 100644 commons/src/main/java/net/swofty/commons/punishment/PunishmentRedis.java create mode 100644 commons/src/main/java/net/swofty/commons/punishment/PunishmentTag.java create mode 100644 commons/src/main/java/net/swofty/commons/punishment/PunishmentType.java create mode 100644 commons/src/main/java/net/swofty/commons/punishment/template/BanType.java create mode 100644 commons/src/main/java/net/swofty/commons/punishment/template/MuteType.java create mode 100644 commons/src/main/java/net/swofty/commons/punishment/template/PunishmentCategory.java create mode 100644 commons/src/main/java/net/swofty/commons/punishment/template/UnpunishReason.java create mode 100644 service.punishment/build.gradle.kts create mode 100644 service.punishment/src/main/java/net/swofty/service/punishment/ProxyRedis.java create mode 100644 service.punishment/src/main/java/net/swofty/service/punishment/PunishmentDatabase.java create mode 100644 service.punishment/src/main/java/net/swofty/service/punishment/PunishmentService.java create mode 100644 service.punishment/src/main/java/net/swofty/service/punishment/endpoints/PunishPlayerEndpoint.java create mode 100644 type.generic/src/main/java/net/swofty/type/generic/command/commands/BanCommand.java create mode 100644 type.generic/src/main/java/net/swofty/type/generic/command/commands/MuteCommand.java create mode 100644 type.generic/src/main/java/net/swofty/type/generic/command/commands/UnBanCommand.java create mode 100644 type.generic/src/main/java/net/swofty/type/generic/event/actions/ActionPlayerMute.java create mode 100644 type.generic/src/main/java/net/swofty/type/generic/terminal/MinestomCompleter.java create mode 100644 type.generic/src/main/java/net/swofty/type/generic/terminal/MinestomTerminal.java create mode 100644 velocity.extension/src/main/java/net/swofty/velocity/redis/listeners/ListenerPlayerPunished.java diff --git a/commons/build.gradle.kts b/commons/build.gradle.kts index 0097897dc..deee33312 100644 --- a/commons/build.gradle.kts +++ b/commons/build.gradle.kts @@ -25,5 +25,8 @@ dependencies { exclude(group = "org.jboss.shrinkwrap.resolver", module = "shrinkwrap-resolver-depchain") } + // Must match AtlasRedisAPI's Jedis version to avoid conflicts + implementation("redis.clients:jedis:4.2.3") + implementation("org.spongepowered:configurate-yaml:4.2.0") } \ No newline at end of file diff --git a/commons/src/main/java/net/swofty/commons/ServiceType.java b/commons/src/main/java/net/swofty/commons/ServiceType.java index bafcf1ea9..b72b4a97e 100644 --- a/commons/src/main/java/net/swofty/commons/ServiceType.java +++ b/commons/src/main/java/net/swofty/commons/ServiceType.java @@ -9,6 +9,7 @@ public enum ServiceType { PARTY, DARK_AUCTION, ORCHESTRATOR, - FRIEND + FRIEND, + PUNISHMENT, ; } diff --git a/commons/src/main/java/net/swofty/commons/StringUtility.java b/commons/src/main/java/net/swofty/commons/StringUtility.java index 8ae3e1aa5..00b5036cc 100644 --- a/commons/src/main/java/net/swofty/commons/StringUtility.java +++ b/commons/src/main/java/net/swofty/commons/StringUtility.java @@ -300,4 +300,21 @@ public static String ntify(int i) { }; }; } + + public static long parseDuration(String duration) { + long totalMillis = 0; + Pattern pattern = Pattern.compile("(\\d+)([dhms])"); + Matcher matcher = pattern.matcher(duration); + while (matcher.find()) { + int value = Integer.parseInt(matcher.group(1)); + char unit = matcher.group(2).charAt(0); + switch (unit) { + case 'd' -> totalMillis += TimeUnit.DAYS.toMillis(value); + case 'h' -> totalMillis += TimeUnit.HOURS.toMillis(value); + case 'm' -> totalMillis += TimeUnit.MINUTES.toMillis(value); + case 's' -> totalMillis += TimeUnit.SECONDS.toMillis(value); + } + } + return totalMillis; + } } diff --git a/commons/src/main/java/net/swofty/commons/config/ConfigProvider.java b/commons/src/main/java/net/swofty/commons/config/ConfigProvider.java index b98e4925e..6d154d064 100644 --- a/commons/src/main/java/net/swofty/commons/config/ConfigProvider.java +++ b/commons/src/main/java/net/swofty/commons/config/ConfigProvider.java @@ -4,6 +4,9 @@ import lombok.Setter; import lombok.experimental.Accessors; import org.spongepowered.configurate.CommentedConfigurationNode; +import org.spongepowered.configurate.loader.ConfigurationLoader; +import org.spongepowered.configurate.objectmapping.ObjectMapper; +import org.spongepowered.configurate.objectmapping.meta.NodeResolver; import org.spongepowered.configurate.yaml.NodeStyle; import org.spongepowered.configurate.yaml.YamlConfigurationLoader; import org.tinylog.Logger; @@ -18,14 +21,22 @@ public class ConfigProvider { @Accessors(fluent = true) private static Settings settings; + static YamlConfigurationLoader createLoader(final Path source) { + final ObjectMapper.Factory customFactory = ObjectMapper.factoryBuilder() + .build(); + + return YamlConfigurationLoader.builder() + .path(source) + .nodeStyle(NodeStyle.BLOCK) + .defaultOptions(opts -> opts.serializers(build -> build.registerAnnotatedObjects(customFactory))) + .build(); + } + static { try { Logger.info("Loading config..."); - YamlConfigurationLoader loader = YamlConfigurationLoader.builder() - .path(Path.of("./configuration/config.yml")) - .nodeStyle(NodeStyle.BLOCK) - .build(); + YamlConfigurationLoader loader = createLoader(Path.of("./configuration/config.yml")); CommentedConfigurationNode root = loader.load(); CommentedConfigurationNode defaults = loader.createNode(); @@ -43,4 +54,5 @@ public class ConfigProvider { throw new RuntimeException("Failed to load configuration", e); } } + } diff --git a/commons/src/main/java/net/swofty/commons/config/Settings.java b/commons/src/main/java/net/swofty/commons/config/Settings.java index 45c127b94..c4ef31625 100644 --- a/commons/src/main/java/net/swofty/commons/config/Settings.java +++ b/commons/src/main/java/net/swofty/commons/config/Settings.java @@ -28,6 +28,9 @@ public class Settings { @Comment("Whether to enable sandbox features (such as editing items)") private boolean sandbox = false; + @Comment("Whether to enable the terminal for Minestom backend servers") + private boolean terminal = false; + @Comment("Integrations with services") private IntegrationSettings integrations = new IntegrationSettings(); diff --git a/commons/src/main/java/net/swofty/commons/protocol/objects/punishment/PunishPlayerProtocolObject.java b/commons/src/main/java/net/swofty/commons/protocol/objects/punishment/PunishPlayerProtocolObject.java new file mode 100644 index 000000000..9ad74f50b --- /dev/null +++ b/commons/src/main/java/net/swofty/commons/protocol/objects/punishment/PunishPlayerProtocolObject.java @@ -0,0 +1,116 @@ +package net.swofty.commons.protocol.objects.punishment; + +import com.google.gson.Gson; +import net.swofty.commons.protocol.ProtocolObject; +import net.swofty.commons.protocol.Serializer; +import net.swofty.commons.punishment.PunishmentReason; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.json.JSONObject; + +import java.util.UUID; + +public class PunishPlayerProtocolObject + extends ProtocolObject { + + @Override + public Serializer getSerializer() { + return new Serializer<>() { + + @Override + public String serialize(PunishPlayerMessage value) { + JSONObject json = new JSONObject(); + json.put("target", value.target().toString()); + json.put("type", value.type()); + json.put("reason", new Gson().toJson(value.reason())); + json.put("expiresAt", value.expiresAt()); + json.put("staff", value.staff().toString()); + return json.toString(); + } + + @Override + public PunishPlayerMessage deserialize(String json) { + JSONObject obj = new JSONObject(json); + + return new PunishPlayerMessage( + UUID.fromString(obj.getString("target")), + obj.getString("type"), + new Gson().fromJson(obj.getString("reason"), PunishmentReason.class), + UUID.fromString(obj.getString("staff")), + obj.getLong("expiresAt") + ); + } + + @Override + public PunishPlayerMessage clone(PunishPlayerMessage value) { + return value; // immutable + } + }; + } + + @Override + public Serializer getReturnSerializer() { + return new Serializer<>() { + + @Override + public String serialize(PunishPlayerResponse value) { + JSONObject json = new JSONObject(); + json.put("success", value.success()); + json.put("punishmentId", value.punishmentId()); + json.put("errorCode", value.errorCode()); + json.put("errorMessage", value.errorMessage()); + return json.toString(); + } + + @Override + public PunishPlayerResponse deserialize(String json) { + JSONObject obj = new JSONObject(json); + + return new PunishPlayerResponse( + obj.getBoolean("success"), + obj.optString("punishmentId", null), + obj.optString("errorCode", null) != null ? ErrorCode.valueOf(obj.getString("errorCode")) : null, + obj.optString("errorMessage", null) + ); + } + + @Override + public PunishPlayerResponse clone(PunishPlayerResponse value) { + return value; // immutable + } + }; + } + + // do NOT change this to use Punishment - friendly note from Ari + public record PunishPlayerMessage( + @NotNull + UUID target, + @NotNull + String type, + @NotNull + PunishmentReason reason, + @NotNull + UUID staff, + long expiresAt + ) { + } + + public record PunishPlayerResponse( + boolean success, + @Nullable + String punishmentId, + @Nullable + ErrorCode errorCode, + @Nullable + String errorMessage + ) { + } + + public enum ErrorCode { + INVALID_TYPE, + DATABASE_ERROR, + INVALID_EXPIRY, + UNKNOWN_ERROR + } +} \ No newline at end of file diff --git a/commons/src/main/java/net/swofty/commons/proxy/ToProxyChannels.java b/commons/src/main/java/net/swofty/commons/proxy/ToProxyChannels.java index 7b1cebe77..e8bc341e4 100644 --- a/commons/src/main/java/net/swofty/commons/proxy/ToProxyChannels.java +++ b/commons/src/main/java/net/swofty/commons/proxy/ToProxyChannels.java @@ -17,6 +17,7 @@ public enum ToProxyChannels { REGISTER_TEST_FLOW("register-test-flow", new RegisterTestFlowRequirements()), TEST_FLOW_SERVER_READY("test-flow-server-ready", new TestFlowServerReadyRequirements()), STAFF_CHAT("staff-chat", new StaffChatRequirements()), + PUNISH_PLAYER("punish-player", new KickPlayerRequirements()) ; @Getter diff --git a/commons/src/main/java/net/swofty/commons/proxy/requirements/to/KickPlayerRequirements.java b/commons/src/main/java/net/swofty/commons/proxy/requirements/to/KickPlayerRequirements.java new file mode 100644 index 000000000..a86cd433a --- /dev/null +++ b/commons/src/main/java/net/swofty/commons/proxy/requirements/to/KickPlayerRequirements.java @@ -0,0 +1,20 @@ +package net.swofty.commons.proxy.requirements.to; + +import net.swofty.commons.proxy.ProxyChannelRequirements; + +import java.util.List; + +public class KickPlayerRequirements extends ProxyChannelRequirements { + @Override + public List getRequiredKeysForProxy() { + return List.of(); + } + + @Override + public List getRequiredKeysForServer() { + return List.of( + new RequiredKey("player_uuid"), + new RequiredKey("message") + ); + } +} diff --git a/commons/src/main/java/net/swofty/commons/proxy/requirements/to/PlayerHandlerRequirements.java b/commons/src/main/java/net/swofty/commons/proxy/requirements/to/PlayerHandlerRequirements.java index 03597710b..2cc58e45b 100644 --- a/commons/src/main/java/net/swofty/commons/proxy/requirements/to/PlayerHandlerRequirements.java +++ b/commons/src/main/java/net/swofty/commons/proxy/requirements/to/PlayerHandlerRequirements.java @@ -28,6 +28,7 @@ public enum PlayerHandlerActions { REFRESH_COOP_DATA, MESSAGE, TRANSFER_WITH_UUID, - GET_SERVER + GET_SERVER, + LIMBO, } } diff --git a/commons/src/main/java/net/swofty/commons/punishment/PunishmentId.java b/commons/src/main/java/net/swofty/commons/punishment/PunishmentId.java new file mode 100644 index 000000000..af42b7a3e --- /dev/null +++ b/commons/src/main/java/net/swofty/commons/punishment/PunishmentId.java @@ -0,0 +1,14 @@ +package net.swofty.commons.punishment; + +public record PunishmentId(String id) { + + public static PunishmentId generateId() { + StringBuilder idBuilder = new StringBuilder("#"); + String hexChars = "0123456789ABCDEF"; + for (int i = 0; i < 8; i++) { + int randomIndex = (int) (Math.random() * hexChars.length()); + idBuilder.append(hexChars.charAt(randomIndex)); + } + return new PunishmentId(idBuilder.toString()); + } +} diff --git a/commons/src/main/java/net/swofty/commons/punishment/PunishmentReason.java b/commons/src/main/java/net/swofty/commons/punishment/PunishmentReason.java new file mode 100644 index 000000000..15dce04ce --- /dev/null +++ b/commons/src/main/java/net/swofty/commons/punishment/PunishmentReason.java @@ -0,0 +1,48 @@ +package net.swofty.commons.punishment; + +import lombok.Getter; +import lombok.NonNull; +import net.swofty.commons.punishment.template.BanType; +import net.swofty.commons.punishment.template.MuteType; +import net.swofty.commons.punishment.template.UnpunishReason; +import org.jetbrains.annotations.Nullable; + +@Getter +public class PunishmentReason { + @Nullable + private String custom; + @Nullable + private BanType banType; + @Nullable + private MuteType muteType; + @Nullable + private UnpunishReason unpunishReason; + + public PunishmentReason(@NonNull BanType banType) { + this.banType = banType; + } + + public PunishmentReason(@NonNull MuteType muteType) { + this.muteType = muteType; + } + + public PunishmentReason(@NonNull UnpunishReason unpunishReason) { + this.unpunishReason = unpunishReason; + } + + public PunishmentReason(@NonNull String custom) { + this.custom = custom; + } + + public String getReasonString() { + if (banType != null) { + return banType.getReason(); + } else if (muteType != null) { + return muteType.getReason(); + } else if (unpunishReason != null) { + return unpunishReason.getReason(); + } else { + return custom; + } + } +} diff --git a/commons/src/main/java/net/swofty/commons/punishment/PunishmentRedis.java b/commons/src/main/java/net/swofty/commons/punishment/PunishmentRedis.java new file mode 100644 index 000000000..deae916e7 --- /dev/null +++ b/commons/src/main/java/net/swofty/commons/punishment/PunishmentRedis.java @@ -0,0 +1,182 @@ +package net.swofty.commons.punishment; + +import com.google.gson.Gson; +import net.kyori.adventure.text.Component; +import net.swofty.commons.StringUtility; +import org.tinylog.Logger; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.JedisPool; +import redis.clients.jedis.JedisPoolConfig; + +import java.net.URI; +import java.time.Duration; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; + +public class PunishmentRedis { + private static final String PREFIX = "punish:"; + private static JedisPool jedisPool; + private static volatile boolean initialized = false; + private static volatile boolean connecting = false; + + public static void connect(String redisUri) { + Thread.startVirtualThread(() -> connectSync(redisUri)); + } + + private static synchronized void connectSync(String redisUri) { + if (initialized || connecting) return; + connecting = true; + + try { + JedisPoolConfig poolConfig = new JedisPoolConfig(); + poolConfig.setMaxTotal(20); + poolConfig.setMaxIdle(5); + poolConfig.setMinIdle(1); + poolConfig.setMaxWait(Duration.ofSeconds(2)); + poolConfig.setTestOnBorrow(true); + poolConfig.setTestWhileIdle(true); + poolConfig.setBlockWhenExhausted(false); + + URI uri = URI.create(redisUri); + jedisPool = new JedisPool(poolConfig, uri); + + // Test connection + try (Jedis jedis = jedisPool.getResource()) { + jedis.ping(); + } + + initialized = true; + Logger.info("PunishmentService: connected to Redis"); + } catch (Exception e) { + Logger.warn("PunishmentService: Redis not available, punishments disabled"); + initialized = false; + jedisPool = null; + } finally { + connecting = false; + } + } + + public static boolean isInitialized() { + return initialized && jedisPool != null && !jedisPool.isClosed(); + } + + public static void saveActivePunishment(UUID playerId, String type, String id, PunishmentReason reason, long expiresAt) { + try (Jedis jedis = jedisPool.getResource()) { + String key = PREFIX + "active:" + playerId; + + Gson gson = new Gson(); + Map data = Map.of( + "type", type, + "banId", id, + "reason", gson.toJson(reason), // most likely not optimal for performance + "expiresAt", String.valueOf(expiresAt) + ); + + jedis.hset(key, data); + if (expiresAt > 0) { + long ttlSeconds = (expiresAt - System.currentTimeMillis()) / 1000; + if (ttlSeconds > 0) { + jedis.expire(key, (int) ttlSeconds); + } + } + } + } + + public static Optional getActive(UUID playerId) { + try (Jedis jedis = jedisPool.getResource()) { + String key = PREFIX + "active:" + playerId; + Map data = jedis.hgetAll(key); + + if (data.isEmpty()) return Optional.empty(); + + String type = data.get("type"); + String banId = data.get("banId"); + long expiresAt = Long.parseLong(data.getOrDefault("expiresAt", "-1")); + + if (expiresAt > 0 && System.currentTimeMillis() > expiresAt) { + jedis.del(key); // clean up expired + return Optional.empty(); + } + + Gson gson = new Gson(); + PunishmentReason reason = gson.fromJson(data.get("reason"), PunishmentReason.class); // most likely not optimal for performance + + return Optional.of(new ActivePunishment(type, banId, reason, expiresAt)); + } + } + + public static CompletableFuture revoke(UUID playerId) { + return CompletableFuture.supplyAsync(() -> { + try (Jedis jedis = jedisPool.getResource()) { + String key = PREFIX + "active:" + playerId; + return jedis.del(key); + } + }); + } + + public record ActivePunishment(String type, String banId, PunishmentReason reason, long expiresAt) {} + + public static Component parseActivePunishmentBanMessage(ActivePunishment punishment) { + long expiresAt = punishment.expiresAt(); + PunishmentReason reason = punishment.reason(); + String banId = punishment.banId(); + + long timeLeft = expiresAt - System.currentTimeMillis(); + String prettyTimeLeft = StringUtility.formatTimeLeft(timeLeft); + + String header; + if (expiresAt <= 0) { + header = "§cYou are permanently banned from this server!\n"; + } else { + header = "§cYou are temporarily banned for §f" + prettyTimeLeft + " §cfrom this server!\n"; + } + + String findOutMore = ""; + if (reason.getBanType() != null && reason.getBanType().getUrl() != null) { + findOutMore = "§7Find out more: §b" + reason.getBanType().getUrl() + "\n"; + } + + String footer = "§7Sharing your Ban ID may affect the processing of your appeal!"; + + return Component.text(header + "\n§7Reason: §f" + reason.getReasonString() + "\n" + findOutMore + "\n§7Ban ID: §f" + banId + "\n" + footer); + } + + public static Component parseActivePunishmentMuteMessage(ActivePunishment punishment) { + long expiresAt = punishment.expiresAt(); + PunishmentReason reason = punishment.reason(); + + long timeLeft = expiresAt - System.currentTimeMillis(); + String prettyTimeLeft = StringUtility.formatTimeLeft(timeLeft); + + String line = "\n§c§m §r\n"; + + String header; + if (expiresAt <= 0) { + header = "§cYou are permanently muted on this server!\n"; + } else { + header = "§cYou are currently muted for muted for " + reason.getReasonString() + "\n"; + } + String time = "§7Your mute will expire in §c" + prettyTimeLeft + "\n\n"; + + String urlInfo = "§7Find out more here: §fwww.hypixel.net/mutes\n"; + String footer = "§7Mute ID: §f" + punishment.banId(); + return Component.text(line + header + time + urlInfo + footer + line); + } + + // lookup for all BANNED players + public static CompletableFuture> getAllBannedPlayerIds() { + return CompletableFuture.supplyAsync(() -> { + try (Jedis jedis = jedisPool.getResource()) { + Set keys = jedis.keys(PREFIX + "active:*"); + return keys.stream() + .map(key -> key.substring((PREFIX + "active:").length())) + .collect(Collectors.toSet()); + } + }); + } + +} diff --git a/commons/src/main/java/net/swofty/commons/punishment/PunishmentTag.java b/commons/src/main/java/net/swofty/commons/punishment/PunishmentTag.java new file mode 100644 index 000000000..20a0bbe72 --- /dev/null +++ b/commons/src/main/java/net/swofty/commons/punishment/PunishmentTag.java @@ -0,0 +1,30 @@ +package net.swofty.commons.punishment; + +import lombok.Getter; + +@Getter +public enum PunishmentTag { + PERSONAL_PROOF("Personal proof", "P", null, null), + GOLIATH("Punishment applied via Goliath", "G", null, null), + PLAYER_REPORT("Player Report", "R", null, null), + FORUMS("Forums", "F", null, null), + SLACK("Slack", "S", null, null), + WELFARE("Punishment applied over Welfare concern", "W", "STAFF", 99), + ACCOUNT_SECURITY_ALERT(null, "ASA", null, null), + RANKED_TEAM(null, "RT", null, null), + CHECK_BEFORE_UNBAN("Check with the punisher before unbanning this user", "U", "STAFF", null), + OVERWRITE("This punishment overwrote another punishment", "O", "STAFF", null); + + private final String description; + private final String shortCode; + private final String requiredRank; + private final Integer group; + + PunishmentTag(String description, String shortCode, String requiredRank, Integer group) { + this.description = description; + this.shortCode = shortCode; + this.requiredRank = requiredRank; + this.group = group; + } + +} \ No newline at end of file diff --git a/commons/src/main/java/net/swofty/commons/punishment/PunishmentType.java b/commons/src/main/java/net/swofty/commons/punishment/PunishmentType.java new file mode 100644 index 000000000..d3e5c6085 --- /dev/null +++ b/commons/src/main/java/net/swofty/commons/punishment/PunishmentType.java @@ -0,0 +1,7 @@ +package net.swofty.commons.punishment; + +public enum PunishmentType { + MUTE, + BAN, + WARNING; +} diff --git a/commons/src/main/java/net/swofty/commons/punishment/template/BanType.java b/commons/src/main/java/net/swofty/commons/punishment/template/BanType.java new file mode 100644 index 000000000..96d197680 --- /dev/null +++ b/commons/src/main/java/net/swofty/commons/punishment/template/BanType.java @@ -0,0 +1,84 @@ +package net.swofty.commons.punishment.template; + +import lombok.Getter; + +import java.util.Arrays; +import java.util.List; + +@Getter +public enum BanType { + WATCHDOG(PunishmentCategory.ADMIN_ONLY, "WATCHDOG CHEAT DETECTION", "WATCHDOG", 4, "STAFF", + "https://www.hypixel.net/watchdog", true, true), + BLACKLISTED_MODIFICATIONS(PunishmentCategory.CHEATING, "Cheating through the use of unfair game advantages.", "BM", 4, null, + null, true, true, Arrays.asList("Blacklisted Modifications", "Cheating/Unfair Advantage", "Using unfair advantages in game")), + CROSS_TEAMING(PunishmentCategory.GAMEPLAY, "Cross teaming, you were found to be working with another team or player.", "CT", 1, null, null, false, false), + TEAM_GRIEFING(PunishmentCategory.GAMEPLAY, "You were found to be negatively affecting your fellow team members.", "TG", 1, null, null, false, false), + INAPPROPRIATE_BUILD(PunishmentCategory.INAPPROPRIATE_CONTENT, "Creating a build or drawing which is not appropriate on the server.", "IB", 1, null, + null, false, false, Arrays.asList("Inappropriate Build", "Inappropriate Drawing")), + INAPPROPRIATE_ITEM_NAME(PunishmentCategory.INAPPROPRIATE_CONTENT, "Creating or using an item that has an inappropriate name", "IN", 1, null, null, false, false), + INAPPROPRIATE_ITEM_USAGE(PunishmentCategory.INAPPROPRIATE_CONTENT, "Using pets or cosmetics in an inappropriate way.", "IU", 1, null, null, false, false), + STAFF_IMPERSONATION(null, "Misleading others to believe you are a youtuber or staff member.", "SI", 1, null, null, false, false), + SCAMMING(null, "Attempting to obtain information or something of value from players.", "SC", 2, null, null, false, false), + ENCOURAGING_CHEATING_LVL2(PunishmentCategory.CHEATING, "Discussing or acting in a manner which encourages cheating or rule breaking.", "EC2", 2, null, null, false, false), + ENCOURAGING_CHEATING_LVL3(PunishmentCategory.CHEATING, "Discussing or acting in a manner which encourages cheating or rule breaking.", "EC3", 4, null, null, false, false), + EXTREME_USER_DISRESPECT(null, "Acting in a manner that is extremely disrespectful to members within the community.", "EUD", 2, null, + null, false, false, List.of("Extreme Negative behaviour")), + STATS_BOOSTING(PunishmentCategory.ADMIN_ONLY, "Boosting your account to improve your stats.", "SB", 4, "STAFF", + null, false, false, List.of("Boosting")), + INAPPROPRIATE_AESTHETICS(PunishmentCategory.INAPPROPRIATE_CONTENT, "Using inappropriate skins or capes on the server.", "IA", 2, null, null, false, false), + EXPLOITING(PunishmentCategory.ADMIN_ONLY, "Exploiting a bug or issue within the server and using it to your advantage.", "EX", 4, "STAFF", + null, true, true, List.of("Exploits")), + FALSIFIED_INFORMATION(null, "Making or sharing fake information.", "FI", 3, null, null, false, false), + CHARGEBACK(PunishmentCategory.ADMIN_ONLY, "Chargeback: for more info and appeal, go to support.hypixel.net.", null, -1, "STAFF", + null, false, false, List.of("Chargeback")), + FRAUD(null, null, "FR", 0, null, null, false, false), + ACCOUNT_SELLING(null, "Attempting to sell minecraft accounts.", "AS", 4, null, null, false, false), + COMPROMISED_ACCOUNT(PunishmentCategory.ACCOUNT_SECURITY, "Your account has a security alert, please secure it and contact appeals.", "CA", -1, null, + null, false, false, Arrays.asList("Compromised Account", "Account Security Alert"), "Account Security Alert"), + ACCOUNT_SECURITY_ALERT_SERVER_ADVERTISING(PunishmentCategory.ACCOUNT_SECURITY, "Your account has a security alert, please secure it and contact appeals.", "CAS", -1, null, null, false, false), + ACCOUNT_SECURITY_ALERT_BLACKLISTED(PunishmentCategory.ACCOUNT_SECURITY, "Your account has a security alert, please secure it and contact appeals.", "CAB", -1, null, null, false, false), + PHISHING_LINK(null, "Attempting to gain access to other user's accounts/information.", "PL", 4, null, null, false, false), + UN_INTENTIONALLY_CAUSING_DISTRESS_2(null, "Unintentionally/Intentionally Causing distress.", "UIB", 3, null, null, false, false), + UN_INTENTIONALLY_CAUSING_DISTRESS_3(PunishmentCategory.ADMIN_ONLY, "Unintentionally/Intentionally Causing distress.", null, -1, "STAFF", null, false, false), + INAPPROPRIATE_CONTENT_LVL2(PunishmentCategory.INAPPROPRIATE_CONTENT, "Talking or sharing inappropriate content with adult themes on the server.", "IC2", 3, null, null, false, false), + ACCOUNT_DELETION(PunishmentCategory.ADMIN_ONLY, "Upon request, data for this user has been deleted.", null, 0, "STAFF", "https://support.hypixel.net", false, false), + CREATOR_BAN(PunishmentCategory.ADMIN_ONLY, "Please contact staff for assistance.", null, -1, "STAFF", // Please contact creators@hypixel.net for assistance. + null, false, false, List.of("Creator Ban"), "Creator Ban"), + CREATOR_ACCOUNT_SECURITY_ALERT(PunishmentCategory.ADMIN_ONLY, "Your account has a security alert, please secure it and contact staff for assistance.", //Your account has a security alert, please secure it and contact creators@hypixel.net for assistance + null, -1, "STAFF", null, false, false, Arrays.asList("Creator Compromised Account", "Creator Account Security Alert"), "Creator Account Security Alert"); + + private final PunishmentCategory category; + private final String reason; + private final String shortName; + private final int weight; + private final String requiredRank; + private final String url; + private final boolean preventRanked; + private final boolean wipe; + private final List aliases; + private final String cleanName; + + BanType(PunishmentCategory category, String reason, String shortName, int weight, String requiredRank, + String url, boolean preventRanked, boolean wipe) { + this(category, reason, shortName, weight, requiredRank, url, preventRanked, wipe, null, null); + } + + BanType(PunishmentCategory category, String reason, String shortName, int weight, String requiredRank, + String url, boolean preventRanked, boolean wipe, List aliases) { + this(category, reason, shortName, weight, requiredRank, url, preventRanked, wipe, aliases, null); + } + + BanType(PunishmentCategory category, String reason, String shortName, int weight, String requiredRank, + String url, boolean preventRanked, boolean wipe, List aliases, String cleanName) { + this.category = category; + this.reason = reason; + this.shortName = shortName; + this.weight = weight; + this.requiredRank = requiredRank; + this.url = url; + this.preventRanked = preventRanked; + this.wipe = wipe; + this.aliases = aliases; + this.cleanName = cleanName; + } +} diff --git a/commons/src/main/java/net/swofty/commons/punishment/template/MuteType.java b/commons/src/main/java/net/swofty/commons/punishment/template/MuteType.java new file mode 100644 index 000000000..086a6e5d8 --- /dev/null +++ b/commons/src/main/java/net/swofty/commons/punishment/template/MuteType.java @@ -0,0 +1,39 @@ +package net.swofty.commons.punishment.template; + +import lombok.Getter; +import java.util.List; + +@Getter +public enum MuteType { + NEGATIVE_REFERENCE("Discussing important people or world events in a negative way.", "NR", 4), + USER_DISRESPECT("Acting in a manner that is disrespectful to members within the community.", "UD", 4), + STAFF_DISRESPECT("Disrespectful behaviour directed at staff members.", "SD", 4), + INAPPROPRIATE_CONTENT_LVL1("Using adult concepts in public chat on the server.", "IC1", 3), + DISCRIMINATION("Discrimination of a player or group of people.", "DI", 3), + EXCESSIVE_SWEARING("Excessive use of swearing in chat.", "ES", 2), + UN_INTENTIONALLY_CAUSING_DISTRESS("Unintentionally/Intentionally Causing distress.", "UI", 2), + ENCOURAGING_CHEATING_LVL1("Discussing or actively promoting cheating or breaking of rules on the server.", "EC1", 2), + MEDIA_ADVERTISING("Media Advertising", "MA", 1), + PUBLIC_SHAMING("Publicly revealing information about a player.", "PS", 1), + RUDE("Being rude or inappropriate.", "RU", 1), + EXCESSIVE_SPAMMING("Repeatedly posting unnecessary messages or content.", "SP", 1), + MISLEADING_INFORMATION("Misleading other players to carry out actions that disrupts their game.", "MI", 1, List.of("Trolling")), + UNNECESSARY_SPOILERS("Giving spoilers, revealing important storylines of popular movies and tv shows.", "US", 1), + ESCALATION("You have been muted for a chat offense and is currently under review.", "ESC", 0); + + private final String reason; + private final String shortName; + private final int weight; + private final List aliases; + + MuteType(String reason, String shortName, int weight) { + this(reason, shortName, weight, null); + } + + MuteType(String reason, String shortName, int weight, List aliases) { + this.reason = reason; + this.shortName = shortName; + this.weight = weight; + this.aliases = aliases; + } +} \ No newline at end of file diff --git a/commons/src/main/java/net/swofty/commons/punishment/template/PunishmentCategory.java b/commons/src/main/java/net/swofty/commons/punishment/template/PunishmentCategory.java new file mode 100644 index 000000000..ac33fc04c --- /dev/null +++ b/commons/src/main/java/net/swofty/commons/punishment/template/PunishmentCategory.java @@ -0,0 +1,9 @@ +package net.swofty.commons.punishment.template; + +enum PunishmentCategory { + ADMIN_ONLY, + CHEATING, + GAMEPLAY, + INAPPROPRIATE_CONTENT, + ACCOUNT_SECURITY +} \ No newline at end of file diff --git a/commons/src/main/java/net/swofty/commons/punishment/template/UnpunishReason.java b/commons/src/main/java/net/swofty/commons/punishment/template/UnpunishReason.java new file mode 100644 index 000000000..3b4dccaf5 --- /dev/null +++ b/commons/src/main/java/net/swofty/commons/punishment/template/UnpunishReason.java @@ -0,0 +1,27 @@ +package net.swofty.commons.punishment.template; + +import lombok.Getter; + +@Getter +public enum UnpunishReason { + WRONG_PLAYER("Wrong Player", null, null), + WRONG_PUNISHMENT("Wrong Punishment", null, null), + INSUFFICIENT_EVIDENCE("Insufficient Evidence", "IE", null), + ACCOUNT_SECURED("Account Secured", null, "MODERATOR"), + SECOND_CHANCE("Second Chance", "SC", "ADMIN"), + CHARGEBACK_APPEALED("Chargeback Appealed", null, "ADMIN"), + CHARGEBACK_FAILED("Chargeback Failed", null, "ADMIN"), + CHARGEBACK_CANCELLED("Chargeback Cancelled", null, "ADMIN"), + NOT_FRAUD("Not Fraud", null, "ADMIN"), + TIME_SERVED_MAINTAIN_WEIGHT("Time Served (Maintain Weight)", "MW", "ADMIN"); + + private final String reason; + private final String tag; + private final String requiredRank; + + UnpunishReason(String reason, String tag, String requiredRank) { + this.reason = reason; + this.tag = tag; + this.requiredRank = requiredRank; + } +} \ No newline at end of file diff --git a/proxy.api/src/main/java/net/swofty/proxyapi/ProxyPlayer.java b/proxy.api/src/main/java/net/swofty/proxyapi/ProxyPlayer.java index 3d6db2e2b..2f5fc0b9e 100644 --- a/proxy.api/src/main/java/net/swofty/proxyapi/ProxyPlayer.java +++ b/proxy.api/src/main/java/net/swofty/proxyapi/ProxyPlayer.java @@ -32,7 +32,7 @@ public ProxyPlayer(UUID uuid) { this.uuid = uuid; } - public void sendMessage(TextComponent message) { + public void sendMessage(Component message) { JSONObject json = new JSONObject(); json.put("uuid", uuid.toString()); json.put("message", JSONComponentSerializer.json().serialize(message)); @@ -149,6 +149,18 @@ public void transferTo(ServerType serverType) { json, (s) -> {}); } + public void transferToLimbo() { + JSONObject json = new JSONObject(); + json.put("uuid", uuid.toString()); + + PlayerHandlerRequirements.PlayerHandlerActions action = + PlayerHandlerRequirements.PlayerHandlerActions.LIMBO; + json.put("action", action.name()); + + ServerOutboundMessage.sendMessageToProxy(ToProxyChannels.PLAYER_HANDLER, + json, (s) -> {}); + } + public CompletableFuture transferToWithIndication(ServerType serverType) { CompletableFuture future = new CompletableFuture<>(); diff --git a/service.generic/src/main/java/net/swofty/service/generic/SkyBlockService.java b/service.generic/src/main/java/net/swofty/service/generic/SkyBlockService.java index fc66719f3..c34684d9d 100644 --- a/service.generic/src/main/java/net/swofty/service/generic/SkyBlockService.java +++ b/service.generic/src/main/java/net/swofty/service/generic/SkyBlockService.java @@ -6,6 +6,7 @@ import java.lang.reflect.InvocationTargetException; import java.util.List; +import java.util.Objects; import java.util.Set; import java.util.stream.Stream; @@ -31,6 +32,6 @@ default Stream loopThroughPackage(String packageName, Class clazz) { return null; } }) - .filter(java.util.Objects::nonNull); + .filter(Objects::nonNull); } } diff --git a/service.punishment/build.gradle.kts b/service.punishment/build.gradle.kts new file mode 100644 index 000000000..5f34aecce --- /dev/null +++ b/service.punishment/build.gradle.kts @@ -0,0 +1,49 @@ +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar + +plugins { + java + application + id("com.gradleup.shadow") version "9.3.1" +} + +group = "net.swofty" +version = "3.0" + +java { + sourceCompatibility = JavaVersion.VERSION_25 + targetCompatibility = JavaVersion.VERSION_25 + toolchain { + languageVersion.set(JavaLanguageVersion.of(25)) + } +} + +repositories { + maven("https://jitpack.io") + mavenCentral() +} + +dependencies { + implementation(project(":service.generic")) + implementation(project(":commons")) + implementation("com.github.ben-manes.caffeine:caffeine:3.2.3") + implementation("org.tinylog:tinylog-api:2.7.0") + implementation("org.tinylog:tinylog-impl:2.7.0") + implementation("com.google.code.gson:gson:2.11.0") + implementation("org.mongodb:bson:5.6.2") + implementation("org.mongodb:mongodb-driver-sync:5.6.2") + + //implementation("com.github.Swofty-Developments:AtlasRedisAPI:1.1.3") + implementation("redis.clients:jedis:4.2.3") +} + +application { + mainClass.set("net.swofty.service.punishment.FriendService") +} + +tasks { + named("shadowJar") { + archiveBaseName.set("ServicePunishment") + archiveClassifier.set("") + archiveVersion.set("") + } +} diff --git a/service.punishment/src/main/java/net/swofty/service/punishment/ProxyRedis.java b/service.punishment/src/main/java/net/swofty/service/punishment/ProxyRedis.java new file mode 100644 index 000000000..f83a17b9d --- /dev/null +++ b/service.punishment/src/main/java/net/swofty/service/punishment/ProxyRedis.java @@ -0,0 +1,65 @@ +package net.swofty.service.punishment; +import org.tinylog.Logger; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.JedisPool; +import redis.clients.jedis.JedisPoolConfig; + +import java.net.URI; +import java.time.Duration; +import java.util.concurrent.CompletableFuture; + +public class ProxyRedis { + private static JedisPool jedisPool; + private static volatile boolean initialized = false; + private static volatile boolean connecting = false; + + public static void connect(String redisUri) { + Thread.startVirtualThread(() -> connectSync(redisUri)); + } + + private static synchronized void connectSync(String redisUri) { + if (initialized || connecting) return; + connecting = true; + + try { + JedisPoolConfig poolConfig = new JedisPoolConfig(); + poolConfig.setMaxTotal(5); + poolConfig.setMaxIdle(1); + poolConfig.setMinIdle(1); + poolConfig.setMaxWait(Duration.ofSeconds(2)); + poolConfig.setTestOnBorrow(true); + poolConfig.setTestWhileIdle(true); + poolConfig.setBlockWhenExhausted(false); + + URI uri = URI.create(redisUri); + jedisPool = new JedisPool(poolConfig, uri); + + try (Jedis jedis = jedisPool.getResource()) { + jedis.ping(); + } + + initialized = true; + Logger.info("ProxyRedis: connected to Redis"); + } catch (Exception e) { + Logger.error("ProxyRedis: Redis not available player ban enforcement unavailable"); + initialized = false; + jedisPool = null; + } finally { + connecting = false; + } + } + + public static CompletableFuture publishMessage(String filterId, String channel, String message) { + return CompletableFuture.runAsync(() -> { + try (Jedis jedis = jedisPool.getResource()) { + jedis.publish(channel, filterId + ";" + message); + } catch (Exception ex) { + throw new RuntimeException("Failed to publish message to Redis", ex); + } + }); + } + + public static boolean isInitialized() { + return initialized && jedisPool != null && !jedisPool.isClosed(); + } +} \ No newline at end of file diff --git a/service.punishment/src/main/java/net/swofty/service/punishment/PunishmentDatabase.java b/service.punishment/src/main/java/net/swofty/service/punishment/PunishmentDatabase.java new file mode 100644 index 000000000..a9e65f38e --- /dev/null +++ b/service.punishment/src/main/java/net/swofty/service/punishment/PunishmentDatabase.java @@ -0,0 +1,75 @@ +package net.swofty.service.punishment; + +import com.mongodb.ConnectionString; +import com.mongodb.MongoClientSettings; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; +import com.mongodb.client.MongoCollection; +import com.mongodb.client.MongoDatabase; +import com.mongodb.client.model.Filters; +import com.mongodb.client.model.Updates; +import net.swofty.service.generic.MongoDB; +import org.bson.Document; + +public record PunishmentDatabase(String playerId) implements MongoDB { + public static MongoClient client; + public static MongoDatabase database; + public static MongoCollection punishmentCollection; + + @Override + public MongoDB connect(String connectionString) { + ConnectionString cs = new ConnectionString(connectionString); + MongoClientSettings settings = MongoClientSettings.builder().applyConnectionString(cs).build(); + client = MongoClients.create(settings); + + database = client.getDatabase("Minestom"); + punishmentCollection = database.getCollection("punishments"); + return this; + } + + @Override + public void set(String key, Object value) { + insertOrUpdate(key, value); + } + + @Override + public boolean exists() { + Document query = new Document("_id", playerId); + Document found = punishmentCollection.find(query).first(); + return found != null; + } + + @Override + public Object get(String key, Object def) { + Document doc = punishmentCollection.find(Filters.eq("_id", playerId)).first(); + if (doc == null) { + return def; + } + return doc.get(key); + } + + @Override + public void insertOrUpdate(String key, Object value) { + if (exists()) { + Document query = new Document("_id", playerId); + Document found = punishmentCollection.find(query).first(); + assert found != null; + punishmentCollection.updateOne(found, Updates.set(key, value)); + return; + } + Document newDoc = new Document("_id", playerId); + newDoc.append(key, value); + punishmentCollection.insertOne(newDoc); + } + + @Override + public boolean remove(String id) { + Document query = new Document("_id", id); + Document found = punishmentCollection.find(query).first(); + if (found == null) { + return false; + } + punishmentCollection.deleteOne(query); + return true; + } +} diff --git a/service.punishment/src/main/java/net/swofty/service/punishment/PunishmentService.java b/service.punishment/src/main/java/net/swofty/service/punishment/PunishmentService.java new file mode 100644 index 000000000..e66174609 --- /dev/null +++ b/service.punishment/src/main/java/net/swofty/service/punishment/PunishmentService.java @@ -0,0 +1,31 @@ +package net.swofty.service.punishment; + +import net.swofty.commons.ServiceType; +import net.swofty.commons.config.ConfigProvider; +import net.swofty.commons.punishment.PunishmentRedis; +import net.swofty.service.generic.SkyBlockService; +import net.swofty.service.generic.redis.ServiceEndpoint; + +import java.io.ObjectInputFilter; +import java.util.List; + +public class PunishmentService implements SkyBlockService { + + static void main() { + String mongoUri = ConfigProvider.settings().getMongodb(); + new PunishmentDatabase(null).connect(mongoUri); + SkyBlockService.init(new PunishmentService()); + PunishmentRedis.connect(ConfigProvider.settings().getRedisUri()); + ProxyRedis.connect(ConfigProvider.settings().getRedisUri()); + } + + @Override + public ServiceType getType() { + return ServiceType.PUNISHMENT; + } + + @Override + public List getEndpoints() { + return loopThroughPackage("net.swofty.service.punishment.endpoints", ServiceEndpoint.class).toList(); + } +} diff --git a/service.punishment/src/main/java/net/swofty/service/punishment/endpoints/PunishPlayerEndpoint.java b/service.punishment/src/main/java/net/swofty/service/punishment/endpoints/PunishPlayerEndpoint.java new file mode 100644 index 000000000..095c3f833 --- /dev/null +++ b/service.punishment/src/main/java/net/swofty/service/punishment/endpoints/PunishPlayerEndpoint.java @@ -0,0 +1,78 @@ +package net.swofty.service.punishment.endpoints; + +import net.swofty.commons.impl.ServiceProxyRequest; +import net.swofty.commons.protocol.ProtocolObject; +import net.swofty.commons.protocol.objects.punishment.PunishPlayerProtocolObject; +import net.swofty.commons.proxy.ToProxyChannels; +import net.swofty.commons.punishment.*; +import net.swofty.service.generic.redis.ServiceEndpoint; +import net.swofty.service.punishment.ProxyRedis; +import org.json.JSONObject; +import org.tinylog.Logger; + +import java.time.Instant; +import java.util.UUID; + +public class PunishPlayerEndpoint implements ServiceEndpoint + { + + @Override + public ProtocolObject associatedProtocolObject() { + return new PunishPlayerProtocolObject(); + } + + @Override + public PunishPlayerProtocolObject.PunishPlayerResponse onMessage(ServiceProxyRequest message, PunishPlayerProtocolObject.PunishPlayerMessage messageObject) { + Logger.info("PunishPlayerEndpoint onMessage"); + try { + // Validate punishment type + PunishmentType.valueOf(messageObject.type()); + } catch (IllegalArgumentException e) { + return new PunishPlayerProtocolObject.PunishPlayerResponse(false, null, PunishPlayerProtocolObject.ErrorCode.INVALID_TYPE, "The punishment type provided is invalid."); + } + + PunishmentReason reason = messageObject.reason(); + PunishmentId id = PunishmentId.generateId(); + + Instant now = Instant.now(); + Instant expiresAt = Instant.ofEpochMilli(messageObject.expiresAt()); + if (expiresAt.isBefore(Instant.now())) { + return new PunishPlayerProtocolObject.PunishPlayerResponse(false, null, PunishPlayerProtocolObject.ErrorCode.INVALID_EXPIRY, "The expiration time provided is invalid."); + } + + PunishmentRedis.saveActivePunishment( + messageObject.target(), + messageObject.type(), + id.id(), + reason, + messageObject.expiresAt() + ); + sendMessageToProxy(ToProxyChannels.PUNISH_PLAYER, new JSONObject() + .put("target", messageObject.target()) + .put("type", messageObject.type()) + .put("id", id.id()) + .put("reason_custom", reason.getCustom() != null ? reason.getCustom() : null) + .put("reason_ban", reason.getBanType() != null ? reason.getBanType().name() : null) + .put("reason_mute", reason.getMuteType() != null ? reason.getMuteType().name() : null) + .put("staff", messageObject.staff()) + .put("issuedAt", now.toEpochMilli()) + .put("expiresAt", messageObject.expiresAt()) + ); + Logger.info("Issued {} punishment to {} for reason '{}' (expires at: {})", + messageObject.type(), + messageObject.target(), + reason.getReasonString(), + expiresAt.toString() + ); + return new PunishPlayerProtocolObject.PunishPlayerResponse(true, id.id(), null, null); + } + + public static void sendMessageToProxy(ToProxyChannels channel, JSONObject message) { + UUID uuid = UUID.randomUUID(); + + ProxyRedis.publishMessage("proxy", + channel.getChannelName(), + message.toString() + "}=-=-={" + uuid + "}=-=-={" + uuid); + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index dec372fa1..aa5316412 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -50,4 +50,5 @@ include(":service.party") include(":service.orchestrator") include(":service.darkauction") include(":service.friend") +include(":service.punishment") include(":anticheat") diff --git a/type.generic/build.gradle.kts b/type.generic/build.gradle.kts index 7d641a2c1..3cafbd919 100644 --- a/type.generic/build.gradle.kts +++ b/type.generic/build.gradle.kts @@ -25,8 +25,10 @@ dependencies { implementation(project(":proxy.api")) implementation("org.mongodb:bson:5.6.2") implementation("org.mongodb:mongodb-driver-sync:5.6.2") + implementation("org.jline:jline-terminal:3.30.6") + implementation("org.jline:jline-reader:3.30.6") // Must match AtlasRedisAPI's Jedis version to avoid conflicts - implementation("redis.clients:jedis:7.2.0") + implementation("redis.clients:jedis:4.2.3") implementation("org.tinylog:tinylog-api:2.7.0") implementation("org.tinylog:tinylog-impl:2.7.0") implementation("org.jetbrains.kotlin:kotlin-stdlib:2.3.0") diff --git a/type.generic/src/main/java/net/swofty/type/generic/HypixelGenericLoader.java b/type.generic/src/main/java/net/swofty/type/generic/HypixelGenericLoader.java index 9a0023000..a89857453 100644 --- a/type.generic/src/main/java/net/swofty/type/generic/HypixelGenericLoader.java +++ b/type.generic/src/main/java/net/swofty/type/generic/HypixelGenericLoader.java @@ -21,6 +21,7 @@ import net.swofty.commons.CustomWorlds; import net.swofty.commons.ServerType; import net.swofty.commons.config.ConfigProvider; +import net.swofty.commons.punishment.PunishmentRedis; import net.swofty.type.generic.block.PlayerHeadBlockHandler; import net.swofty.type.generic.block.SignBlockHandler; import net.swofty.type.generic.command.HypixelCommand; @@ -47,6 +48,7 @@ import net.swofty.type.generic.packet.HypixelPacketServerListener; import net.swofty.type.generic.quest.QuestRegistry; import net.swofty.type.generic.redis.RedisOriginServer; +import net.swofty.type.generic.terminal.MinestomTerminal; import net.swofty.type.generic.user.HypixelPlayer; import org.jetbrains.annotations.Nullable; import org.reflections.Reflections; @@ -194,6 +196,9 @@ public void initialize(MinecraftServer server) { // Initialize leaderboard service (uses Redis for O(log N) leaderboard operations) LeaderboardService.connect(ConfigProvider.settings().getRedisUri()); + // Initialize punishment Redis connection + PunishmentRedis.connect(ConfigProvider.settings().getRedisUri()); + // Load achievement and quest registries from YAML configuration AchievementRegistry.loadFromConfiguration(); QuestRegistry.loadFromConfiguration(); @@ -242,6 +247,14 @@ public void initialize(MinecraftServer server) { return player; }); } + + if (ConfigProvider.settings().isTerminal()) { + try (MinestomTerminal terminal = new MinestomTerminal(MinecraftServer.getCommandManager())) { + terminal.start(); + } catch (Exception e) { + Logger.warn(e, "Failed to start Minestom terminal."); + } + } } public static List getLoadedPlayers() { diff --git a/type.generic/src/main/java/net/swofty/type/generic/command/commands/BanCommand.java b/type.generic/src/main/java/net/swofty/type/generic/command/commands/BanCommand.java new file mode 100644 index 000000000..4d39ebb53 --- /dev/null +++ b/type.generic/src/main/java/net/swofty/type/generic/command/commands/BanCommand.java @@ -0,0 +1,123 @@ +package net.swofty.type.generic.command.commands; + +import net.kyori.adventure.text.Component; +import net.minestom.server.command.CommandSender; +import net.minestom.server.command.builder.arguments.*; +import net.minestom.server.command.builder.suggestion.SuggestionEntry; +import net.minestom.server.entity.Player; +import net.minestom.server.utils.mojang.MojangUtils; +import net.swofty.commons.ServiceType; +import net.swofty.commons.StringUtility; +import net.swofty.commons.protocol.objects.punishment.PunishPlayerProtocolObject; +import net.swofty.commons.punishment.PunishmentReason; +import net.swofty.commons.punishment.PunishmentType; +import net.swofty.commons.punishment.template.BanType; +import net.swofty.proxyapi.ProxyPlayer; +import net.swofty.proxyapi.ProxyService; +import net.swofty.type.generic.command.CommandParameters; +import net.swofty.type.generic.command.HypixelCommand; +import net.swofty.type.generic.user.categories.Rank; + +import java.io.IOException; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +@CommandParameters( + aliases = "ban tempban banip tempbanip", + permission = Rank.STAFF, + description = "Ban a player from the server.", + usage = "/ban [duration] ", + allowsConsole = true +) +public class BanCommand extends HypixelCommand { + + @Override + public void registerUsage(MinestomCommand command) { + ArgumentString playerArg = ArgumentType.String("player"); + ArgumentString durationArg = ArgumentType.String("duration"); + Argument reasonArg = ArgumentType.String("reason").setSuggestionCallback((sender, context, suggestion) -> { + for (BanType type : BanType.values()) { + suggestion.addEntry(new SuggestionEntry(type.name(), Component.text("§c" + type.getReason() + " §7| Wipe: " + type.isWipe()))); + } + }); + + command.addSyntax((sender, context) -> { + String playerName = context.get(playerArg); + String duration = context.get(durationArg); + BanType type = BanType.valueOf(context.get(reasonArg)); + + + UUID targetUuid; + try { + targetUuid = MojangUtils.getUUID(playerName); + sender.sendMessage("§8Processing ban for player §e" + playerName + "§7... (" + targetUuid + ")"); + } catch (IOException e) { + sender.sendMessage("§cCould not find player: " + playerName); + return; + } + + UUID senderUuid; + if (sender instanceof Player player) { + senderUuid = player.getUuid(); + } else { + senderUuid = UUID.fromString("00000000-0000-0000-0000-000000000000"); + } + + long actualTime = StringUtility.parseDuration(duration); + long expiryTime = System.currentTimeMillis() + actualTime; + + CompletableFuture.runAsync(() -> { + banPlayer(sender, targetUuid, type, senderUuid, actualTime, expiryTime, playerName); + }); + }, playerArg, durationArg, reasonArg); + + // permanent ban + command.addSyntax((sender, context) -> { + String playerName = context.get(playerArg); + BanType reason = BanType.valueOf(context.get(reasonArg)); + + CompletableFuture.runAsync(() -> { + try { + banPlayer(sender, + MojangUtils.getUUID(playerName), + reason, + sender instanceof Player player ? player.getUuid() : UUID.fromString("00000000-0000-0000-0000-000000000000"), + 0, + -1, + playerName); + } catch (IOException e) { + sender.sendMessage("§cCould not find player: " + playerName); + return; + } + }); + }, playerArg, reasonArg); + } + + private void banPlayer(CommandSender sender, UUID targetUuid, BanType type, UUID senderUuid, long actualTime, long expiryTime, String playerName) { + ProxyService punishmentService = new ProxyService(ServiceType.PUNISHMENT); + PunishmentReason reason = new PunishmentReason(type); + PunishPlayerProtocolObject.PunishPlayerMessage message = new PunishPlayerProtocolObject.PunishPlayerMessage( + targetUuid, + PunishmentType.BAN.name(), + reason, + senderUuid, + actualTime > 0 ? expiryTime : -1 + ); + + new ProxyPlayer(targetUuid).transferToLimbo(); + + punishmentService.handleRequest(message).thenAccept(result -> { + if (result instanceof PunishPlayerProtocolObject.PunishPlayerResponse response) { + if (response.success()) { + sender.sendMessage("§aSuccessfully banned player §e" + playerName + "§a. §8Punishment ID: §7" + response.punishmentId()); + } else { + sender.sendMessage("§cFailed to ban player: " + response.errorMessage()); + } + } + }).orTimeout(5, TimeUnit.SECONDS).exceptionally(_ -> { + sender.sendMessage("§cCould not ban this player at this time. The punishment service may be offline."); + return null; + }); + } +} diff --git a/type.generic/src/main/java/net/swofty/type/generic/command/commands/MuteCommand.java b/type.generic/src/main/java/net/swofty/type/generic/command/commands/MuteCommand.java new file mode 100644 index 000000000..42b6a65f3 --- /dev/null +++ b/type.generic/src/main/java/net/swofty/type/generic/command/commands/MuteCommand.java @@ -0,0 +1,123 @@ +package net.swofty.type.generic.command.commands; + +import net.kyori.adventure.text.Component; +import net.minestom.server.command.CommandSender; +import net.minestom.server.command.builder.arguments.Argument; +import net.minestom.server.command.builder.arguments.ArgumentString; +import net.minestom.server.command.builder.arguments.ArgumentType; +import net.minestom.server.command.builder.suggestion.SuggestionEntry; +import net.minestom.server.entity.Player; +import net.minestom.server.utils.mojang.MojangUtils; +import net.swofty.commons.ServiceType; +import net.swofty.commons.StringUtility; +import net.swofty.commons.protocol.objects.punishment.PunishPlayerProtocolObject; +import net.swofty.commons.punishment.PunishmentReason; +import net.swofty.commons.punishment.PunishmentType; +import net.swofty.commons.punishment.template.MuteType; +import net.swofty.proxyapi.ProxyService; +import net.swofty.type.generic.command.CommandParameters; +import net.swofty.type.generic.command.HypixelCommand; +import net.swofty.type.generic.user.categories.Rank; + +import java.io.IOException; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +@CommandParameters( + aliases = "mute tempmute", + permission = Rank.STAFF, + description = "Mute a player from the server.", + usage = "/mute [duration] ", + allowsConsole = true +) +public class MuteCommand extends HypixelCommand { + + @Override + public void registerUsage(MinestomCommand command) { + ArgumentString playerArg = ArgumentType.String("player"); + ArgumentString durationArg = ArgumentType.String("duration"); + Argument reasonArg = ArgumentType.String("reason").setSuggestionCallback((sender, context, suggestion) -> { + for (MuteType type : MuteType.values()) { + suggestion.addEntry(new SuggestionEntry(type.name(), Component.text("§c" + type.getReason()))); + } + }); + + command.addSyntax((sender, context) -> { + String playerName = context.get(playerArg); + String duration = context.get(durationArg); + MuteType type = MuteType.valueOf(context.get(reasonArg)); + + + UUID targetUuid; + try { + targetUuid = MojangUtils.getUUID(playerName); + sender.sendMessage("§8Processing mute for player §e" + playerName + "§7... (" + targetUuid + ")"); + } catch (IOException e) { + sender.sendMessage("§cCould not find player: " + playerName); + return; + } + + UUID senderUuid; + if (sender instanceof Player player) { + senderUuid = player.getUuid(); + } else { + senderUuid = UUID.fromString("00000000-0000-0000-0000-000000000000"); + } + + long actualTime = StringUtility.parseDuration(duration); + long expiryTime = System.currentTimeMillis() + actualTime; + + CompletableFuture.runAsync(() -> { + mutePlayer(sender, targetUuid, type, senderUuid, actualTime, expiryTime, playerName); + }); + }, playerArg, durationArg, reasonArg); + + // permanent mute + command.addSyntax((sender, context) -> { + String playerName = context.get(playerArg); + MuteType reason = MuteType.valueOf(context.get(reasonArg)); + + CompletableFuture.runAsync(() -> { + try { + mutePlayer(sender, + MojangUtils.getUUID(playerName), + reason, + sender instanceof Player player ? player.getUuid() : UUID.fromString("00000000-0000-0000-0000-000000000000"), + 0, + -1, + playerName); + } catch (IOException e) { + sender.sendMessage("§cCould not find player: " + playerName); + } + }); + + + }, playerArg, reasonArg); + } + + private void mutePlayer(CommandSender sender, UUID targetUuid, MuteType type, UUID senderUuid, long actualTime, long expiryTime, String playerName) { + ProxyService punishmentService = new ProxyService(ServiceType.PUNISHMENT); + PunishmentReason reason = new PunishmentReason(type); + PunishPlayerProtocolObject.PunishPlayerMessage message = new PunishPlayerProtocolObject.PunishPlayerMessage( + targetUuid, + PunishmentType.MUTE.name(), + reason, + senderUuid, + actualTime > 0 ? expiryTime : -1 + ); + + punishmentService.handleRequest(message).thenAccept(result -> { + if (result instanceof PunishPlayerProtocolObject.PunishPlayerResponse response) { + if (response.success()) { + sender.sendMessage("§aSuccessfully muted player §e" + playerName + "§a. §8Punishment ID: §7" + response.punishmentId()); + } else { + sender.sendMessage("§cFailed to mute player: " + response.errorMessage()); + } + } + }).orTimeout(5, TimeUnit.SECONDS).exceptionally(_ -> { + sender.sendMessage("§cCould not mute this player at this time. The punishment service may be offline."); + return null; + }); + } +} diff --git a/type.generic/src/main/java/net/swofty/type/generic/command/commands/UnBanCommand.java b/type.generic/src/main/java/net/swofty/type/generic/command/commands/UnBanCommand.java new file mode 100644 index 000000000..80635adcb --- /dev/null +++ b/type.generic/src/main/java/net/swofty/type/generic/command/commands/UnBanCommand.java @@ -0,0 +1,49 @@ +package net.swofty.type.generic.command.commands; + +import net.minestom.server.command.builder.arguments.Argument; +import net.minestom.server.command.builder.arguments.ArgumentString; +import net.minestom.server.command.builder.arguments.ArgumentType; +import net.minestom.server.command.builder.suggestion.SuggestionEntry; +import net.minestom.server.utils.mojang.MojangUtils; +import net.swofty.commons.punishment.PunishmentRedis; +import net.swofty.type.generic.command.CommandParameters; +import net.swofty.type.generic.command.HypixelCommand; +import net.swofty.type.generic.user.categories.Rank; + +import java.io.IOException; +import java.util.UUID; + +@CommandParameters( + description = "Unban a player from the server.", + usage = "/unban ", + aliases = "unban pardon unbanip pardonip", + permission = Rank.STAFF, + allowsConsole = true +) +public class UnBanCommand extends HypixelCommand { + + @Override + public void registerUsage(MinestomCommand command) { + Argument argument = ArgumentType.String("player").setSuggestionCallback((sender, context, suggestion) -> { + PunishmentRedis.getAllBannedPlayerIds().thenAccept((id) -> { + for (String playerName : id) { + UUID playerUuid = UUID.fromString(playerName); + suggestion.addEntry(new SuggestionEntry(playerUuid.toString())); + } + }); + }); + + command.addSyntax((sender, context) -> { + String playerName = context.get(argument); + + sender.sendMessage("§8Processing unban for player " + playerName + "..."); + try { + PunishmentRedis.revoke(MojangUtils.getUUID(playerName)).thenRun(() -> { + sender.sendMessage("§aSuccessfully unbanned player: " + playerName); + }); + } catch (IOException e) { + sender.sendMessage("§cCould not find player: " + playerName); + } + }, argument); + } +} diff --git a/type.generic/src/main/java/net/swofty/type/generic/event/actions/ActionPlayerMute.java b/type.generic/src/main/java/net/swofty/type/generic/event/actions/ActionPlayerMute.java new file mode 100644 index 000000000..89eed281f --- /dev/null +++ b/type.generic/src/main/java/net/swofty/type/generic/event/actions/ActionPlayerMute.java @@ -0,0 +1,29 @@ +package net.swofty.type.generic.event.actions; + +import net.minestom.server.entity.Player; +import net.minestom.server.event.player.PlayerChatEvent; +import net.swofty.commons.punishment.PunishmentRedis; +import net.swofty.commons.punishment.PunishmentType; +import net.swofty.type.generic.event.EventNodes; +import net.swofty.type.generic.event.HypixelEvent; +import net.swofty.type.generic.event.HypixelEventClass; + +import java.util.Optional; + +public class ActionPlayerMute implements HypixelEventClass { + + @HypixelEvent(node = EventNodes.PLAYER, requireDataLoaded = false) + public void onPlayerChat(PlayerChatEvent event) { + Player player = event.getPlayer(); + Optional activePunishment = PunishmentRedis.getActive(player.getUuid()); + activePunishment.ifPresent(punishment -> { + PunishmentType type = PunishmentType.valueOf(punishment.type()); + if (type != PunishmentType.MUTE) { + return; + } + event.setCancelled(true); + player.sendMessage(PunishmentRedis.parseActivePunishmentMuteMessage(punishment)); + }); + } + +} diff --git a/type.generic/src/main/java/net/swofty/type/generic/terminal/MinestomCompleter.java b/type.generic/src/main/java/net/swofty/type/generic/terminal/MinestomCompleter.java new file mode 100644 index 000000000..69d015509 --- /dev/null +++ b/type.generic/src/main/java/net/swofty/type/generic/terminal/MinestomCompleter.java @@ -0,0 +1,47 @@ +package net.swofty.type.generic.terminal; + +import net.minestom.server.command.CommandManager; +import net.minestom.server.command.builder.Command; +import org.jline.reader.Candidate; +import org.jline.reader.Completer; +import org.jline.reader.LineReader; +import org.jline.reader.ParsedLine; + +import java.util.List; + +public final class MinestomCompleter implements Completer { + + private final CommandManager commandManager; + + public MinestomCompleter(CommandManager commandManager) { + this.commandManager = commandManager; + } + + @Override + public void complete(LineReader reader, ParsedLine line, List candidates) { + String buffer = line.line(); + String[] args = buffer.split(" ", -1); + + if (args.length <= 1) { + String prefix = args[0]; + for (Command command : commandManager.getCommands()) { + if (command.getName().startsWith(prefix)) { + candidates.add(new Candidate(command.getName())); + } + } + return; + } + + String commandName = args[0]; + Command command = commandManager.getCommand(commandName); + if (command == null) return; + + List subCommands = command.getSubcommands(); + String lastArg = args[args.length - 1]; + for (Command subCommand : subCommands) { + if (subCommand.getName().startsWith(lastArg)) { + candidates.add(new Candidate(subCommand.getName())); + } + } + } +} diff --git a/type.generic/src/main/java/net/swofty/type/generic/terminal/MinestomTerminal.java b/type.generic/src/main/java/net/swofty/type/generic/terminal/MinestomTerminal.java new file mode 100644 index 000000000..ca9c0ea32 --- /dev/null +++ b/type.generic/src/main/java/net/swofty/type/generic/terminal/MinestomTerminal.java @@ -0,0 +1,86 @@ +package net.swofty.type.generic.terminal; + +import net.minestom.server.MinecraftServer; +import net.minestom.server.command.CommandManager; +import org.jetbrains.annotations.ApiStatus; +import org.jline.reader.EndOfFileException; +import org.jline.reader.LineReader; +import org.jline.reader.LineReaderBuilder; +import org.jline.reader.UserInterruptException; +import org.jline.terminal.Terminal; +import org.jline.terminal.TerminalBuilder; +import org.tinylog.Logger; + +import java.io.IOException; + +public final class MinestomTerminal implements AutoCloseable { + + private static final String PROMPT = "> "; + + private final CommandManager commandManager; + private Terminal terminal; + private LineReader reader; + private volatile boolean running; + + public MinestomTerminal(CommandManager commandManager) { + this.commandManager = commandManager; + } + + public void start() throws IOException { + if (System.console() == null) { + Logger.warn("No console detected; terminal disabled."); + return; + } + + Thread thread = new Thread(() -> { + try { + terminal = TerminalBuilder.builder() + .system(true) + .streams(System.in, System.out) + .build(); + + reader = LineReaderBuilder.builder() + .terminal(terminal) + .completer(new MinestomCompleter(commandManager)) + .build(); + + running = true; + runLoop(); + } catch (IOException e) { + e.printStackTrace(); + } + }, "terminal-thread"); + + thread.start(); + } + + + private void runLoop() { + while (running) { + Logger.info("Waiting for terminal input..."); + try { + String command = reader.readLine(PROMPT); + commandManager.execute( + commandManager.getConsoleSender(), + command + ); + } catch (UserInterruptException e) { + MinecraftServer.stopCleanly(); + break; + } catch (EndOfFileException e) { + Logger.info("Terminal closed."); + break; + } catch (Throwable t) { + t.printStackTrace(); + } + } + } + + @Override + public void close() { + running = false; + try { + if (terminal != null) terminal.close(); + } catch (IOException ignored) {} + } +} diff --git a/velocity.extension/src/main/java/net/swofty/velocity/SkyBlockVelocity.java b/velocity.extension/src/main/java/net/swofty/velocity/SkyBlockVelocity.java index ac6f471e0..ae75dde7b 100644 --- a/velocity.extension/src/main/java/net/swofty/velocity/SkyBlockVelocity.java +++ b/velocity.extension/src/main/java/net/swofty/velocity/SkyBlockVelocity.java @@ -10,10 +10,7 @@ import com.velocitypowered.api.event.connection.DisconnectEvent; import com.velocitypowered.api.event.connection.PostLoginEvent; import com.velocitypowered.api.event.permission.PermissionsSetupEvent; -import com.velocitypowered.api.event.player.KickedFromServerEvent; -import com.velocitypowered.api.event.player.PlayerChooseInitialServerEvent; -import com.velocitypowered.api.event.player.ServerConnectedEvent; -import com.velocitypowered.api.event.player.ServerPostConnectEvent; +import com.velocitypowered.api.event.player.*; import com.velocitypowered.api.event.proxy.ProxyInitializeEvent; import com.velocitypowered.api.event.proxy.ProxyPingEvent; import com.velocitypowered.api.network.ProtocolVersion; @@ -39,6 +36,8 @@ import net.swofty.commons.config.ConfigProvider; import net.swofty.commons.config.Settings; import net.swofty.commons.proxy.FromProxyChannels; +import net.swofty.commons.punishment.PunishmentRedis; +import net.swofty.commons.punishment.PunishmentType; import net.swofty.redisapi.api.RedisAPI; import net.swofty.velocity.command.ProtocolVersionCommand; import net.swofty.velocity.command.ServerStatusCommand; @@ -66,9 +65,11 @@ import java.nio.file.Path; import java.time.Duration; import java.util.List; +import java.util.Optional; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; @@ -208,12 +209,34 @@ public void onProxyInitialization(ProxyInitializeEvent event) { * Setup GameManager */ GameManager.loopServers(server); + PunishmentRedis.connect(ConfigProvider.settings().getRedisUri()); } + public boolean punished(Player player) { + AtomicBoolean shouldConnect = new AtomicBoolean(true); + Optional activePunishment = PunishmentRedis.getActive(player.getUniqueId()); + activePunishment.ifPresent(punishment -> { + PunishmentType type = PunishmentType.valueOf(punishment.type()); + if (type == PunishmentType.BAN) { + player.disconnect(PunishmentRedis.parseActivePunishmentBanMessage(punishment)); + shouldConnect.set(false); + } + if (type == PunishmentType.MUTE) { + player.sendMessage(PunishmentRedis.parseActivePunishmentMuteMessage(punishment)); + } + }); + + return !shouldConnect.get(); + } + @Subscribe public void onPlayerJoin(PlayerChooseInitialServerEvent event) { Player player = event.getPlayer(); + if (punished(player)) { + return; + } + if (!GameManager.hasType(ServerType.PROTOTYPE_LOBBY) || !GameManager.isAnyEmpty(ServerType.PROTOTYPE_LOBBY)) { player.disconnect( Component.text("§cThere are no Prototype Lobby servers available at the moment.") @@ -276,6 +299,10 @@ public void onPlayerJoin(PlayerChooseInitialServerEvent event) { @Subscribe public void onServerCrash(KickedFromServerEvent event) { + if (punished(event.getPlayer())) { + return; + } + // Send the player to the limbo RegisteredServer originalServer = event.getServer(); Component reason = event.getServerKickReason().orElse(Component.text( diff --git a/velocity.extension/src/main/java/net/swofty/velocity/gamemanager/BalanceConfigurations.java b/velocity.extension/src/main/java/net/swofty/velocity/gamemanager/BalanceConfigurations.java index 1d213ec82..14b991b71 100644 --- a/velocity.extension/src/main/java/net/swofty/velocity/gamemanager/BalanceConfigurations.java +++ b/velocity.extension/src/main/java/net/swofty/velocity/gamemanager/BalanceConfigurations.java @@ -7,6 +7,7 @@ import net.swofty.velocity.gamemanager.balanceconfigurations.ReadyGames; import net.swofty.velocity.testflow.TestFlowManager; import org.jetbrains.annotations.Nullable; +import org.tinylog.Logger; import java.util.HashMap; import java.util.List; diff --git a/velocity.extension/src/main/java/net/swofty/velocity/redis/listeners/ListenerPlayerHandler.java b/velocity.extension/src/main/java/net/swofty/velocity/redis/listeners/ListenerPlayerHandler.java index 7aeb7efe5..1fc05bb28 100644 --- a/velocity.extension/src/main/java/net/swofty/velocity/redis/listeners/ListenerPlayerHandler.java +++ b/velocity.extension/src/main/java/net/swofty/velocity/redis/listeners/ListenerPlayerHandler.java @@ -120,6 +120,9 @@ public JSONObject receivedMessage(JSONObject message, UUID serverUUID) { type ); } + case LIMBO -> { + new TransferHandler(player).sendToLimbo().join(); + } case TELEPORT -> { if (potentialServer.isEmpty()) { return new JSONObject(); diff --git a/velocity.extension/src/main/java/net/swofty/velocity/redis/listeners/ListenerPlayerPunished.java b/velocity.extension/src/main/java/net/swofty/velocity/redis/listeners/ListenerPlayerPunished.java new file mode 100644 index 000000000..5ba73aca4 --- /dev/null +++ b/velocity.extension/src/main/java/net/swofty/velocity/redis/listeners/ListenerPlayerPunished.java @@ -0,0 +1,72 @@ +package net.swofty.velocity.redis.listeners; + +import com.velocitypowered.api.proxy.ProxyServer; +import net.kyori.adventure.text.Component; +import net.swofty.commons.proxy.ToProxyChannels; +import net.swofty.commons.punishment.PunishmentId; +import net.swofty.commons.punishment.PunishmentReason; +import net.swofty.commons.punishment.PunishmentRedis; +import net.swofty.commons.punishment.PunishmentType; +import net.swofty.commons.punishment.template.BanType; +import net.swofty.commons.punishment.template.MuteType; +import net.swofty.proxyapi.ProxyPlayer; +import net.swofty.velocity.SkyBlockVelocity; +import net.swofty.velocity.redis.ChannelListener; +import net.swofty.velocity.redis.RedisListener; +import org.jetbrains.annotations.Nullable; +import org.json.JSONException; +import org.json.JSONObject; +import org.tinylog.Logger; + +import java.util.UUID; + +@ChannelListener(channel = ToProxyChannels.PUNISH_PLAYER) +public class ListenerPlayerPunished extends RedisListener { + + @Override + public JSONObject receivedMessage(JSONObject message, UUID serverUUID) { + UUID target = UUID.fromString(message.getString("target")); + String type = message.getString("type"); + String id = message.getString("id"); + long expiresAt = message.getLong("expiresAt"); + + PunishmentReason reason; + try { + String banString = message.optString("reason_ban", null); + String muteString = message.optString("reason_mute", null); + + if (banString != null) { + reason = new PunishmentReason(BanType.valueOf(banString)); + } else if (muteString != null) { + reason = new PunishmentReason(MuteType.valueOf(muteString)); + } else { + reason = new PunishmentReason(message.getString("reason_custom")); + } + } catch (IllegalArgumentException | JSONException e) { + reason = new PunishmentReason("Unknown Reason - Report Error"); + Logger.error("Failed to parse punishment reason from message: " + message, e); + } + + + PunishmentType punishmentType = PunishmentType.valueOf(type); + PunishmentReason finalReason = reason; + SkyBlockVelocity.getServer().getPlayer(target).ifPresent((player) -> { + PunishmentRedis.ActivePunishment activePunishment = new PunishmentRedis.ActivePunishment(type, id, finalReason, expiresAt); + switch (punishmentType) { + case BAN -> { + player.disconnect(PunishmentRedis.parseActivePunishmentBanMessage(activePunishment)); + } + case MUTE -> { + player.sendMessage(PunishmentRedis.parseActivePunishmentMuteMessage(activePunishment)); + } + case WARNING -> { + player.sendMessage(Component.text("§c[WARNING] §7You have received a warning for the following reason: §e" + finalReason)); + } + default -> { + } + } + }); + + return null; + } +} diff --git a/velocity.extension/src/main/java/net/swofty/velocity/viaversion/handler/PacketDecodeHandler.java b/velocity.extension/src/main/java/net/swofty/velocity/viaversion/handler/PacketDecodeHandler.java index d9ea4c3e9..deb25638e 100644 --- a/velocity.extension/src/main/java/net/swofty/velocity/viaversion/handler/PacketDecodeHandler.java +++ b/velocity.extension/src/main/java/net/swofty/velocity/viaversion/handler/PacketDecodeHandler.java @@ -23,7 +23,7 @@ public PacketDecodeHandler(UserConnection info) { @Override protected void decode(ChannelHandlerContext ctx, ByteBuf bytebuf, List out) { - if (!info.checkIncomingPacket()) throw CancelDecoderException.generate(null); + if (!info.checkIncomingPacket(0)) throw CancelDecoderException.generate(null); if (!info.shouldTransformPacket()) { out.add(bytebuf.retain()); return; From eeadfc13b23d03070ce543f911b260bdd1c2418e Mon Sep 17 00:00:00 2001 From: ArikSquad <75741608+ArikSquad@users.noreply.github.com> Date: Fri, 16 Jan 2026 23:29:31 +0200 Subject: [PATCH 02/27] refactor: remove old unused message --- .../protocol/objects/punishment/PunishPlayerProtocolObject.java | 1 - 1 file changed, 1 deletion(-) diff --git a/commons/src/main/java/net/swofty/commons/protocol/objects/punishment/PunishPlayerProtocolObject.java b/commons/src/main/java/net/swofty/commons/protocol/objects/punishment/PunishPlayerProtocolObject.java index 9ad74f50b..2c944d1c4 100644 --- a/commons/src/main/java/net/swofty/commons/protocol/objects/punishment/PunishPlayerProtocolObject.java +++ b/commons/src/main/java/net/swofty/commons/protocol/objects/punishment/PunishPlayerProtocolObject.java @@ -82,7 +82,6 @@ public PunishPlayerResponse clone(PunishPlayerResponse value) { }; } - // do NOT change this to use Punishment - friendly note from Ari public record PunishPlayerMessage( @NotNull UUID target, From f7d03495c99efad55ac325a221141ed4f2410227 Mon Sep 17 00:00:00 2001 From: ArikSquad <75741608+ArikSquad@users.noreply.github.com> Date: Fri, 16 Jan 2026 23:39:43 +0200 Subject: [PATCH 03/27] refactor: remove requiredRank --- .../commons/punishment/template/BanType.java | 72 +++++++++---------- 1 file changed, 35 insertions(+), 37 deletions(-) diff --git a/commons/src/main/java/net/swofty/commons/punishment/template/BanType.java b/commons/src/main/java/net/swofty/commons/punishment/template/BanType.java index 96d197680..50c4d634a 100644 --- a/commons/src/main/java/net/swofty/commons/punishment/template/BanType.java +++ b/commons/src/main/java/net/swofty/commons/punishment/template/BanType.java @@ -7,74 +7,72 @@ @Getter public enum BanType { - WATCHDOG(PunishmentCategory.ADMIN_ONLY, "WATCHDOG CHEAT DETECTION", "WATCHDOG", 4, "STAFF", + WATCHDOG(PunishmentCategory.ADMIN_ONLY, "WATCHDOG CHEAT DETECTION", "WATCHDOG", 4, "https://www.hypixel.net/watchdog", true, true), - BLACKLISTED_MODIFICATIONS(PunishmentCategory.CHEATING, "Cheating through the use of unfair game advantages.", "BM", 4, null, + BLACKLISTED_MODIFICATIONS(PunishmentCategory.CHEATING, "Cheating through the use of unfair game advantages.", "BM", 4, null, true, true, Arrays.asList("Blacklisted Modifications", "Cheating/Unfair Advantage", "Using unfair advantages in game")), - CROSS_TEAMING(PunishmentCategory.GAMEPLAY, "Cross teaming, you were found to be working with another team or player.", "CT", 1, null, null, false, false), - TEAM_GRIEFING(PunishmentCategory.GAMEPLAY, "You were found to be negatively affecting your fellow team members.", "TG", 1, null, null, false, false), - INAPPROPRIATE_BUILD(PunishmentCategory.INAPPROPRIATE_CONTENT, "Creating a build or drawing which is not appropriate on the server.", "IB", 1, null, + CROSS_TEAMING(PunishmentCategory.GAMEPLAY, "Cross teaming, you were found to be working with another team or player.", "CT", 1, null, false, false), + TEAM_GRIEFING(PunishmentCategory.GAMEPLAY, "You were found to be negatively affecting your fellow team members.", "TG", 1, null, false, false), + INAPPROPRIATE_BUILD(PunishmentCategory.INAPPROPRIATE_CONTENT, "Creating a build or drawing which is not appropriate on the server.", "IB", 1, null, false, false, Arrays.asList("Inappropriate Build", "Inappropriate Drawing")), - INAPPROPRIATE_ITEM_NAME(PunishmentCategory.INAPPROPRIATE_CONTENT, "Creating or using an item that has an inappropriate name", "IN", 1, null, null, false, false), - INAPPROPRIATE_ITEM_USAGE(PunishmentCategory.INAPPROPRIATE_CONTENT, "Using pets or cosmetics in an inappropriate way.", "IU", 1, null, null, false, false), - STAFF_IMPERSONATION(null, "Misleading others to believe you are a youtuber or staff member.", "SI", 1, null, null, false, false), - SCAMMING(null, "Attempting to obtain information or something of value from players.", "SC", 2, null, null, false, false), - ENCOURAGING_CHEATING_LVL2(PunishmentCategory.CHEATING, "Discussing or acting in a manner which encourages cheating or rule breaking.", "EC2", 2, null, null, false, false), - ENCOURAGING_CHEATING_LVL3(PunishmentCategory.CHEATING, "Discussing or acting in a manner which encourages cheating or rule breaking.", "EC3", 4, null, null, false, false), - EXTREME_USER_DISRESPECT(null, "Acting in a manner that is extremely disrespectful to members within the community.", "EUD", 2, null, + INAPPROPRIATE_ITEM_NAME(PunishmentCategory.INAPPROPRIATE_CONTENT, "Creating or using an item that has an inappropriate name", "IN", 1, null, false, false), + INAPPROPRIATE_ITEM_USAGE(PunishmentCategory.INAPPROPRIATE_CONTENT, "Using pets or cosmetics in an inappropriate way.", "IU", 1, null, false, false), + STAFF_IMPERSONATION(null, "Misleading others to believe you are a youtuber or staff member.", "SI", 1, null, false, false), + SCAMMING(null, "Attempting to obtain information or something of value from players.", "SC", 2, null, false, false), + ENCOURAGING_CHEATING_LVL2(PunishmentCategory.CHEATING, "Discussing or acting in a manner which encourages cheating or rule breaking.", "EC2", 2, null, false, false), + ENCOURAGING_CHEATING_LVL3(PunishmentCategory.CHEATING, "Discussing or acting in a manner which encourages cheating or rule breaking.", "EC3", 4, null, false, false), + EXTREME_USER_DISRESPECT(null, "Acting in a manner that is extremely disrespectful to members within the community.", "EUD", 2, null, false, false, List.of("Extreme Negative behaviour")), - STATS_BOOSTING(PunishmentCategory.ADMIN_ONLY, "Boosting your account to improve your stats.", "SB", 4, "STAFF", + STATS_BOOSTING(PunishmentCategory.ADMIN_ONLY, "Boosting your account to improve your stats.", "SB", 4, null, false, false, List.of("Boosting")), - INAPPROPRIATE_AESTHETICS(PunishmentCategory.INAPPROPRIATE_CONTENT, "Using inappropriate skins or capes on the server.", "IA", 2, null, null, false, false), - EXPLOITING(PunishmentCategory.ADMIN_ONLY, "Exploiting a bug or issue within the server and using it to your advantage.", "EX", 4, "STAFF", + INAPPROPRIATE_AESTHETICS(PunishmentCategory.INAPPROPRIATE_CONTENT, "Using inappropriate skins or capes on the server.", "IA", 2, null, false, false), + EXPLOITING(PunishmentCategory.ADMIN_ONLY, "Exploiting a bug or issue within the server and using it to your advantage.", "EX", 4, null, true, true, List.of("Exploits")), - FALSIFIED_INFORMATION(null, "Making or sharing fake information.", "FI", 3, null, null, false, false), - CHARGEBACK(PunishmentCategory.ADMIN_ONLY, "Chargeback: for more info and appeal, go to support.hypixel.net.", null, -1, "STAFF", + FALSIFIED_INFORMATION(null, "Making or sharing fake information.", "FI", 3, null, false, false), + CHARGEBACK(PunishmentCategory.ADMIN_ONLY, "Chargeback: for more info and appeal, go to support.hypixel.net.", null, -1, null, false, false, List.of("Chargeback")), - FRAUD(null, null, "FR", 0, null, null, false, false), - ACCOUNT_SELLING(null, "Attempting to sell minecraft accounts.", "AS", 4, null, null, false, false), - COMPROMISED_ACCOUNT(PunishmentCategory.ACCOUNT_SECURITY, "Your account has a security alert, please secure it and contact appeals.", "CA", -1, null, + FRAUD(null, null, "FR", 0, null, false, false), + ACCOUNT_SELLING(null, "Attempting to sell minecraft accounts.", "AS", 4, null, false, false), + COMPROMISED_ACCOUNT(PunishmentCategory.ACCOUNT_SECURITY, "Your account has a security alert, please secure it and contact appeals.", "CA", -1, null, false, false, Arrays.asList("Compromised Account", "Account Security Alert"), "Account Security Alert"), - ACCOUNT_SECURITY_ALERT_SERVER_ADVERTISING(PunishmentCategory.ACCOUNT_SECURITY, "Your account has a security alert, please secure it and contact appeals.", "CAS", -1, null, null, false, false), - ACCOUNT_SECURITY_ALERT_BLACKLISTED(PunishmentCategory.ACCOUNT_SECURITY, "Your account has a security alert, please secure it and contact appeals.", "CAB", -1, null, null, false, false), - PHISHING_LINK(null, "Attempting to gain access to other user's accounts/information.", "PL", 4, null, null, false, false), - UN_INTENTIONALLY_CAUSING_DISTRESS_2(null, "Unintentionally/Intentionally Causing distress.", "UIB", 3, null, null, false, false), - UN_INTENTIONALLY_CAUSING_DISTRESS_3(PunishmentCategory.ADMIN_ONLY, "Unintentionally/Intentionally Causing distress.", null, -1, "STAFF", null, false, false), - INAPPROPRIATE_CONTENT_LVL2(PunishmentCategory.INAPPROPRIATE_CONTENT, "Talking or sharing inappropriate content with adult themes on the server.", "IC2", 3, null, null, false, false), - ACCOUNT_DELETION(PunishmentCategory.ADMIN_ONLY, "Upon request, data for this user has been deleted.", null, 0, "STAFF", "https://support.hypixel.net", false, false), - CREATOR_BAN(PunishmentCategory.ADMIN_ONLY, "Please contact staff for assistance.", null, -1, "STAFF", // Please contact creators@hypixel.net for assistance. + ACCOUNT_SECURITY_ALERT_SERVER_ADVERTISING(PunishmentCategory.ACCOUNT_SECURITY, "Your account has a security alert, please secure it and contact appeals.", "CAS", -1, null, false, false), + ACCOUNT_SECURITY_ALERT_BLACKLISTED(PunishmentCategory.ACCOUNT_SECURITY, "Your account has a security alert, please secure it and contact appeals.", "CAB", -1, null, false, false), + PHISHING_LINK(null, "Attempting to gain access to other user's accounts/information.", "PL", 4, null, false, false), + UN_INTENTIONALLY_CAUSING_DISTRESS_2(null, "Unintentionally/Intentionally Causing distress.", "UIB", 3, null, false, false), + UN_INTENTIONALLY_CAUSING_DISTRESS_3(PunishmentCategory.ADMIN_ONLY, "Unintentionally/Intentionally Causing distress.", null, -1, null, false, false), + INAPPROPRIATE_CONTENT_LVL2(PunishmentCategory.INAPPROPRIATE_CONTENT, "Talking or sharing inappropriate content with adult themes on the server.", "IC2", 3, null, false, false), + ACCOUNT_DELETION(PunishmentCategory.ADMIN_ONLY, "Upon request, data for this user has been deleted.", null, 0, "https://support.hypixel.net", false, false), + CREATOR_BAN(PunishmentCategory.ADMIN_ONLY, "Please contact staff for assistance.", null, -1, // Please contact creators@hypixel.net for assistance. null, false, false, List.of("Creator Ban"), "Creator Ban"), - CREATOR_ACCOUNT_SECURITY_ALERT(PunishmentCategory.ADMIN_ONLY, "Your account has a security alert, please secure it and contact staff for assistance.", //Your account has a security alert, please secure it and contact creators@hypixel.net for assistance - null, -1, "STAFF", null, false, false, Arrays.asList("Creator Compromised Account", "Creator Account Security Alert"), "Creator Account Security Alert"); + CREATOR_ACCOUNT_SECURITY_ALERT(PunishmentCategory.ADMIN_ONLY, "Your account has a security alert, please secure it and contact staff for assistance.", // Your account has a security alert, please secure it and contact creators@hypixel.net for assistance + null, -1, null, false, false, Arrays.asList("Creator Compromised Account", "Creator Account Security Alert"), "Creator Account Security Alert"); private final PunishmentCategory category; private final String reason; private final String shortName; private final int weight; - private final String requiredRank; private final String url; private final boolean preventRanked; private final boolean wipe; private final List aliases; private final String cleanName; - BanType(PunishmentCategory category, String reason, String shortName, int weight, String requiredRank, + BanType(PunishmentCategory category, String reason, String shortName, int weight, String url, boolean preventRanked, boolean wipe) { - this(category, reason, shortName, weight, requiredRank, url, preventRanked, wipe, null, null); + this(category, reason, shortName, weight, url, preventRanked, wipe, null, null); } - BanType(PunishmentCategory category, String reason, String shortName, int weight, String requiredRank, + BanType(PunishmentCategory category, String reason, String shortName, int weight, String url, boolean preventRanked, boolean wipe, List aliases) { - this(category, reason, shortName, weight, requiredRank, url, preventRanked, wipe, aliases, null); + this(category, reason, shortName, weight, url, preventRanked, wipe, aliases, null); } - BanType(PunishmentCategory category, String reason, String shortName, int weight, String requiredRank, + BanType(PunishmentCategory category, String reason, String shortName, int weight, String url, boolean preventRanked, boolean wipe, List aliases, String cleanName) { this.category = category; this.reason = reason; this.shortName = shortName; this.weight = weight; - this.requiredRank = requiredRank; this.url = url; this.preventRanked = preventRanked; this.wipe = wipe; From ae3ff64fcbe4059dd0eeaf2b121f54f622c5dfc5 Mon Sep 17 00:00:00 2001 From: ArikSquad <75741608+ArikSquad@users.noreply.github.com> Date: Fri, 16 Jan 2026 23:40:13 +0200 Subject: [PATCH 04/27] refactor: remove requiredRank --- .../punishment/template/UnpunishReason.java | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/commons/src/main/java/net/swofty/commons/punishment/template/UnpunishReason.java b/commons/src/main/java/net/swofty/commons/punishment/template/UnpunishReason.java index 3b4dccaf5..af131c0e9 100644 --- a/commons/src/main/java/net/swofty/commons/punishment/template/UnpunishReason.java +++ b/commons/src/main/java/net/swofty/commons/punishment/template/UnpunishReason.java @@ -4,24 +4,22 @@ @Getter public enum UnpunishReason { - WRONG_PLAYER("Wrong Player", null, null), - WRONG_PUNISHMENT("Wrong Punishment", null, null), - INSUFFICIENT_EVIDENCE("Insufficient Evidence", "IE", null), - ACCOUNT_SECURED("Account Secured", null, "MODERATOR"), - SECOND_CHANCE("Second Chance", "SC", "ADMIN"), - CHARGEBACK_APPEALED("Chargeback Appealed", null, "ADMIN"), - CHARGEBACK_FAILED("Chargeback Failed", null, "ADMIN"), - CHARGEBACK_CANCELLED("Chargeback Cancelled", null, "ADMIN"), - NOT_FRAUD("Not Fraud", null, "ADMIN"), - TIME_SERVED_MAINTAIN_WEIGHT("Time Served (Maintain Weight)", "MW", "ADMIN"); + WRONG_PLAYER("Wrong Player", null), + WRONG_PUNISHMENT("Wrong Punishment", null), + INSUFFICIENT_EVIDENCE("Insufficient Evidence", "IE"), + ACCOUNT_SECURED("Account Secured", null), + SECOND_CHANCE("Second Chance", "SC"), + CHARGEBACK_APPEALED("Chargeback Appealed", null), + CHARGEBACK_FAILED("Chargeback Failed", null), + CHARGEBACK_CANCELLED("Chargeback Cancelled", null), + NOT_FRAUD("Not Fraud", null), + TIME_SERVED_MAINTAIN_WEIGHT("Time Served (Maintain Weight)", "MW"); private final String reason; private final String tag; - private final String requiredRank; - UnpunishReason(String reason, String tag, String requiredRank) { + UnpunishReason(String reason, String tag) { this.reason = reason; this.tag = tag; - this.requiredRank = requiredRank; } } \ No newline at end of file From b62e28299c355706e8fc3deea2a17570d1e5772a Mon Sep 17 00:00:00 2001 From: ArikSquad <75741608+ArikSquad@users.noreply.github.com> Date: Fri, 16 Jan 2026 23:42:30 +0200 Subject: [PATCH 05/27] refactor: remove UnpunishType from PunishmentReason --- .../net/swofty/commons/punishment/PunishmentReason.java | 8 -------- 1 file changed, 8 deletions(-) diff --git a/commons/src/main/java/net/swofty/commons/punishment/PunishmentReason.java b/commons/src/main/java/net/swofty/commons/punishment/PunishmentReason.java index 15dce04ce..607fd9ba2 100644 --- a/commons/src/main/java/net/swofty/commons/punishment/PunishmentReason.java +++ b/commons/src/main/java/net/swofty/commons/punishment/PunishmentReason.java @@ -15,8 +15,6 @@ public class PunishmentReason { private BanType banType; @Nullable private MuteType muteType; - @Nullable - private UnpunishReason unpunishReason; public PunishmentReason(@NonNull BanType banType) { this.banType = banType; @@ -26,10 +24,6 @@ public PunishmentReason(@NonNull MuteType muteType) { this.muteType = muteType; } - public PunishmentReason(@NonNull UnpunishReason unpunishReason) { - this.unpunishReason = unpunishReason; - } - public PunishmentReason(@NonNull String custom) { this.custom = custom; } @@ -39,8 +33,6 @@ public String getReasonString() { return banType.getReason(); } else if (muteType != null) { return muteType.getReason(); - } else if (unpunishReason != null) { - return unpunishReason.getReason(); } else { return custom; } From f9036e3e41627cc14a454d9970977bc7e3ff7742 Mon Sep 17 00:00:00 2001 From: ArikSquad <75741608+ArikSquad@users.noreply.github.com> Date: Sat, 17 Jan 2026 18:46:12 +0200 Subject: [PATCH 06/27] refactor: remove requiredRank from PunishmentTag --- .../commons/punishment/PunishmentTag.java | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/commons/src/main/java/net/swofty/commons/punishment/PunishmentTag.java b/commons/src/main/java/net/swofty/commons/punishment/PunishmentTag.java index 20a0bbe72..13a718fb2 100644 --- a/commons/src/main/java/net/swofty/commons/punishment/PunishmentTag.java +++ b/commons/src/main/java/net/swofty/commons/punishment/PunishmentTag.java @@ -4,26 +4,24 @@ @Getter public enum PunishmentTag { - PERSONAL_PROOF("Personal proof", "P", null, null), - GOLIATH("Punishment applied via Goliath", "G", null, null), - PLAYER_REPORT("Player Report", "R", null, null), - FORUMS("Forums", "F", null, null), - SLACK("Slack", "S", null, null), - WELFARE("Punishment applied over Welfare concern", "W", "STAFF", 99), - ACCOUNT_SECURITY_ALERT(null, "ASA", null, null), - RANKED_TEAM(null, "RT", null, null), - CHECK_BEFORE_UNBAN("Check with the punisher before unbanning this user", "U", "STAFF", null), - OVERWRITE("This punishment overwrote another punishment", "O", "STAFF", null); + PERSONAL_PROOF("Personal proof", "P", null), + GOLIATH("Punishment applied via Goliath", "G", null), + PLAYER_REPORT("Player Report", "R", null), + FORUMS("Forums", "F", null), + SLACK("Slack", "S", null), + WELFARE("Punishment applied over Welfare concern", "W", 99), + ACCOUNT_SECURITY_ALERT(null, "ASA", null), + RANKED_TEAM(null, "RT", null), + CHECK_BEFORE_UNBAN("Check with the punisher before unbanning this user", "U", null), + OVERWRITE("This punishment overwrote another punishment", "O", null); private final String description; private final String shortCode; - private final String requiredRank; private final Integer group; - PunishmentTag(String description, String shortCode, String requiredRank, Integer group) { + PunishmentTag(String description, String shortCode, Integer group) { this.description = description; this.shortCode = shortCode; - this.requiredRank = requiredRank; this.group = group; } From 1e69773b4350ca7721e5d1a4745fc9cff3c90906 Mon Sep 17 00:00:00 2001 From: ArikSquad <75741608+ArikSquad@users.noreply.github.com> Date: Mon, 19 Jan 2026 23:40:23 +0200 Subject: [PATCH 07/27] feat: start tags --- .../PunishPlayerProtocolObject.java | 5 ++ .../commons/punishment/PunishmentReason.java | 8 +- .../commons/punishment/PunishmentTag.java | 24 +++--- .../endpoints/PunishPlayerEndpoint.java | 1 - .../generic/command/commands/BanCommand.java | 73 ++++++++++++++++++- .../listeners/ListenerPlayerPunished.java | 6 +- 6 files changed, 91 insertions(+), 26 deletions(-) diff --git a/commons/src/main/java/net/swofty/commons/protocol/objects/punishment/PunishPlayerProtocolObject.java b/commons/src/main/java/net/swofty/commons/protocol/objects/punishment/PunishPlayerProtocolObject.java index 2c944d1c4..756461e29 100644 --- a/commons/src/main/java/net/swofty/commons/protocol/objects/punishment/PunishPlayerProtocolObject.java +++ b/commons/src/main/java/net/swofty/commons/protocol/objects/punishment/PunishPlayerProtocolObject.java @@ -4,10 +4,12 @@ import net.swofty.commons.protocol.ProtocolObject; import net.swofty.commons.protocol.Serializer; import net.swofty.commons.punishment.PunishmentReason; +import net.swofty.commons.punishment.PunishmentTag; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.json.JSONObject; +import java.util.List; import java.util.UUID; public class PunishPlayerProtocolObject @@ -25,6 +27,7 @@ public String serialize(PunishPlayerMessage value) { json.put("type", value.type()); json.put("reason", new Gson().toJson(value.reason())); json.put("expiresAt", value.expiresAt()); + json.put("tags", new Gson().toJson(value.tags())); json.put("staff", value.staff().toString()); return json.toString(); } @@ -38,6 +41,7 @@ public PunishPlayerMessage deserialize(String json) { obj.getString("type"), new Gson().fromJson(obj.getString("reason"), PunishmentReason.class), UUID.fromString(obj.getString("staff")), + List.of(new Gson().fromJson(obj.getString("tags"), PunishmentTag[].class)), obj.getLong("expiresAt") ); } @@ -91,6 +95,7 @@ public record PunishPlayerMessage( PunishmentReason reason, @NotNull UUID staff, + List tags, long expiresAt ) { } diff --git a/commons/src/main/java/net/swofty/commons/punishment/PunishmentReason.java b/commons/src/main/java/net/swofty/commons/punishment/PunishmentReason.java index 607fd9ba2..956a23975 100644 --- a/commons/src/main/java/net/swofty/commons/punishment/PunishmentReason.java +++ b/commons/src/main/java/net/swofty/commons/punishment/PunishmentReason.java @@ -9,8 +9,6 @@ @Getter public class PunishmentReason { - @Nullable - private String custom; @Nullable private BanType banType; @Nullable @@ -24,17 +22,13 @@ public PunishmentReason(@NonNull MuteType muteType) { this.muteType = muteType; } - public PunishmentReason(@NonNull String custom) { - this.custom = custom; - } - public String getReasonString() { if (banType != null) { return banType.getReason(); } else if (muteType != null) { return muteType.getReason(); } else { - return custom; + return "Could not resolve reason."; } } } diff --git a/commons/src/main/java/net/swofty/commons/punishment/PunishmentTag.java b/commons/src/main/java/net/swofty/commons/punishment/PunishmentTag.java index 13a718fb2..c95196e0c 100644 --- a/commons/src/main/java/net/swofty/commons/punishment/PunishmentTag.java +++ b/commons/src/main/java/net/swofty/commons/punishment/PunishmentTag.java @@ -4,25 +4,23 @@ @Getter public enum PunishmentTag { - PERSONAL_PROOF("Personal proof", "P", null), - GOLIATH("Punishment applied via Goliath", "G", null), - PLAYER_REPORT("Player Report", "R", null), - FORUMS("Forums", "F", null), - SLACK("Slack", "S", null), - WELFARE("Punishment applied over Welfare concern", "W", 99), - ACCOUNT_SECURITY_ALERT(null, "ASA", null), - RANKED_TEAM(null, "RT", null), - CHECK_BEFORE_UNBAN("Check with the punisher before unbanning this user", "U", null), - OVERWRITE("This punishment overwrote another punishment", "O", null); + PERSONAL_PROOF("Personal proof", "P"), + GOLIATH("Punishment applied via Goliath", "G"), + PLAYER_REPORT("Player Report", "R"), + FORUMS("Forums", "F"), + SLACK("Slack", "S"), + WELFARE("Punishment applied over Welfare concern", "W"), + ACCOUNT_SECURITY_ALERT(null, "ASA"), + RANKED_TEAM(null, "RT"), + CHECK_BEFORE_UNBAN("Check with the punisher before unbanning this user", "U"), + OVERWRITE("This punishment overwrote another punishment", "O"); private final String description; private final String shortCode; - private final Integer group; - PunishmentTag(String description, String shortCode, Integer group) { + PunishmentTag(String description, String shortCode) { this.description = description; this.shortCode = shortCode; - this.group = group; } } \ No newline at end of file diff --git a/service.punishment/src/main/java/net/swofty/service/punishment/endpoints/PunishPlayerEndpoint.java b/service.punishment/src/main/java/net/swofty/service/punishment/endpoints/PunishPlayerEndpoint.java index 095c3f833..3c0459972 100644 --- a/service.punishment/src/main/java/net/swofty/service/punishment/endpoints/PunishPlayerEndpoint.java +++ b/service.punishment/src/main/java/net/swofty/service/punishment/endpoints/PunishPlayerEndpoint.java @@ -52,7 +52,6 @@ public PunishPlayerProtocolObject.PunishPlayerResponse onMessage(ServiceProxyReq .put("target", messageObject.target()) .put("type", messageObject.type()) .put("id", id.id()) - .put("reason_custom", reason.getCustom() != null ? reason.getCustom() : null) .put("reason_ban", reason.getBanType() != null ? reason.getBanType().name() : null) .put("reason_mute", reason.getMuteType() != null ? reason.getMuteType().name() : null) .put("staff", messageObject.staff()) diff --git a/type.generic/src/main/java/net/swofty/type/generic/command/commands/BanCommand.java b/type.generic/src/main/java/net/swofty/type/generic/command/commands/BanCommand.java index 4d39ebb53..e30f3b1ba 100644 --- a/type.generic/src/main/java/net/swofty/type/generic/command/commands/BanCommand.java +++ b/type.generic/src/main/java/net/swofty/type/generic/command/commands/BanCommand.java @@ -10,6 +10,8 @@ import net.swofty.commons.StringUtility; import net.swofty.commons.protocol.objects.punishment.PunishPlayerProtocolObject; import net.swofty.commons.punishment.PunishmentReason; +import net.swofty.commons.punishment.PunishmentRedis; +import net.swofty.commons.punishment.PunishmentTag; import net.swofty.commons.punishment.PunishmentType; import net.swofty.commons.punishment.template.BanType; import net.swofty.proxyapi.ProxyPlayer; @@ -17,11 +19,16 @@ import net.swofty.type.generic.command.CommandParameters; import net.swofty.type.generic.command.HypixelCommand; import net.swofty.type.generic.user.categories.Rank; +import org.jetbrains.annotations.Nullable; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; @CommandParameters( aliases = "ban tempban banip tempbanip", @@ -41,6 +48,11 @@ public void registerUsage(MinestomCommand command) { suggestion.addEntry(new SuggestionEntry(type.name(), Component.text("§c" + type.getReason() + " §7| Wipe: " + type.isWipe()))); } }); + Argument extraArg = ArgumentType.StringArray("extra").setSuggestionCallback((sender, context, suggestion) -> { + for (PunishmentTag tag : PunishmentTag.values()) { + suggestion.addEntry(new SuggestionEntry("-" + tag.getShortCode(), Component.text("§e" + tag.getShortCode() + " §7| " + (tag.getDescription() != null ? tag.getDescription() : "No description")))); + } + }); // can be -O -U etc. command.addSyntax((sender, context) -> { String playerName = context.get(playerArg); @@ -68,7 +80,7 @@ public void registerUsage(MinestomCommand command) { long expiryTime = System.currentTimeMillis() + actualTime; CompletableFuture.runAsync(() -> { - banPlayer(sender, targetUuid, type, senderUuid, actualTime, expiryTime, playerName); + banPlayer(sender, targetUuid, type, senderUuid, actualTime, expiryTime, playerName, null); }); }, playerArg, durationArg, reasonArg); @@ -85,23 +97,78 @@ public void registerUsage(MinestomCommand command) { sender instanceof Player player ? player.getUuid() : UUID.fromString("00000000-0000-0000-0000-000000000000"), 0, -1, - playerName); + playerName, null); } catch (IOException e) { sender.sendMessage("§cCould not find player: " + playerName); return; } }); }, playerArg, reasonArg); + + command.addSyntax((sender, context) -> { + String playerName = context.get(playerArg); + BanType reason = BanType.valueOf(context.get(reasonArg)); + List tags = parseTags(List.of(context.get(extraArg))); + + CompletableFuture.runAsync(() -> { + try { + banPlayer(sender, + MojangUtils.getUUID(playerName), + reason, + sender instanceof Player player ? player.getUuid() : UUID.fromString("00000000-0000-0000-0000-000000000000"), + 0, + -1, + playerName, tags); + } catch (IOException e) { + sender.sendMessage("§cCould not find player: " + playerName); + } + }); + }, playerArg, reasonArg, extraArg); + } + + private List parseTags(List rawTags) { + List tags = new ArrayList<>(); + + for (String rawTag : rawTags) { + if (rawTag.startsWith("-")) { + String tagCode = rawTag.substring(1).toUpperCase(); + for (PunishmentTag tag : PunishmentTag.values()) { + if (tag.getShortCode().equalsIgnoreCase(tagCode)) { + tags.add(tag); + break; + } + } + } + } + + return tags; } - private void banPlayer(CommandSender sender, UUID targetUuid, BanType type, UUID senderUuid, long actualTime, long expiryTime, String playerName) { + private void banPlayer(CommandSender sender, UUID targetUuid, BanType type, UUID senderUuid, long actualTime, long expiryTime, String playerName, @Nullable List tags) { + if (tags != null && !tags.contains(PunishmentTag.OVERWRITE)) { + Optional activePunishment = PunishmentRedis.getActive(targetUuid); + AtomicBoolean alreadyBanned = new AtomicBoolean(false); + activePunishment.ifPresent(punishment -> { + PunishmentType t = PunishmentType.valueOf(punishment.type()); + if (t == PunishmentType.BAN) { + sender.sendMessage("§cThis player is already banned. If you want to replace this ban use the tag -O, Punishment ID: §7" + punishment.banId()); + alreadyBanned.set(true); + } + }); + if (alreadyBanned.get()) { + return; + } + } + ProxyService punishmentService = new ProxyService(ServiceType.PUNISHMENT); PunishmentReason reason = new PunishmentReason(type); + ArrayList tagList = (tags != null) ? new ArrayList<>(tags) : new ArrayList<>(); PunishPlayerProtocolObject.PunishPlayerMessage message = new PunishPlayerProtocolObject.PunishPlayerMessage( targetUuid, PunishmentType.BAN.name(), reason, senderUuid, + tagList, actualTime > 0 ? expiryTime : -1 ); diff --git a/velocity.extension/src/main/java/net/swofty/velocity/redis/listeners/ListenerPlayerPunished.java b/velocity.extension/src/main/java/net/swofty/velocity/redis/listeners/ListenerPlayerPunished.java index 5ba73aca4..58e2d97b0 100644 --- a/velocity.extension/src/main/java/net/swofty/velocity/redis/listeners/ListenerPlayerPunished.java +++ b/velocity.extension/src/main/java/net/swofty/velocity/redis/listeners/ListenerPlayerPunished.java @@ -1,6 +1,7 @@ package net.swofty.velocity.redis.listeners; import com.velocitypowered.api.proxy.ProxyServer; +import io.sentry.Sentry; import net.kyori.adventure.text.Component; import net.swofty.commons.proxy.ToProxyChannels; import net.swofty.commons.punishment.PunishmentId; @@ -40,11 +41,12 @@ public JSONObject receivedMessage(JSONObject message, UUID serverUUID) { } else if (muteString != null) { reason = new PunishmentReason(MuteType.valueOf(muteString)); } else { - reason = new PunishmentReason(message.getString("reason_custom")); + throw new JSONException("Missing reason ban, mute or reason_mute"); } } catch (IllegalArgumentException | JSONException e) { - reason = new PunishmentReason("Unknown Reason - Report Error"); Logger.error("Failed to parse punishment reason from message: " + message, e); + Sentry.captureException(e); + return null; } From 6d76fb825822b4b6a6bdf070bed028c3341440ba Mon Sep 17 00:00:00 2001 From: Swofty Date: Tue, 17 Feb 2026 20:46:10 +1100 Subject: [PATCH 08/27] fix(punishment): resolve compilation errors in punishment module Fix wrong mainClass (FriendService -> PunishmentService), add public static void main(String[] args) signature, add missing tags parameter to MuteCommand's PunishPlayerMessage constructor, remove unused import. --- service.punishment/build.gradle.kts | 2 +- .../java/net/swofty/service/punishment/PunishmentService.java | 3 +-- .../net/swofty/type/generic/command/commands/MuteCommand.java | 2 ++ 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/service.punishment/build.gradle.kts b/service.punishment/build.gradle.kts index 5f34aecce..c2cd546d3 100644 --- a/service.punishment/build.gradle.kts +++ b/service.punishment/build.gradle.kts @@ -37,7 +37,7 @@ dependencies { } application { - mainClass.set("net.swofty.service.punishment.FriendService") + mainClass.set("net.swofty.service.punishment.PunishmentService") } tasks { diff --git a/service.punishment/src/main/java/net/swofty/service/punishment/PunishmentService.java b/service.punishment/src/main/java/net/swofty/service/punishment/PunishmentService.java index e66174609..9e61be754 100644 --- a/service.punishment/src/main/java/net/swofty/service/punishment/PunishmentService.java +++ b/service.punishment/src/main/java/net/swofty/service/punishment/PunishmentService.java @@ -6,12 +6,11 @@ import net.swofty.service.generic.SkyBlockService; import net.swofty.service.generic.redis.ServiceEndpoint; -import java.io.ObjectInputFilter; import java.util.List; public class PunishmentService implements SkyBlockService { - static void main() { + public static void main(String[] args) { String mongoUri = ConfigProvider.settings().getMongodb(); new PunishmentDatabase(null).connect(mongoUri); SkyBlockService.init(new PunishmentService()); diff --git a/type.generic/src/main/java/net/swofty/type/generic/command/commands/MuteCommand.java b/type.generic/src/main/java/net/swofty/type/generic/command/commands/MuteCommand.java index 42b6a65f3..46f361961 100644 --- a/type.generic/src/main/java/net/swofty/type/generic/command/commands/MuteCommand.java +++ b/type.generic/src/main/java/net/swofty/type/generic/command/commands/MuteCommand.java @@ -20,6 +20,7 @@ import net.swofty.type.generic.user.categories.Rank; import java.io.IOException; +import java.util.List; import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; @@ -104,6 +105,7 @@ private void mutePlayer(CommandSender sender, UUID targetUuid, MuteType type, UU PunishmentType.MUTE.name(), reason, senderUuid, + List.of(), actualTime > 0 ? expiryTime : -1 ); From 5df6d477f34c97caee7c471a04f926008c3c2262 Mon Sep 17 00:00:00 2001 From: Swofty Date: Tue, 17 Feb 2026 20:46:24 +1100 Subject: [PATCH 09/27] fix(punishment): fix runtime bugs in punishment system Add no-arg constructor to PunishmentReason for Gson deserialization, fix "muted for muted for" typo, replace jedis.keys() with SCAN, make getAllBannedPlayerIds synchronous so UnBanCommand suggestions populate, use SecureRandom for PunishmentId generation, fix permanent ban expiry validation in PunishPlayerEndpoint. --- .../commons/punishment/PunishmentId.java | 6 +++-- .../commons/punishment/PunishmentReason.java | 3 ++- .../commons/punishment/PunishmentRedis.java | 27 ++++++++++--------- .../endpoints/PunishPlayerEndpoint.java | 5 ++-- .../command/commands/UnBanCommand.java | 13 +++++---- 5 files changed, 29 insertions(+), 25 deletions(-) diff --git a/commons/src/main/java/net/swofty/commons/punishment/PunishmentId.java b/commons/src/main/java/net/swofty/commons/punishment/PunishmentId.java index af42b7a3e..2cd6bc7f0 100644 --- a/commons/src/main/java/net/swofty/commons/punishment/PunishmentId.java +++ b/commons/src/main/java/net/swofty/commons/punishment/PunishmentId.java @@ -1,13 +1,15 @@ package net.swofty.commons.punishment; +import java.security.SecureRandom; + public record PunishmentId(String id) { + private static final SecureRandom RANDOM = new SecureRandom(); public static PunishmentId generateId() { StringBuilder idBuilder = new StringBuilder("#"); String hexChars = "0123456789ABCDEF"; for (int i = 0; i < 8; i++) { - int randomIndex = (int) (Math.random() * hexChars.length()); - idBuilder.append(hexChars.charAt(randomIndex)); + idBuilder.append(hexChars.charAt(RANDOM.nextInt(hexChars.length()))); } return new PunishmentId(idBuilder.toString()); } diff --git a/commons/src/main/java/net/swofty/commons/punishment/PunishmentReason.java b/commons/src/main/java/net/swofty/commons/punishment/PunishmentReason.java index 956a23975..5b99ca61d 100644 --- a/commons/src/main/java/net/swofty/commons/punishment/PunishmentReason.java +++ b/commons/src/main/java/net/swofty/commons/punishment/PunishmentReason.java @@ -1,13 +1,14 @@ package net.swofty.commons.punishment; import lombok.Getter; +import lombok.NoArgsConstructor; import lombok.NonNull; import net.swofty.commons.punishment.template.BanType; import net.swofty.commons.punishment.template.MuteType; -import net.swofty.commons.punishment.template.UnpunishReason; import org.jetbrains.annotations.Nullable; @Getter +@NoArgsConstructor public class PunishmentReason { @Nullable private BanType banType; diff --git a/commons/src/main/java/net/swofty/commons/punishment/PunishmentRedis.java b/commons/src/main/java/net/swofty/commons/punishment/PunishmentRedis.java index deae916e7..42dbea07d 100644 --- a/commons/src/main/java/net/swofty/commons/punishment/PunishmentRedis.java +++ b/commons/src/main/java/net/swofty/commons/punishment/PunishmentRedis.java @@ -15,7 +15,6 @@ import java.util.Set; import java.util.UUID; import java.util.concurrent.CompletableFuture; -import java.util.stream.Collectors; public class PunishmentRedis { private static final String PREFIX = "punish:"; @@ -158,7 +157,7 @@ public static Component parseActivePunishmentMuteMessage(ActivePunishment punish if (expiresAt <= 0) { header = "§cYou are permanently muted on this server!\n"; } else { - header = "§cYou are currently muted for muted for " + reason.getReasonString() + "\n"; + header = "§cYou are currently muted for " + reason.getReasonString() + "\n"; } String time = "§7Your mute will expire in §c" + prettyTimeLeft + "\n\n"; @@ -167,16 +166,20 @@ public static Component parseActivePunishmentMuteMessage(ActivePunishment punish return Component.text(line + header + time + urlInfo + footer + line); } - // lookup for all BANNED players - public static CompletableFuture> getAllBannedPlayerIds() { - return CompletableFuture.supplyAsync(() -> { - try (Jedis jedis = jedisPool.getResource()) { - Set keys = jedis.keys(PREFIX + "active:*"); - return keys.stream() - .map(key -> key.substring((PREFIX + "active:").length())) - .collect(Collectors.toSet()); - } - }); + public static Set getAllBannedPlayerIds() { + try (Jedis jedis = jedisPool.getResource()) { + var cursor = "0"; + Set result = new java.util.HashSet<>(); + var params = new redis.clients.jedis.params.ScanParams().match(PREFIX + "active:*").count(100); + do { + var scanResult = jedis.scan(cursor, params); + for (String key : scanResult.getResult()) { + result.add(key.substring((PREFIX + "active:").length())); + } + cursor = scanResult.getCursor(); + } while (!"0".equals(cursor)); + return result; + } } } diff --git a/service.punishment/src/main/java/net/swofty/service/punishment/endpoints/PunishPlayerEndpoint.java b/service.punishment/src/main/java/net/swofty/service/punishment/endpoints/PunishPlayerEndpoint.java index 3c0459972..0c4431aef 100644 --- a/service.punishment/src/main/java/net/swofty/service/punishment/endpoints/PunishPlayerEndpoint.java +++ b/service.punishment/src/main/java/net/swofty/service/punishment/endpoints/PunishPlayerEndpoint.java @@ -36,8 +36,7 @@ public PunishPlayerProtocolObject.PunishPlayerResponse onMessage(ServiceProxyReq PunishmentId id = PunishmentId.generateId(); Instant now = Instant.now(); - Instant expiresAt = Instant.ofEpochMilli(messageObject.expiresAt()); - if (expiresAt.isBefore(Instant.now())) { + if (messageObject.expiresAt() > 0 && Instant.ofEpochMilli(messageObject.expiresAt()).isBefore(now)) { return new PunishPlayerProtocolObject.PunishPlayerResponse(false, null, PunishPlayerProtocolObject.ErrorCode.INVALID_EXPIRY, "The expiration time provided is invalid."); } @@ -62,7 +61,7 @@ public PunishPlayerProtocolObject.PunishPlayerResponse onMessage(ServiceProxyReq messageObject.type(), messageObject.target(), reason.getReasonString(), - expiresAt.toString() + messageObject.expiresAt() ); return new PunishPlayerProtocolObject.PunishPlayerResponse(true, id.id(), null, null); } diff --git a/type.generic/src/main/java/net/swofty/type/generic/command/commands/UnBanCommand.java b/type.generic/src/main/java/net/swofty/type/generic/command/commands/UnBanCommand.java index 80635adcb..e72accde3 100644 --- a/type.generic/src/main/java/net/swofty/type/generic/command/commands/UnBanCommand.java +++ b/type.generic/src/main/java/net/swofty/type/generic/command/commands/UnBanCommand.java @@ -1,7 +1,6 @@ package net.swofty.type.generic.command.commands; import net.minestom.server.command.builder.arguments.Argument; -import net.minestom.server.command.builder.arguments.ArgumentString; import net.minestom.server.command.builder.arguments.ArgumentType; import net.minestom.server.command.builder.suggestion.SuggestionEntry; import net.minestom.server.utils.mojang.MojangUtils; @@ -11,6 +10,7 @@ import net.swofty.type.generic.user.categories.Rank; import java.io.IOException; +import java.util.Set; import java.util.UUID; @CommandParameters( @@ -25,12 +25,11 @@ public class UnBanCommand extends HypixelCommand { @Override public void registerUsage(MinestomCommand command) { Argument argument = ArgumentType.String("player").setSuggestionCallback((sender, context, suggestion) -> { - PunishmentRedis.getAllBannedPlayerIds().thenAccept((id) -> { - for (String playerName : id) { - UUID playerUuid = UUID.fromString(playerName); - suggestion.addEntry(new SuggestionEntry(playerUuid.toString())); - } - }); + if (!PunishmentRedis.isInitialized()) return; + Set ids = PunishmentRedis.getAllBannedPlayerIds(); + for (String playerId : ids) { + suggestion.addEntry(new SuggestionEntry(playerId)); + } }); command.addSyntax((sender, context) -> { From fe60040fec2337d6b6bcf2335a8c1184b5f00e67 Mon Sep 17 00:00:00 2001 From: Swofty Date: Tue, 17 Feb 2026 20:46:42 +1100 Subject: [PATCH 10/27] refactor(config): revert unnecessary ConfigProvider createLoader refactor Remove unused ObjectMapper, NodeResolver, and ConfigurationLoader imports that were added without purpose. --- .../swofty/commons/config/ConfigProvider.java | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/commons/src/main/java/net/swofty/commons/config/ConfigProvider.java b/commons/src/main/java/net/swofty/commons/config/ConfigProvider.java index 6d154d064..d22df7264 100644 --- a/commons/src/main/java/net/swofty/commons/config/ConfigProvider.java +++ b/commons/src/main/java/net/swofty/commons/config/ConfigProvider.java @@ -4,9 +4,6 @@ import lombok.Setter; import lombok.experimental.Accessors; import org.spongepowered.configurate.CommentedConfigurationNode; -import org.spongepowered.configurate.loader.ConfigurationLoader; -import org.spongepowered.configurate.objectmapping.ObjectMapper; -import org.spongepowered.configurate.objectmapping.meta.NodeResolver; import org.spongepowered.configurate.yaml.NodeStyle; import org.spongepowered.configurate.yaml.YamlConfigurationLoader; import org.tinylog.Logger; @@ -21,22 +18,14 @@ public class ConfigProvider { @Accessors(fluent = true) private static Settings settings; - static YamlConfigurationLoader createLoader(final Path source) { - final ObjectMapper.Factory customFactory = ObjectMapper.factoryBuilder() - .build(); - - return YamlConfigurationLoader.builder() - .path(source) - .nodeStyle(NodeStyle.BLOCK) - .defaultOptions(opts -> opts.serializers(build -> build.registerAnnotatedObjects(customFactory))) - .build(); - } - static { try { Logger.info("Loading config..."); - YamlConfigurationLoader loader = createLoader(Path.of("./configuration/config.yml")); + YamlConfigurationLoader loader = YamlConfigurationLoader.builder() + .path(Path.of("./configuration/config.yml")) + .nodeStyle(NodeStyle.BLOCK) + .build(); CommentedConfigurationNode root = loader.load(); CommentedConfigurationNode defaults = loader.createNode(); From 642d69854a8a734a5d8c10d82ebe087fdec5b368 Mon Sep 17 00:00:00 2001 From: Swofty Date: Tue, 17 Feb 2026 20:47:04 +1100 Subject: [PATCH 11/27] refactor(punishment): remove unrelated terminal feature Remove MinestomTerminal, MinestomCompleter, jline dependencies, and terminal config setting. This feature should be in a separate PR. --- .../net/swofty/commons/config/Settings.java | 3 - type.generic/build.gradle.kts | 2 - .../type/generic/HypixelGenericLoader.java | 8 -- .../generic/terminal/MinestomCompleter.java | 47 ---------- .../generic/terminal/MinestomTerminal.java | 86 ------------------- 5 files changed, 146 deletions(-) delete mode 100644 type.generic/src/main/java/net/swofty/type/generic/terminal/MinestomCompleter.java delete mode 100644 type.generic/src/main/java/net/swofty/type/generic/terminal/MinestomTerminal.java diff --git a/commons/src/main/java/net/swofty/commons/config/Settings.java b/commons/src/main/java/net/swofty/commons/config/Settings.java index c4ef31625..45c127b94 100644 --- a/commons/src/main/java/net/swofty/commons/config/Settings.java +++ b/commons/src/main/java/net/swofty/commons/config/Settings.java @@ -28,9 +28,6 @@ public class Settings { @Comment("Whether to enable sandbox features (such as editing items)") private boolean sandbox = false; - @Comment("Whether to enable the terminal for Minestom backend servers") - private boolean terminal = false; - @Comment("Integrations with services") private IntegrationSettings integrations = new IntegrationSettings(); diff --git a/type.generic/build.gradle.kts b/type.generic/build.gradle.kts index 3cafbd919..e723fc9e9 100644 --- a/type.generic/build.gradle.kts +++ b/type.generic/build.gradle.kts @@ -25,8 +25,6 @@ dependencies { implementation(project(":proxy.api")) implementation("org.mongodb:bson:5.6.2") implementation("org.mongodb:mongodb-driver-sync:5.6.2") - implementation("org.jline:jline-terminal:3.30.6") - implementation("org.jline:jline-reader:3.30.6") // Must match AtlasRedisAPI's Jedis version to avoid conflicts implementation("redis.clients:jedis:4.2.3") implementation("org.tinylog:tinylog-api:2.7.0") diff --git a/type.generic/src/main/java/net/swofty/type/generic/HypixelGenericLoader.java b/type.generic/src/main/java/net/swofty/type/generic/HypixelGenericLoader.java index a89857453..b572965a8 100644 --- a/type.generic/src/main/java/net/swofty/type/generic/HypixelGenericLoader.java +++ b/type.generic/src/main/java/net/swofty/type/generic/HypixelGenericLoader.java @@ -48,7 +48,6 @@ import net.swofty.type.generic.packet.HypixelPacketServerListener; import net.swofty.type.generic.quest.QuestRegistry; import net.swofty.type.generic.redis.RedisOriginServer; -import net.swofty.type.generic.terminal.MinestomTerminal; import net.swofty.type.generic.user.HypixelPlayer; import org.jetbrains.annotations.Nullable; import org.reflections.Reflections; @@ -248,13 +247,6 @@ public void initialize(MinecraftServer server) { }); } - if (ConfigProvider.settings().isTerminal()) { - try (MinestomTerminal terminal = new MinestomTerminal(MinecraftServer.getCommandManager())) { - terminal.start(); - } catch (Exception e) { - Logger.warn(e, "Failed to start Minestom terminal."); - } - } } public static List getLoadedPlayers() { diff --git a/type.generic/src/main/java/net/swofty/type/generic/terminal/MinestomCompleter.java b/type.generic/src/main/java/net/swofty/type/generic/terminal/MinestomCompleter.java deleted file mode 100644 index 69d015509..000000000 --- a/type.generic/src/main/java/net/swofty/type/generic/terminal/MinestomCompleter.java +++ /dev/null @@ -1,47 +0,0 @@ -package net.swofty.type.generic.terminal; - -import net.minestom.server.command.CommandManager; -import net.minestom.server.command.builder.Command; -import org.jline.reader.Candidate; -import org.jline.reader.Completer; -import org.jline.reader.LineReader; -import org.jline.reader.ParsedLine; - -import java.util.List; - -public final class MinestomCompleter implements Completer { - - private final CommandManager commandManager; - - public MinestomCompleter(CommandManager commandManager) { - this.commandManager = commandManager; - } - - @Override - public void complete(LineReader reader, ParsedLine line, List candidates) { - String buffer = line.line(); - String[] args = buffer.split(" ", -1); - - if (args.length <= 1) { - String prefix = args[0]; - for (Command command : commandManager.getCommands()) { - if (command.getName().startsWith(prefix)) { - candidates.add(new Candidate(command.getName())); - } - } - return; - } - - String commandName = args[0]; - Command command = commandManager.getCommand(commandName); - if (command == null) return; - - List subCommands = command.getSubcommands(); - String lastArg = args[args.length - 1]; - for (Command subCommand : subCommands) { - if (subCommand.getName().startsWith(lastArg)) { - candidates.add(new Candidate(subCommand.getName())); - } - } - } -} diff --git a/type.generic/src/main/java/net/swofty/type/generic/terminal/MinestomTerminal.java b/type.generic/src/main/java/net/swofty/type/generic/terminal/MinestomTerminal.java deleted file mode 100644 index ca9c0ea32..000000000 --- a/type.generic/src/main/java/net/swofty/type/generic/terminal/MinestomTerminal.java +++ /dev/null @@ -1,86 +0,0 @@ -package net.swofty.type.generic.terminal; - -import net.minestom.server.MinecraftServer; -import net.minestom.server.command.CommandManager; -import org.jetbrains.annotations.ApiStatus; -import org.jline.reader.EndOfFileException; -import org.jline.reader.LineReader; -import org.jline.reader.LineReaderBuilder; -import org.jline.reader.UserInterruptException; -import org.jline.terminal.Terminal; -import org.jline.terminal.TerminalBuilder; -import org.tinylog.Logger; - -import java.io.IOException; - -public final class MinestomTerminal implements AutoCloseable { - - private static final String PROMPT = "> "; - - private final CommandManager commandManager; - private Terminal terminal; - private LineReader reader; - private volatile boolean running; - - public MinestomTerminal(CommandManager commandManager) { - this.commandManager = commandManager; - } - - public void start() throws IOException { - if (System.console() == null) { - Logger.warn("No console detected; terminal disabled."); - return; - } - - Thread thread = new Thread(() -> { - try { - terminal = TerminalBuilder.builder() - .system(true) - .streams(System.in, System.out) - .build(); - - reader = LineReaderBuilder.builder() - .terminal(terminal) - .completer(new MinestomCompleter(commandManager)) - .build(); - - running = true; - runLoop(); - } catch (IOException e) { - e.printStackTrace(); - } - }, "terminal-thread"); - - thread.start(); - } - - - private void runLoop() { - while (running) { - Logger.info("Waiting for terminal input..."); - try { - String command = reader.readLine(PROMPT); - commandManager.execute( - commandManager.getConsoleSender(), - command - ); - } catch (UserInterruptException e) { - MinecraftServer.stopCleanly(); - break; - } catch (EndOfFileException e) { - Logger.info("Terminal closed."); - break; - } catch (Throwable t) { - t.printStackTrace(); - } - } - } - - @Override - public void close() { - running = false; - try { - if (terminal != null) terminal.close(); - } catch (IOException ignored) {} - } -} From 7bb9b9bad50ab2e153be075409bd909fc29e1e15 Mon Sep 17 00:00:00 2001 From: Swofty Date: Tue, 17 Feb 2026 21:05:09 +1100 Subject: [PATCH 12/27] refactor(punishment): extract PunishmentMessages from PunishmentRedis (SRP) Move message formatting (banMessage, muteMessage) into a dedicated PunishmentMessages class. PunishmentRedis now only handles connection management and data access. Also simplify SkyBlockVelocity.punished() to use early returns instead of AtomicBoolean. --- .../punishment/PunishmentMessages.java | 55 +++++++++++++++++++ .../commons/punishment/PunishmentRedis.java | 49 ----------------- .../event/actions/ActionPlayerMute.java | 3 +- .../net/swofty/velocity/SkyBlockVelocity.java | 26 ++++----- .../listeners/ListenerPlayerPunished.java | 5 +- 5 files changed, 73 insertions(+), 65 deletions(-) create mode 100644 commons/src/main/java/net/swofty/commons/punishment/PunishmentMessages.java diff --git a/commons/src/main/java/net/swofty/commons/punishment/PunishmentMessages.java b/commons/src/main/java/net/swofty/commons/punishment/PunishmentMessages.java new file mode 100644 index 000000000..b351a59cc --- /dev/null +++ b/commons/src/main/java/net/swofty/commons/punishment/PunishmentMessages.java @@ -0,0 +1,55 @@ +package net.swofty.commons.punishment; + +import net.kyori.adventure.text.Component; +import net.swofty.commons.StringUtility; + +public final class PunishmentMessages { + private PunishmentMessages() {} + + public static Component banMessage(PunishmentRedis.ActivePunishment punishment) { + long expiresAt = punishment.expiresAt(); + PunishmentReason reason = punishment.reason(); + String banId = punishment.banId(); + + long timeLeft = expiresAt - System.currentTimeMillis(); + String prettyTimeLeft = StringUtility.formatTimeLeft(timeLeft); + + String header; + if (expiresAt <= 0) { + header = "§cYou are permanently banned from this server!\n"; + } else { + header = "§cYou are temporarily banned for §f" + prettyTimeLeft + " §cfrom this server!\n"; + } + + String findOutMore = ""; + if (reason.getBanType() != null && reason.getBanType().getUrl() != null) { + findOutMore = "§7Find out more: §b" + reason.getBanType().getUrl() + "\n"; + } + + String footer = "§7Sharing your Ban ID may affect the processing of your appeal!"; + + return Component.text(header + "\n§7Reason: §f" + reason.getReasonString() + "\n" + findOutMore + "\n§7Ban ID: §f" + banId + "\n" + footer); + } + + public static Component muteMessage(PunishmentRedis.ActivePunishment punishment) { + long expiresAt = punishment.expiresAt(); + PunishmentReason reason = punishment.reason(); + + long timeLeft = expiresAt - System.currentTimeMillis(); + String prettyTimeLeft = StringUtility.formatTimeLeft(timeLeft); + + String line = "\n§c§m §r\n"; + + String header; + if (expiresAt <= 0) { + header = "§cYou are permanently muted on this server!\n"; + } else { + header = "§cYou are currently muted for " + reason.getReasonString() + "\n"; + } + String time = "§7Your mute will expire in §c" + prettyTimeLeft + "\n\n"; + + String urlInfo = "§7Find out more here: §fwww.hypixel.net/mutes\n"; + String footer = "§7Mute ID: §f" + punishment.banId(); + return Component.text(line + header + time + urlInfo + footer + line); + } +} diff --git a/commons/src/main/java/net/swofty/commons/punishment/PunishmentRedis.java b/commons/src/main/java/net/swofty/commons/punishment/PunishmentRedis.java index 42dbea07d..e9ee203a2 100644 --- a/commons/src/main/java/net/swofty/commons/punishment/PunishmentRedis.java +++ b/commons/src/main/java/net/swofty/commons/punishment/PunishmentRedis.java @@ -1,8 +1,6 @@ package net.swofty.commons.punishment; import com.google.gson.Gson; -import net.kyori.adventure.text.Component; -import net.swofty.commons.StringUtility; import org.tinylog.Logger; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; @@ -119,53 +117,6 @@ public static CompletableFuture revoke(UUID playerId) { public record ActivePunishment(String type, String banId, PunishmentReason reason, long expiresAt) {} - public static Component parseActivePunishmentBanMessage(ActivePunishment punishment) { - long expiresAt = punishment.expiresAt(); - PunishmentReason reason = punishment.reason(); - String banId = punishment.banId(); - - long timeLeft = expiresAt - System.currentTimeMillis(); - String prettyTimeLeft = StringUtility.formatTimeLeft(timeLeft); - - String header; - if (expiresAt <= 0) { - header = "§cYou are permanently banned from this server!\n"; - } else { - header = "§cYou are temporarily banned for §f" + prettyTimeLeft + " §cfrom this server!\n"; - } - - String findOutMore = ""; - if (reason.getBanType() != null && reason.getBanType().getUrl() != null) { - findOutMore = "§7Find out more: §b" + reason.getBanType().getUrl() + "\n"; - } - - String footer = "§7Sharing your Ban ID may affect the processing of your appeal!"; - - return Component.text(header + "\n§7Reason: §f" + reason.getReasonString() + "\n" + findOutMore + "\n§7Ban ID: §f" + banId + "\n" + footer); - } - - public static Component parseActivePunishmentMuteMessage(ActivePunishment punishment) { - long expiresAt = punishment.expiresAt(); - PunishmentReason reason = punishment.reason(); - - long timeLeft = expiresAt - System.currentTimeMillis(); - String prettyTimeLeft = StringUtility.formatTimeLeft(timeLeft); - - String line = "\n§c§m §r\n"; - - String header; - if (expiresAt <= 0) { - header = "§cYou are permanently muted on this server!\n"; - } else { - header = "§cYou are currently muted for " + reason.getReasonString() + "\n"; - } - String time = "§7Your mute will expire in §c" + prettyTimeLeft + "\n\n"; - - String urlInfo = "§7Find out more here: §fwww.hypixel.net/mutes\n"; - String footer = "§7Mute ID: §f" + punishment.banId(); - return Component.text(line + header + time + urlInfo + footer + line); - } - public static Set getAllBannedPlayerIds() { try (Jedis jedis = jedisPool.getResource()) { var cursor = "0"; diff --git a/type.generic/src/main/java/net/swofty/type/generic/event/actions/ActionPlayerMute.java b/type.generic/src/main/java/net/swofty/type/generic/event/actions/ActionPlayerMute.java index 89eed281f..b539dabf8 100644 --- a/type.generic/src/main/java/net/swofty/type/generic/event/actions/ActionPlayerMute.java +++ b/type.generic/src/main/java/net/swofty/type/generic/event/actions/ActionPlayerMute.java @@ -2,6 +2,7 @@ import net.minestom.server.entity.Player; import net.minestom.server.event.player.PlayerChatEvent; +import net.swofty.commons.punishment.PunishmentMessages; import net.swofty.commons.punishment.PunishmentRedis; import net.swofty.commons.punishment.PunishmentType; import net.swofty.type.generic.event.EventNodes; @@ -22,7 +23,7 @@ public void onPlayerChat(PlayerChatEvent event) { return; } event.setCancelled(true); - player.sendMessage(PunishmentRedis.parseActivePunishmentMuteMessage(punishment)); + player.sendMessage(PunishmentMessages.muteMessage(punishment)); }); } diff --git a/velocity.extension/src/main/java/net/swofty/velocity/SkyBlockVelocity.java b/velocity.extension/src/main/java/net/swofty/velocity/SkyBlockVelocity.java index ae75dde7b..3f0194173 100644 --- a/velocity.extension/src/main/java/net/swofty/velocity/SkyBlockVelocity.java +++ b/velocity.extension/src/main/java/net/swofty/velocity/SkyBlockVelocity.java @@ -36,6 +36,7 @@ import net.swofty.commons.config.ConfigProvider; import net.swofty.commons.config.Settings; import net.swofty.commons.proxy.FromProxyChannels; +import net.swofty.commons.punishment.PunishmentMessages; import net.swofty.commons.punishment.PunishmentRedis; import net.swofty.commons.punishment.PunishmentType; import net.swofty.redisapi.api.RedisAPI; @@ -69,7 +70,7 @@ import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; + import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; @@ -213,20 +214,19 @@ public void onProxyInitialization(ProxyInitializeEvent event) { } public boolean punished(Player player) { - AtomicBoolean shouldConnect = new AtomicBoolean(true); Optional activePunishment = PunishmentRedis.getActive(player.getUniqueId()); - activePunishment.ifPresent(punishment -> { - PunishmentType type = PunishmentType.valueOf(punishment.type()); - if (type == PunishmentType.BAN) { - player.disconnect(PunishmentRedis.parseActivePunishmentBanMessage(punishment)); - shouldConnect.set(false); - } - if (type == PunishmentType.MUTE) { - player.sendMessage(PunishmentRedis.parseActivePunishmentMuteMessage(punishment)); - } - }); + if (activePunishment.isEmpty()) return false; - return !shouldConnect.get(); + PunishmentRedis.ActivePunishment punishment = activePunishment.get(); + PunishmentType type = PunishmentType.valueOf(punishment.type()); + if (type == PunishmentType.BAN) { + player.disconnect(PunishmentMessages.banMessage(punishment)); + return true; + } + if (type == PunishmentType.MUTE) { + player.sendMessage(PunishmentMessages.muteMessage(punishment)); + } + return false; } @Subscribe diff --git a/velocity.extension/src/main/java/net/swofty/velocity/redis/listeners/ListenerPlayerPunished.java b/velocity.extension/src/main/java/net/swofty/velocity/redis/listeners/ListenerPlayerPunished.java index 58e2d97b0..f26b0134d 100644 --- a/velocity.extension/src/main/java/net/swofty/velocity/redis/listeners/ListenerPlayerPunished.java +++ b/velocity.extension/src/main/java/net/swofty/velocity/redis/listeners/ListenerPlayerPunished.java @@ -6,6 +6,7 @@ import net.swofty.commons.proxy.ToProxyChannels; import net.swofty.commons.punishment.PunishmentId; import net.swofty.commons.punishment.PunishmentReason; +import net.swofty.commons.punishment.PunishmentMessages; import net.swofty.commons.punishment.PunishmentRedis; import net.swofty.commons.punishment.PunishmentType; import net.swofty.commons.punishment.template.BanType; @@ -56,10 +57,10 @@ public JSONObject receivedMessage(JSONObject message, UUID serverUUID) { PunishmentRedis.ActivePunishment activePunishment = new PunishmentRedis.ActivePunishment(type, id, finalReason, expiresAt); switch (punishmentType) { case BAN -> { - player.disconnect(PunishmentRedis.parseActivePunishmentBanMessage(activePunishment)); + player.disconnect(PunishmentMessages.banMessage(activePunishment)); } case MUTE -> { - player.sendMessage(PunishmentRedis.parseActivePunishmentMuteMessage(activePunishment)); + player.sendMessage(PunishmentMessages.muteMessage(activePunishment)); } case WARNING -> { player.sendMessage(Component.text("§c[WARNING] §7You have received a warning for the following reason: §e" + finalReason)); From 7ec546060cdf3a897a712b1c685c3b51501594b7 Mon Sep 17 00:00:00 2001 From: Swofty Date: Tue, 17 Feb 2026 21:05:31 +1100 Subject: [PATCH 13/27] refactor(punishment): extract shared command helpers, reduce duplication Add CONSOLE_UUID, resolvePlayerUuid(), and senderUuid() helpers to HypixelCommand base class. Refactor BanCommand and MuteCommand to use them, eliminating duplicated UUID resolution and sender extraction. Remove direct PunishmentRedis dependency from BanCommand. --- .../type/generic/command/HypixelCommand.java | 15 ++++ .../generic/command/commands/BanCommand.java | 78 ++++--------------- .../generic/command/commands/MuteCommand.java | 48 ++++-------- 3 files changed, 46 insertions(+), 95 deletions(-) diff --git a/type.generic/src/main/java/net/swofty/type/generic/command/HypixelCommand.java b/type.generic/src/main/java/net/swofty/type/generic/command/HypixelCommand.java index a0193e526..8cdb1bbea 100644 --- a/type.generic/src/main/java/net/swofty/type/generic/command/HypixelCommand.java +++ b/type.generic/src/main/java/net/swofty/type/generic/command/HypixelCommand.java @@ -4,16 +4,21 @@ import net.minestom.server.command.CommandSender; import net.minestom.server.command.ConsoleSender; import net.minestom.server.command.builder.Command; +import net.minestom.server.entity.Player; +import net.minestom.server.utils.mojang.MojangUtils; import net.swofty.type.generic.data.HypixelDataHandler; import net.swofty.type.generic.data.datapoints.DatapointRank; import net.swofty.type.generic.user.HypixelPlayer; +import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.UUID; public abstract class HypixelCommand { public static final String COMMAND_SUFFIX = "Command"; + public static final UUID CONSOLE_UUID = new UUID(0, 0); @Getter private final CommandParameters params; @@ -40,6 +45,16 @@ protected HypixelCommand() { public abstract void registerUsage(MinestomCommand command); + protected static UUID resolvePlayerUuid(CommandSender sender, String playerName, String action) throws IOException { + UUID uuid = MojangUtils.getUUID(playerName); + sender.sendMessage("§8Processing " + action + " for player §e" + playerName + "§7... (" + uuid + ")"); + return uuid; + } + + protected static UUID senderUuid(CommandSender sender) { + return sender instanceof Player p ? p.getUuid() : CONSOLE_UUID; + } + public boolean permissionCheck(CommandSender sender) { HypixelPlayer player = (HypixelPlayer) sender; HypixelDataHandler dataHandler = player.getDataHandler(); diff --git a/type.generic/src/main/java/net/swofty/type/generic/command/commands/BanCommand.java b/type.generic/src/main/java/net/swofty/type/generic/command/commands/BanCommand.java index e30f3b1ba..0c7f487ab 100644 --- a/type.generic/src/main/java/net/swofty/type/generic/command/commands/BanCommand.java +++ b/type.generic/src/main/java/net/swofty/type/generic/command/commands/BanCommand.java @@ -4,13 +4,10 @@ import net.minestom.server.command.CommandSender; import net.minestom.server.command.builder.arguments.*; import net.minestom.server.command.builder.suggestion.SuggestionEntry; -import net.minestom.server.entity.Player; -import net.minestom.server.utils.mojang.MojangUtils; import net.swofty.commons.ServiceType; import net.swofty.commons.StringUtility; import net.swofty.commons.protocol.objects.punishment.PunishPlayerProtocolObject; import net.swofty.commons.punishment.PunishmentReason; -import net.swofty.commons.punishment.PunishmentRedis; import net.swofty.commons.punishment.PunishmentTag; import net.swofty.commons.punishment.PunishmentType; import net.swofty.commons.punishment.template.BanType; @@ -24,11 +21,9 @@ import java.io.IOException; import java.util.ArrayList; import java.util.List; -import java.util.Optional; import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; @CommandParameters( aliases = "ban tempban banip tempbanip", @@ -52,55 +47,35 @@ public void registerUsage(MinestomCommand command) { for (PunishmentTag tag : PunishmentTag.values()) { suggestion.addEntry(new SuggestionEntry("-" + tag.getShortCode(), Component.text("§e" + tag.getShortCode() + " §7| " + (tag.getDescription() != null ? tag.getDescription() : "No description")))); } - }); // can be -O -U etc. + }); command.addSyntax((sender, context) -> { String playerName = context.get(playerArg); String duration = context.get(durationArg); BanType type = BanType.valueOf(context.get(reasonArg)); - - UUID targetUuid; - try { - targetUuid = MojangUtils.getUUID(playerName); - sender.sendMessage("§8Processing ban for player §e" + playerName + "§7... (" + targetUuid + ")"); - } catch (IOException e) { - sender.sendMessage("§cCould not find player: " + playerName); - return; - } - - UUID senderUuid; - if (sender instanceof Player player) { - senderUuid = player.getUuid(); - } else { - senderUuid = UUID.fromString("00000000-0000-0000-0000-000000000000"); - } - - long actualTime = StringUtility.parseDuration(duration); - long expiryTime = System.currentTimeMillis() + actualTime; - CompletableFuture.runAsync(() -> { - banPlayer(sender, targetUuid, type, senderUuid, actualTime, expiryTime, playerName, null); + try { + UUID targetUuid = resolvePlayerUuid(sender, playerName, "ban"); + long actualTime = StringUtility.parseDuration(duration); + long expiryTime = System.currentTimeMillis() + actualTime; + banPlayer(sender, targetUuid, type, senderUuid(sender), actualTime, expiryTime, playerName, null); + } catch (IOException e) { + sender.sendMessage("§cCould not find player: " + playerName); + } }); }, playerArg, durationArg, reasonArg); - // permanent ban command.addSyntax((sender, context) -> { String playerName = context.get(playerArg); BanType reason = BanType.valueOf(context.get(reasonArg)); CompletableFuture.runAsync(() -> { try { - banPlayer(sender, - MojangUtils.getUUID(playerName), - reason, - sender instanceof Player player ? player.getUuid() : UUID.fromString("00000000-0000-0000-0000-000000000000"), - 0, - -1, - playerName, null); + banPlayer(sender, resolvePlayerUuid(sender, playerName, "ban"), reason, + senderUuid(sender), 0, -1, playerName, null); } catch (IOException e) { sender.sendMessage("§cCould not find player: " + playerName); - return; } }); }, playerArg, reasonArg); @@ -112,13 +87,8 @@ public void registerUsage(MinestomCommand command) { CompletableFuture.runAsync(() -> { try { - banPlayer(sender, - MojangUtils.getUUID(playerName), - reason, - sender instanceof Player player ? player.getUuid() : UUID.fromString("00000000-0000-0000-0000-000000000000"), - 0, - -1, - playerName, tags); + banPlayer(sender, resolvePlayerUuid(sender, playerName, "ban"), reason, + senderUuid(sender), 0, -1, playerName, tags); } catch (IOException e) { sender.sendMessage("§cCould not find player: " + playerName); } @@ -128,7 +98,6 @@ public void registerUsage(MinestomCommand command) { private List parseTags(List rawTags) { List tags = new ArrayList<>(); - for (String rawTag : rawTags) { if (rawTag.startsWith("-")) { String tagCode = rawTag.substring(1).toUpperCase(); @@ -140,26 +109,11 @@ private List parseTags(List rawTags) { } } } - return tags; } - private void banPlayer(CommandSender sender, UUID targetUuid, BanType type, UUID senderUuid, long actualTime, long expiryTime, String playerName, @Nullable List tags) { - if (tags != null && !tags.contains(PunishmentTag.OVERWRITE)) { - Optional activePunishment = PunishmentRedis.getActive(targetUuid); - AtomicBoolean alreadyBanned = new AtomicBoolean(false); - activePunishment.ifPresent(punishment -> { - PunishmentType t = PunishmentType.valueOf(punishment.type()); - if (t == PunishmentType.BAN) { - sender.sendMessage("§cThis player is already banned. If you want to replace this ban use the tag -O, Punishment ID: §7" + punishment.banId()); - alreadyBanned.set(true); - } - }); - if (alreadyBanned.get()) { - return; - } - } - + private void banPlayer(CommandSender sender, UUID targetUuid, BanType type, UUID senderUuid, + long actualTime, long expiryTime, String playerName, @Nullable List tags) { ProxyService punishmentService = new ProxyService(ServiceType.PUNISHMENT); PunishmentReason reason = new PunishmentReason(type); ArrayList tagList = (tags != null) ? new ArrayList<>(tags) : new ArrayList<>(); @@ -178,6 +132,8 @@ private void banPlayer(CommandSender sender, UUID targetUuid, BanType type, UUID if (result instanceof PunishPlayerProtocolObject.PunishPlayerResponse response) { if (response.success()) { sender.sendMessage("§aSuccessfully banned player §e" + playerName + "§a. §8Punishment ID: §7" + response.punishmentId()); + } else if (response.errorCode() == PunishPlayerProtocolObject.ErrorCode.ALREADY_PUNISHED) { + sender.sendMessage("§cThis player is already banned. Use the tag -O to overwrite. Punishment ID: §7" + response.errorMessage()); } else { sender.sendMessage("§cFailed to ban player: " + response.errorMessage()); } diff --git a/type.generic/src/main/java/net/swofty/type/generic/command/commands/MuteCommand.java b/type.generic/src/main/java/net/swofty/type/generic/command/commands/MuteCommand.java index 46f361961..595f9e01f 100644 --- a/type.generic/src/main/java/net/swofty/type/generic/command/commands/MuteCommand.java +++ b/type.generic/src/main/java/net/swofty/type/generic/command/commands/MuteCommand.java @@ -6,8 +6,6 @@ import net.minestom.server.command.builder.arguments.ArgumentString; import net.minestom.server.command.builder.arguments.ArgumentType; import net.minestom.server.command.builder.suggestion.SuggestionEntry; -import net.minestom.server.entity.Player; -import net.minestom.server.utils.mojang.MojangUtils; import net.swofty.commons.ServiceType; import net.swofty.commons.StringUtility; import net.swofty.commons.protocol.objects.punishment.PunishPlayerProtocolObject; @@ -49,55 +47,35 @@ public void registerUsage(MinestomCommand command) { String duration = context.get(durationArg); MuteType type = MuteType.valueOf(context.get(reasonArg)); - - UUID targetUuid; - try { - targetUuid = MojangUtils.getUUID(playerName); - sender.sendMessage("§8Processing mute for player §e" + playerName + "§7... (" + targetUuid + ")"); - } catch (IOException e) { - sender.sendMessage("§cCould not find player: " + playerName); - return; - } - - UUID senderUuid; - if (sender instanceof Player player) { - senderUuid = player.getUuid(); - } else { - senderUuid = UUID.fromString("00000000-0000-0000-0000-000000000000"); - } - - long actualTime = StringUtility.parseDuration(duration); - long expiryTime = System.currentTimeMillis() + actualTime; - CompletableFuture.runAsync(() -> { - mutePlayer(sender, targetUuid, type, senderUuid, actualTime, expiryTime, playerName); + try { + UUID targetUuid = resolvePlayerUuid(sender, playerName, "mute"); + long actualTime = StringUtility.parseDuration(duration); + long expiryTime = System.currentTimeMillis() + actualTime; + mutePlayer(sender, targetUuid, type, senderUuid(sender), actualTime, expiryTime, playerName); + } catch (IOException e) { + sender.sendMessage("§cCould not find player: " + playerName); + } }); }, playerArg, durationArg, reasonArg); - // permanent mute command.addSyntax((sender, context) -> { String playerName = context.get(playerArg); MuteType reason = MuteType.valueOf(context.get(reasonArg)); CompletableFuture.runAsync(() -> { try { - mutePlayer(sender, - MojangUtils.getUUID(playerName), - reason, - sender instanceof Player player ? player.getUuid() : UUID.fromString("00000000-0000-0000-0000-000000000000"), - 0, - -1, - playerName); + mutePlayer(sender, resolvePlayerUuid(sender, playerName, "mute"), reason, + senderUuid(sender), 0, -1, playerName); } catch (IOException e) { sender.sendMessage("§cCould not find player: " + playerName); } }); - - }, playerArg, reasonArg); } - private void mutePlayer(CommandSender sender, UUID targetUuid, MuteType type, UUID senderUuid, long actualTime, long expiryTime, String playerName) { + private void mutePlayer(CommandSender sender, UUID targetUuid, MuteType type, UUID senderUuid, + long actualTime, long expiryTime, String playerName) { ProxyService punishmentService = new ProxyService(ServiceType.PUNISHMENT); PunishmentReason reason = new PunishmentReason(type); PunishPlayerProtocolObject.PunishPlayerMessage message = new PunishPlayerProtocolObject.PunishPlayerMessage( @@ -113,6 +91,8 @@ private void mutePlayer(CommandSender sender, UUID targetUuid, MuteType type, UU if (result instanceof PunishPlayerProtocolObject.PunishPlayerResponse response) { if (response.success()) { sender.sendMessage("§aSuccessfully muted player §e" + playerName + "§a. §8Punishment ID: §7" + response.punishmentId()); + } else if (response.errorCode() == PunishPlayerProtocolObject.ErrorCode.ALREADY_PUNISHED) { + sender.sendMessage("§cThis player already has an active punishment. Punishment ID: §7" + response.errorMessage()); } else { sender.sendMessage("§cFailed to mute player: " + response.errorMessage()); } From 6ebaf9b6058f44b46a7b9f5b7770359c2abebb94 Mon Sep 17 00:00:00 2001 From: Swofty Date: Tue, 17 Feb 2026 21:07:02 +1100 Subject: [PATCH 14/27] refactor(punishment): move business logic to service layer (DIP) Move duplicate-punishment check from BanCommand into PunishPlayerEndpoint with ALREADY_PUNISHED error code. Create UnpunishPlayerProtocolObject and UnpunishPlayerEndpoint so UnBanCommand goes through the service layer instead of calling PunishmentRedis directly. Move proxy message framing into ProxyRedis.publishToProxy(). --- .../PunishPlayerProtocolObject.java | 1 + .../UnpunishPlayerProtocolObject.java | 78 +++++++++++++++++++ .../swofty/service/punishment/ProxyRedis.java | 10 +++ .../endpoints/PunishPlayerEndpoint.java | 36 +++++---- .../endpoints/UnpunishPlayerEndpoint.java | 33 ++++++++ .../command/commands/UnBanCommand.java | 38 ++++++--- 6 files changed, 171 insertions(+), 25 deletions(-) create mode 100644 commons/src/main/java/net/swofty/commons/protocol/objects/punishment/UnpunishPlayerProtocolObject.java create mode 100644 service.punishment/src/main/java/net/swofty/service/punishment/endpoints/UnpunishPlayerEndpoint.java diff --git a/commons/src/main/java/net/swofty/commons/protocol/objects/punishment/PunishPlayerProtocolObject.java b/commons/src/main/java/net/swofty/commons/protocol/objects/punishment/PunishPlayerProtocolObject.java index 756461e29..7e7ceb563 100644 --- a/commons/src/main/java/net/swofty/commons/protocol/objects/punishment/PunishPlayerProtocolObject.java +++ b/commons/src/main/java/net/swofty/commons/protocol/objects/punishment/PunishPlayerProtocolObject.java @@ -115,6 +115,7 @@ public enum ErrorCode { INVALID_TYPE, DATABASE_ERROR, INVALID_EXPIRY, + ALREADY_PUNISHED, UNKNOWN_ERROR } } \ No newline at end of file diff --git a/commons/src/main/java/net/swofty/commons/protocol/objects/punishment/UnpunishPlayerProtocolObject.java b/commons/src/main/java/net/swofty/commons/protocol/objects/punishment/UnpunishPlayerProtocolObject.java new file mode 100644 index 000000000..b2f9b7b46 --- /dev/null +++ b/commons/src/main/java/net/swofty/commons/protocol/objects/punishment/UnpunishPlayerProtocolObject.java @@ -0,0 +1,78 @@ +package net.swofty.commons.protocol.objects.punishment; + +import net.swofty.commons.protocol.ProtocolObject; +import net.swofty.commons.protocol.Serializer; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.json.JSONObject; + +import java.util.UUID; + +public class UnpunishPlayerProtocolObject + extends ProtocolObject { + + @Override + public Serializer getSerializer() { + return new Serializer<>() { + @Override + public String serialize(UnpunishPlayerMessage value) { + JSONObject json = new JSONObject(); + json.put("target", value.target().toString()); + json.put("staff", value.staff().toString()); + return json.toString(); + } + + @Override + public UnpunishPlayerMessage deserialize(String json) { + JSONObject obj = new JSONObject(json); + return new UnpunishPlayerMessage( + UUID.fromString(obj.getString("target")), + UUID.fromString(obj.getString("staff")) + ); + } + + @Override + public UnpunishPlayerMessage clone(UnpunishPlayerMessage value) { + return value; + } + }; + } + + @Override + public Serializer getReturnSerializer() { + return new Serializer<>() { + @Override + public String serialize(UnpunishPlayerResponse value) { + JSONObject json = new JSONObject(); + json.put("success", value.success()); + json.put("errorMessage", value.errorMessage()); + return json.toString(); + } + + @Override + public UnpunishPlayerResponse deserialize(String json) { + JSONObject obj = new JSONObject(json); + return new UnpunishPlayerResponse( + obj.getBoolean("success"), + obj.optString("errorMessage", null) + ); + } + + @Override + public UnpunishPlayerResponse clone(UnpunishPlayerResponse value) { + return value; + } + }; + } + + public record UnpunishPlayerMessage( + @NotNull UUID target, + @NotNull UUID staff + ) {} + + public record UnpunishPlayerResponse( + boolean success, + @Nullable String errorMessage + ) {} +} diff --git a/service.punishment/src/main/java/net/swofty/service/punishment/ProxyRedis.java b/service.punishment/src/main/java/net/swofty/service/punishment/ProxyRedis.java index f83a17b9d..d068ab929 100644 --- a/service.punishment/src/main/java/net/swofty/service/punishment/ProxyRedis.java +++ b/service.punishment/src/main/java/net/swofty/service/punishment/ProxyRedis.java @@ -4,8 +4,12 @@ import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisPoolConfig; +import net.swofty.commons.proxy.ToProxyChannels; +import org.json.JSONObject; + import java.net.URI; import java.time.Duration; +import java.util.UUID; import java.util.concurrent.CompletableFuture; public class ProxyRedis { @@ -59,6 +63,12 @@ public static CompletableFuture publishMessage(String filterId, String cha }); } + public static void publishToProxy(ToProxyChannels channel, JSONObject message) { + UUID uuid = UUID.randomUUID(); + publishMessage("proxy", channel.getChannelName(), + message.toString() + "}=-=-={" + uuid + "}=-=-={" + uuid); + } + public static boolean isInitialized() { return initialized && jedisPool != null && !jedisPool.isClosed(); } diff --git a/service.punishment/src/main/java/net/swofty/service/punishment/endpoints/PunishPlayerEndpoint.java b/service.punishment/src/main/java/net/swofty/service/punishment/endpoints/PunishPlayerEndpoint.java index 0c4431aef..15c8695f8 100644 --- a/service.punishment/src/main/java/net/swofty/service/punishment/endpoints/PunishPlayerEndpoint.java +++ b/service.punishment/src/main/java/net/swofty/service/punishment/endpoints/PunishPlayerEndpoint.java @@ -11,7 +11,7 @@ import org.tinylog.Logger; import java.time.Instant; -import java.util.UUID; +import java.util.Optional; public class PunishPlayerEndpoint implements ServiceEndpoint 0 && Instant.ofEpochMilli(messageObject.expiresAt()).isBefore(now)) { return new PunishPlayerProtocolObject.PunishPlayerResponse(false, null, PunishPlayerProtocolObject.ErrorCode.INVALID_EXPIRY, "The expiration time provided is invalid."); } + boolean hasOverwriteTag = messageObject.tags() != null && messageObject.tags().contains(PunishmentTag.OVERWRITE); + if (!hasOverwriteTag) { + Optional existing = PunishmentRedis.getActive(messageObject.target()); + if (existing.isPresent()) { + PunishmentRedis.ActivePunishment active = existing.get(); + PunishmentType existingType = PunishmentType.valueOf(active.type()); + if (existingType == punishmentType) { + return new PunishPlayerProtocolObject.PunishPlayerResponse(false, null, + PunishPlayerProtocolObject.ErrorCode.ALREADY_PUNISHED, active.banId()); + } + } + } + + PunishmentReason reason = messageObject.reason(); + PunishmentId id = PunishmentId.generateId(); + PunishmentRedis.saveActivePunishment( messageObject.target(), messageObject.type(), @@ -47,7 +59,7 @@ public PunishPlayerProtocolObject.PunishPlayerResponse onMessage(ServiceProxyReq reason, messageObject.expiresAt() ); - sendMessageToProxy(ToProxyChannels.PUNISH_PLAYER, new JSONObject() + ProxyRedis.publishToProxy(ToProxyChannels.PUNISH_PLAYER, new JSONObject() .put("target", messageObject.target()) .put("type", messageObject.type()) .put("id", id.id()) @@ -65,12 +77,4 @@ public PunishPlayerProtocolObject.PunishPlayerResponse onMessage(ServiceProxyReq ); return new PunishPlayerProtocolObject.PunishPlayerResponse(true, id.id(), null, null); } - - public static void sendMessageToProxy(ToProxyChannels channel, JSONObject message) { - UUID uuid = UUID.randomUUID(); - - ProxyRedis.publishMessage("proxy", - channel.getChannelName(), - message.toString() + "}=-=-={" + uuid + "}=-=-={" + uuid); - } } diff --git a/service.punishment/src/main/java/net/swofty/service/punishment/endpoints/UnpunishPlayerEndpoint.java b/service.punishment/src/main/java/net/swofty/service/punishment/endpoints/UnpunishPlayerEndpoint.java new file mode 100644 index 000000000..e5c909981 --- /dev/null +++ b/service.punishment/src/main/java/net/swofty/service/punishment/endpoints/UnpunishPlayerEndpoint.java @@ -0,0 +1,33 @@ +package net.swofty.service.punishment.endpoints; + +import net.swofty.commons.impl.ServiceProxyRequest; +import net.swofty.commons.protocol.ProtocolObject; +import net.swofty.commons.protocol.objects.punishment.UnpunishPlayerProtocolObject; +import net.swofty.commons.punishment.PunishmentRedis; +import net.swofty.service.generic.redis.ServiceEndpoint; +import org.tinylog.Logger; + +import java.util.Optional; + +public class UnpunishPlayerEndpoint implements ServiceEndpoint + { + + @Override + public ProtocolObject associatedProtocolObject() { + return new UnpunishPlayerProtocolObject(); + } + + @Override + public UnpunishPlayerProtocolObject.UnpunishPlayerResponse onMessage(ServiceProxyRequest message, UnpunishPlayerProtocolObject.UnpunishPlayerMessage messageObject) { + Optional existing = PunishmentRedis.getActive(messageObject.target()); + if (existing.isEmpty()) { + return new UnpunishPlayerProtocolObject.UnpunishPlayerResponse(false, "No active punishment found for this player."); + } + + PunishmentRedis.revoke(messageObject.target()).join(); + Logger.info("Revoked punishment for {} by staff {}", + messageObject.target(), messageObject.staff()); + return new UnpunishPlayerProtocolObject.UnpunishPlayerResponse(true, null); + } +} diff --git a/type.generic/src/main/java/net/swofty/type/generic/command/commands/UnBanCommand.java b/type.generic/src/main/java/net/swofty/type/generic/command/commands/UnBanCommand.java index e72accde3..4bf38c021 100644 --- a/type.generic/src/main/java/net/swofty/type/generic/command/commands/UnBanCommand.java +++ b/type.generic/src/main/java/net/swofty/type/generic/command/commands/UnBanCommand.java @@ -4,14 +4,18 @@ import net.minestom.server.command.builder.arguments.ArgumentType; import net.minestom.server.command.builder.suggestion.SuggestionEntry; import net.minestom.server.utils.mojang.MojangUtils; +import net.swofty.commons.ServiceType; +import net.swofty.commons.protocol.objects.punishment.UnpunishPlayerProtocolObject; import net.swofty.commons.punishment.PunishmentRedis; +import net.swofty.proxyapi.ProxyService; import net.swofty.type.generic.command.CommandParameters; import net.swofty.type.generic.command.HypixelCommand; import net.swofty.type.generic.user.categories.Rank; import java.io.IOException; import java.util.Set; -import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; @CommandParameters( description = "Unban a player from the server.", @@ -35,14 +39,30 @@ public void registerUsage(MinestomCommand command) { command.addSyntax((sender, context) -> { String playerName = context.get(argument); - sender.sendMessage("§8Processing unban for player " + playerName + "..."); - try { - PunishmentRedis.revoke(MojangUtils.getUUID(playerName)).thenRun(() -> { - sender.sendMessage("§aSuccessfully unbanned player: " + playerName); - }); - } catch (IOException e) { - sender.sendMessage("§cCould not find player: " + playerName); - } + CompletableFuture.runAsync(() -> { + try { + var targetUuid = resolvePlayerUuid(sender, playerName, "unban"); + ProxyService punishmentService = new ProxyService(ServiceType.PUNISHMENT); + var message = new UnpunishPlayerProtocolObject.UnpunishPlayerMessage( + targetUuid, senderUuid(sender) + ); + + punishmentService.handleRequest(message).thenAccept(result -> { + if (result instanceof UnpunishPlayerProtocolObject.UnpunishPlayerResponse response) { + if (response.success()) { + sender.sendMessage("§aSuccessfully unbanned player: " + playerName); + } else { + sender.sendMessage("§c" + response.errorMessage()); + } + } + }).orTimeout(5, TimeUnit.SECONDS).exceptionally(_ -> { + sender.sendMessage("§cCould not unban this player at this time. The punishment service may be offline."); + return null; + }); + } catch (IOException e) { + sender.sendMessage("§cCould not find player: " + playerName); + } + }); }, argument); } } From 7594c45e1243f1e0a1467917e525884430667747 Mon Sep 17 00:00:00 2001 From: Swofty Date: Tue, 17 Feb 2026 21:09:39 +1100 Subject: [PATCH 15/27] fix(punishment): use package-private main to match project convention Java 25 doesn't require public on main methods. Other services like FriendService use package-private, so match the existing pattern. --- .../java/net/swofty/service/punishment/PunishmentService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/service.punishment/src/main/java/net/swofty/service/punishment/PunishmentService.java b/service.punishment/src/main/java/net/swofty/service/punishment/PunishmentService.java index 9e61be754..2d2fb5ddf 100644 --- a/service.punishment/src/main/java/net/swofty/service/punishment/PunishmentService.java +++ b/service.punishment/src/main/java/net/swofty/service/punishment/PunishmentService.java @@ -10,7 +10,7 @@ public class PunishmentService implements SkyBlockService { - public static void main(String[] args) { + static void main(String[] args) { String mongoUri = ConfigProvider.settings().getMongodb(); new PunishmentDatabase(null).connect(mongoUri); SkyBlockService.init(new PunishmentService()); From 827a2afea09271084cde7212aec512ef49395169 Mon Sep 17 00:00:00 2001 From: Swofty Date: Tue, 17 Feb 2026 21:52:33 +1100 Subject: [PATCH 16/27] feat(punishment): add query protocol objects for punishment service Add GetActivePunishmentProtocolObject and GetAllBannedIdsProtocolObject so proxy and game servers can query the punishment service instead of accessing PunishmentRedis directly. --- .../GetActivePunishmentProtocolObject.java | 90 +++++++++++++++++++ .../GetAllBannedIdsProtocolObject.java | 66 ++++++++++++++ .../GetActivePunishmentEndpoint.java | 36 ++++++++ .../endpoints/GetAllBannedIdsEndpoint.java | 25 ++++++ 4 files changed, 217 insertions(+) create mode 100644 commons/src/main/java/net/swofty/commons/protocol/objects/punishment/GetActivePunishmentProtocolObject.java create mode 100644 commons/src/main/java/net/swofty/commons/protocol/objects/punishment/GetAllBannedIdsProtocolObject.java create mode 100644 service.punishment/src/main/java/net/swofty/service/punishment/endpoints/GetActivePunishmentEndpoint.java create mode 100644 service.punishment/src/main/java/net/swofty/service/punishment/endpoints/GetAllBannedIdsEndpoint.java diff --git a/commons/src/main/java/net/swofty/commons/protocol/objects/punishment/GetActivePunishmentProtocolObject.java b/commons/src/main/java/net/swofty/commons/protocol/objects/punishment/GetActivePunishmentProtocolObject.java new file mode 100644 index 000000000..79a25547f --- /dev/null +++ b/commons/src/main/java/net/swofty/commons/protocol/objects/punishment/GetActivePunishmentProtocolObject.java @@ -0,0 +1,90 @@ +package net.swofty.commons.protocol.objects.punishment; + +import com.google.gson.Gson; +import net.swofty.commons.protocol.ProtocolObject; +import net.swofty.commons.protocol.Serializer; +import net.swofty.commons.punishment.PunishmentReason; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.json.JSONObject; + +import java.util.UUID; + +public class GetActivePunishmentProtocolObject + extends ProtocolObject { + + @Override + public Serializer getSerializer() { + return new Serializer<>() { + @Override + public String serialize(GetActivePunishmentMessage value) { + JSONObject json = new JSONObject(); + json.put("target", value.target().toString()); + return json.toString(); + } + + @Override + public GetActivePunishmentMessage deserialize(String json) { + JSONObject obj = new JSONObject(json); + return new GetActivePunishmentMessage( + UUID.fromString(obj.getString("target")) + ); + } + + @Override + public GetActivePunishmentMessage clone(GetActivePunishmentMessage value) { + return value; + } + }; + } + + @Override + public Serializer getReturnSerializer() { + return new Serializer<>() { + @Override + public String serialize(GetActivePunishmentResponse value) { + JSONObject json = new JSONObject(); + json.put("found", value.found()); + json.put("type", value.type()); + json.put("banId", value.banId()); + json.put("reason", value.reason() != null ? new Gson().toJson(value.reason()) : null); + json.put("expiresAt", value.expiresAt()); + return json.toString(); + } + + @Override + public GetActivePunishmentResponse deserialize(String json) { + JSONObject obj = new JSONObject(json); + boolean found = obj.getBoolean("found"); + if (!found) { + return new GetActivePunishmentResponse(false, null, null, null, 0); + } + return new GetActivePunishmentResponse( + true, + obj.optString("type", null), + obj.optString("banId", null), + obj.isNull("reason") ? null : new Gson().fromJson(obj.getString("reason"), PunishmentReason.class), + obj.getLong("expiresAt") + ); + } + + @Override + public GetActivePunishmentResponse clone(GetActivePunishmentResponse value) { + return value; + } + }; + } + + public record GetActivePunishmentMessage( + @NotNull UUID target + ) {} + + public record GetActivePunishmentResponse( + boolean found, + @Nullable String type, + @Nullable String banId, + @Nullable PunishmentReason reason, + long expiresAt + ) {} +} diff --git a/commons/src/main/java/net/swofty/commons/protocol/objects/punishment/GetAllBannedIdsProtocolObject.java b/commons/src/main/java/net/swofty/commons/protocol/objects/punishment/GetAllBannedIdsProtocolObject.java new file mode 100644 index 000000000..e3f1792a7 --- /dev/null +++ b/commons/src/main/java/net/swofty/commons/protocol/objects/punishment/GetAllBannedIdsProtocolObject.java @@ -0,0 +1,66 @@ +package net.swofty.commons.protocol.objects.punishment; + +import net.swofty.commons.protocol.ProtocolObject; +import net.swofty.commons.protocol.Serializer; +import org.json.JSONArray; +import org.json.JSONObject; + +import java.util.HashSet; +import java.util.Set; + +public class GetAllBannedIdsProtocolObject + extends ProtocolObject { + + @Override + public Serializer getSerializer() { + return new Serializer<>() { + @Override + public String serialize(GetAllBannedIdsMessage value) { + return new JSONObject().toString(); + } + + @Override + public GetAllBannedIdsMessage deserialize(String json) { + return new GetAllBannedIdsMessage(); + } + + @Override + public GetAllBannedIdsMessage clone(GetAllBannedIdsMessage value) { + return value; + } + }; + } + + @Override + public Serializer getReturnSerializer() { + return new Serializer<>() { + @Override + public String serialize(GetAllBannedIdsResponse value) { + JSONObject json = new JSONObject(); + json.put("ids", new JSONArray(value.ids())); + return json.toString(); + } + + @Override + public GetAllBannedIdsResponse deserialize(String json) { + JSONObject obj = new JSONObject(json); + JSONArray arr = obj.getJSONArray("ids"); + Set ids = new HashSet<>(); + for (int i = 0; i < arr.length(); i++) { + ids.add(arr.getString(i)); + } + return new GetAllBannedIdsResponse(ids); + } + + @Override + public GetAllBannedIdsResponse clone(GetAllBannedIdsResponse value) { + return value; + } + }; + } + + public record GetAllBannedIdsMessage() {} + + public record GetAllBannedIdsResponse(Set ids) {} +} diff --git a/service.punishment/src/main/java/net/swofty/service/punishment/endpoints/GetActivePunishmentEndpoint.java b/service.punishment/src/main/java/net/swofty/service/punishment/endpoints/GetActivePunishmentEndpoint.java new file mode 100644 index 000000000..4881a143b --- /dev/null +++ b/service.punishment/src/main/java/net/swofty/service/punishment/endpoints/GetActivePunishmentEndpoint.java @@ -0,0 +1,36 @@ +package net.swofty.service.punishment.endpoints; + +import net.swofty.commons.impl.ServiceProxyRequest; +import net.swofty.commons.protocol.ProtocolObject; +import net.swofty.commons.protocol.objects.punishment.GetActivePunishmentProtocolObject; +import net.swofty.commons.punishment.PunishmentRedis; +import net.swofty.service.generic.redis.ServiceEndpoint; + +import java.util.Optional; + +public class GetActivePunishmentEndpoint implements ServiceEndpoint + { + + @Override + public ProtocolObject associatedProtocolObject() { + return new GetActivePunishmentProtocolObject(); + } + + @Override + public GetActivePunishmentProtocolObject.GetActivePunishmentResponse onMessage(ServiceProxyRequest message, GetActivePunishmentProtocolObject.GetActivePunishmentMessage messageObject) { + Optional existing = PunishmentRedis.getActive(messageObject.target()); + if (existing.isEmpty()) { + return new GetActivePunishmentProtocolObject.GetActivePunishmentResponse(false, null, null, null, 0); + } + + PunishmentRedis.ActivePunishment punishment = existing.get(); + return new GetActivePunishmentProtocolObject.GetActivePunishmentResponse( + true, + punishment.type(), + punishment.banId(), + punishment.reason(), + punishment.expiresAt() + ); + } +} diff --git a/service.punishment/src/main/java/net/swofty/service/punishment/endpoints/GetAllBannedIdsEndpoint.java b/service.punishment/src/main/java/net/swofty/service/punishment/endpoints/GetAllBannedIdsEndpoint.java new file mode 100644 index 000000000..549a07bc8 --- /dev/null +++ b/service.punishment/src/main/java/net/swofty/service/punishment/endpoints/GetAllBannedIdsEndpoint.java @@ -0,0 +1,25 @@ +package net.swofty.service.punishment.endpoints; + +import net.swofty.commons.impl.ServiceProxyRequest; +import net.swofty.commons.protocol.ProtocolObject; +import net.swofty.commons.protocol.objects.punishment.GetAllBannedIdsProtocolObject; +import net.swofty.commons.punishment.PunishmentRedis; +import net.swofty.service.generic.redis.ServiceEndpoint; + +import java.util.Set; + +public class GetAllBannedIdsEndpoint implements ServiceEndpoint + { + + @Override + public ProtocolObject associatedProtocolObject() { + return new GetAllBannedIdsProtocolObject(); + } + + @Override + public GetAllBannedIdsProtocolObject.GetAllBannedIdsResponse onMessage(ServiceProxyRequest message, GetAllBannedIdsProtocolObject.GetAllBannedIdsMessage messageObject) { + Set ids = PunishmentRedis.getAllBannedPlayerIds(); + return new GetAllBannedIdsProtocolObject.GetAllBannedIdsResponse(ids); + } +} From 87752e0eb511ae1f52ee73a0c8919a0d72b2b6f9 Mon Sep 17 00:00:00 2001 From: Swofty Date: Tue, 17 Feb 2026 21:52:52 +1100 Subject: [PATCH 17/27] refactor(punishment): route all queries through punishment service Remove direct PunishmentRedis access from proxy and game server. Both now query the punishment service via protocol objects instead. - Fix ServerOutboundMessage.sendMessageToService to accept non-UUID filter IDs, enabling proxy-to-service communication - Register protocol objects in proxy before startListeners - Update SkyBlockVelocity.punished() to use GetActivePunishment - Update ActionPlayerMute to use GetActivePunishment - Update UnBanCommand suggestions to use GetAllBannedIds - Remove PunishmentRedis.connect() from HypixelGenericLoader --- .../proxyapi/redis/ServerOutboundMessage.java | 11 ++--- .../type/generic/HypixelGenericLoader.java | 4 -- .../command/commands/UnBanCommand.java | 18 +++++--- .../event/actions/ActionPlayerMute.java | 27 +++++++---- .../net/swofty/velocity/SkyBlockVelocity.java | 46 +++++++++++++------ 5 files changed, 64 insertions(+), 42 deletions(-) diff --git a/proxy.api/src/main/java/net/swofty/proxyapi/redis/ServerOutboundMessage.java b/proxy.api/src/main/java/net/swofty/proxyapi/redis/ServerOutboundMessage.java index 9a721cd3f..65b71d361 100644 --- a/proxy.api/src/main/java/net/swofty/proxyapi/redis/ServerOutboundMessage.java +++ b/proxy.api/src/main/java/net/swofty/proxyapi/redis/ServerOutboundMessage.java @@ -74,19 +74,16 @@ public static void sendMessageToService(ServiceType service, Object rawMessage, Consumer response) { UUID requestId = UUID.randomUUID(); - UUID toCallback = null; - try { - toCallback = UUID.fromString(RedisAPI.getInstance().getFilterId()); - } catch (Exception e) { - return; - } + String callbackId = RedisAPI.getInstance().getFilterId(); + if (callbackId == null) return; + redisMessageListeners.put(requestId, response); String message = specification.translateToString(rawMessage); RedisAPI.getInstance().publishMessage(service.name(), ChannelRegistry.getFromName(specification.channel()), - new ServiceProxyRequest(requestId, toCallback.toString(), + new ServiceProxyRequest(requestId, callbackId, specification.channel(), message).toJSON().toString()); } diff --git a/type.generic/src/main/java/net/swofty/type/generic/HypixelGenericLoader.java b/type.generic/src/main/java/net/swofty/type/generic/HypixelGenericLoader.java index b572965a8..b5f39d503 100644 --- a/type.generic/src/main/java/net/swofty/type/generic/HypixelGenericLoader.java +++ b/type.generic/src/main/java/net/swofty/type/generic/HypixelGenericLoader.java @@ -21,7 +21,6 @@ import net.swofty.commons.CustomWorlds; import net.swofty.commons.ServerType; import net.swofty.commons.config.ConfigProvider; -import net.swofty.commons.punishment.PunishmentRedis; import net.swofty.type.generic.block.PlayerHeadBlockHandler; import net.swofty.type.generic.block.SignBlockHandler; import net.swofty.type.generic.command.HypixelCommand; @@ -195,9 +194,6 @@ public void initialize(MinecraftServer server) { // Initialize leaderboard service (uses Redis for O(log N) leaderboard operations) LeaderboardService.connect(ConfigProvider.settings().getRedisUri()); - // Initialize punishment Redis connection - PunishmentRedis.connect(ConfigProvider.settings().getRedisUri()); - // Load achievement and quest registries from YAML configuration AchievementRegistry.loadFromConfiguration(); QuestRegistry.loadFromConfiguration(); diff --git a/type.generic/src/main/java/net/swofty/type/generic/command/commands/UnBanCommand.java b/type.generic/src/main/java/net/swofty/type/generic/command/commands/UnBanCommand.java index 4bf38c021..0b8cf8442 100644 --- a/type.generic/src/main/java/net/swofty/type/generic/command/commands/UnBanCommand.java +++ b/type.generic/src/main/java/net/swofty/type/generic/command/commands/UnBanCommand.java @@ -3,10 +3,9 @@ import net.minestom.server.command.builder.arguments.Argument; import net.minestom.server.command.builder.arguments.ArgumentType; import net.minestom.server.command.builder.suggestion.SuggestionEntry; -import net.minestom.server.utils.mojang.MojangUtils; import net.swofty.commons.ServiceType; +import net.swofty.commons.protocol.objects.punishment.GetAllBannedIdsProtocolObject; import net.swofty.commons.protocol.objects.punishment.UnpunishPlayerProtocolObject; -import net.swofty.commons.punishment.PunishmentRedis; import net.swofty.proxyapi.ProxyService; import net.swofty.type.generic.command.CommandParameters; import net.swofty.type.generic.command.HypixelCommand; @@ -29,10 +28,17 @@ public class UnBanCommand extends HypixelCommand { @Override public void registerUsage(MinestomCommand command) { Argument argument = ArgumentType.String("player").setSuggestionCallback((sender, context, suggestion) -> { - if (!PunishmentRedis.isInitialized()) return; - Set ids = PunishmentRedis.getAllBannedPlayerIds(); - for (String playerId : ids) { - suggestion.addEntry(new SuggestionEntry(playerId)); + try { + ProxyService service = new ProxyService(ServiceType.PUNISHMENT); + var response = service.handleRequest(new GetAllBannedIdsProtocolObject.GetAllBannedIdsMessage()) + .orTimeout(2, TimeUnit.SECONDS) + .join(); + if (response instanceof GetAllBannedIdsProtocolObject.GetAllBannedIdsResponse r) { + for (String playerId : r.ids()) { + suggestion.addEntry(new SuggestionEntry(playerId)); + } + } + } catch (Exception ignored) { } }); diff --git a/type.generic/src/main/java/net/swofty/type/generic/event/actions/ActionPlayerMute.java b/type.generic/src/main/java/net/swofty/type/generic/event/actions/ActionPlayerMute.java index b539dabf8..ee1af0229 100644 --- a/type.generic/src/main/java/net/swofty/type/generic/event/actions/ActionPlayerMute.java +++ b/type.generic/src/main/java/net/swofty/type/generic/event/actions/ActionPlayerMute.java @@ -2,29 +2,36 @@ import net.minestom.server.entity.Player; import net.minestom.server.event.player.PlayerChatEvent; +import net.swofty.commons.ServiceType; +import net.swofty.commons.protocol.objects.punishment.GetActivePunishmentProtocolObject; import net.swofty.commons.punishment.PunishmentMessages; import net.swofty.commons.punishment.PunishmentRedis; import net.swofty.commons.punishment.PunishmentType; +import net.swofty.proxyapi.ProxyService; import net.swofty.type.generic.event.EventNodes; import net.swofty.type.generic.event.HypixelEvent; import net.swofty.type.generic.event.HypixelEventClass; -import java.util.Optional; +import java.util.concurrent.TimeUnit; public class ActionPlayerMute implements HypixelEventClass { @HypixelEvent(node = EventNodes.PLAYER, requireDataLoaded = false) public void onPlayerChat(PlayerChatEvent event) { Player player = event.getPlayer(); - Optional activePunishment = PunishmentRedis.getActive(player.getUuid()); - activePunishment.ifPresent(punishment -> { - PunishmentType type = PunishmentType.valueOf(punishment.type()); - if (type != PunishmentType.MUTE) { - return; + try { + var response = new ProxyService(ServiceType.PUNISHMENT) + .handleRequest(new GetActivePunishmentProtocolObject.GetActivePunishmentMessage(player.getUuid())) + .orTimeout(2, TimeUnit.SECONDS) + .join(); + + if (response instanceof GetActivePunishmentProtocolObject.GetActivePunishmentResponse r + && r.found() && PunishmentType.valueOf(r.type()) == PunishmentType.MUTE) { + event.setCancelled(true); + var punishment = new PunishmentRedis.ActivePunishment(r.type(), r.banId(), r.reason(), r.expiresAt()); + player.sendMessage(PunishmentMessages.muteMessage(punishment)); } - event.setCancelled(true); - player.sendMessage(PunishmentMessages.muteMessage(punishment)); - }); + } catch (Exception ignored) { + } } - } diff --git a/velocity.extension/src/main/java/net/swofty/velocity/SkyBlockVelocity.java b/velocity.extension/src/main/java/net/swofty/velocity/SkyBlockVelocity.java index 3f0194173..2d930a451 100644 --- a/velocity.extension/src/main/java/net/swofty/velocity/SkyBlockVelocity.java +++ b/velocity.extension/src/main/java/net/swofty/velocity/SkyBlockVelocity.java @@ -32,13 +32,18 @@ import io.netty.channel.ChannelPipeline; import lombok.Getter; import net.kyori.adventure.text.Component; +import net.swofty.commons.ServiceType; import net.swofty.commons.ServerType; import net.swofty.commons.config.ConfigProvider; import net.swofty.commons.config.Settings; +import net.swofty.commons.protocol.ProtocolObject; +import net.swofty.commons.protocol.objects.punishment.GetActivePunishmentProtocolObject; import net.swofty.commons.proxy.FromProxyChannels; import net.swofty.commons.punishment.PunishmentMessages; import net.swofty.commons.punishment.PunishmentRedis; import net.swofty.commons.punishment.PunishmentType; +import net.swofty.proxyapi.ProxyService; +import net.swofty.proxyapi.redis.ServerOutboundMessage; import net.swofty.redisapi.api.RedisAPI; import net.swofty.velocity.command.ProtocolVersionCommand; import net.swofty.velocity.command.ServerStatusCommand; @@ -66,7 +71,6 @@ import java.nio.file.Path; import java.time.Duration; import java.util.List; -import java.util.Optional; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; @@ -204,30 +208,42 @@ public void onProxyInitialization(ProxyInitializeEvent event) { for (FromProxyChannels channel : FromProxyChannels.values()) { RedisMessage.registerProxyToServer(channel); } + loopThroughPackage("net.swofty.commons.protocol.objects", ProtocolObject.class) + .forEach(ServerOutboundMessage::registerFromProtocolObject); RedisAPI.getInstance().startListeners(); /** * Setup GameManager */ GameManager.loopServers(server); - PunishmentRedis.connect(ConfigProvider.settings().getRedisUri()); } public boolean punished(Player player) { - Optional activePunishment = PunishmentRedis.getActive(player.getUniqueId()); - if (activePunishment.isEmpty()) return false; - - PunishmentRedis.ActivePunishment punishment = activePunishment.get(); - PunishmentType type = PunishmentType.valueOf(punishment.type()); - if (type == PunishmentType.BAN) { - player.disconnect(PunishmentMessages.banMessage(punishment)); - return true; - } - if (type == PunishmentType.MUTE) { - player.sendMessage(PunishmentMessages.muteMessage(punishment)); + try { + var response = new ProxyService(ServiceType.PUNISHMENT) + .handleRequest(new GetActivePunishmentProtocolObject.GetActivePunishmentMessage(player.getUniqueId())) + .orTimeout(3, TimeUnit.SECONDS) + .join(); + + if (!(response instanceof GetActivePunishmentProtocolObject.GetActivePunishmentResponse r) || !r.found()) { + return false; + } + + PunishmentRedis.ActivePunishment punishment = new PunishmentRedis.ActivePunishment( + r.type(), r.banId(), r.reason(), r.expiresAt()); + PunishmentType type = PunishmentType.valueOf(r.type()); + if (type == PunishmentType.BAN) { + player.disconnect(PunishmentMessages.banMessage(punishment)); + return true; + } + if (type == PunishmentType.MUTE) { + player.sendMessage(PunishmentMessages.muteMessage(punishment)); + } + return false; + } catch (Exception e) { + return false; } - return false; - } + } @Subscribe public void onPlayerJoin(PlayerChooseInitialServerEvent event) { From 85f8b4f8b310edadebadd649740b385cea7ddf8a Mon Sep 17 00:00:00 2001 From: Swofty Date: Tue, 17 Feb 2026 22:16:56 +1100 Subject: [PATCH 18/27] refactor(punishment): extract ActivePunishment record, fix unban and mute bugs - Extract ActivePunishment from PunishmentRedis into standalone record so proxy/game server no longer depend on Redis class (DIP) - Fix UnpunishPlayerEndpoint: verify punishment type is BAN before revoking, preventing /unban from clearing mutes - Fix permanent mute message showing "will expire in" with negative time --- .../swofty/commons/punishment/ActivePunishment.java | 3 +++ .../commons/punishment/PunishmentMessages.java | 13 +++++++------ .../swofty/commons/punishment/PunishmentRedis.java | 2 -- .../endpoints/GetActivePunishmentEndpoint.java | 5 +++-- .../punishment/endpoints/PunishPlayerEndpoint.java | 4 ++-- .../endpoints/UnpunishPlayerEndpoint.java | 12 ++++++++++-- .../generic/event/actions/ActionPlayerMute.java | 4 ++-- .../java/net/swofty/velocity/SkyBlockVelocity.java | 4 ++-- .../redis/listeners/ListenerPlayerPunished.java | 4 ++-- 9 files changed, 31 insertions(+), 20 deletions(-) create mode 100644 commons/src/main/java/net/swofty/commons/punishment/ActivePunishment.java diff --git a/commons/src/main/java/net/swofty/commons/punishment/ActivePunishment.java b/commons/src/main/java/net/swofty/commons/punishment/ActivePunishment.java new file mode 100644 index 000000000..797904019 --- /dev/null +++ b/commons/src/main/java/net/swofty/commons/punishment/ActivePunishment.java @@ -0,0 +1,3 @@ +package net.swofty.commons.punishment; + +public record ActivePunishment(String type, String banId, PunishmentReason reason, long expiresAt) {} diff --git a/commons/src/main/java/net/swofty/commons/punishment/PunishmentMessages.java b/commons/src/main/java/net/swofty/commons/punishment/PunishmentMessages.java index b351a59cc..cee5c82c3 100644 --- a/commons/src/main/java/net/swofty/commons/punishment/PunishmentMessages.java +++ b/commons/src/main/java/net/swofty/commons/punishment/PunishmentMessages.java @@ -6,7 +6,7 @@ public final class PunishmentMessages { private PunishmentMessages() {} - public static Component banMessage(PunishmentRedis.ActivePunishment punishment) { + public static Component banMessage(ActivePunishment punishment) { long expiresAt = punishment.expiresAt(); PunishmentReason reason = punishment.reason(); String banId = punishment.banId(); @@ -31,22 +31,23 @@ public static Component banMessage(PunishmentRedis.ActivePunishment punishment) return Component.text(header + "\n§7Reason: §f" + reason.getReasonString() + "\n" + findOutMore + "\n§7Ban ID: §f" + banId + "\n" + footer); } - public static Component muteMessage(PunishmentRedis.ActivePunishment punishment) { + public static Component muteMessage(ActivePunishment punishment) { long expiresAt = punishment.expiresAt(); PunishmentReason reason = punishment.reason(); - long timeLeft = expiresAt - System.currentTimeMillis(); - String prettyTimeLeft = StringUtility.formatTimeLeft(timeLeft); - String line = "\n§c§m §r\n"; String header; + String time; if (expiresAt <= 0) { header = "§cYou are permanently muted on this server!\n"; + time = ""; } else { + long timeLeft = expiresAt - System.currentTimeMillis(); + String prettyTimeLeft = StringUtility.formatTimeLeft(timeLeft); header = "§cYou are currently muted for " + reason.getReasonString() + "\n"; + time = "§7Your mute will expire in §c" + prettyTimeLeft + "\n\n"; } - String time = "§7Your mute will expire in §c" + prettyTimeLeft + "\n\n"; String urlInfo = "§7Find out more here: §fwww.hypixel.net/mutes\n"; String footer = "§7Mute ID: §f" + punishment.banId(); diff --git a/commons/src/main/java/net/swofty/commons/punishment/PunishmentRedis.java b/commons/src/main/java/net/swofty/commons/punishment/PunishmentRedis.java index e9ee203a2..87aacf10b 100644 --- a/commons/src/main/java/net/swofty/commons/punishment/PunishmentRedis.java +++ b/commons/src/main/java/net/swofty/commons/punishment/PunishmentRedis.java @@ -115,8 +115,6 @@ public static CompletableFuture revoke(UUID playerId) { }); } - public record ActivePunishment(String type, String banId, PunishmentReason reason, long expiresAt) {} - public static Set getAllBannedPlayerIds() { try (Jedis jedis = jedisPool.getResource()) { var cursor = "0"; diff --git a/service.punishment/src/main/java/net/swofty/service/punishment/endpoints/GetActivePunishmentEndpoint.java b/service.punishment/src/main/java/net/swofty/service/punishment/endpoints/GetActivePunishmentEndpoint.java index 4881a143b..cad1f7deb 100644 --- a/service.punishment/src/main/java/net/swofty/service/punishment/endpoints/GetActivePunishmentEndpoint.java +++ b/service.punishment/src/main/java/net/swofty/service/punishment/endpoints/GetActivePunishmentEndpoint.java @@ -3,6 +3,7 @@ import net.swofty.commons.impl.ServiceProxyRequest; import net.swofty.commons.protocol.ProtocolObject; import net.swofty.commons.protocol.objects.punishment.GetActivePunishmentProtocolObject; +import net.swofty.commons.punishment.ActivePunishment; import net.swofty.commons.punishment.PunishmentRedis; import net.swofty.service.generic.redis.ServiceEndpoint; @@ -19,12 +20,12 @@ public ProtocolObject existing = PunishmentRedis.getActive(messageObject.target()); + Optional existing = PunishmentRedis.getActive(messageObject.target()); if (existing.isEmpty()) { return new GetActivePunishmentProtocolObject.GetActivePunishmentResponse(false, null, null, null, 0); } - PunishmentRedis.ActivePunishment punishment = existing.get(); + ActivePunishment punishment = existing.get(); return new GetActivePunishmentProtocolObject.GetActivePunishmentResponse( true, punishment.type(), diff --git a/service.punishment/src/main/java/net/swofty/service/punishment/endpoints/PunishPlayerEndpoint.java b/service.punishment/src/main/java/net/swofty/service/punishment/endpoints/PunishPlayerEndpoint.java index 15c8695f8..e3b543b48 100644 --- a/service.punishment/src/main/java/net/swofty/service/punishment/endpoints/PunishPlayerEndpoint.java +++ b/service.punishment/src/main/java/net/swofty/service/punishment/endpoints/PunishPlayerEndpoint.java @@ -38,9 +38,9 @@ public PunishPlayerProtocolObject.PunishPlayerResponse onMessage(ServiceProxyReq boolean hasOverwriteTag = messageObject.tags() != null && messageObject.tags().contains(PunishmentTag.OVERWRITE); if (!hasOverwriteTag) { - Optional existing = PunishmentRedis.getActive(messageObject.target()); + Optional existing = PunishmentRedis.getActive(messageObject.target()); if (existing.isPresent()) { - PunishmentRedis.ActivePunishment active = existing.get(); + ActivePunishment active = existing.get(); PunishmentType existingType = PunishmentType.valueOf(active.type()); if (existingType == punishmentType) { return new PunishPlayerProtocolObject.PunishPlayerResponse(false, null, diff --git a/service.punishment/src/main/java/net/swofty/service/punishment/endpoints/UnpunishPlayerEndpoint.java b/service.punishment/src/main/java/net/swofty/service/punishment/endpoints/UnpunishPlayerEndpoint.java index e5c909981..e465b2d73 100644 --- a/service.punishment/src/main/java/net/swofty/service/punishment/endpoints/UnpunishPlayerEndpoint.java +++ b/service.punishment/src/main/java/net/swofty/service/punishment/endpoints/UnpunishPlayerEndpoint.java @@ -3,7 +3,9 @@ import net.swofty.commons.impl.ServiceProxyRequest; import net.swofty.commons.protocol.ProtocolObject; import net.swofty.commons.protocol.objects.punishment.UnpunishPlayerProtocolObject; +import net.swofty.commons.punishment.ActivePunishment; import net.swofty.commons.punishment.PunishmentRedis; +import net.swofty.commons.punishment.PunishmentType; import net.swofty.service.generic.redis.ServiceEndpoint; import org.tinylog.Logger; @@ -20,13 +22,19 @@ public ProtocolObject existing = PunishmentRedis.getActive(messageObject.target()); + Optional existing = PunishmentRedis.getActive(messageObject.target()); if (existing.isEmpty()) { return new UnpunishPlayerProtocolObject.UnpunishPlayerResponse(false, "No active punishment found for this player."); } + ActivePunishment punishment = existing.get(); + PunishmentType type = PunishmentType.valueOf(punishment.type()); + if (type != PunishmentType.BAN) { + return new UnpunishPlayerProtocolObject.UnpunishPlayerResponse(false, "Player is not banned (active punishment is " + type.name() + ")."); + } + PunishmentRedis.revoke(messageObject.target()).join(); - Logger.info("Revoked punishment for {} by staff {}", + Logger.info("Revoked ban for {} by staff {}", messageObject.target(), messageObject.staff()); return new UnpunishPlayerProtocolObject.UnpunishPlayerResponse(true, null); } diff --git a/type.generic/src/main/java/net/swofty/type/generic/event/actions/ActionPlayerMute.java b/type.generic/src/main/java/net/swofty/type/generic/event/actions/ActionPlayerMute.java index ee1af0229..6d7d90fc2 100644 --- a/type.generic/src/main/java/net/swofty/type/generic/event/actions/ActionPlayerMute.java +++ b/type.generic/src/main/java/net/swofty/type/generic/event/actions/ActionPlayerMute.java @@ -4,8 +4,8 @@ import net.minestom.server.event.player.PlayerChatEvent; import net.swofty.commons.ServiceType; import net.swofty.commons.protocol.objects.punishment.GetActivePunishmentProtocolObject; +import net.swofty.commons.punishment.ActivePunishment; import net.swofty.commons.punishment.PunishmentMessages; -import net.swofty.commons.punishment.PunishmentRedis; import net.swofty.commons.punishment.PunishmentType; import net.swofty.proxyapi.ProxyService; import net.swofty.type.generic.event.EventNodes; @@ -28,7 +28,7 @@ public void onPlayerChat(PlayerChatEvent event) { if (response instanceof GetActivePunishmentProtocolObject.GetActivePunishmentResponse r && r.found() && PunishmentType.valueOf(r.type()) == PunishmentType.MUTE) { event.setCancelled(true); - var punishment = new PunishmentRedis.ActivePunishment(r.type(), r.banId(), r.reason(), r.expiresAt()); + var punishment = new ActivePunishment(r.type(), r.banId(), r.reason(), r.expiresAt()); player.sendMessage(PunishmentMessages.muteMessage(punishment)); } } catch (Exception ignored) { diff --git a/velocity.extension/src/main/java/net/swofty/velocity/SkyBlockVelocity.java b/velocity.extension/src/main/java/net/swofty/velocity/SkyBlockVelocity.java index 2d930a451..5ea89580f 100644 --- a/velocity.extension/src/main/java/net/swofty/velocity/SkyBlockVelocity.java +++ b/velocity.extension/src/main/java/net/swofty/velocity/SkyBlockVelocity.java @@ -39,8 +39,8 @@ import net.swofty.commons.protocol.ProtocolObject; import net.swofty.commons.protocol.objects.punishment.GetActivePunishmentProtocolObject; import net.swofty.commons.proxy.FromProxyChannels; +import net.swofty.commons.punishment.ActivePunishment; import net.swofty.commons.punishment.PunishmentMessages; -import net.swofty.commons.punishment.PunishmentRedis; import net.swofty.commons.punishment.PunishmentType; import net.swofty.proxyapi.ProxyService; import net.swofty.proxyapi.redis.ServerOutboundMessage; @@ -229,7 +229,7 @@ public boolean punished(Player player) { return false; } - PunishmentRedis.ActivePunishment punishment = new PunishmentRedis.ActivePunishment( + ActivePunishment punishment = new ActivePunishment( r.type(), r.banId(), r.reason(), r.expiresAt()); PunishmentType type = PunishmentType.valueOf(r.type()); if (type == PunishmentType.BAN) { diff --git a/velocity.extension/src/main/java/net/swofty/velocity/redis/listeners/ListenerPlayerPunished.java b/velocity.extension/src/main/java/net/swofty/velocity/redis/listeners/ListenerPlayerPunished.java index f26b0134d..21be00908 100644 --- a/velocity.extension/src/main/java/net/swofty/velocity/redis/listeners/ListenerPlayerPunished.java +++ b/velocity.extension/src/main/java/net/swofty/velocity/redis/listeners/ListenerPlayerPunished.java @@ -6,8 +6,8 @@ import net.swofty.commons.proxy.ToProxyChannels; import net.swofty.commons.punishment.PunishmentId; import net.swofty.commons.punishment.PunishmentReason; +import net.swofty.commons.punishment.ActivePunishment; import net.swofty.commons.punishment.PunishmentMessages; -import net.swofty.commons.punishment.PunishmentRedis; import net.swofty.commons.punishment.PunishmentType; import net.swofty.commons.punishment.template.BanType; import net.swofty.commons.punishment.template.MuteType; @@ -54,7 +54,7 @@ public JSONObject receivedMessage(JSONObject message, UUID serverUUID) { PunishmentType punishmentType = PunishmentType.valueOf(type); PunishmentReason finalReason = reason; SkyBlockVelocity.getServer().getPlayer(target).ifPresent((player) -> { - PunishmentRedis.ActivePunishment activePunishment = new PunishmentRedis.ActivePunishment(type, id, finalReason, expiresAt); + ActivePunishment activePunishment = new ActivePunishment(type, id, finalReason, expiresAt); switch (punishmentType) { case BAN -> { player.disconnect(PunishmentMessages.banMessage(activePunishment)); From 4e35348f88ce7eae3049133d734c002d67b95f63 Mon Sep 17 00:00:00 2001 From: Swofty Date: Tue, 17 Feb 2026 22:31:13 +1100 Subject: [PATCH 19/27] refactor(punishment): inline UUID resolution into individual commands --- .../type/generic/command/HypixelCommand.java | 15 --------------- .../type/generic/command/commands/BanCommand.java | 14 ++++++++------ .../generic/command/commands/MuteCommand.java | 10 ++++++---- .../generic/command/commands/UnBanCommand.java | 8 +++++--- 4 files changed, 19 insertions(+), 28 deletions(-) diff --git a/type.generic/src/main/java/net/swofty/type/generic/command/HypixelCommand.java b/type.generic/src/main/java/net/swofty/type/generic/command/HypixelCommand.java index 8cdb1bbea..a0193e526 100644 --- a/type.generic/src/main/java/net/swofty/type/generic/command/HypixelCommand.java +++ b/type.generic/src/main/java/net/swofty/type/generic/command/HypixelCommand.java @@ -4,21 +4,16 @@ import net.minestom.server.command.CommandSender; import net.minestom.server.command.ConsoleSender; import net.minestom.server.command.builder.Command; -import net.minestom.server.entity.Player; -import net.minestom.server.utils.mojang.MojangUtils; import net.swofty.type.generic.data.HypixelDataHandler; import net.swofty.type.generic.data.datapoints.DatapointRank; import net.swofty.type.generic.user.HypixelPlayer; -import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import java.util.UUID; public abstract class HypixelCommand { public static final String COMMAND_SUFFIX = "Command"; - public static final UUID CONSOLE_UUID = new UUID(0, 0); @Getter private final CommandParameters params; @@ -45,16 +40,6 @@ protected HypixelCommand() { public abstract void registerUsage(MinestomCommand command); - protected static UUID resolvePlayerUuid(CommandSender sender, String playerName, String action) throws IOException { - UUID uuid = MojangUtils.getUUID(playerName); - sender.sendMessage("§8Processing " + action + " for player §e" + playerName + "§7... (" + uuid + ")"); - return uuid; - } - - protected static UUID senderUuid(CommandSender sender) { - return sender instanceof Player p ? p.getUuid() : CONSOLE_UUID; - } - public boolean permissionCheck(CommandSender sender) { HypixelPlayer player = (HypixelPlayer) sender; HypixelDataHandler dataHandler = player.getDataHandler(); diff --git a/type.generic/src/main/java/net/swofty/type/generic/command/commands/BanCommand.java b/type.generic/src/main/java/net/swofty/type/generic/command/commands/BanCommand.java index 0c7f487ab..f3ad4ec7f 100644 --- a/type.generic/src/main/java/net/swofty/type/generic/command/commands/BanCommand.java +++ b/type.generic/src/main/java/net/swofty/type/generic/command/commands/BanCommand.java @@ -3,6 +3,8 @@ import net.kyori.adventure.text.Component; import net.minestom.server.command.CommandSender; import net.minestom.server.command.builder.arguments.*; +import net.minestom.server.entity.Player; +import net.minestom.server.utils.mojang.MojangUtils; import net.minestom.server.command.builder.suggestion.SuggestionEntry; import net.swofty.commons.ServiceType; import net.swofty.commons.StringUtility; @@ -56,10 +58,10 @@ public void registerUsage(MinestomCommand command) { CompletableFuture.runAsync(() -> { try { - UUID targetUuid = resolvePlayerUuid(sender, playerName, "ban"); + UUID targetUuid = MojangUtils.getUUID(playerName); long actualTime = StringUtility.parseDuration(duration); long expiryTime = System.currentTimeMillis() + actualTime; - banPlayer(sender, targetUuid, type, senderUuid(sender), actualTime, expiryTime, playerName, null); + banPlayer(sender, targetUuid, type, (sender instanceof Player p ? p.getUuid() : new UUID(0, 0)), actualTime, expiryTime, playerName, null); } catch (IOException e) { sender.sendMessage("§cCould not find player: " + playerName); } @@ -72,8 +74,8 @@ public void registerUsage(MinestomCommand command) { CompletableFuture.runAsync(() -> { try { - banPlayer(sender, resolvePlayerUuid(sender, playerName, "ban"), reason, - senderUuid(sender), 0, -1, playerName, null); + banPlayer(sender, MojangUtils.getUUID(playerName), reason, + (sender instanceof Player p ? p.getUuid() : new UUID(0, 0)), 0, -1, playerName, null); } catch (IOException e) { sender.sendMessage("§cCould not find player: " + playerName); } @@ -87,8 +89,8 @@ public void registerUsage(MinestomCommand command) { CompletableFuture.runAsync(() -> { try { - banPlayer(sender, resolvePlayerUuid(sender, playerName, "ban"), reason, - senderUuid(sender), 0, -1, playerName, tags); + banPlayer(sender, MojangUtils.getUUID(playerName), reason, + (sender instanceof Player p ? p.getUuid() : new UUID(0, 0)), 0, -1, playerName, tags); } catch (IOException e) { sender.sendMessage("§cCould not find player: " + playerName); } diff --git a/type.generic/src/main/java/net/swofty/type/generic/command/commands/MuteCommand.java b/type.generic/src/main/java/net/swofty/type/generic/command/commands/MuteCommand.java index 595f9e01f..066316a18 100644 --- a/type.generic/src/main/java/net/swofty/type/generic/command/commands/MuteCommand.java +++ b/type.generic/src/main/java/net/swofty/type/generic/command/commands/MuteCommand.java @@ -3,6 +3,8 @@ import net.kyori.adventure.text.Component; import net.minestom.server.command.CommandSender; import net.minestom.server.command.builder.arguments.Argument; +import net.minestom.server.entity.Player; +import net.minestom.server.utils.mojang.MojangUtils; import net.minestom.server.command.builder.arguments.ArgumentString; import net.minestom.server.command.builder.arguments.ArgumentType; import net.minestom.server.command.builder.suggestion.SuggestionEntry; @@ -49,10 +51,10 @@ public void registerUsage(MinestomCommand command) { CompletableFuture.runAsync(() -> { try { - UUID targetUuid = resolvePlayerUuid(sender, playerName, "mute"); + UUID targetUuid = MojangUtils.getUUID(playerName); long actualTime = StringUtility.parseDuration(duration); long expiryTime = System.currentTimeMillis() + actualTime; - mutePlayer(sender, targetUuid, type, senderUuid(sender), actualTime, expiryTime, playerName); + mutePlayer(sender, targetUuid, type, (sender instanceof Player p ? p.getUuid() : new UUID(0, 0)), actualTime, expiryTime, playerName); } catch (IOException e) { sender.sendMessage("§cCould not find player: " + playerName); } @@ -65,8 +67,8 @@ public void registerUsage(MinestomCommand command) { CompletableFuture.runAsync(() -> { try { - mutePlayer(sender, resolvePlayerUuid(sender, playerName, "mute"), reason, - senderUuid(sender), 0, -1, playerName); + mutePlayer(sender, MojangUtils.getUUID(playerName), reason, + (sender instanceof Player p ? p.getUuid() : new UUID(0, 0)), 0, -1, playerName); } catch (IOException e) { sender.sendMessage("§cCould not find player: " + playerName); } diff --git a/type.generic/src/main/java/net/swofty/type/generic/command/commands/UnBanCommand.java b/type.generic/src/main/java/net/swofty/type/generic/command/commands/UnBanCommand.java index 0b8cf8442..0ea90bf0f 100644 --- a/type.generic/src/main/java/net/swofty/type/generic/command/commands/UnBanCommand.java +++ b/type.generic/src/main/java/net/swofty/type/generic/command/commands/UnBanCommand.java @@ -2,6 +2,8 @@ import net.minestom.server.command.builder.arguments.Argument; import net.minestom.server.command.builder.arguments.ArgumentType; +import net.minestom.server.entity.Player; +import net.minestom.server.utils.mojang.MojangUtils; import net.minestom.server.command.builder.suggestion.SuggestionEntry; import net.swofty.commons.ServiceType; import net.swofty.commons.protocol.objects.punishment.GetAllBannedIdsProtocolObject; @@ -12,7 +14,7 @@ import net.swofty.type.generic.user.categories.Rank; import java.io.IOException; -import java.util.Set; +import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; @@ -47,10 +49,10 @@ public void registerUsage(MinestomCommand command) { CompletableFuture.runAsync(() -> { try { - var targetUuid = resolvePlayerUuid(sender, playerName, "unban"); + var targetUuid = MojangUtils.getUUID(playerName); ProxyService punishmentService = new ProxyService(ServiceType.PUNISHMENT); var message = new UnpunishPlayerProtocolObject.UnpunishPlayerMessage( - targetUuid, senderUuid(sender) + targetUuid, (sender instanceof Player p ? p.getUuid() : new UUID(0, 0)) ); punishmentService.handleRequest(message).thenAccept(result -> { From 7656fb415a4d5666da5717caf1a53a915af75352 Mon Sep 17 00:00:00 2001 From: Swofty Date: Tue, 17 Feb 2026 23:53:35 +1100 Subject: [PATCH 20/27] fix(punishment): remove GetAllBannedIds endpoint that doesn't scale --- .../GetAllBannedIdsProtocolObject.java | 66 ------------------- .../endpoints/GetAllBannedIdsEndpoint.java | 25 ------- .../command/commands/UnBanCommand.java | 18 +---- 3 files changed, 1 insertion(+), 108 deletions(-) delete mode 100644 commons/src/main/java/net/swofty/commons/protocol/objects/punishment/GetAllBannedIdsProtocolObject.java delete mode 100644 service.punishment/src/main/java/net/swofty/service/punishment/endpoints/GetAllBannedIdsEndpoint.java diff --git a/commons/src/main/java/net/swofty/commons/protocol/objects/punishment/GetAllBannedIdsProtocolObject.java b/commons/src/main/java/net/swofty/commons/protocol/objects/punishment/GetAllBannedIdsProtocolObject.java deleted file mode 100644 index e3f1792a7..000000000 --- a/commons/src/main/java/net/swofty/commons/protocol/objects/punishment/GetAllBannedIdsProtocolObject.java +++ /dev/null @@ -1,66 +0,0 @@ -package net.swofty.commons.protocol.objects.punishment; - -import net.swofty.commons.protocol.ProtocolObject; -import net.swofty.commons.protocol.Serializer; -import org.json.JSONArray; -import org.json.JSONObject; - -import java.util.HashSet; -import java.util.Set; - -public class GetAllBannedIdsProtocolObject - extends ProtocolObject { - - @Override - public Serializer getSerializer() { - return new Serializer<>() { - @Override - public String serialize(GetAllBannedIdsMessage value) { - return new JSONObject().toString(); - } - - @Override - public GetAllBannedIdsMessage deserialize(String json) { - return new GetAllBannedIdsMessage(); - } - - @Override - public GetAllBannedIdsMessage clone(GetAllBannedIdsMessage value) { - return value; - } - }; - } - - @Override - public Serializer getReturnSerializer() { - return new Serializer<>() { - @Override - public String serialize(GetAllBannedIdsResponse value) { - JSONObject json = new JSONObject(); - json.put("ids", new JSONArray(value.ids())); - return json.toString(); - } - - @Override - public GetAllBannedIdsResponse deserialize(String json) { - JSONObject obj = new JSONObject(json); - JSONArray arr = obj.getJSONArray("ids"); - Set ids = new HashSet<>(); - for (int i = 0; i < arr.length(); i++) { - ids.add(arr.getString(i)); - } - return new GetAllBannedIdsResponse(ids); - } - - @Override - public GetAllBannedIdsResponse clone(GetAllBannedIdsResponse value) { - return value; - } - }; - } - - public record GetAllBannedIdsMessage() {} - - public record GetAllBannedIdsResponse(Set ids) {} -} diff --git a/service.punishment/src/main/java/net/swofty/service/punishment/endpoints/GetAllBannedIdsEndpoint.java b/service.punishment/src/main/java/net/swofty/service/punishment/endpoints/GetAllBannedIdsEndpoint.java deleted file mode 100644 index 549a07bc8..000000000 --- a/service.punishment/src/main/java/net/swofty/service/punishment/endpoints/GetAllBannedIdsEndpoint.java +++ /dev/null @@ -1,25 +0,0 @@ -package net.swofty.service.punishment.endpoints; - -import net.swofty.commons.impl.ServiceProxyRequest; -import net.swofty.commons.protocol.ProtocolObject; -import net.swofty.commons.protocol.objects.punishment.GetAllBannedIdsProtocolObject; -import net.swofty.commons.punishment.PunishmentRedis; -import net.swofty.service.generic.redis.ServiceEndpoint; - -import java.util.Set; - -public class GetAllBannedIdsEndpoint implements ServiceEndpoint - { - - @Override - public ProtocolObject associatedProtocolObject() { - return new GetAllBannedIdsProtocolObject(); - } - - @Override - public GetAllBannedIdsProtocolObject.GetAllBannedIdsResponse onMessage(ServiceProxyRequest message, GetAllBannedIdsProtocolObject.GetAllBannedIdsMessage messageObject) { - Set ids = PunishmentRedis.getAllBannedPlayerIds(); - return new GetAllBannedIdsProtocolObject.GetAllBannedIdsResponse(ids); - } -} diff --git a/type.generic/src/main/java/net/swofty/type/generic/command/commands/UnBanCommand.java b/type.generic/src/main/java/net/swofty/type/generic/command/commands/UnBanCommand.java index 0ea90bf0f..3887228bd 100644 --- a/type.generic/src/main/java/net/swofty/type/generic/command/commands/UnBanCommand.java +++ b/type.generic/src/main/java/net/swofty/type/generic/command/commands/UnBanCommand.java @@ -1,12 +1,9 @@ package net.swofty.type.generic.command.commands; -import net.minestom.server.command.builder.arguments.Argument; import net.minestom.server.command.builder.arguments.ArgumentType; import net.minestom.server.entity.Player; import net.minestom.server.utils.mojang.MojangUtils; -import net.minestom.server.command.builder.suggestion.SuggestionEntry; import net.swofty.commons.ServiceType; -import net.swofty.commons.protocol.objects.punishment.GetAllBannedIdsProtocolObject; import net.swofty.commons.protocol.objects.punishment.UnpunishPlayerProtocolObject; import net.swofty.proxyapi.ProxyService; import net.swofty.type.generic.command.CommandParameters; @@ -29,20 +26,7 @@ public class UnBanCommand extends HypixelCommand { @Override public void registerUsage(MinestomCommand command) { - Argument argument = ArgumentType.String("player").setSuggestionCallback((sender, context, suggestion) -> { - try { - ProxyService service = new ProxyService(ServiceType.PUNISHMENT); - var response = service.handleRequest(new GetAllBannedIdsProtocolObject.GetAllBannedIdsMessage()) - .orTimeout(2, TimeUnit.SECONDS) - .join(); - if (response instanceof GetAllBannedIdsProtocolObject.GetAllBannedIdsResponse r) { - for (String playerId : r.ids()) { - suggestion.addEntry(new SuggestionEntry(playerId)); - } - } - } catch (Exception ignored) { - } - }); + var argument = ArgumentType.String("player"); command.addSyntax((sender, context) -> { String playerName = context.get(argument); From d52b19c32551fe7d3eb7fe3f9d1f714df3844546 Mon Sep 17 00:00:00 2001 From: Swofty Date: Tue, 17 Feb 2026 23:57:52 +1100 Subject: [PATCH 21/27] feat(punishment): persist PunishmentTags to Redis with active punishments --- .../GetActivePunishmentProtocolObject.java | 21 +++++++--- .../commons/punishment/ActivePunishment.java | 4 +- .../commons/punishment/PunishmentRedis.java | 39 +++++++------------ .../GetActivePunishmentEndpoint.java | 5 ++- .../endpoints/PunishPlayerEndpoint.java | 3 +- .../event/actions/ActionPlayerMute.java | 2 +- .../net/swofty/velocity/SkyBlockVelocity.java | 2 +- .../listeners/ListenerPlayerPunished.java | 2 +- 8 files changed, 42 insertions(+), 36 deletions(-) diff --git a/commons/src/main/java/net/swofty/commons/protocol/objects/punishment/GetActivePunishmentProtocolObject.java b/commons/src/main/java/net/swofty/commons/protocol/objects/punishment/GetActivePunishmentProtocolObject.java index 79a25547f..cc7f6a593 100644 --- a/commons/src/main/java/net/swofty/commons/protocol/objects/punishment/GetActivePunishmentProtocolObject.java +++ b/commons/src/main/java/net/swofty/commons/protocol/objects/punishment/GetActivePunishmentProtocolObject.java @@ -4,10 +4,12 @@ import net.swofty.commons.protocol.ProtocolObject; import net.swofty.commons.protocol.Serializer; import net.swofty.commons.punishment.PunishmentReason; +import net.swofty.commons.punishment.PunishmentTag; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.json.JSONObject; +import java.util.List; import java.util.UUID; public class GetActivePunishmentProtocolObject @@ -44,28 +46,36 @@ public Serializer getReturnSerializer() { return new Serializer<>() { @Override public String serialize(GetActivePunishmentResponse value) { + Gson gson = new Gson(); JSONObject json = new JSONObject(); json.put("found", value.found()); json.put("type", value.type()); json.put("banId", value.banId()); - json.put("reason", value.reason() != null ? new Gson().toJson(value.reason()) : null); + json.put("reason", value.reason() != null ? gson.toJson(value.reason()) : null); json.put("expiresAt", value.expiresAt()); + json.put("tags", value.tags() != null ? gson.toJson(value.tags()) : null); return json.toString(); } @Override public GetActivePunishmentResponse deserialize(String json) { + Gson gson = new Gson(); JSONObject obj = new JSONObject(json); boolean found = obj.getBoolean("found"); if (!found) { - return new GetActivePunishmentResponse(false, null, null, null, 0); + return new GetActivePunishmentResponse(false, null, null, null, 0, List.of()); + } + List tags = List.of(); + if (!obj.isNull("tags")) { + tags = List.of(gson.fromJson(obj.getString("tags"), PunishmentTag[].class)); } return new GetActivePunishmentResponse( true, obj.optString("type", null), obj.optString("banId", null), - obj.isNull("reason") ? null : new Gson().fromJson(obj.getString("reason"), PunishmentReason.class), - obj.getLong("expiresAt") + obj.isNull("reason") ? null : gson.fromJson(obj.getString("reason"), PunishmentReason.class), + obj.getLong("expiresAt"), + tags ); } @@ -85,6 +95,7 @@ public record GetActivePunishmentResponse( @Nullable String type, @Nullable String banId, @Nullable PunishmentReason reason, - long expiresAt + long expiresAt, + @NotNull List tags ) {} } diff --git a/commons/src/main/java/net/swofty/commons/punishment/ActivePunishment.java b/commons/src/main/java/net/swofty/commons/punishment/ActivePunishment.java index 797904019..52fbde459 100644 --- a/commons/src/main/java/net/swofty/commons/punishment/ActivePunishment.java +++ b/commons/src/main/java/net/swofty/commons/punishment/ActivePunishment.java @@ -1,3 +1,5 @@ package net.swofty.commons.punishment; -public record ActivePunishment(String type, String banId, PunishmentReason reason, long expiresAt) {} +import java.util.List; + +public record ActivePunishment(String type, String banId, PunishmentReason reason, long expiresAt, List tags) {} diff --git a/commons/src/main/java/net/swofty/commons/punishment/PunishmentRedis.java b/commons/src/main/java/net/swofty/commons/punishment/PunishmentRedis.java index 87aacf10b..d629d6543 100644 --- a/commons/src/main/java/net/swofty/commons/punishment/PunishmentRedis.java +++ b/commons/src/main/java/net/swofty/commons/punishment/PunishmentRedis.java @@ -10,7 +10,6 @@ import java.time.Duration; import java.util.Map; import java.util.Optional; -import java.util.Set; import java.util.UUID; import java.util.concurrent.CompletableFuture; @@ -61,17 +60,20 @@ public static boolean isInitialized() { return initialized && jedisPool != null && !jedisPool.isClosed(); } - public static void saveActivePunishment(UUID playerId, String type, String id, PunishmentReason reason, long expiresAt) { + public static void saveActivePunishment(UUID playerId, String type, String id, PunishmentReason reason, long expiresAt, java.util.List tags) { try (Jedis jedis = jedisPool.getResource()) { String key = PREFIX + "active:" + playerId; Gson gson = new Gson(); - Map data = Map.of( + java.util.HashMap data = new java.util.HashMap<>(Map.of( "type", type, "banId", id, - "reason", gson.toJson(reason), // most likely not optimal for performance + "reason", gson.toJson(reason), "expiresAt", String.valueOf(expiresAt) - ); + )); + if (tags != null && !tags.isEmpty()) { + data.put("tags", gson.toJson(tags)); + } jedis.hset(key, data); if (expiresAt > 0) { @@ -100,9 +102,15 @@ public static Optional getActive(UUID playerId) { } Gson gson = new Gson(); - PunishmentReason reason = gson.fromJson(data.get("reason"), PunishmentReason.class); // most likely not optimal for performance + PunishmentReason reason = gson.fromJson(data.get("reason"), PunishmentReason.class); + + java.util.List tags = java.util.List.of(); + String tagsJson = data.get("tags"); + if (tagsJson != null && !tagsJson.isBlank()) { + tags = java.util.List.of(gson.fromJson(tagsJson, PunishmentTag[].class)); + } - return Optional.of(new ActivePunishment(type, banId, reason, expiresAt)); + return Optional.of(new ActivePunishment(type, banId, reason, expiresAt, tags)); } } @@ -114,21 +122,4 @@ public static CompletableFuture revoke(UUID playerId) { } }); } - - public static Set getAllBannedPlayerIds() { - try (Jedis jedis = jedisPool.getResource()) { - var cursor = "0"; - Set result = new java.util.HashSet<>(); - var params = new redis.clients.jedis.params.ScanParams().match(PREFIX + "active:*").count(100); - do { - var scanResult = jedis.scan(cursor, params); - for (String key : scanResult.getResult()) { - result.add(key.substring((PREFIX + "active:").length())); - } - cursor = scanResult.getCursor(); - } while (!"0".equals(cursor)); - return result; - } - } - } diff --git a/service.punishment/src/main/java/net/swofty/service/punishment/endpoints/GetActivePunishmentEndpoint.java b/service.punishment/src/main/java/net/swofty/service/punishment/endpoints/GetActivePunishmentEndpoint.java index cad1f7deb..a873f28d4 100644 --- a/service.punishment/src/main/java/net/swofty/service/punishment/endpoints/GetActivePunishmentEndpoint.java +++ b/service.punishment/src/main/java/net/swofty/service/punishment/endpoints/GetActivePunishmentEndpoint.java @@ -22,7 +22,7 @@ public ProtocolObject existing = PunishmentRedis.getActive(messageObject.target()); if (existing.isEmpty()) { - return new GetActivePunishmentProtocolObject.GetActivePunishmentResponse(false, null, null, null, 0); + return new GetActivePunishmentProtocolObject.GetActivePunishmentResponse(false, null, null, null, 0, java.util.List.of()); } ActivePunishment punishment = existing.get(); @@ -31,7 +31,8 @@ public GetActivePunishmentProtocolObject.GetActivePunishmentResponse onMessage(S punishment.type(), punishment.banId(), punishment.reason(), - punishment.expiresAt() + punishment.expiresAt(), + punishment.tags() ); } } diff --git a/service.punishment/src/main/java/net/swofty/service/punishment/endpoints/PunishPlayerEndpoint.java b/service.punishment/src/main/java/net/swofty/service/punishment/endpoints/PunishPlayerEndpoint.java index e3b543b48..6713ef250 100644 --- a/service.punishment/src/main/java/net/swofty/service/punishment/endpoints/PunishPlayerEndpoint.java +++ b/service.punishment/src/main/java/net/swofty/service/punishment/endpoints/PunishPlayerEndpoint.java @@ -57,7 +57,8 @@ public PunishPlayerProtocolObject.PunishPlayerResponse onMessage(ServiceProxyReq messageObject.type(), id.id(), reason, - messageObject.expiresAt() + messageObject.expiresAt(), + messageObject.tags() ); ProxyRedis.publishToProxy(ToProxyChannels.PUNISH_PLAYER, new JSONObject() .put("target", messageObject.target()) diff --git a/type.generic/src/main/java/net/swofty/type/generic/event/actions/ActionPlayerMute.java b/type.generic/src/main/java/net/swofty/type/generic/event/actions/ActionPlayerMute.java index 6d7d90fc2..34f810300 100644 --- a/type.generic/src/main/java/net/swofty/type/generic/event/actions/ActionPlayerMute.java +++ b/type.generic/src/main/java/net/swofty/type/generic/event/actions/ActionPlayerMute.java @@ -28,7 +28,7 @@ public void onPlayerChat(PlayerChatEvent event) { if (response instanceof GetActivePunishmentProtocolObject.GetActivePunishmentResponse r && r.found() && PunishmentType.valueOf(r.type()) == PunishmentType.MUTE) { event.setCancelled(true); - var punishment = new ActivePunishment(r.type(), r.banId(), r.reason(), r.expiresAt()); + var punishment = new ActivePunishment(r.type(), r.banId(), r.reason(), r.expiresAt(), r.tags()); player.sendMessage(PunishmentMessages.muteMessage(punishment)); } } catch (Exception ignored) { diff --git a/velocity.extension/src/main/java/net/swofty/velocity/SkyBlockVelocity.java b/velocity.extension/src/main/java/net/swofty/velocity/SkyBlockVelocity.java index 5ea89580f..94662bbbb 100644 --- a/velocity.extension/src/main/java/net/swofty/velocity/SkyBlockVelocity.java +++ b/velocity.extension/src/main/java/net/swofty/velocity/SkyBlockVelocity.java @@ -230,7 +230,7 @@ public boolean punished(Player player) { } ActivePunishment punishment = new ActivePunishment( - r.type(), r.banId(), r.reason(), r.expiresAt()); + r.type(), r.banId(), r.reason(), r.expiresAt(), r.tags()); PunishmentType type = PunishmentType.valueOf(r.type()); if (type == PunishmentType.BAN) { player.disconnect(PunishmentMessages.banMessage(punishment)); diff --git a/velocity.extension/src/main/java/net/swofty/velocity/redis/listeners/ListenerPlayerPunished.java b/velocity.extension/src/main/java/net/swofty/velocity/redis/listeners/ListenerPlayerPunished.java index 21be00908..510bfbd13 100644 --- a/velocity.extension/src/main/java/net/swofty/velocity/redis/listeners/ListenerPlayerPunished.java +++ b/velocity.extension/src/main/java/net/swofty/velocity/redis/listeners/ListenerPlayerPunished.java @@ -54,7 +54,7 @@ public JSONObject receivedMessage(JSONObject message, UUID serverUUID) { PunishmentType punishmentType = PunishmentType.valueOf(type); PunishmentReason finalReason = reason; SkyBlockVelocity.getServer().getPlayer(target).ifPresent((player) -> { - ActivePunishment activePunishment = new ActivePunishment(type, id, finalReason, expiresAt); + ActivePunishment activePunishment = new ActivePunishment(type, id, finalReason, expiresAt, java.util.List.of()); switch (punishmentType) { case BAN -> { player.disconnect(PunishmentMessages.banMessage(activePunishment)); From 1a54367bd8811b64eb5fafaac9a6fbbb2bf0e91f Mon Sep 17 00:00:00 2001 From: Swofty Date: Wed, 18 Feb 2026 00:25:21 +1100 Subject: [PATCH 22/27] fix(punishment): per-type Redis keys, init guards, persist for permanent punishments - Key schema changed to punish:active:{uuid}:{type} so bans and mutes coexist - Added isInitialized() guard on all public methods to prevent NPE - Added jedis.persist() for permanent punishments to clear stale TTLs - revoke() now takes a type parameter and is synchronous - Fixed banMessage computing timeLeft unconditionally for permanent bans --- .../punishment/PunishmentMessages.java | 5 +- .../commons/punishment/PunishmentRedis.java | 74 +++++++++++-------- 2 files changed, 47 insertions(+), 32 deletions(-) diff --git a/commons/src/main/java/net/swofty/commons/punishment/PunishmentMessages.java b/commons/src/main/java/net/swofty/commons/punishment/PunishmentMessages.java index cee5c82c3..548bdf9a0 100644 --- a/commons/src/main/java/net/swofty/commons/punishment/PunishmentMessages.java +++ b/commons/src/main/java/net/swofty/commons/punishment/PunishmentMessages.java @@ -11,13 +11,12 @@ public static Component banMessage(ActivePunishment punishment) { PunishmentReason reason = punishment.reason(); String banId = punishment.banId(); - long timeLeft = expiresAt - System.currentTimeMillis(); - String prettyTimeLeft = StringUtility.formatTimeLeft(timeLeft); - String header; if (expiresAt <= 0) { header = "§cYou are permanently banned from this server!\n"; } else { + long timeLeft = expiresAt - System.currentTimeMillis(); + String prettyTimeLeft = StringUtility.formatTimeLeft(timeLeft); header = "§cYou are temporarily banned for §f" + prettyTimeLeft + " §cfrom this server!\n"; } diff --git a/commons/src/main/java/net/swofty/commons/punishment/PunishmentRedis.java b/commons/src/main/java/net/swofty/commons/punishment/PunishmentRedis.java index d629d6543..b37884690 100644 --- a/commons/src/main/java/net/swofty/commons/punishment/PunishmentRedis.java +++ b/commons/src/main/java/net/swofty/commons/punishment/PunishmentRedis.java @@ -8,13 +8,12 @@ import java.net.URI; import java.time.Duration; -import java.util.Map; -import java.util.Optional; -import java.util.UUID; +import java.util.*; import java.util.concurrent.CompletableFuture; public class PunishmentRedis { - private static final String PREFIX = "punish:"; + private static final String PREFIX = "punish:active:"; + private static final Gson GSON = new Gson(); private static JedisPool jedisPool; private static volatile boolean initialized = false; private static volatile boolean connecting = false; @@ -40,7 +39,6 @@ private static synchronized void connectSync(String redisUri) { URI uri = URI.create(redisUri); jedisPool = new JedisPool(poolConfig, uri); - // Test connection try (Jedis jedis = jedisPool.getResource()) { jedis.ping(); } @@ -60,66 +58,84 @@ public static boolean isInitialized() { return initialized && jedisPool != null && !jedisPool.isClosed(); } - public static void saveActivePunishment(UUID playerId, String type, String id, PunishmentReason reason, long expiresAt, java.util.List tags) { + private static String key(UUID playerId, String type) { + return PREFIX + playerId + ":" + type; + } + + public static void saveActivePunishment(UUID playerId, String type, String id, + PunishmentReason reason, long expiresAt, + List tags) { + if (!isInitialized()) throw new IllegalStateException("PunishmentRedis not initialized"); + try (Jedis jedis = jedisPool.getResource()) { - String key = PREFIX + "active:" + playerId; + String k = key(playerId, type); - Gson gson = new Gson(); - java.util.HashMap data = new java.util.HashMap<>(Map.of( + HashMap data = new HashMap<>(Map.of( "type", type, "banId", id, - "reason", gson.toJson(reason), + "reason", GSON.toJson(reason), "expiresAt", String.valueOf(expiresAt) )); if (tags != null && !tags.isEmpty()) { - data.put("tags", gson.toJson(tags)); + data.put("tags", GSON.toJson(tags)); } - jedis.hset(key, data); + jedis.hset(k, data); if (expiresAt > 0) { long ttlSeconds = (expiresAt - System.currentTimeMillis()) / 1000; if (ttlSeconds > 0) { - jedis.expire(key, (int) ttlSeconds); + jedis.expire(k, (int) ttlSeconds); } + } else { + jedis.persist(k); } } } - public static Optional getActive(UUID playerId) { + public static Optional getActive(UUID playerId, String type) { + if (!isInitialized()) return Optional.empty(); + try (Jedis jedis = jedisPool.getResource()) { - String key = PREFIX + "active:" + playerId; - Map data = jedis.hgetAll(key); + String k = key(playerId, type); + Map data = jedis.hgetAll(k); if (data.isEmpty()) return Optional.empty(); - String type = data.get("type"); String banId = data.get("banId"); long expiresAt = Long.parseLong(data.getOrDefault("expiresAt", "-1")); if (expiresAt > 0 && System.currentTimeMillis() > expiresAt) { - jedis.del(key); // clean up expired + jedis.del(k); return Optional.empty(); } - Gson gson = new Gson(); - PunishmentReason reason = gson.fromJson(data.get("reason"), PunishmentReason.class); + PunishmentReason reason = GSON.fromJson(data.get("reason"), PunishmentReason.class); - java.util.List tags = java.util.List.of(); + List tags = List.of(); String tagsJson = data.get("tags"); if (tagsJson != null && !tagsJson.isBlank()) { - tags = java.util.List.of(gson.fromJson(tagsJson, PunishmentTag[].class)); + tags = List.of(GSON.fromJson(tagsJson, PunishmentTag[].class)); } return Optional.of(new ActivePunishment(type, banId, reason, expiresAt, tags)); } } - public static CompletableFuture revoke(UUID playerId) { - return CompletableFuture.supplyAsync(() -> { - try (Jedis jedis = jedisPool.getResource()) { - String key = PREFIX + "active:" + playerId; - return jedis.del(key); - } - }); + public static List getAllActive(UUID playerId) { + if (!isInitialized()) return List.of(); + + List result = new ArrayList<>(); + for (PunishmentType pt : PunishmentType.values()) { + getActive(playerId, pt.name()).ifPresent(result::add); + } + return result; + } + + public static void revoke(UUID playerId, String type) { + if (!isInitialized()) throw new IllegalStateException("PunishmentRedis not initialized"); + + try (Jedis jedis = jedisPool.getResource()) { + jedis.del(key(playerId, type)); + } } } From c48e75541930580391221697023857be0d473fd6 Mon Sep 17 00:00:00 2001 From: Swofty Date: Wed, 18 Feb 2026 00:25:41 +1100 Subject: [PATCH 23/27] fix(punishment): update protocol objects and endpoints for per-type queries - GetActivePunishmentMessage now requires a type field - UnpunishPlayerMessage now carries a type field (enables /unmute) - UnpunishPlayerEndpoint no longer hard-codes BAN-only - PunishPlayerEndpoint wraps save in try/catch returning DATABASE_ERROR - PunishPlayerEndpoint includes tags in pub/sub JSON payload - Fixed UnpunishPlayerProtocolObject null deserialization (optString -> isNull) --- .../GetActivePunishmentProtocolObject.java | 7 ++-- .../UnpunishPlayerProtocolObject.java | 9 +++-- .../GetActivePunishmentEndpoint.java | 5 +-- .../endpoints/PunishPlayerEndpoint.java | 36 +++++++++++-------- .../endpoints/UnpunishPlayerEndpoint.java | 19 +++++----- 5 files changed, 44 insertions(+), 32 deletions(-) diff --git a/commons/src/main/java/net/swofty/commons/protocol/objects/punishment/GetActivePunishmentProtocolObject.java b/commons/src/main/java/net/swofty/commons/protocol/objects/punishment/GetActivePunishmentProtocolObject.java index cc7f6a593..8dd0b5333 100644 --- a/commons/src/main/java/net/swofty/commons/protocol/objects/punishment/GetActivePunishmentProtocolObject.java +++ b/commons/src/main/java/net/swofty/commons/protocol/objects/punishment/GetActivePunishmentProtocolObject.java @@ -23,6 +23,7 @@ public Serializer getSerializer() { public String serialize(GetActivePunishmentMessage value) { JSONObject json = new JSONObject(); json.put("target", value.target().toString()); + json.put("type", value.type()); return json.toString(); } @@ -30,7 +31,8 @@ public String serialize(GetActivePunishmentMessage value) { public GetActivePunishmentMessage deserialize(String json) { JSONObject obj = new JSONObject(json); return new GetActivePunishmentMessage( - UUID.fromString(obj.getString("target")) + UUID.fromString(obj.getString("target")), + obj.getString("type") ); } @@ -87,7 +89,8 @@ public GetActivePunishmentResponse clone(GetActivePunishmentResponse value) { } public record GetActivePunishmentMessage( - @NotNull UUID target + @NotNull UUID target, + @NotNull String type ) {} public record GetActivePunishmentResponse( diff --git a/commons/src/main/java/net/swofty/commons/protocol/objects/punishment/UnpunishPlayerProtocolObject.java b/commons/src/main/java/net/swofty/commons/protocol/objects/punishment/UnpunishPlayerProtocolObject.java index b2f9b7b46..6a09563f3 100644 --- a/commons/src/main/java/net/swofty/commons/protocol/objects/punishment/UnpunishPlayerProtocolObject.java +++ b/commons/src/main/java/net/swofty/commons/protocol/objects/punishment/UnpunishPlayerProtocolObject.java @@ -20,6 +20,7 @@ public String serialize(UnpunishPlayerMessage value) { JSONObject json = new JSONObject(); json.put("target", value.target().toString()); json.put("staff", value.staff().toString()); + json.put("type", value.type()); return json.toString(); } @@ -28,7 +29,8 @@ public UnpunishPlayerMessage deserialize(String json) { JSONObject obj = new JSONObject(json); return new UnpunishPlayerMessage( UUID.fromString(obj.getString("target")), - UUID.fromString(obj.getString("staff")) + UUID.fromString(obj.getString("staff")), + obj.getString("type") ); } @@ -55,7 +57,7 @@ public UnpunishPlayerResponse deserialize(String json) { JSONObject obj = new JSONObject(json); return new UnpunishPlayerResponse( obj.getBoolean("success"), - obj.optString("errorMessage", null) + obj.isNull("errorMessage") ? null : obj.getString("errorMessage") ); } @@ -68,7 +70,8 @@ public UnpunishPlayerResponse clone(UnpunishPlayerResponse value) { public record UnpunishPlayerMessage( @NotNull UUID target, - @NotNull UUID staff + @NotNull UUID staff, + @NotNull String type ) {} public record UnpunishPlayerResponse( diff --git a/service.punishment/src/main/java/net/swofty/service/punishment/endpoints/GetActivePunishmentEndpoint.java b/service.punishment/src/main/java/net/swofty/service/punishment/endpoints/GetActivePunishmentEndpoint.java index a873f28d4..225887560 100644 --- a/service.punishment/src/main/java/net/swofty/service/punishment/endpoints/GetActivePunishmentEndpoint.java +++ b/service.punishment/src/main/java/net/swofty/service/punishment/endpoints/GetActivePunishmentEndpoint.java @@ -7,6 +7,7 @@ import net.swofty.commons.punishment.PunishmentRedis; import net.swofty.service.generic.redis.ServiceEndpoint; +import java.util.List; import java.util.Optional; public class GetActivePunishmentEndpoint implements ServiceEndpoint @@ -20,9 +21,9 @@ public ProtocolObject existing = PunishmentRedis.getActive(messageObject.target()); + Optional existing = PunishmentRedis.getActive(messageObject.target(), messageObject.type()); if (existing.isEmpty()) { - return new GetActivePunishmentProtocolObject.GetActivePunishmentResponse(false, null, null, null, 0, java.util.List.of()); + return new GetActivePunishmentProtocolObject.GetActivePunishmentResponse(false, null, null, null, 0, List.of()); } ActivePunishment punishment = existing.get(); diff --git a/service.punishment/src/main/java/net/swofty/service/punishment/endpoints/PunishPlayerEndpoint.java b/service.punishment/src/main/java/net/swofty/service/punishment/endpoints/PunishPlayerEndpoint.java index 6713ef250..8d7ea68f2 100644 --- a/service.punishment/src/main/java/net/swofty/service/punishment/endpoints/PunishPlayerEndpoint.java +++ b/service.punishment/src/main/java/net/swofty/service/punishment/endpoints/PunishPlayerEndpoint.java @@ -1,5 +1,6 @@ package net.swofty.service.punishment.endpoints; +import com.google.gson.Gson; import net.swofty.commons.impl.ServiceProxyRequest; import net.swofty.commons.protocol.ProtocolObject; import net.swofty.commons.protocol.objects.punishment.PunishPlayerProtocolObject; @@ -38,28 +39,32 @@ public PunishPlayerProtocolObject.PunishPlayerResponse onMessage(ServiceProxyReq boolean hasOverwriteTag = messageObject.tags() != null && messageObject.tags().contains(PunishmentTag.OVERWRITE); if (!hasOverwriteTag) { - Optional existing = PunishmentRedis.getActive(messageObject.target()); + Optional existing = PunishmentRedis.getActive(messageObject.target(), messageObject.type()); if (existing.isPresent()) { - ActivePunishment active = existing.get(); - PunishmentType existingType = PunishmentType.valueOf(active.type()); - if (existingType == punishmentType) { - return new PunishPlayerProtocolObject.PunishPlayerResponse(false, null, - PunishPlayerProtocolObject.ErrorCode.ALREADY_PUNISHED, active.banId()); - } + return new PunishPlayerProtocolObject.PunishPlayerResponse(false, null, + PunishPlayerProtocolObject.ErrorCode.ALREADY_PUNISHED, existing.get().banId()); } } PunishmentReason reason = messageObject.reason(); PunishmentId id = PunishmentId.generateId(); - PunishmentRedis.saveActivePunishment( - messageObject.target(), - messageObject.type(), - id.id(), - reason, - messageObject.expiresAt(), - messageObject.tags() - ); + try { + PunishmentRedis.saveActivePunishment( + messageObject.target(), + messageObject.type(), + id.id(), + reason, + messageObject.expiresAt(), + messageObject.tags() + ); + } catch (Exception e) { + Logger.error("Failed to save punishment to Redis", e); + return new PunishPlayerProtocolObject.PunishPlayerResponse(false, null, + PunishPlayerProtocolObject.ErrorCode.DATABASE_ERROR, "Failed to save punishment."); + } + + Gson gson = new Gson(); ProxyRedis.publishToProxy(ToProxyChannels.PUNISH_PLAYER, new JSONObject() .put("target", messageObject.target()) .put("type", messageObject.type()) @@ -69,6 +74,7 @@ public PunishPlayerProtocolObject.PunishPlayerResponse onMessage(ServiceProxyReq .put("staff", messageObject.staff()) .put("issuedAt", now.toEpochMilli()) .put("expiresAt", messageObject.expiresAt()) + .put("tags", messageObject.tags() != null ? gson.toJson(messageObject.tags()) : null) ); Logger.info("Issued {} punishment to {} for reason '{}' (expires at: {})", messageObject.type(), diff --git a/service.punishment/src/main/java/net/swofty/service/punishment/endpoints/UnpunishPlayerEndpoint.java b/service.punishment/src/main/java/net/swofty/service/punishment/endpoints/UnpunishPlayerEndpoint.java index e465b2d73..1479c8901 100644 --- a/service.punishment/src/main/java/net/swofty/service/punishment/endpoints/UnpunishPlayerEndpoint.java +++ b/service.punishment/src/main/java/net/swofty/service/punishment/endpoints/UnpunishPlayerEndpoint.java @@ -5,7 +5,6 @@ import net.swofty.commons.protocol.objects.punishment.UnpunishPlayerProtocolObject; import net.swofty.commons.punishment.ActivePunishment; import net.swofty.commons.punishment.PunishmentRedis; -import net.swofty.commons.punishment.PunishmentType; import net.swofty.service.generic.redis.ServiceEndpoint; import org.tinylog.Logger; @@ -22,20 +21,20 @@ public ProtocolObject existing = PunishmentRedis.getActive(messageObject.target()); + Optional existing = PunishmentRedis.getActive(messageObject.target(), messageObject.type()); if (existing.isEmpty()) { - return new UnpunishPlayerProtocolObject.UnpunishPlayerResponse(false, "No active punishment found for this player."); + return new UnpunishPlayerProtocolObject.UnpunishPlayerResponse(false, "No active " + messageObject.type().toLowerCase() + " found for this player."); } - ActivePunishment punishment = existing.get(); - PunishmentType type = PunishmentType.valueOf(punishment.type()); - if (type != PunishmentType.BAN) { - return new UnpunishPlayerProtocolObject.UnpunishPlayerResponse(false, "Player is not banned (active punishment is " + type.name() + ")."); + try { + PunishmentRedis.revoke(messageObject.target(), messageObject.type()); + } catch (Exception e) { + Logger.error("Failed to revoke punishment", e); + return new UnpunishPlayerProtocolObject.UnpunishPlayerResponse(false, "Failed to revoke punishment from database."); } - PunishmentRedis.revoke(messageObject.target()).join(); - Logger.info("Revoked ban for {} by staff {}", - messageObject.target(), messageObject.staff()); + Logger.info("Revoked {} for {} by staff {}", + messageObject.type(), messageObject.target(), messageObject.staff()); return new UnpunishPlayerProtocolObject.UnpunishPlayerResponse(true, null); } } From e350d4fb648ad885c184df636b3c4aad047d5d5e Mon Sep 17 00:00:00 2001 From: Swofty Date: Wed, 18 Feb 2026 00:26:15 +1100 Subject: [PATCH 24/27] fix(punishment): fix commands, add /unmute, async Velocity login check - BanCommand: transferToLimbo moved into success callback, valueOf try/catch - MuteCommand: valueOf try/catch for invalid reason input - UnBanCommand/UnMuteCommand: pass punishment type in unpunish request - All commands use HypixelPlayer directly (no console path) - SkyBlockVelocity: login punishment check is now async via EventTask - SkyBlockVelocity: checks BAN and MUTE in parallel - ActionPlayerMute: passes MUTE type to service query - ListenerPlayerPunished: parses tags from pub/sub JSON, uses getReasonString() --- .../generic/command/commands/BanCommand.java | 60 ++++++++++++------ .../generic/command/commands/MuteCommand.java | 42 ++++++++----- .../command/commands/UnBanCommand.java | 17 +++--- .../command/commands/UnMuteCommand.java | 61 +++++++++++++++++++ .../event/actions/ActionPlayerMute.java | 6 +- .../net/swofty/velocity/SkyBlockVelocity.java | 36 ++++++----- .../listeners/ListenerPlayerPunished.java | 38 ++++++------ 7 files changed, 181 insertions(+), 79 deletions(-) create mode 100644 type.generic/src/main/java/net/swofty/type/generic/command/commands/UnMuteCommand.java diff --git a/type.generic/src/main/java/net/swofty/type/generic/command/commands/BanCommand.java b/type.generic/src/main/java/net/swofty/type/generic/command/commands/BanCommand.java index f3ad4ec7f..7a53ce4fd 100644 --- a/type.generic/src/main/java/net/swofty/type/generic/command/commands/BanCommand.java +++ b/type.generic/src/main/java/net/swofty/type/generic/command/commands/BanCommand.java @@ -1,10 +1,7 @@ package net.swofty.type.generic.command.commands; import net.kyori.adventure.text.Component; -import net.minestom.server.command.CommandSender; import net.minestom.server.command.builder.arguments.*; -import net.minestom.server.entity.Player; -import net.minestom.server.utils.mojang.MojangUtils; import net.minestom.server.command.builder.suggestion.SuggestionEntry; import net.swofty.commons.ServiceType; import net.swofty.commons.StringUtility; @@ -17,6 +14,7 @@ import net.swofty.proxyapi.ProxyService; import net.swofty.type.generic.command.CommandParameters; import net.swofty.type.generic.command.HypixelCommand; +import net.swofty.type.generic.user.HypixelPlayer; import net.swofty.type.generic.user.categories.Rank; import org.jetbrains.annotations.Nullable; @@ -32,7 +30,7 @@ permission = Rank.STAFF, description = "Ban a player from the server.", usage = "/ban [duration] ", - allowsConsole = true + allowsConsole = false ) public class BanCommand extends HypixelCommand { @@ -52,47 +50,72 @@ public void registerUsage(MinestomCommand command) { }); command.addSyntax((sender, context) -> { + HypixelPlayer player = (HypixelPlayer) sender; String playerName = context.get(playerArg); String duration = context.get(durationArg); - BanType type = BanType.valueOf(context.get(reasonArg)); + + BanType type; + try { + type = BanType.valueOf(context.get(reasonArg)); + } catch (IllegalArgumentException e) { + player.sendMessage("§cInvalid ban reason. Use tab-completion to see valid options."); + return; + } CompletableFuture.runAsync(() -> { try { - UUID targetUuid = MojangUtils.getUUID(playerName); + UUID targetUuid = net.minestom.server.utils.mojang.MojangUtils.getUUID(playerName); long actualTime = StringUtility.parseDuration(duration); long expiryTime = System.currentTimeMillis() + actualTime; - banPlayer(sender, targetUuid, type, (sender instanceof Player p ? p.getUuid() : new UUID(0, 0)), actualTime, expiryTime, playerName, null); + banPlayer(player, targetUuid, type, player.getUuid(), actualTime, expiryTime, playerName, null); } catch (IOException e) { - sender.sendMessage("§cCould not find player: " + playerName); + player.sendMessage("§cCould not find player: " + playerName); } }); }, playerArg, durationArg, reasonArg); command.addSyntax((sender, context) -> { + HypixelPlayer player = (HypixelPlayer) sender; String playerName = context.get(playerArg); - BanType reason = BanType.valueOf(context.get(reasonArg)); + + BanType reason; + try { + reason = BanType.valueOf(context.get(reasonArg)); + } catch (IllegalArgumentException e) { + player.sendMessage("§cInvalid ban reason. Use tab-completion to see valid options."); + return; + } CompletableFuture.runAsync(() -> { try { - banPlayer(sender, MojangUtils.getUUID(playerName), reason, - (sender instanceof Player p ? p.getUuid() : new UUID(0, 0)), 0, -1, playerName, null); + banPlayer(player, net.minestom.server.utils.mojang.MojangUtils.getUUID(playerName), reason, + player.getUuid(), 0, -1, playerName, null); } catch (IOException e) { - sender.sendMessage("§cCould not find player: " + playerName); + player.sendMessage("§cCould not find player: " + playerName); } }); }, playerArg, reasonArg); command.addSyntax((sender, context) -> { + HypixelPlayer player = (HypixelPlayer) sender; String playerName = context.get(playerArg); - BanType reason = BanType.valueOf(context.get(reasonArg)); + + BanType reason; + try { + reason = BanType.valueOf(context.get(reasonArg)); + } catch (IllegalArgumentException e) { + player.sendMessage("§cInvalid ban reason. Use tab-completion to see valid options."); + return; + } + List tags = parseTags(List.of(context.get(extraArg))); CompletableFuture.runAsync(() -> { try { - banPlayer(sender, MojangUtils.getUUID(playerName), reason, - (sender instanceof Player p ? p.getUuid() : new UUID(0, 0)), 0, -1, playerName, tags); + banPlayer(player, net.minestom.server.utils.mojang.MojangUtils.getUUID(playerName), reason, + player.getUuid(), 0, -1, playerName, tags); } catch (IOException e) { - sender.sendMessage("§cCould not find player: " + playerName); + player.sendMessage("§cCould not find player: " + playerName); } }); }, playerArg, reasonArg, extraArg); @@ -114,7 +137,7 @@ private List parseTags(List rawTags) { return tags; } - private void banPlayer(CommandSender sender, UUID targetUuid, BanType type, UUID senderUuid, + private void banPlayer(HypixelPlayer sender, UUID targetUuid, BanType type, UUID senderUuid, long actualTime, long expiryTime, String playerName, @Nullable List tags) { ProxyService punishmentService = new ProxyService(ServiceType.PUNISHMENT); PunishmentReason reason = new PunishmentReason(type); @@ -128,11 +151,10 @@ private void banPlayer(CommandSender sender, UUID targetUuid, BanType type, UUID actualTime > 0 ? expiryTime : -1 ); - new ProxyPlayer(targetUuid).transferToLimbo(); - punishmentService.handleRequest(message).thenAccept(result -> { if (result instanceof PunishPlayerProtocolObject.PunishPlayerResponse response) { if (response.success()) { + new ProxyPlayer(targetUuid).transferToLimbo(); sender.sendMessage("§aSuccessfully banned player §e" + playerName + "§a. §8Punishment ID: §7" + response.punishmentId()); } else if (response.errorCode() == PunishPlayerProtocolObject.ErrorCode.ALREADY_PUNISHED) { sender.sendMessage("§cThis player is already banned. Use the tag -O to overwrite. Punishment ID: §7" + response.errorMessage()); diff --git a/type.generic/src/main/java/net/swofty/type/generic/command/commands/MuteCommand.java b/type.generic/src/main/java/net/swofty/type/generic/command/commands/MuteCommand.java index 066316a18..1e5b76771 100644 --- a/type.generic/src/main/java/net/swofty/type/generic/command/commands/MuteCommand.java +++ b/type.generic/src/main/java/net/swofty/type/generic/command/commands/MuteCommand.java @@ -1,10 +1,7 @@ package net.swofty.type.generic.command.commands; import net.kyori.adventure.text.Component; -import net.minestom.server.command.CommandSender; import net.minestom.server.command.builder.arguments.Argument; -import net.minestom.server.entity.Player; -import net.minestom.server.utils.mojang.MojangUtils; import net.minestom.server.command.builder.arguments.ArgumentString; import net.minestom.server.command.builder.arguments.ArgumentType; import net.minestom.server.command.builder.suggestion.SuggestionEntry; @@ -17,6 +14,7 @@ import net.swofty.proxyapi.ProxyService; import net.swofty.type.generic.command.CommandParameters; import net.swofty.type.generic.command.HypixelCommand; +import net.swofty.type.generic.user.HypixelPlayer; import net.swofty.type.generic.user.categories.Rank; import java.io.IOException; @@ -30,7 +28,7 @@ permission = Rank.STAFF, description = "Mute a player from the server.", usage = "/mute [duration] ", - allowsConsole = true + allowsConsole = false ) public class MuteCommand extends HypixelCommand { @@ -45,38 +43,54 @@ public void registerUsage(MinestomCommand command) { }); command.addSyntax((sender, context) -> { + HypixelPlayer player = (HypixelPlayer) sender; String playerName = context.get(playerArg); String duration = context.get(durationArg); - MuteType type = MuteType.valueOf(context.get(reasonArg)); + + MuteType type; + try { + type = MuteType.valueOf(context.get(reasonArg)); + } catch (IllegalArgumentException e) { + player.sendMessage("§cInvalid mute reason. Use tab-completion to see valid options."); + return; + } CompletableFuture.runAsync(() -> { try { - UUID targetUuid = MojangUtils.getUUID(playerName); + UUID targetUuid = net.minestom.server.utils.mojang.MojangUtils.getUUID(playerName); long actualTime = StringUtility.parseDuration(duration); long expiryTime = System.currentTimeMillis() + actualTime; - mutePlayer(sender, targetUuid, type, (sender instanceof Player p ? p.getUuid() : new UUID(0, 0)), actualTime, expiryTime, playerName); + mutePlayer(player, targetUuid, type, player.getUuid(), actualTime, expiryTime, playerName); } catch (IOException e) { - sender.sendMessage("§cCould not find player: " + playerName); + player.sendMessage("§cCould not find player: " + playerName); } }); }, playerArg, durationArg, reasonArg); command.addSyntax((sender, context) -> { + HypixelPlayer player = (HypixelPlayer) sender; String playerName = context.get(playerArg); - MuteType reason = MuteType.valueOf(context.get(reasonArg)); + + MuteType reason; + try { + reason = MuteType.valueOf(context.get(reasonArg)); + } catch (IllegalArgumentException e) { + player.sendMessage("§cInvalid mute reason. Use tab-completion to see valid options."); + return; + } CompletableFuture.runAsync(() -> { try { - mutePlayer(sender, MojangUtils.getUUID(playerName), reason, - (sender instanceof Player p ? p.getUuid() : new UUID(0, 0)), 0, -1, playerName); + mutePlayer(player, net.minestom.server.utils.mojang.MojangUtils.getUUID(playerName), reason, + player.getUuid(), 0, -1, playerName); } catch (IOException e) { - sender.sendMessage("§cCould not find player: " + playerName); + player.sendMessage("§cCould not find player: " + playerName); } }); }, playerArg, reasonArg); } - private void mutePlayer(CommandSender sender, UUID targetUuid, MuteType type, UUID senderUuid, + private void mutePlayer(HypixelPlayer sender, UUID targetUuid, MuteType type, UUID senderUuid, long actualTime, long expiryTime, String playerName) { ProxyService punishmentService = new ProxyService(ServiceType.PUNISHMENT); PunishmentReason reason = new PunishmentReason(type); @@ -94,7 +108,7 @@ private void mutePlayer(CommandSender sender, UUID targetUuid, MuteType type, UU if (response.success()) { sender.sendMessage("§aSuccessfully muted player §e" + playerName + "§a. §8Punishment ID: §7" + response.punishmentId()); } else if (response.errorCode() == PunishPlayerProtocolObject.ErrorCode.ALREADY_PUNISHED) { - sender.sendMessage("§cThis player already has an active punishment. Punishment ID: §7" + response.errorMessage()); + sender.sendMessage("§cThis player already has an active mute. Punishment ID: §7" + response.errorMessage()); } else { sender.sendMessage("§cFailed to mute player: " + response.errorMessage()); } diff --git a/type.generic/src/main/java/net/swofty/type/generic/command/commands/UnBanCommand.java b/type.generic/src/main/java/net/swofty/type/generic/command/commands/UnBanCommand.java index 3887228bd..f251e4ece 100644 --- a/type.generic/src/main/java/net/swofty/type/generic/command/commands/UnBanCommand.java +++ b/type.generic/src/main/java/net/swofty/type/generic/command/commands/UnBanCommand.java @@ -1,17 +1,17 @@ package net.swofty.type.generic.command.commands; import net.minestom.server.command.builder.arguments.ArgumentType; -import net.minestom.server.entity.Player; import net.minestom.server.utils.mojang.MojangUtils; import net.swofty.commons.ServiceType; import net.swofty.commons.protocol.objects.punishment.UnpunishPlayerProtocolObject; +import net.swofty.commons.punishment.PunishmentType; import net.swofty.proxyapi.ProxyService; import net.swofty.type.generic.command.CommandParameters; import net.swofty.type.generic.command.HypixelCommand; +import net.swofty.type.generic.user.HypixelPlayer; import net.swofty.type.generic.user.categories.Rank; import java.io.IOException; -import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; @@ -20,7 +20,7 @@ usage = "/unban ", aliases = "unban pardon unbanip pardonip", permission = Rank.STAFF, - allowsConsole = true + allowsConsole = false ) public class UnBanCommand extends HypixelCommand { @@ -29,6 +29,7 @@ public void registerUsage(MinestomCommand command) { var argument = ArgumentType.String("player"); command.addSyntax((sender, context) -> { + HypixelPlayer player = (HypixelPlayer) sender; String playerName = context.get(argument); CompletableFuture.runAsync(() -> { @@ -36,23 +37,23 @@ public void registerUsage(MinestomCommand command) { var targetUuid = MojangUtils.getUUID(playerName); ProxyService punishmentService = new ProxyService(ServiceType.PUNISHMENT); var message = new UnpunishPlayerProtocolObject.UnpunishPlayerMessage( - targetUuid, (sender instanceof Player p ? p.getUuid() : new UUID(0, 0)) + targetUuid, player.getUuid(), PunishmentType.BAN.name() ); punishmentService.handleRequest(message).thenAccept(result -> { if (result instanceof UnpunishPlayerProtocolObject.UnpunishPlayerResponse response) { if (response.success()) { - sender.sendMessage("§aSuccessfully unbanned player: " + playerName); + player.sendMessage("§aSuccessfully unbanned player: " + playerName); } else { - sender.sendMessage("§c" + response.errorMessage()); + player.sendMessage("§c" + response.errorMessage()); } } }).orTimeout(5, TimeUnit.SECONDS).exceptionally(_ -> { - sender.sendMessage("§cCould not unban this player at this time. The punishment service may be offline."); + player.sendMessage("§cCould not unban this player at this time. The punishment service may be offline."); return null; }); } catch (IOException e) { - sender.sendMessage("§cCould not find player: " + playerName); + player.sendMessage("§cCould not find player: " + playerName); } }); }, argument); diff --git a/type.generic/src/main/java/net/swofty/type/generic/command/commands/UnMuteCommand.java b/type.generic/src/main/java/net/swofty/type/generic/command/commands/UnMuteCommand.java new file mode 100644 index 000000000..0e6aa3b22 --- /dev/null +++ b/type.generic/src/main/java/net/swofty/type/generic/command/commands/UnMuteCommand.java @@ -0,0 +1,61 @@ +package net.swofty.type.generic.command.commands; + +import net.minestom.server.command.builder.arguments.ArgumentType; +import net.minestom.server.utils.mojang.MojangUtils; +import net.swofty.commons.ServiceType; +import net.swofty.commons.protocol.objects.punishment.UnpunishPlayerProtocolObject; +import net.swofty.commons.punishment.PunishmentType; +import net.swofty.proxyapi.ProxyService; +import net.swofty.type.generic.command.CommandParameters; +import net.swofty.type.generic.command.HypixelCommand; +import net.swofty.type.generic.user.HypixelPlayer; +import net.swofty.type.generic.user.categories.Rank; + +import java.io.IOException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +@CommandParameters( + description = "Unmute a player on the server.", + usage = "/unmute ", + aliases = "unmute", + permission = Rank.STAFF, + allowsConsole = false +) +public class UnMuteCommand extends HypixelCommand { + + @Override + public void registerUsage(MinestomCommand command) { + var argument = ArgumentType.String("player"); + + command.addSyntax((sender, context) -> { + HypixelPlayer player = (HypixelPlayer) sender; + String playerName = context.get(argument); + + CompletableFuture.runAsync(() -> { + try { + var targetUuid = MojangUtils.getUUID(playerName); + ProxyService punishmentService = new ProxyService(ServiceType.PUNISHMENT); + var message = new UnpunishPlayerProtocolObject.UnpunishPlayerMessage( + targetUuid, player.getUuid(), PunishmentType.MUTE.name() + ); + + punishmentService.handleRequest(message).thenAccept(result -> { + if (result instanceof UnpunishPlayerProtocolObject.UnpunishPlayerResponse response) { + if (response.success()) { + player.sendMessage("§aSuccessfully unmuted player: " + playerName); + } else { + player.sendMessage("§c" + response.errorMessage()); + } + } + }).orTimeout(5, TimeUnit.SECONDS).exceptionally(_ -> { + player.sendMessage("§cCould not unmute this player at this time. The punishment service may be offline."); + return null; + }); + } catch (IOException e) { + player.sendMessage("§cCould not find player: " + playerName); + } + }); + }, argument); + } +} diff --git a/type.generic/src/main/java/net/swofty/type/generic/event/actions/ActionPlayerMute.java b/type.generic/src/main/java/net/swofty/type/generic/event/actions/ActionPlayerMute.java index 34f810300..d5d066562 100644 --- a/type.generic/src/main/java/net/swofty/type/generic/event/actions/ActionPlayerMute.java +++ b/type.generic/src/main/java/net/swofty/type/generic/event/actions/ActionPlayerMute.java @@ -21,12 +21,12 @@ public void onPlayerChat(PlayerChatEvent event) { Player player = event.getPlayer(); try { var response = new ProxyService(ServiceType.PUNISHMENT) - .handleRequest(new GetActivePunishmentProtocolObject.GetActivePunishmentMessage(player.getUuid())) + .handleRequest(new GetActivePunishmentProtocolObject.GetActivePunishmentMessage( + player.getUuid(), PunishmentType.MUTE.name())) .orTimeout(2, TimeUnit.SECONDS) .join(); - if (response instanceof GetActivePunishmentProtocolObject.GetActivePunishmentResponse r - && r.found() && PunishmentType.valueOf(r.type()) == PunishmentType.MUTE) { + if (response instanceof GetActivePunishmentProtocolObject.GetActivePunishmentResponse r && r.found()) { event.setCancelled(true); var punishment = new ActivePunishment(r.type(), r.banId(), r.reason(), r.expiresAt(), r.tags()); player.sendMessage(PunishmentMessages.muteMessage(punishment)); diff --git a/velocity.extension/src/main/java/net/swofty/velocity/SkyBlockVelocity.java b/velocity.extension/src/main/java/net/swofty/velocity/SkyBlockVelocity.java index 94662bbbb..44c3a9cba 100644 --- a/velocity.extension/src/main/java/net/swofty/velocity/SkyBlockVelocity.java +++ b/velocity.extension/src/main/java/net/swofty/velocity/SkyBlockVelocity.java @@ -218,25 +218,27 @@ public void onProxyInitialization(ProxyInitializeEvent event) { GameManager.loopServers(server); } - public boolean punished(Player player) { + private boolean checkPunished(Player player) { try { - var response = new ProxyService(ServiceType.PUNISHMENT) - .handleRequest(new GetActivePunishmentProtocolObject.GetActivePunishmentMessage(player.getUniqueId())) - .orTimeout(3, TimeUnit.SECONDS) - .join(); + ProxyService service = new ProxyService(ServiceType.PUNISHMENT); - if (!(response instanceof GetActivePunishmentProtocolObject.GetActivePunishmentResponse r) || !r.found()) { - return false; - } + var banFuture = service.handleRequest( + new GetActivePunishmentProtocolObject.GetActivePunishmentMessage(player.getUniqueId(), PunishmentType.BAN.name())); + var muteFuture = service.handleRequest( + new GetActivePunishmentProtocolObject.GetActivePunishmentMessage(player.getUniqueId(), PunishmentType.MUTE.name())); + + CompletableFuture.allOf(banFuture, muteFuture).orTimeout(3, TimeUnit.SECONDS).join(); - ActivePunishment punishment = new ActivePunishment( - r.type(), r.banId(), r.reason(), r.expiresAt(), r.tags()); - PunishmentType type = PunishmentType.valueOf(r.type()); - if (type == PunishmentType.BAN) { + var banResponse = banFuture.join(); + if (banResponse instanceof GetActivePunishmentProtocolObject.GetActivePunishmentResponse r && r.found()) { + ActivePunishment punishment = new ActivePunishment(r.type(), r.banId(), r.reason(), r.expiresAt(), r.tags()); player.disconnect(PunishmentMessages.banMessage(punishment)); return true; } - if (type == PunishmentType.MUTE) { + + var muteResponse = muteFuture.join(); + if (muteResponse instanceof GetActivePunishmentProtocolObject.GetActivePunishmentResponse r && r.found()) { + ActivePunishment punishment = new ActivePunishment(r.type(), r.banId(), r.reason(), r.expiresAt(), r.tags()); player.sendMessage(PunishmentMessages.muteMessage(punishment)); } return false; @@ -246,10 +248,11 @@ public boolean punished(Player player) { } @Subscribe - public void onPlayerJoin(PlayerChooseInitialServerEvent event) { + public EventTask onPlayerJoin(PlayerChooseInitialServerEvent event) { + return EventTask.async(() -> { Player player = event.getPlayer(); - if (punished(player)) { + if (checkPunished(player)) { return; } @@ -311,11 +314,12 @@ public void onPlayerJoin(PlayerChooseInitialServerEvent event) { FromProxyChannels.PROMPT_PLAYER_FOR_AUTHENTICATION, new JSONObject().put("uuid", player.getUniqueId().toString())); } + }); } @Subscribe public void onServerCrash(KickedFromServerEvent event) { - if (punished(event.getPlayer())) { + if (checkPunished(event.getPlayer())) { return; } diff --git a/velocity.extension/src/main/java/net/swofty/velocity/redis/listeners/ListenerPlayerPunished.java b/velocity.extension/src/main/java/net/swofty/velocity/redis/listeners/ListenerPlayerPunished.java index 510bfbd13..9a686fbb8 100644 --- a/velocity.extension/src/main/java/net/swofty/velocity/redis/listeners/ListenerPlayerPunished.java +++ b/velocity.extension/src/main/java/net/swofty/velocity/redis/listeners/ListenerPlayerPunished.java @@ -1,25 +1,23 @@ package net.swofty.velocity.redis.listeners; -import com.velocitypowered.api.proxy.ProxyServer; +import com.google.gson.Gson; import io.sentry.Sentry; import net.kyori.adventure.text.Component; import net.swofty.commons.proxy.ToProxyChannels; -import net.swofty.commons.punishment.PunishmentId; import net.swofty.commons.punishment.PunishmentReason; +import net.swofty.commons.punishment.PunishmentTag; import net.swofty.commons.punishment.ActivePunishment; import net.swofty.commons.punishment.PunishmentMessages; import net.swofty.commons.punishment.PunishmentType; import net.swofty.commons.punishment.template.BanType; import net.swofty.commons.punishment.template.MuteType; -import net.swofty.proxyapi.ProxyPlayer; import net.swofty.velocity.SkyBlockVelocity; import net.swofty.velocity.redis.ChannelListener; import net.swofty.velocity.redis.RedisListener; -import org.jetbrains.annotations.Nullable; -import org.json.JSONException; import org.json.JSONObject; import org.tinylog.Logger; +import java.util.List; import java.util.UUID; @ChannelListener(channel = ToProxyChannels.PUNISH_PLAYER) @@ -42,31 +40,33 @@ public JSONObject receivedMessage(JSONObject message, UUID serverUUID) { } else if (muteString != null) { reason = new PunishmentReason(MuteType.valueOf(muteString)); } else { - throw new JSONException("Missing reason ban, mute or reason_mute"); + throw new org.json.JSONException("Missing reason ban or reason_mute"); } - } catch (IllegalArgumentException | JSONException e) { + } catch (IllegalArgumentException | org.json.JSONException e) { Logger.error("Failed to parse punishment reason from message: " + message, e); Sentry.captureException(e); return null; } + List tags = List.of(); + if (!message.isNull("tags")) { + try { + tags = List.of(new Gson().fromJson(message.getString("tags"), PunishmentTag[].class)); + } catch (Exception ignored) { + } + } PunishmentType punishmentType = PunishmentType.valueOf(type); PunishmentReason finalReason = reason; + List finalTags = tags; SkyBlockVelocity.getServer().getPlayer(target).ifPresent((player) -> { - ActivePunishment activePunishment = new ActivePunishment(type, id, finalReason, expiresAt, java.util.List.of()); + ActivePunishment activePunishment = new ActivePunishment(type, id, finalReason, expiresAt, finalTags); switch (punishmentType) { - case BAN -> { - player.disconnect(PunishmentMessages.banMessage(activePunishment)); - } - case MUTE -> { - player.sendMessage(PunishmentMessages.muteMessage(activePunishment)); - } - case WARNING -> { - player.sendMessage(Component.text("§c[WARNING] §7You have received a warning for the following reason: §e" + finalReason)); - } - default -> { - } + case BAN -> player.disconnect(PunishmentMessages.banMessage(activePunishment)); + case MUTE -> player.sendMessage(PunishmentMessages.muteMessage(activePunishment)); + case WARNING -> player.sendMessage(Component.text( + "§c[WARNING] §7You have received a warning for the following reason: §e" + finalReason.getReasonString())); + default -> {} } }); From e60e2e2303bb056138f770d06db3d9974cb0390c Mon Sep 17 00:00:00 2001 From: Swofty Date: Wed, 18 Feb 2026 00:52:41 +1100 Subject: [PATCH 25/27] style(punishment): replace var with explicit types, rename single-char variables --- .../commons/punishment/PunishmentRedis.java | 18 +++++++++--------- .../generic/command/commands/UnBanCommand.java | 13 ++++++++----- .../command/commands/UnMuteCommand.java | 13 ++++++++----- .../event/actions/ActionPlayerMute.java | 7 ++++--- .../net/swofty/velocity/SkyBlockVelocity.java | 18 ++++++++++-------- 5 files changed, 39 insertions(+), 30 deletions(-) diff --git a/commons/src/main/java/net/swofty/commons/punishment/PunishmentRedis.java b/commons/src/main/java/net/swofty/commons/punishment/PunishmentRedis.java index b37884690..fdd5a572a 100644 --- a/commons/src/main/java/net/swofty/commons/punishment/PunishmentRedis.java +++ b/commons/src/main/java/net/swofty/commons/punishment/PunishmentRedis.java @@ -68,7 +68,7 @@ public static void saveActivePunishment(UUID playerId, String type, String id, if (!isInitialized()) throw new IllegalStateException("PunishmentRedis not initialized"); try (Jedis jedis = jedisPool.getResource()) { - String k = key(playerId, type); + String redisKey = key(playerId, type); HashMap data = new HashMap<>(Map.of( "type", type, @@ -80,14 +80,14 @@ public static void saveActivePunishment(UUID playerId, String type, String id, data.put("tags", GSON.toJson(tags)); } - jedis.hset(k, data); + jedis.hset(redisKey, data); if (expiresAt > 0) { long ttlSeconds = (expiresAt - System.currentTimeMillis()) / 1000; if (ttlSeconds > 0) { - jedis.expire(k, (int) ttlSeconds); + jedis.expire(redisKey, (int) ttlSeconds); } } else { - jedis.persist(k); + jedis.persist(redisKey); } } } @@ -96,8 +96,8 @@ public static Optional getActive(UUID playerId, String type) { if (!isInitialized()) return Optional.empty(); try (Jedis jedis = jedisPool.getResource()) { - String k = key(playerId, type); - Map data = jedis.hgetAll(k); + String redisKey = key(playerId, type); + Map data = jedis.hgetAll(redisKey); if (data.isEmpty()) return Optional.empty(); @@ -105,7 +105,7 @@ public static Optional getActive(UUID playerId, String type) { long expiresAt = Long.parseLong(data.getOrDefault("expiresAt", "-1")); if (expiresAt > 0 && System.currentTimeMillis() > expiresAt) { - jedis.del(k); + jedis.del(redisKey); return Optional.empty(); } @@ -125,8 +125,8 @@ public static List getAllActive(UUID playerId) { if (!isInitialized()) return List.of(); List result = new ArrayList<>(); - for (PunishmentType pt : PunishmentType.values()) { - getActive(playerId, pt.name()).ifPresent(result::add); + for (PunishmentType punishmentType : PunishmentType.values()) { + getActive(playerId, punishmentType.name()).ifPresent(result::add); } return result; } diff --git a/type.generic/src/main/java/net/swofty/type/generic/command/commands/UnBanCommand.java b/type.generic/src/main/java/net/swofty/type/generic/command/commands/UnBanCommand.java index f251e4ece..83b63449f 100644 --- a/type.generic/src/main/java/net/swofty/type/generic/command/commands/UnBanCommand.java +++ b/type.generic/src/main/java/net/swofty/type/generic/command/commands/UnBanCommand.java @@ -11,7 +11,10 @@ import net.swofty.type.generic.user.HypixelPlayer; import net.swofty.type.generic.user.categories.Rank; +import net.minestom.server.command.builder.arguments.Argument; + import java.io.IOException; +import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; @@ -26,17 +29,17 @@ public class UnBanCommand extends HypixelCommand { @Override public void registerUsage(MinestomCommand command) { - var argument = ArgumentType.String("player"); + Argument playerArg = ArgumentType.String("player"); command.addSyntax((sender, context) -> { HypixelPlayer player = (HypixelPlayer) sender; - String playerName = context.get(argument); + String playerName = context.get(playerArg); CompletableFuture.runAsync(() -> { try { - var targetUuid = MojangUtils.getUUID(playerName); + UUID targetUuid = MojangUtils.getUUID(playerName); ProxyService punishmentService = new ProxyService(ServiceType.PUNISHMENT); - var message = new UnpunishPlayerProtocolObject.UnpunishPlayerMessage( + UnpunishPlayerProtocolObject.UnpunishPlayerMessage message = new UnpunishPlayerProtocolObject.UnpunishPlayerMessage( targetUuid, player.getUuid(), PunishmentType.BAN.name() ); @@ -56,6 +59,6 @@ public void registerUsage(MinestomCommand command) { player.sendMessage("§cCould not find player: " + playerName); } }); - }, argument); + }, playerArg); } } diff --git a/type.generic/src/main/java/net/swofty/type/generic/command/commands/UnMuteCommand.java b/type.generic/src/main/java/net/swofty/type/generic/command/commands/UnMuteCommand.java index 0e6aa3b22..43e3a8fa2 100644 --- a/type.generic/src/main/java/net/swofty/type/generic/command/commands/UnMuteCommand.java +++ b/type.generic/src/main/java/net/swofty/type/generic/command/commands/UnMuteCommand.java @@ -11,7 +11,10 @@ import net.swofty.type.generic.user.HypixelPlayer; import net.swofty.type.generic.user.categories.Rank; +import net.minestom.server.command.builder.arguments.Argument; + import java.io.IOException; +import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; @@ -26,17 +29,17 @@ public class UnMuteCommand extends HypixelCommand { @Override public void registerUsage(MinestomCommand command) { - var argument = ArgumentType.String("player"); + Argument playerArg = ArgumentType.String("player"); command.addSyntax((sender, context) -> { HypixelPlayer player = (HypixelPlayer) sender; - String playerName = context.get(argument); + String playerName = context.get(playerArg); CompletableFuture.runAsync(() -> { try { - var targetUuid = MojangUtils.getUUID(playerName); + UUID targetUuid = MojangUtils.getUUID(playerName); ProxyService punishmentService = new ProxyService(ServiceType.PUNISHMENT); - var message = new UnpunishPlayerProtocolObject.UnpunishPlayerMessage( + UnpunishPlayerProtocolObject.UnpunishPlayerMessage message = new UnpunishPlayerProtocolObject.UnpunishPlayerMessage( targetUuid, player.getUuid(), PunishmentType.MUTE.name() ); @@ -56,6 +59,6 @@ public void registerUsage(MinestomCommand command) { player.sendMessage("§cCould not find player: " + playerName); } }); - }, argument); + }, playerArg); } } diff --git a/type.generic/src/main/java/net/swofty/type/generic/event/actions/ActionPlayerMute.java b/type.generic/src/main/java/net/swofty/type/generic/event/actions/ActionPlayerMute.java index d5d066562..9dd79bc1f 100644 --- a/type.generic/src/main/java/net/swofty/type/generic/event/actions/ActionPlayerMute.java +++ b/type.generic/src/main/java/net/swofty/type/generic/event/actions/ActionPlayerMute.java @@ -20,15 +20,16 @@ public class ActionPlayerMute implements HypixelEventClass { public void onPlayerChat(PlayerChatEvent event) { Player player = event.getPlayer(); try { - var response = new ProxyService(ServiceType.PUNISHMENT) + Object response = new ProxyService(ServiceType.PUNISHMENT) .handleRequest(new GetActivePunishmentProtocolObject.GetActivePunishmentMessage( player.getUuid(), PunishmentType.MUTE.name())) .orTimeout(2, TimeUnit.SECONDS) .join(); - if (response instanceof GetActivePunishmentProtocolObject.GetActivePunishmentResponse r && r.found()) { + if (response instanceof GetActivePunishmentProtocolObject.GetActivePunishmentResponse muteResponse && muteResponse.found()) { event.setCancelled(true); - var punishment = new ActivePunishment(r.type(), r.banId(), r.reason(), r.expiresAt(), r.tags()); + ActivePunishment punishment = new ActivePunishment( + muteResponse.type(), muteResponse.banId(), muteResponse.reason(), muteResponse.expiresAt(), muteResponse.tags()); player.sendMessage(PunishmentMessages.muteMessage(punishment)); } } catch (Exception ignored) { diff --git a/velocity.extension/src/main/java/net/swofty/velocity/SkyBlockVelocity.java b/velocity.extension/src/main/java/net/swofty/velocity/SkyBlockVelocity.java index 44c3a9cba..48c27ef79 100644 --- a/velocity.extension/src/main/java/net/swofty/velocity/SkyBlockVelocity.java +++ b/velocity.extension/src/main/java/net/swofty/velocity/SkyBlockVelocity.java @@ -222,23 +222,25 @@ private boolean checkPunished(Player player) { try { ProxyService service = new ProxyService(ServiceType.PUNISHMENT); - var banFuture = service.handleRequest( + CompletableFuture banFuture = service.handleRequest( new GetActivePunishmentProtocolObject.GetActivePunishmentMessage(player.getUniqueId(), PunishmentType.BAN.name())); - var muteFuture = service.handleRequest( + CompletableFuture muteFuture = service.handleRequest( new GetActivePunishmentProtocolObject.GetActivePunishmentMessage(player.getUniqueId(), PunishmentType.MUTE.name())); CompletableFuture.allOf(banFuture, muteFuture).orTimeout(3, TimeUnit.SECONDS).join(); - var banResponse = banFuture.join(); - if (banResponse instanceof GetActivePunishmentProtocolObject.GetActivePunishmentResponse r && r.found()) { - ActivePunishment punishment = new ActivePunishment(r.type(), r.banId(), r.reason(), r.expiresAt(), r.tags()); + Object banResult = banFuture.join(); + if (banResult instanceof GetActivePunishmentProtocolObject.GetActivePunishmentResponse banResponse && banResponse.found()) { + ActivePunishment punishment = new ActivePunishment( + banResponse.type(), banResponse.banId(), banResponse.reason(), banResponse.expiresAt(), banResponse.tags()); player.disconnect(PunishmentMessages.banMessage(punishment)); return true; } - var muteResponse = muteFuture.join(); - if (muteResponse instanceof GetActivePunishmentProtocolObject.GetActivePunishmentResponse r && r.found()) { - ActivePunishment punishment = new ActivePunishment(r.type(), r.banId(), r.reason(), r.expiresAt(), r.tags()); + Object muteResult = muteFuture.join(); + if (muteResult instanceof GetActivePunishmentProtocolObject.GetActivePunishmentResponse muteResponse && muteResponse.found()) { + ActivePunishment punishment = new ActivePunishment( + muteResponse.type(), muteResponse.banId(), muteResponse.reason(), muteResponse.expiresAt(), muteResponse.tags()); player.sendMessage(PunishmentMessages.muteMessage(punishment)); } return false; From 925f752b449ca4a1e6fd777a8e4b636ef74397a5 Mon Sep 17 00:00:00 2001 From: Swofty Date: Wed, 18 Feb 2026 01:18:05 +1100 Subject: [PATCH 26/27] fix(punishment): validate parseDuration result to prevent silent permanent bans --- .../swofty/type/generic/command/commands/BanCommand.java | 6 +++++- .../swofty/type/generic/command/commands/MuteCommand.java | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/type.generic/src/main/java/net/swofty/type/generic/command/commands/BanCommand.java b/type.generic/src/main/java/net/swofty/type/generic/command/commands/BanCommand.java index 7a53ce4fd..981f0ef7c 100644 --- a/type.generic/src/main/java/net/swofty/type/generic/command/commands/BanCommand.java +++ b/type.generic/src/main/java/net/swofty/type/generic/command/commands/BanCommand.java @@ -64,8 +64,12 @@ public void registerUsage(MinestomCommand command) { CompletableFuture.runAsync(() -> { try { - UUID targetUuid = net.minestom.server.utils.mojang.MojangUtils.getUUID(playerName); long actualTime = StringUtility.parseDuration(duration); + if (actualTime <= 0) { + player.sendMessage("§cInvalid duration format. Use e.g. 30d, 12h, 30m, 10s."); + return; + } + UUID targetUuid = net.minestom.server.utils.mojang.MojangUtils.getUUID(playerName); long expiryTime = System.currentTimeMillis() + actualTime; banPlayer(player, targetUuid, type, player.getUuid(), actualTime, expiryTime, playerName, null); } catch (IOException e) { diff --git a/type.generic/src/main/java/net/swofty/type/generic/command/commands/MuteCommand.java b/type.generic/src/main/java/net/swofty/type/generic/command/commands/MuteCommand.java index 1e5b76771..2f369acae 100644 --- a/type.generic/src/main/java/net/swofty/type/generic/command/commands/MuteCommand.java +++ b/type.generic/src/main/java/net/swofty/type/generic/command/commands/MuteCommand.java @@ -57,8 +57,12 @@ public void registerUsage(MinestomCommand command) { CompletableFuture.runAsync(() -> { try { - UUID targetUuid = net.minestom.server.utils.mojang.MojangUtils.getUUID(playerName); long actualTime = StringUtility.parseDuration(duration); + if (actualTime <= 0) { + player.sendMessage("§cInvalid duration format. Use e.g. 30d, 12h, 30m, 10s."); + return; + } + UUID targetUuid = net.minestom.server.utils.mojang.MojangUtils.getUUID(playerName); long expiryTime = System.currentTimeMillis() + actualTime; mutePlayer(player, targetUuid, type, player.getUuid(), actualTime, expiryTime, playerName); } catch (IOException e) { From d400b0a43bd793fcee2864185f4837199ddf2f67 Mon Sep 17 00:00:00 2001 From: Swofty Date: Wed, 18 Feb 2026 01:23:34 +1100 Subject: [PATCH 27/27] revert(punishment): remove duration validation and restore mute message format Invalid duration defaults to permanent punishment (stricter, not weaker). Restores original mute message format with reason line. --- .../net/swofty/commons/punishment/PunishmentMessages.java | 3 ++- .../swofty/type/generic/command/commands/BanCommand.java | 6 +----- .../swofty/type/generic/command/commands/MuteCommand.java | 6 +----- 3 files changed, 4 insertions(+), 11 deletions(-) diff --git a/commons/src/main/java/net/swofty/commons/punishment/PunishmentMessages.java b/commons/src/main/java/net/swofty/commons/punishment/PunishmentMessages.java index 548bdf9a0..6dc73a046 100644 --- a/commons/src/main/java/net/swofty/commons/punishment/PunishmentMessages.java +++ b/commons/src/main/java/net/swofty/commons/punishment/PunishmentMessages.java @@ -48,8 +48,9 @@ public static Component muteMessage(ActivePunishment punishment) { time = "§7Your mute will expire in §c" + prettyTimeLeft + "\n\n"; } + String reasonLine = "§7Reason: §f" + reason.getReasonString() + "\n"; String urlInfo = "§7Find out more here: §fwww.hypixel.net/mutes\n"; String footer = "§7Mute ID: §f" + punishment.banId(); - return Component.text(line + header + time + urlInfo + footer + line); + return Component.text(line + header + reasonLine + time + urlInfo + footer + line); } } diff --git a/type.generic/src/main/java/net/swofty/type/generic/command/commands/BanCommand.java b/type.generic/src/main/java/net/swofty/type/generic/command/commands/BanCommand.java index 981f0ef7c..7a53ce4fd 100644 --- a/type.generic/src/main/java/net/swofty/type/generic/command/commands/BanCommand.java +++ b/type.generic/src/main/java/net/swofty/type/generic/command/commands/BanCommand.java @@ -64,12 +64,8 @@ public void registerUsage(MinestomCommand command) { CompletableFuture.runAsync(() -> { try { - long actualTime = StringUtility.parseDuration(duration); - if (actualTime <= 0) { - player.sendMessage("§cInvalid duration format. Use e.g. 30d, 12h, 30m, 10s."); - return; - } UUID targetUuid = net.minestom.server.utils.mojang.MojangUtils.getUUID(playerName); + long actualTime = StringUtility.parseDuration(duration); long expiryTime = System.currentTimeMillis() + actualTime; banPlayer(player, targetUuid, type, player.getUuid(), actualTime, expiryTime, playerName, null); } catch (IOException e) { diff --git a/type.generic/src/main/java/net/swofty/type/generic/command/commands/MuteCommand.java b/type.generic/src/main/java/net/swofty/type/generic/command/commands/MuteCommand.java index 2f369acae..1e5b76771 100644 --- a/type.generic/src/main/java/net/swofty/type/generic/command/commands/MuteCommand.java +++ b/type.generic/src/main/java/net/swofty/type/generic/command/commands/MuteCommand.java @@ -57,12 +57,8 @@ public void registerUsage(MinestomCommand command) { CompletableFuture.runAsync(() -> { try { - long actualTime = StringUtility.parseDuration(duration); - if (actualTime <= 0) { - player.sendMessage("§cInvalid duration format. Use e.g. 30d, 12h, 30m, 10s."); - return; - } UUID targetUuid = net.minestom.server.utils.mojang.MojangUtils.getUUID(playerName); + long actualTime = StringUtility.parseDuration(duration); long expiryTime = System.currentTimeMillis() + actualTime; mutePlayer(player, targetUuid, type, player.getUuid(), actualTime, expiryTime, playerName); } catch (IOException e) {